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

The Swift 5.1 features that power SwiftUI’s API

Published on 09 Jun 2019
Discover page available: SwiftUI

The introduction of SwiftUI, Apple’s declarative new UI framework, was clearly one of the most impactful announcements made during this year’s WWDC conference. As a brand new way of building UIs for all of Apple’s platforms, using a coding style that’s vastly different from the way UIKit works, SwiftUI isn’t just a new framework — it’s a paradigm shift.

As a new, modern take on UI development for Apple’s platforms, SwiftUI also pushes the Swift language itself to new limits — by making heavy use of a set of key new syntax features, that are being introduced as part of Swift 5.1, in order to provide a very DSL-like API.

This week, let’s take a look at those features, and how learning more about them — and how they work — can let us gain a more thorough understanding of SwiftUI’s API and how it was built. While this won’t be an introduction to SwiftUI per se (see this WWDC by Sundell article for that), it’ll hopefully serve as a bit of a peek under the hood of Apple’s exciting new UI framework.

Opaque return types

One feature that sort of stands out when looking at most of the SwiftUI sample code that has been shared so far, is the new some keyword. Introduced through Swift Evolution proposal SE-0244 — this new keyword enables functions, subscripts, and computed properties to declare opaque return types.

What that means, is that even generic protocols (a protocol that either has associated types or references to Self) can now be used as return types — just as if they were concrete types — like non-generic protocols, classes or structs. When using SwiftUI, some is very often used when declaring a view’s body — like this:

struct ContentView: View {
    var body: some View {
        Text("Hello, world!")
    }
}

The View protocol is used to define SwiftUI view descriptions, and in order to enable each view to decide what type to use for its body property — that property requirement is defined using an associated Body type. Prior to Swift 5.1, attempting to reference such a protocol (without the some keyword), would lead to a compiler error saying that View can only be used as a generic constraint. To work around that, we’d then have to specify a concrete type conforming to View instead — for example like this:

struct ContentView: View {
    var body: Text {
        Text("Hello, world!")
    }
}

Another option would be to use type erasure, and require each View implementation to be boxed into a type-erased AnyView instance before being returned:

struct ContentView: View {
    var body: AnyView {
        AnyView(Text("Hello, world!"))
    }
}

But now, by using the some keyword, we’re free to return any value conforming to the specified protocol (like View, in the case of SwiftUI) — and any other code that calls into our implementation can still use all of the properties and methods from that protocol when working with our return value — without requiring us to use wrapper types (like AnyView), or to break our code’s encapsulation by exposing concrete types as part our API.

A nice side-effect of this new keyword is the additional flexibility it gives us, since we no longer have to modify our public API in order to change what exact return type that’s used under the hood. That’s especially important for a view framework, like SwiftUI — since a key part of writing maintainable view code is to constantly refactor and split up the various parts of a UI into separate, smaller building blocks.

Omitted return keywords

Perhaps not as important as the new some keyword, but a nice improvement in terms of consistency, and a big factor when it comes to how lightweight SwiftUI’s API feels — is the fact that the return keyword can now be omitted for single-expression functions.

Swift Evolution proposal SE-0255 made functions and computed properties act the same way as closures — in that if there’s only one expression within them, using the return keyword is no longer required — making the following two implementations act the exact same way:

struct ContentView: View {
    var body: some View {
        // Using an explicit return keyword
        return Text("Hello, world!")
    }
}

struct ContentView: View {
    var body: some View {
        // Omitting the return keyword, just like within a closure  
        Text("Hello, world!")
    }
}

While the above might take a while to get used to, it does serve as a way to make single expressions within functions and computed properties a bit more clean — for example in things like factory methods:

func makeProfileViewController(for user: User) -> UIViewController {
    ProfileViewController(
        logicController: ProfileLogicController(
            user: user,
            networking: networking
        )
    )
}

However, the compiler will continue to accept code that uses the return keyword, as well as code that omits it — so each developer is free to choose whichever style that they prefer.

Function builders

With both the some keyword and omitted returns, we now have an answer as to how SwiftUI’s top-level View declaration API is made possible — but so far we still don’t have an explanation for how multiple views can be grouped together, without any sort of keyword or additional syntax — like this:

struct HeaderView: View {
    let image: UIImage
    let title: String
    let subtitle: String

    var body: some View {
        VStack {
            // Here three seperate expressions are evaluated,
            // without any return keyword or additional syntax.
            Image(uiImage: image)
            Text(title)
            Text(subtitle)
        }
    }
}

SwiftUI’s grouping views — such as VStack, HStack, and Group — enable multiple views to be grouped together by simply creating new instances within a closure. Since these are closures with multiple expressions, it means that we’re not dealing with an omitted return keyword here — so how exactly is that kind of syntax made possible? 🤔

The answer, is function builders — which is such a new feature that, at the time of writing, it doesn’t even have a formal proposal yet. An initial draft for a proposal can be found here, but interestingly this feature has already been implemented in the Swift compiler.

Function builders enables the builder pattern to be implemented using closures — providing a very DSL-like development experience, by passing the expressions defined within such a closure to a dedicated builder type.

Without the new function builder feature, we’d have to manually create a builder in order construct instances of containers like VStack, giving us code that’d look something like this:

struct HeaderView: View {
    let image: UIImage
    let title: String
    let subtitle: String

    var body: some View {
        var builder = VStackBuilder()
        builder.add(Image(uiImage: image))
        builder.add(Text(title))
        builder.add(Text(subtitle))
        return builder.build()
    }
}

The above definitely isn’t bad, but it does make the API feel much less lightweight.

So how do function builders work? It all starts with the new @functionBuilder attribute (or @_functionBuilder, as it’s currently implemented as, since this feature is still considered a private implementation detail) — which marks a given type as being a builder.

Similar to how the new custom string literal API works, a builder then declares different overloads of the buildBlock method in order to provide support for closures containing various kinds of expressions. For example, here is a “paraphrased” implementation of what SwiftUI’s own ViewBuilder type might look like:

@functionBuilder
struct ViewBuilder {
    // Build a value from an empty closure, resulting in an
    // empty view in this case:
    static func buildBlock() -> EmptyView {
        return EmptyView()
    }

    // Build a single view from a closure that contains a single
    // view expression:
    static func buildBlock<V: View>(_ view: V) -> some View {
        return view
    }

    // Build a combining TupleView from a closure that contains
    // two view expressions:
    static func buildBlock<A: View, B: View>(
        _ viewA: A,
        _ viewB: B
    ) -> some View {
        return TupleView((viewA, viewB))
    }

    // And so on, and so forth.
    ...
}

Note how each closure variant needs to be explicitly handled by the builder above, since we might be dealing with different kinds of View implementations defined within the same closure. If that wasn’t the case, ViewBuilder could’ve instead used a variadic parameter to handle closures containing multiple expressions — like this:

@functionBuilder
struct ViewBuilder {
    static func buildBlock(_ views: View...) -> CombinedView {
        return CombinedView(views: views)
    }
}

The above code is just an example, it won’t even compile, since View has an associated type.

With the above ViewBuilder type in place, the compiler will now synthesize an attribute that matches its name (@ViewBuilder) — which we can then use to mark all the closure parameters that we wish to use our new builder with, like this:

struct VStack<Content: View>: View {
    init(@ViewBuilder builder: () -> Content) {
        // A function builder closure can be called just like
        // any other, and the resulting expression can then be
        // used to, for instance, construct a container view.
        let content = builder()
        ...
    }
}

Using the above two pieces — a function builder type, and closures marked as users of that type, building really lightweight DSLs now becomes possible — which is exactly what Apple has done to achieve Swift UI’s view building syntax:

VStack {
    Image(uiImage: image)
    Text(title)
    Text(subtitle)
}

As a feature, function builders definitely lean towards the advanced end of the spectrum — but the beauty of them is that developers using DSL-based frameworks, like SwiftUI, should ideally never even notice them — since the whole builder part simply becomes an implementation detail of the DSL itself.

Property wrappers

The final core new Swift 5.1 feature that SwiftUI’s API is powered by is property wrappers (formally known as “property delegates”). Introduced as part of proposal SE-0258, this new feature enables property values to be automatically wrapped using specific types. It works quite similarly to function builders in that regard — in that implementing property wrappers requires both a custom attribute, and a type that handles that attribute.

SwiftUI uses property wrappers to make it much easier to define various kinds of bindable properties. For example, in order to define a property for managing a part of a view’s state, the @State attribute can be used to automatically wrap such a property’s value in an instance of the bindable State type:

struct SettingsView: View {
    @State var saveHistory: Bool
    @State var enableAutofill: Bool

    var body: some View {
        return VStack {
            // We can now access bindable versions of our state
            // properties by prefixing their name with '$':
            Toggle(isOn: $saveHistory) {
                Text("Save browsing history")
            }
            Toggle(isOn: $enableAutofill) {
                Text("Autofill my information")
            }
        }
    }
}

Since all that property wrappers really do is to act as a sort of interface between a property value and an underlying storage type, the above code sample is essentially equivalent to this next implementation — which does the exact same thing, but by using the underlying State struct directly instead:

struct SettingsView: View {
    var saveHistory: State<Bool>
    var enableAutofill: State<Bool>

    var body: some View {
        return VStack {
            Toggle(isOn: saveHistory.binding) {
                Text("Save browsing history")
            }
            Toggle(isOn: enableAutofill.binding) {
                Text("Autofill my information")
            }
        }
    }
}

Again, the design of property wrappers is very similar to that of function builders, in that delegation attributes (such as @State) gets mapped to their corresponding underlying type using the @propertyWrapper attribute. For example, here’s a simplified version of what the public API of SwiftUI’s State struct looks like:

@propertyWrapper
struct State<Value> {
    init(initialValue: Value) {
        ...
    }

    var wrappedValue: Value {
        get { ... }
        set { ... }  
    }
}

What’s particularly exciting about property wrappers, is that they open up opportunities for a lot of different kinds of boilerplate to be eliminated, even in our own code as well. For example, we could define a @Transformed attribute that lets us automatically apply transformations to various values, or a @Database attribute for automatically syncing property values to an underlying database — there’s a ton of different possibilities here.

Conclusion

SwiftUI doesn’t only bring a new way of building UIs for Apple’s platforms, and brand new Swift coding styles — it has also most likely been the driving factor behind many of the new features that are being introduced in Swift 5.1 — which makes the language more powerful for everyone, even those who are not yet adopting SwiftUI itself.

While this article has barely scratched the surface of what these new features can do, I hope that it has added some additional clarity as to how SwiftUI’s APIs work — and that it has served as a bit of an introduction to some of the new capabilities that are coming to Swift later this year.

We’ll dive much deeper into these features, and some of the new techniques that they enable, in several upcoming articles. Normally, I only write articles about techniques that I’ve used myself when building real projects — which I believe makes the examples more real, and the patterns I write about easier to adopt — so it’ll take a little while before you’ll see articles on this site about using SwiftUI, Combine, and some of Apple’s other new frameworks.

Until then, feel free to give me any feedback you might have, or ask any questions you’d like to ask — by contacting me, or by finding me on Twitter @johnsundell.

Thanks for reading! 🚀