Checking API Availability with Swift

Apple introduces new API to iOS (and macOS, watchOS and tvOS) every year at WWDC. Unless you can drop support for older versions of the operating system you need to think about backwards compatibility when adopting new API.

Let’s compare how Swift and Objective-C allow you to check if an API is available to use for the versions of the OS you’re supporting.

Last updated: Feb 21, 2020

The Objective-C Approach

Before looking at Swift let’s take a brief recap of the different ways we can check for SDK availability with Objective-C:

Checking for class/framework availability

Each major release of iOS introduces new frameworks. If your deployment target is older than the latest iOS release you need to weak link any new framework you use and then check the availability of the class at run time. For example, if we want to use the new Contacts framework on iOS 9 but be able to fall back to the older address book framework on iOS 8:

if ([CNContactStore class]) {
  CNContactStore *store = [CNContactStore new];
  //...
} else {
  // Fallback to old framework
}

Check for method availability

Use respondsToSelector to check for a method added to a framework. For example, iOS 9 introduced the allowsBackgroundLocationUpdates property to Core Location:

CLLocationManager *manager = [CLLocationManager new];
if ([manager respondsToSelector:@selector(setAllowsBackgroundLocationUpdates:)]) {
  // Not available in iOS 8
  manager.allowsBackgroundLocationUpdates = YES;
}

Pitfalls

These methods are painful to maintain and not as safe as they seem. Consider what happens if we test for the availability of a now public symbol that in earlier releases was private to Apple. For example, there are several new text styles in iOS 9 including UIFontTextStyleCallout. To only use this style from iOS 9 you could try to check for the symbol, expecting it to be null on iOS 8:

if (&UIFontTextStyleCallout != nil) {
  textLabel.font = [UIFont preferredFontForTextStyle:UIFontTextStyleCallout];
}

Unfortunately this does not work as expected. It turns out the symbol did exist in iOS 8 it just was not publicly declared. Using a private method or value risks unexpected results and is not what we want to be doing.

Compile Time Availability Checks

With Swift the availability checks are built-in and checked at compile time. This means Xcode can tell us when we use an API that is unavailable for our deployment target. For example if I try to use CNContactStore with an iOS 8 deployment target Xcode suggests the fix below:

if #available(iOS 9.0, *) {
  let store = CNContactStore()
} else {
  // Fallback on earlier versions
}

You can use the same approach for methods where before we used respondsToSelector:

let manager = CLLocationManager()
if #available(iOS 9.0, *) {
  manager.allowsBackgroundLocationUpdates = true
}

Availability Condition

The #available condition takes a list of platforms (ios, OSX, watchOS) and versions. For example, for code that should only run on iOS 9 or OS X 10.10:

if #available(iOS 9, OSX 10.10, *) {
  // Code to execute on iOS 9, OS X 10.10
}

You always need the final * wildcard to cover the other unspecified platforms even if your App is not targeting them.

You can improve readability and use #available with a guard statement for early return from a function if you have a block of code that is conditional:

private func somethingNew() {
  guard #available(iOS 9, *) else { return }

  // do something on iOS 9
  let store = CNContactStore()
  let predicate = CNContact.predicateForContactsMatchingName("Zakroff")
  let keys = [CNContactGivenNameKey, CNContactFamilyNameKey]
  ...
}

If you have a whole function or class that should be conditionally available use @available:

@available(iOS 9.0, *)
private func checkContact() {
  let store = CNContactStore()
  // ...
}

Availability Checks For Objective-C (Xcode 9)

Since Xcode 9 the same compiler availability checks can also be used with Objective-C. The syntax is almost identical. For example, instead of using respondsToSelector we can use @available to test if we are running on at least iOS 9:

if (@available(iOS 9, *)) {
  manager.allowsBackgroundLocationUpdates = YES;
}

Compile Time Safety

To finish up, let’s look again at the problem with a symbol public in iOS 9 but private in iOS 8. If we try to set the preferred font to an iOS 9 only style we now get a compile time error for iOS 8 targets:

label.font = UIFont.preferredFont(forTextStyle: .callout)
> 'UIFontTextStyleCallout' is only available on iOS 9.0 or newer

Swift makes it easy to test and fallback to a sensible default depending on the platform version:

if #available(iOS 9.0, *) {
  label.font = UIFont.preferredFont(forTextStyle: .callout)
} else {
  label.font = UIFont.preferredFont(forTextStyle: .body)
}

Further Reading