SDK conditional code with canImport(module, _version: "1.2.3.4")

SDK conditional code with canImport(module, _version: "1.2.3.4")

In this blog post, I explain when and why you might want to use the versioned canImport compilation statement, which challenges arise, and what better alternatives exist.

Example: #if canImport(SwiftUI, _version: "4.1.17")

Example of SDK conditional code

At work, we wanted to use SwiftUI.Font.width(_:) function in a Swift package with // swift-tools-version: 5.7 and minimum deployment target of iOS 15.

// is this sufficient?
if #available(iOS 16.0, *) {
  let font = Font.system(.headline)
  _ = font.width(Font.Width.compressed)
}

Our CI job reported a compilation error when using Xcode 14 (iOS 16.0). We were using Xcode 14.1 (or higher) and did not encounter an issue before.

SwiftUI.Font.width(_:) was actually introduced in iOS 16.1 and not in iOS 16.0 as the documentation suggests. This explains why Xcode 14.1 worked fine, as Xcode 14.1 contains the iOS 16.1 SDK.

Versioned canImport

While researching a solution, I stumbled on an interesting fact that #if canImport allows specifying a version that gets checked during compile-time. The version gets compared against the -user-module-version flag in the .swiftmodule file using.

swiftinterface file for SwiftUI shipped in iOS SDK 16.2

It returns true if the module version on disk is greater or equal to the specified value and returns false otherwise.

If you are curious: user-module-version for SwiftUI is

  • 4.0.90.1.107 when shipped in iOS 16

  • 4.1.17.100 when shipped in OS 16.1

  • 4.2.11 when shipped in iOS 16.2

#if canImport(SwiftUI, _version: "4.1.17")
  // code executes when compiled with iOS SDK 16.1 or higher
#endif

Advantages

Using the versioned canImport has some interesting advantages as pointed out by Allan Shortlidge in the Swift Forums:

  • Suppose the framework and the APIs in question are cross-platform. In that case, you can (theoretically) write a single query to determine the build time availability of the API across multiple platforms because module versions tend to be aligned across the aligned platform-specific SDKs.

  • It allows you to gracefully handle things like APIs introduced midway through the betas even though the overall system/SDK version number hasn't changed.

  • It works for any Swift module that has an embedded user-module-version, regardless of whether the module is distributed with an SDK that is legible to the compiler.

Disadvantages

It is tedious to scrape the user-module-version of the relevant framework out of the SDK as it is not documented on the internet.

The easiest but still complex procedure is:

  • install the Xcode with the desired SDK

  • Determine the SDK path where the frameworks are stored on your disk

xcrun -sdk iphoneos --show-sdk-path
  • Then lookup the user-module-version in the .swiftinterface file.
    On my machine, using Xcodes for installing multiple Xcode installations in parallel, the path for the swiftinterface of SwiftUI shipped in iOS SDK 16.2 is: /Applications/Xcode-14.2.0.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk/System/Library/Frameworks/SwiftUI.framework/Modules/SwiftUI.swiftmodule/arm64-apple-ios.swiftinterface

Alternatives

We decided not to use canImport for our example because of the non-intuitive version number, and we discussed the following options:

  • increase the supported swift-tools-version to 5.7.1 OR

  • use an additional compilation statement

We ended up with the latter.

#if swift(>=5.7.1)
  if #available(iOS 16.0, watchOS 9.0, *) {
    // use a iOS 16.1 specific API like SwiftUI's Font.weight
  }
#endif

Shoutout and thanks to Jon Shier for his input in Swift Forums:

There’s no good solution for this as we can’t check for SDK versions at build time or dynamically check for symbols at runtime. What you can do is a build time check for the Swift version (#if swift(>=5.8)) around the checks for the new API. Technically it’s not a guarantee but Apple’s inflexibility with SDK versions in Xcode works in your favor here, so it should be stable.

I hope the Swift team will introduce a better option to support SDK-conditional code in the future.

Did you find this article valuable?

Support Marco Eidinger by becoming a sponsor. Any amount is appreciated!