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

Which of the SwiftUI APIs introduced in iOS 15 are backward compatible?

Published on 10 Oct 2021
Discover page available: SwiftUI

As a general rule of thumb, all of the APIs and system features that Apple introduces in a given version of iOS can only be used when targeting that particular version, or any subsequent ones.

However, this year there are a few very interesting exceptions to that rule, in that certain SwiftUI APIs that were introduced in iOS 15 can actually be used and deployed all the way back to iOS 13. Let’s take a look at what some of those APIs are and how come it’s possible for them to be fully backward compatible.

Static styling

A very neat syntax improvement this year is that we can now refer to instances of SwiftUI’s various styling protocols using static APIs, rather than having to explicitly instantiate those values. For example, here’s a before-and-after comparison between using the old and new syntax when applying a ListStyle to a SwiftUI List:

// Before:

List(items) { item in
    ...
}
.listStyle(GroupedListStyle())

// After:

List(items) { item in
    ...
}
.listStyle(.grouped)

The above is using a new Swift language feature which enables us to define static protocol APIs that can be used to create instances that conform to that protocol (see this article for more information). But how come Apple is able to make SwiftUI’s implementation of that new language feature backward compatible with earlier OS versions?

Always emit into client

This is where a relatively new attribute called _alwaysEmitIntoClient comes in, which enables framework authors to tell the Swift compiler to emit the implementation of a given function, computed property, or subscript into the binary that is consuming that framework. This new attribute is part of Swift’s library evolution effort, which aims to provide a set of tools for smoothly evolving framework and library APIs over time.

While this is still an underscored attribute (which, in the world of Apple API design basically means “technically public, but shouldn’t be considered public”), it’s what makes it possible for us to use the above new styling APIs on older operating system versions — since by annotating those APIs with @_alwaysEmitIntoClient, Apple is telling the compiler to embed the implementation of those APIs within our app binary, rather than linking to implementations provided by the OS.

Bindable list elements

Another new List (and ForEach) capability that’s fully backward compatible is how we can now create lists that contain Binding references to the collection elements that are being rendered.

Previously, this required quite a lot of code to implement reliably, but now, we can create bindable list elements using the same $-prefixing syntax that we use when referencing other bindings — like this:

struct TodoList: View {
    @Binding var items: [Item]

    var body: some View {
        List($items) { $item in
            TextField("Item", text: $item.text)
        }
    }
}

Neat! Just like the styling APIs that we took a look at earlier, the above new feature is backward compatible thanks to a combination of new Swift compiler capabilities and @_alwaysEmitIntoClient annotations.

Replacing single-view arguments with view builder closures

Finally, let’s take a look at how we can now use the full power of SwiftUI’s ViewBuilder API when creating certain built-in views that previously required us to pass some of its View-based arguments as separate values. For example, here’s how we can now use a ViewBuilder closure when creating the destination for a NavigationLink:

// Before:

NavigationLink(
    article.title,
    destination: ArticleView(article: article)
)

// After:

NavigationLink(article.title) {
    ArticleView(article: article)
}

Not only can the above new closure-based API make our code easier to read (especially when we wish to apply modifiers directly to the view that’s being passed as an argument), but it also enables us to use any ViewBuilder-compatible expression when computing our arguments. For example, here’s how we could now easily return separate views for a navigation link’s destination depending on what data that it’ll represent:

NavigationLink(article.title) {
    if article.isFeatured {
        FeaturedArticleView(article: article)
    } else {
        ArticleView(article: article)
    }
}

Other views that have been upgraded with similar, fully backward compatible APIs include Section (which now accepts ViewBuilder-powered closures for its header and footer arguments), and Picker (which now lets us use a closure when constructing its label).

Conclusion

I’m really happy to see Apple start exploring the idea of making certain new APIs backward compatible with earlier OS versions, as that lets us third party developers take advantage of those new system features without requiring us to increase our minimum deployment target, or use availability checks.

Granted, these backward compatible APIs don’t include any brand new components, or any new user-facing functionality — but just like how most new Swift language features enable us to evolve our code without breaking backward compatibility, these new APIs should let us improve our SwiftUI-based code in somewhat significant ways — especially when working with bindable list elements.

Let me know what you think, and feel free to ask me any questions or send me feedback, by reaching out via email.

Thanks for reading!