LAST CHANCE: Register for my live Swift workshop days! >>

Availability checking in Swift 2: backwards compatibility the smart way

Paul Hudson    June 11th 2015    @twostraws

If you already read my article discussing all the new features in Swift 2 but wanted to know more about the new defer keyword, this article is for you.

Please note: this article covers Swift 2, iOS 9 and Xcode 7. You must have installed Xcode 7 if you want to follow along.

How it used to be: manual version checking

Without the new features in Swift 2, you would have to check for version compatibility by hand. For example, if you want to use UIStackView in your app but wanted to support iOS 8 users, you'd need to do a run-time operating system version number check and show the stack view only if it were supported. For example:

if NSProcessInfo().isOperatingSystemAtLeastVersion(NSOperatingSystemVersion(majorVersion: 9, minorVersion: 0, patchVersion: 0)) {
    print("Create the stack view!")
}

This approach was fraught with problems, not least trying to remember when each component was introduced. Worse, what if you missed some code? If your app tried to use iOS 9 code on iOS 8, or iOS 8 code on iOS 7, it would just crash, which meant developers who were keen use the latest and greatest APIs had to spend a lot of time adding checks to their code, and ensuring it was crash-free.

Swift 2: Automatic operating system API availability checking

This is probably my favorite feature in all of Swift 2, partly because it makes developers' lives easier, but also because it makes apps more stable for end users.

In Swift 2, Apple introduced API availability checking. If you set your app's Deployment Target to a lower iOS release than the base SDK, Xcode will automatically scan every API you use to make sure it's available in your lowest deployment target version. This information has been in Apple's API headers for years, but it's only now being exposed to the compiler. What it means is that if your app compiles, you can be guaranteed it doesn't call any code that can't run because of missing APIs.

By default, you don't need to do anything: Swift will compare your actual usage against your minimum deployment target, and if it finds any unavailable API then you'll get an error – and that's when the work begins.

Returning to our example, if you have used UIStackView and your deployment target is set to 8.0, you'll get a compile error because stack views aren't available before 9.0. The solution is to tell Xcode you want certain code to execute only on iOS 9.0 or later, like this:

if #available(iOS 9, *) {
    // use UIStackView
} else {
    // show sad face emoji
}

In that code, #available is going to check whether we're on iOS 9 or later, or any other unknown platforms like watchOS – that's the * at the end, and it's required. And that's it: all the code you'll put in place of "// use UIStackView" effectively has elevated rights to use iOS 9.0-only technology, whether that's classes, methods or enums.

If code inside a method should only be run on certain iOS versions, you can also use #available with guard to produce code like this:

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

The power of #available is that the compiler can now check and enforce API usage on older operating systems, which previously was entirely human – it's a huge improvement, and one I know will catch on quickly.

Marking whole methods and classes with Swift 2's @available

As you just saw, you can use if #available to run version-specific code in small blocks. But what if whole methods are off limits? Or perhaps even whole classes? Swift 2 has these scenarios covered too, using the @available attribute.

@available works similarly to #available in that you specify the iOS release you want to target, and then Xcode handles the rest. For example:

@available(iOS 9, *)
func useStackView() {
    // use UIStackView
}

If your deployment target is iOS 8, you can't call that useStackView() method without some availability checking first. You can stack up these checks if you need to, for example:

@available(iOS 7, *)
func iOS7Work() {
    // do stuff

    if #available(iOS 8, *) {
        iOS8Work()
    }
}

@available(iOS 8, *)
func iOS8Work() {
    // do stuff
    if #available(iOS 9, *) {
        iOS9Work()
    }
}

@available(iOS 9, *)
func iOS9Work() {
    // do stuff
}

Each time, there's effectively a privilege elevation so you can use version-limited APIs.

For the ultimate in restrictions, you can also mark whole classes as being available only in a specific iOS release or later – just move the @available code wherever you want it.

There is one last neat feature about these availability checks in Swift: you no longer need to worry about "Required" and "Optional" frameworks – the compiler sorts all that out for you now. Hurray for developer productivity!

About the author

Paul Hudson is the creator of Hacking with Swift, the most comprehensive series of Swift books in the world. He's also the editor of Swift Developer News, the maintainer of the Swift Knowledge Base, and Mario Kart world champion. OK, so that last part isn't true. If you're curious you can learn more here.

Build for watchOS

Take your existing Swift skills to Apple's tiniest platform – check out Hacking with watchOS!

Click here to visit the Hacking with Swift store >>