Weekly Swift articles, podcasts and tips by John Sundell.

SwiftUI

Published on 12 Sep 2019

Apple’s new declarative UI framework, SwiftUI, offers a brand new way to construct views across all Apple platforms. Rather than explicitly programming instructions as to how each view should be rendered, with SwiftUI, we’re instead declaring what we want the end result of our UI to be — letting us leverage the framework to do most of the actual rendering work for us.

When using SwiftUI, each view is essentially a description of a single piece of our UI — both in terms of how it’ll look and behave in terms of layout, as well as how it should handle user interactions and other events. As an example, here’s a ProductView that shows information about a product within a shopping app — by conforming to SwiftUI’s View protocol, and then returning its subviews through its computed body property:

struct ProductView: View {
    var product: Product

    var body: some View {
        // We'll render our product view as a vertical stack
        // containing the product's image, name, and price:
        VStack {
            Image(uiImage: product.image)
            Text(product.name)
            Text(product.price)
        }
    }
}

The syntax used above may at first seem a bit unfamiliar. That’s because SwiftUI makes heavy use of a few key new features introduced in Swift 5.1. To learn more about those, check out “The Swift 5.1 features that power SwiftUI’s API”.

What’s interesting about the above implementation, especially if compared to UI code written using Apple’s older frameworks (like UIKit and AppKit), is that it doesn’t contain any layout instructions, apart from its VStack. That’s really one of the core advantages of SwiftUI — since we’re no longer dealing with view objects that need to be explicitly positioned and sized, but rather with view descriptions, it lets the system make many different kinds of layout decisions on our behalf.

Besides VStack, SwiftUI offers two other main stack types that can be used to leverage its automatic layout system — HStack and ZStack. For example, here’s how we could wrap our name and price labels in an HStack to render them side-by-side, rather than stacking them vertically:

struct ProductView: View {
    var product: Product

    var body: some View {
        VStack {
            Image(uiImage: product.image)
        
            HStack {
                Text(product.name)
                Text(product.price)
            }
        }
    }
}

Finally, the ZStack type can be used to stack views in terms of depth — which comes very much in handy when adding some form of background to a view. For example, here’s how we could give our ProductView a gradient background, by placing a LinearGradient on top of our previous VStack, and then wrapping both within a ZStack:

struct ProductView: View {
    var product: Product

    var body: some View {
        ZStack {
            LinearGradient(
                gradient: Gradient(colors: [.red, .blue]),
                startPoint: .topLeading,
                endPoint: .bottomTrailing
            ).edgesIgnoringSafeArea(.all)
            
            VStack {
                Image(uiImage: product.image)
            
                HStack {
                    Text(product.name)
                    Text(product.price)
                }
            }
        }
    }
}

The reason we call edgesIgnoringSafeArea above is to make our gradient extend beyond the safe area insets of our view, which will make it fill the entire screen on all devices.

While it’s really convenient to be able to quickly add all of our views in one place, doing so tends to get a bit messy once we reach a certain number of views or levels of indentation. Thankfully, since SwiftUI is all about building views as separate component-like building blocks, extracting parts of a view into its own type is often quite trivial.

Let’s do exactly that, and split our ProductView up into two separate subviews instead — one for our gradient, and one for displaying our product info:

struct ProductGradientView: View {
    var body: some View {
        LinearGradient(
            gradient: Gradient(colors: [.red, .blue]),
            startPoint: .topLeading,
            endPoint: .bottomTrailing
        ).edgesIgnoringSafeArea(.all)
    }
}

struct ProductInfoView: View {
    var product: Product

    var body: some View {
        VStack {
            Image(uiImage: product.image)
        
            HStack {
                Text(product.name)
                Text(product.price)
            }
        }
    }
}

With those two new views in place, we can now simply make ProductView compose them into our final UI, making our code a lot easier to read:

struct ProductView: View {
    var product: Product

    var body: some View {
        ZStack {
            ProductGradientView()
            ProductInfoView(product: product)
        }
    }
}

Another way that SwiftUI enables us to create custom views is by applying modifiers to existing ones. For example, here’s how we could use modifiers to turn our product label bold, as well as limit its text to two lines — and also change the text color of our price label:

HStack {
    Text(product.name).bold().lineLimit(2)
    Text(product.price).foregroundColor(.white)
}

Besides defining what our views will look like in terms of visuals, another important aspect of any kind of UI development is keeping track of state — both when it comes to handling user input, and to ensure that our UI gets properly updated as our state changes. While we’ll explore all of SwiftUI’s various state handling APIs in much more detail in upcoming articles, let’s start by taking a look at how the new @State property wrapper can be used to manage local state within a view.

Let’s say that we wanted to add a stepper to our product view that lets the user pick the number of items to order. To do that, we both need to display the current quantity of items, and also modify it whenever the user interacted with our stepper — in order words, we need to establish a two-way binding between our UI and the state (the quantity of items) that it depends on.

Since our state will only be read and modified within our view, let’s define it using a @State property, which will enable us to establish such a two-way binding between our new property and the stepper that’ll render and modify it:

struct ProductView: View {
    var product: Product

    // Using the '@State' property wrapper we can define pieces
    // of state that are local (private) to the current view:
    @State private var quantity = 1

    var body: some View {
        ZStack {
            ProductGradientView()

            VStack {
                ProductInfoView(product: product)

                // Here we can both directly interpolate our value
                // into the stepper's label, and also bind it to
                // the stepper itself, enabling it to be modified:
                Stepper("Quantity: \(quantity)",
                    value: $quantity,
                    in: 1...99
                ).padding()
            }
        }
    }
}

The reason we prefix our quantity property with $ when injecting it as our stepper’s value above is because we want to pass a reference to the wrapped @State property, rather than its current Int value.

It’s quite clear that SwiftUI isn’t just a new UI framework — it’s a substantial paradigm shift in terms of how apps are built for Apple’s platforms. It’ll definitely take a while to get used to, and it’ll also take some time for common patterns and best practices to emerge, given that many of the patterns we might have used in the past won’t be directly compatible with SwiftUI’s very declarative structure. But it’s a really exciting time to be an Apple platforms developer, isn’t it?

Thanks for reading! 🚀