Every year around this time, I find myself needing to write code that compiles differently based on the availability of a particular SDK version. Unfortunately, there's no way to do that. My goal is to be able to write code that uses features from the new iOS or Mac SDKs if they are available.
I wrote about this two years ago, and everyone seemed to agree that it was necessary then. I think we just need someone to actually implement it.
TL;DR: We really need something like #if sdk()
or #if moduleVersion()
. This comes up every year.
For example, I would like to be able to write code like this, and have it compile with both Xcode 13 and 14:
// This code may build on mac, iOS, and watchOS.
// The iOS 16 and watchOS 9 SDKs are not yet
// released, so most of the team is still building
// with iOS 15 and watchOS 8 SDKs. We want the
// code to compile for everyone.
func widgetView(for family: WidgetFamily) -> some View {
switch family {
case .systemSmall: // ...
case .systemMedium: // ...
case .systemLarge: // ...
#if sdk(iOS 15)
case .systemExtraLarge: // ... [case 1]
#endif
// If we're building with the iOS 16 SDK, handle the new
// widget size families
#if sdk(iOS 16, watchOS 9)
case .accessoryCircular: // ... [case 2]
case .accessoryRectangular: // ...
case .accessoryInline: // ...
#endif
// This one is watch-only
#if sdk(watchOS 9)
case .accessoryCorner: // ... [case 3]
#endif
// This doesn't actually exist; only for illustrative purposes
#if sdk(macOS 13)
case .someMacOnlyVariant: // [case 4]
#endif
@unknown default: // ...
}
}
Unfortunately, the above code does not work, because #if sdk()
doesn't exist. We have to work around it with various ugly hacks.
For the first case, since it's in a stable release, we can just use #if os()
:
#if os(iOS)
case .systemExtraLarge: // ... [case 1]
#endif
For the second case, though, it's more ugly. We have to use a combination of #if os()
and #if compiler()
:
// The iOS 16 SDK ships with Xcode 14, which
// contains the Swift 5.7 compiler. So we're
// conflating the compiler version with the
// SDK version
#if compiler(>=5.7)
#if os(iOS) || os(watchOS)
case .accessoryCircular: // ... [case 2]
case .accessoryRectangular: // ...
case .accessoryInline: // ...
#endif
#endif
The third case is similar; we have to use both #if compiler()
and #if os()
to only build on watchOS:
#if compiler(>=5.7)
#if os(watchOS)
case .accessoryCorner: // ... [case 3]
#endif
#endif
The fourth case gets especially tricky. At first glance, it looks the same; it's just a Mac-specific variant. However, historically the macOS SDK has usually shipped about a month after the corresponding iOS SDK. That means that this code would work during the beta period:
#if compiler(>=5.7)
#if os(macOS)
case .someMacOnlyVariant: // [case 4]
#endif
#endif
However, once Xcode 14 is released, this code actually fails to build! This is because even though the Xcode 14 betas included the macOS 13 SDK, if Apple follows their usual pattern, the final release of Xcode 14 will not include the macOS 13 SDK. Instead, that will be deferred to an Xcode 14.1 beta.
That makes things difficult, because now we can no longer use #if compiler(>=5.7)
as a proxy for SDK availability, because both Xcode 14.0 and 14.1 will be using Swift 5.7. At that point, the only way we have to manage this is to rely on #if canImport()
, because that is SDK-dependent. However, WidgetFamily
here doesn't actually need to import any new frameworks, so we end up with a very confusing check:
// AppIntents is an unrelated framework also
// introduced in the macOS 13 SDK. We can use
// it as a proxy for "is the macOS 13 SDK
// available"
#if canImport(AppIntents)
#if os(macOS)
case .someMacOnlyVariant: // [case 4]
#endif
#endif
We could use the #if canImport(AppIntents)
trick in other places too, since we have evidence that the compiler version is not always coupled to the SDK version. This leaves us with this as our final body:
// This code may build on mac, iOS, and watchOS.
// The iOS 16 and watchOS 9 SDKs are not yet
// released, so most of the team is still building
// with iOS 15 and watchOS 8 SDKs. We want the
// code to compile for everyone.
func widgetView(for family: WidgetFamily) -> some View {
switch family {
case .systemSmall: // ...
case .systemMedium: // ...
case .systemLarge: // ...
// By luck, AppIntents is available across macOS, iOS, and watchOS,
// so we can use this single check for all the following cases
#if canImport(AppIntents)
#if os(iOS) || os(watchOS)
case .accessoryCircular: // ... [case 2]
case .accessoryRectangular: // ...
case .accessoryInline: // ...
#endif
// This one is watch-only
#if os(watchOS)
case .accessoryCorner: // ... [case 3]
#endif
// This doesn't actually exist; only for illustrative purposes
#if os(macOS)
case .someMacOnlyVariant: // [case 4]
#endif
#endif
@unknown default: // ...
}
This works, but that's a very load-bearing #if canImport(AppIntents)
, and it only works because Apple happened to introduce an unrelated framework in this same SDK release.
We need something that lets us more specifically say "this code should only build if this particular version of the SDK is available." It seems like most everyone agrees this would be useful (based on the previous thread); it sounds like we just need an actual proposal and then someone willing to implement it.