Articles, podcasts and news about Swift development, by John Sundell.

Availability checks

Published on 23 Jun 2021

Every year, Apple’s platforms keep progressing at a quite rapid pace, but very often, we also need our apps to support older versions of the various operating systems that they run on. So the challenge then becomes — how to adopt new system APIs and features without sacrificing our overall backward compatibility? That’s where availability checks come in.

An availability check essentially enables us to mark a function, property, type, or extension as only being available on certain platforms and system versions. That in turn lets us conditionally use new system APIs and features while still enabling the rest of our code to keep running on older system versions.

For example, the following function uses WidgetKit to reload all of an app’s home screen widgets, and since that API is only available on iOS 14, we’re using the available attribute to still make it possible to run our app on iOS 13 (or earlier):

import WidgetKit

@available(iOS 14, *)
func reloadWidgets() {
    WidgetCenter.shared.reloadAllTimelines()
}

The star, or asterisk, within our available attribute is used to represent “all future versions”. So the above function will automatically be available on iOS 15, and any future iOS versions that Apple will release in the future.

However, while our app itself can now keep running on iOS 13 and earlier, we can only call our reloadWidgets function when our code is actually running on iOS 14 (or above). So adding the above available attribute is just the first part of the puzzle. Next, we’ll need to perform our actual availability check, which can be done using the #available keyword at the point where we’re calling our function:

if #available(iOS 14, *) {
    reloadWidgets()
}

Since the above is just a standard if statement, we can also attach other clauses to it, such as an else block. In this case, we’re falling back to reloading the data that’s being displayed in our app’s legacy “Today view” widget (which is the widget system that was offered before WidgetKit was introduced in iOS 14):

if #available(iOS 14, *) {
    reloadWidgets()
} else {
    updateTodayViewData()
}

In this case, though, we might not want to leave it up to each call site to perform the above availability check, since — if we’re reloading our widget data in multiple places throughout our app — that will lead to a fair amount of code duplication.

So, another option would be to instead inline our availability check within our actual reloadWidgets function, rather than marking it as being iOS 14-only:

func reloadWidgets() {
    if #available(iOS 14, *) {
        WidgetCenter.shared.reloadAllTimelines()
    } else {
        updateTodayViewWidgetData()
    }
}

That way, we can now call our function wherever we’d like, and it’ll automatically call the correct code path depending on what system version that our app is running on — neat!

It’s also possible to add multiple platforms when performing an availability check. For example, if the app we’re working on supports both iOS and macOS, then we could check for either iOS 14 or macOS 11 in one go by doing this:

func reloadWidgets() {
    if #available(iOS 14, macOS 11, *) {
        WidgetCenter.shared.reloadAllTimelines()
    } else {
        updateTodayViewWidgetData()
    }
}

The available attribute can also be applied to an entire type or extension, which is particularly useful when extending certain system types with custom functionality that requires one of the latest operating system versions. For example, here we’re extending UIButton to add a few methods that use iOS 15’s new button configuration API to apply certain styles:

@available(iOS 15, *)
extension UIButton {
    func applyRoundedStyling() {
        var config = UIButton.Configuration.filled()
        config.background.backgroundColor = .systemBlue
        config.cornerStyle = .medium
        config.contentInsets = NSDirectionalEdgeInsets(
            top: 10, leading: 8, bottom: 10, trailing: 8
        )
        configuration = config
    }

    func applyPrimaryStyling() {
        ...
    }

    func applySecondaryStyling() {
        ...
    }
}

To learn more about the above API, check out “Taking UIKit’s new button configuration API for a spin” over on WWDC by Sundell & Friends.

Finally, let’s also take a look at how availability checks can be used within a SwiftUI view. For example, let’s say that we’re working on an ItemListView that we’d like to adopt the InsetGrouped list style when that’s available (on iOS 14 and later), while falling back to GroupedListStyle when that’s not the case.

Using the techniques that we’ve explored so far, an initial idea on how to do that might be to add an #available check inline within our view’s body — like this:

struct ItemList: View {
    var items: [Item]

    var body: some View {
        let list = List(items) { item in
            ...
        }

        if #available(iOS 14, *) {
    list.listStyle(InsetGroupedListStyle())
} else {
    list.listStyle(GroupedListStyle())
}
    }
}

However, while the above works, it’s arguably not a very elegant solution, and sort of makes it harder to get an overview of what our actual view hierarchy looks like.

So, let’s use the following pattern instead — which involves creating a ViewBuilder-marked function that lets us apply our default list style just like how standard SwiftUI modifiers are applied:

extension View {
    @ViewBuilder
    func defaultListStyle() -> some View {
        if #available(iOS 14, *) {
            listStyle(InsetGroupedListStyle())
        } else {
            listStyle(GroupedListStyle())
        }
    }
}

To learn more about the above technique, check out “Adding SwiftUI’s ViewBuilder attribute to functions”.

With the above in place, we can now make our ItemList view (and any other view that wants to use the above list styling logic) much simpler:

struct ItemList: View {
    var items: [Item]

    var body: some View {
        List(items) { item in
            ...
        }
        .defaultListStyle()
    }
}

So that’s a few ways to use Swift’s available attribute and its associated availability checks, both in the context of SwiftUI, and within any other Swift code. It’s worth pointing out, though, that all of these checks are performed at runtime, based on the system version of the device that our app is running on. To instead perform compile-time checks, we can use some of the techniques that were covered in “Using compiler directives in Swift”.

While Swift’s availability features are incredibly convenient, and can enable us to start adopting some of the latest system features without having to increase our app’s deployment target, I really recommend using them sparingly. Scattering a ton of availability checks all over a code base can often lead to code that’s quite hard to read and maintain, so whenever possible, isolating such checks within convenience APIs (like our reloadWidgets and defaultListStyle functions) is definitely my preferred approach.

Got questions, comments, or feedback? Feel free to send me an email, or contact me on Twitter.

Thanks for reading!

Support Swift by Sundell by checking out this sponsor:

RevenueCat

RevenueCat: Easily build and manage iOS and Android in-app purchases. With just a few lines of code RevenueCat provides IAP infrastructure, customer analytics, data integrations, and gives you time back from dealing with edge cases and updates across all platforms.