The iOS SDK is an ever-changing beast.
With each new release Apple introduces a slew of new classes, methods and symbols whilst simultaneously deprecating a whole bunch of others. As developers, each time a new version comes out we’re tempted to dive headlong into the widgets and gizmos that these frameworks provide and along with it comes the ever-growing temptation to simply drop support for previous versions of the OS so that we can exclusively focus on developing with these cutting edge features.
For many developers this focus on the shiny and new is actually a viable option, especially if they are launching brand new apps and want to leverage the latest capabilities of the hardware but for others exclusively focusing on the latest and greatest just isn’t an option, at least not without potentially losing a significant portion of their existing user base. Inevitably, this leaves this later group of developers faced with the challenge of leveraging the new capabilities of the SDK whilst simultaneously trying to support portions of their user base who are yet to upgrade and this challenge in turn, inevitably leads to those developers performing weak-linking and runtime checks to try to determine which APIs are, and are not, available on the users platform.
Up until now performing these checks successfully has always been a little tricky but now, in Swift 2.0, Apple has introduced a new availability API that goes along way towards simplifying this situation.
Table of Contents
Checking for API Availability in Objective-C
Traditionally, there are three main techniques that developers have used to check for API availability in Objective-C and the early releases of Swift. These are:
- Check the operating system version.
- Checking for the existence of a particular class.
- Checking whether a class or object responds to a particular selector.
Let’s take a look at each of these in turn.
Check for the Operating System Version
The first way to check for the availability of a particular class, method or symbol within the iOS SDK is to check the version of the operating system that the app is running on and then compare that to minimum OS version required (extracted from Apples documentation).
This usually involves defining a number of macros within the code that, at their core, encapsulate calls to [[UIDevice currentDevice] systemVersion]
to identify the particular version of the operating system that the app is running on. The macros usually look something like this:
#define SYSTEM_VERSION_EQUAL_TO(v) ([[[UIDevice currentDevice] systemVersion] compare:v options:NSNumericSearch] == NSOrderedSame)
#define SYSTEM_VERSION_GREATER_THAN(v) ([[[UIDevice currentDevice] systemVersion] compare:v options:NSNumericSearch] == NSOrderedDescending)
#define SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(v) ([[[UIDevice currentDevice] systemVersion] compare:v options:NSNumericSearch] != NSOrderedAscending)
#define SYSTEM_VERSION_LESS_THAN(v) ([[[UIDevice currentDevice] systemVersion] compare:v options:NSNumericSearch] == NSOrderedAscending)
#define SYSTEM_VERSION_LESS_THAN_OR_EQUAL_TO(v) ([[[UIDevice currentDevice] systemVersion] compare:v options:NSNumericSearch] != NSOrderedDescending)
Once defined, these macros are then used in conjunction with an if
statement to conditionally execute different blocks of code.
For example, say I wanted to user a particular API that was only available on iOS 9 or later I could write:
if (SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(@"9.0")) {
// iOS 9 or later code
} else {
// Code using the frameworks that were available before iOS 9.
}
Check for Class Availability
The next approach developers use, is one that focuses on checking whether a particular class exists.
Again this is done in conjunction with an if
statement and in this case, they use the class
method provided by NSObject
to access the class object for a particular class to determine if it exists. If it does, they make use of the functionality that the class provides, if not, they execute some fallback statements or simply exit:
if ([SomeClass class]) {
// class exists
SomeClass *instance = [[SomeClass alloc] init];
} else {
// class doesn't exist
}
Check for Method Availability
The final technique is one that checks for individual methods within a framework or class. Commonly this involves using either the instanceRepondsToSelector:
method on the class itself:
if ([SomeClass instancesRespondToSelector:@selector (newMethod:)]) {
// Method is available for use.
} else {
// Method is not available.
}
Or the respondToSelector
method on an object:
if ([obj respondsToSelector:@selector(newMethod:)]) {
// Use the new method...
} else {
// Work around the fact that the new method isn't available.
}
The Issues With The Traditional Approaches
So, as you can see, there are a number of techniques that developers can employ to check for API availability and although (for the most part) they work, they introduce the need to continually dig through the documentation to determine what API is available, on what platforms, and at what versions and in reality, it’s can be pretty painful to maintain.
On top of this, although these checks appear to be safe, in some cases, these actually fail.
One example of this is where you are checking for a symbol or class in the latest version of iOS that you believed to be new functionality and expect to fail in previous releases of the OS. In some situations though, that symbol may in fact present but part of a private unpublished API that you didn’t know about and without you knowing about it you start using an unpublished APIs with potential side-effects that you don’t know about. Not ideal.
In Swift 2.0 though, the whole problem becomes a lot simpler.
Availability Checking in Swift 2.0
Built-in to Swift 2.0, is language-level support that allows us to explicitly check if a particular class, function or symbol is available on the current device and platform.
The Swift compiler can now make use of information baked into the SDK to check the availability of particular API calls and is able to do so at compiler-time rather than run-time. This allows it to raise warnings or error during compilation if your code tries to use an APIs that would either not be available on your deployment target at runtime or may not be in future, giving you a chance to fix the issues before your app is deployed.
So what is it that we need to do to leverage this new capability? Well this is where availability conditions come in.
Availability Conditions
Availability conditions are a bit like a boolean expression and use the #available
keyword in conjunction with either an if
or guard
statement to conditionally execute a block of code depending on the runtime availability of the API you want to use.
The general syntax is as follows:
if #available(platformName version, ..., *) {
// Statements to execute if the API is available
} else {
// Fallback statements to execute otherwise.
}
As you can see, the availability condition takes a list of platform names and versions with each platform-version pair being separated from the next with a comma.
At the time of writing, there are eight platforms that you can make use of:
iOS
iOSApplicationExtension
OSX
OSXApplicationExtension
watchOS
watchOSApplicationExtension
tvOS
tvOSApplicationExtension
These platforms can then be individually combined with either a positive integer or floating-point decimal version number. For example both 9
and 9.1
would be valid.
In addition to these platform-version pairs, we must also include an asterisk (*
) character as the last parameter within the parentheses. This asterisk is used as a wild card to cover any additional platforms (including an unknown number of future platforms), that have not been specifically listed and we have to include it regardless of whether our code is targeting those platforms or not.
The asterisk specifies that in the case of these other platforms, the body of the if
or guard
statements will execute on the minimum deployment target specified by your project and if you forget to include it, you will get a polite slap on the wrist from the compiler indicating that you: "Must handle potential future platforms with '*'
.
So with the basics nailed down, let’s have a look at a couple of concrete examples. In this example, I’m going to use the availability condition in conjunction with an if
statement:
if #available(iOS 8.0, OSX 10.10, *) {
// Make use of the new API.
} else {
// Fall back to old API or exit.
}
The availability condition in the code above indicates that when this code runs on an iOS platform, the first branch of the if
statement will only be executed if the platform is iOS 8.0 or later.
In addition to this, it is also indicating that when executed on OS X, it has to be version 10.10 or later in order for the first branch run.
I’ve also (as required), included the *
to indicate that on all other platforms that have I have not specifically listed the code will run on the minimum deployment target specified by my build target.
As I mentioned, you can also use the availability conditions in conjunction with the new guard
statement in Swift 2.0. This allows you to exit a function or method early if a particular API that you want to use is not available:
guard #available(iOS 8, watchOS 2.0) else {
return
}
// Make use of the new API.
In this case, the code after the closing brace of the guard statement will only execute if the platform is iOS 8 or later, or watchOS 2.0 or later. In all other cases, the body of the guard
statement will execute and, as is required by the guard
statement, the code will return. Get the idea?
Now, in addition to giving us the ability to check for the availability of particular APIs within our code, Swift 2.0, also introduces the flip side to this equation, the ability for us to annotate our own code to indicate which platforms and versions it will run on. Let’s have a look at that next.
Availability Attributes
In Swift, we have the ability to add attributes to a declaration or type. An attribute is additional metadata about the declaration and is written immediately preceding the declaration and uses the @
symbol followed by the attributes name and any arguments that the attribute accepts. For example:
@attribute name
@attribute name(arguments)
One of these attributes in Swift is the available
attribute and it follows the latter of these two formats.
The available
attribute provides the counterpoint to the #available
condition we just looked at and allows us to specify the additional details about a declaration in relation to certain platforms and operating system versions.
The syntax for the available
attribute should be pretty familiar by now:
@available(platformName version, ..., *)
Essentially, it follows the same syntax as the #available
condition we just looked at, using the same range of platforms and same versioning rules.
As with the #available
condition, in this short-hand syntax, we are also required to include the asterisk (*
) character as the last argument to the @available
annotation to cover platforms that we have either not specified explicitly, or that may come along in the future.
So at this point, you might be thinking that this pretty much the same as the #available
condition and for the most part you’d be correct, but there are a couple of other things that make availability annotations a little different.
In addition to the shorthand syntax we’ve seen so far, availability annotations can also be written in a longer form and combined with additional annotation arguments that provide extra detail.
In this long form, you can write one or more annotations immediately before a declaration and the compiler will only use an available
attribute if the attribute specifies a platform that matches the current target platform.
In this syntax, the first argument to the annotation is the platform to which the annotation applies and can be any of the platforms we looked at earlier or the asterisk (*
) character to indicate that the annotation applies to all platforms. This can then be followed (after a comma) with one or more additional arguments. The first of these is the unavailable
argument.
The unavailable
Argument
The unavailable
argument is used to indicate that the item is not available at all on the given platform. Anyone trying to use the declaration on that platform will then get a compiler error similar to the one below:
We also have the option of using one of the introduced
, deprecated
or obsoleted
arguments. Each of these is followed by an =
character and then a version and as with the unavailable
argument, we can use more than one of them if we separate them with a commas.
The introduced
Argument
The introduced
argument specifies the first version of the platform in which the declaration was available and is the long-form syntax for specifying the version numbers we saw earlier.
The deprecated
Argument
The next keyword, deprecated
, indicates the first version in which the declaration was deprecated and will result in a compiler warning similar to the following for anyone trying to use the declaration on a later version of the matching platform:
The obsoleted
Argument
The obsoleted
keyword specifies the version of the platform on which the declaration was first removed. In this case, any use of that declaration in later versions of the platform will result in a compiler error:
In addition to these version-oriented arguments, we can also use the renamed
and message
arguments.
The renamed
Argument
The renamed
argument, is used with a string literal and is used to indicate the new name for a declaration should it have been renamed. As you can see in the screenshot below, the new name for the declaration is then displayed by the compiler in the error message and in these simple cases can be used by the fix-it system to automatically rename any calls to the API for you:
The message
Argument
The last annotation argument that is supported is the message
argument and again this is combined with a string literal. The message argument is used to provide additional text that is displayed by the compiler when displaying either a warning or error about a declaration being deprecated or obsolete:
Further Information
That, pretty much wraps up API availability checking and API annotation in Swift. As we’ve seen, the new features that Swift 2.0 has introduced allow us to not only check for API availability but also to annotate our own APIs and has gone along way toward eliminating the maintenance overheads developers have suffered in the past.
If you want to find out more information about API Availability Checking or Annotation in Swift, make sure you check out the Checking API Availability and Attributes sections of the Swift Programming Language Guide and as ever, if you have any questions, tips, observations, feel free to get in touch.