Upcoming and OnDemand Webinars View full list

A Brief Tour of Swift UI

Amit Bijlani

The best user interfaces are interactive and responsive to the user. UIKit is a capable framework that lets you build apps that users expect, but it can be tedious at time, not to mention having the flavor of an Objective-C toolkit.

SwiftUI takes advantage of Swift’s modern language features, giving us a modern framework that hits modern programming buzzwords.

It’s Declarative. Describe your UI in broad strokes using simple code.

It’s Compositional. You take existing views and connect them together and arrange them on the screen as you want.

It’s Automatic. Complex operations, like animations, can be done as a one-liner.

It’s Consistent. SwiftUI can be run on all of Apple’s platforms. It will take care to configure your UI (such as padding distance between on-screen controls, or dynamic type)
so that it feels at home on whatever platform it’s running, whether it’s a Mac or the AppleTV.

Views and View Modifiers

Views are the heart of SwiftUI. According to Apple:

Views are defined declaratively as a function of their input

A View in SwiftUI is a struct that conforms to the View protocol. They are unrelated to UIViews (which are classes). A SwiftUI View is a value type, composed of the data that explains what you want to display.

Here’s a simple view:

struct SimpleView : View {
    var body: some View {
        Text("I seem to be a verb")
    }
}

Here SimpleView conforms to the View protocol. To appease the protocol, you need to provide a body, which is an opaque type. By using some new syntax, some View, we’re declaring that body is some kind of View, without having to specify a
concrete type. Check out Swift Evolution Proposal 244 for more about opaque types.

The body is some View that describes what should be on the screen. In this case it’s a simple text label with a default configuration. Your body can be composed of multiple Views, such as a stack that arranges images horizontally.

Because body is a View, and it’s an opaque type, nobody really cares if a given View ends up being a single on-screen entity or a bunch of views composed in a stack or a list. This lets us compose complex hierarchies easily.

Chain Reaction

SwiftUI Views are value types, so we really don’t have stored properties to modify their look and feel. We need to declare our look and feel intentions to customize our views.

SwiftUI has ViewModifiers, such as .foreground to change the foreground color or .font to change a view’s font, that applies an attribute to a View. These modifiers also return the view they’re modifying, so you can chain them:

    Text("SwiftUI is Great!")
        .foreground(.white)
        .padding()
        .background(Color.red)

This declarative syntax is very easy to read, letting you accomplish a lot with little code.

Preview of `SimpleView` with view modifiers (alt text)

Stacks

You can build hierarchies of views. In UIKit, we use UIStackView to make vertical and horizontal stacks of UIViews. In SwiftUI, there’s an analogous HStack and VStack views that let you pile views horizontally or vertically.

This view positions one label above another.

VStack {
    Text("Swift UI is Great!")
        .foreground(.white)
        .padding()
        .background(Color.red)
    Text("I seem to be a verb")
}      

There’s also ZStack that lets you stack views on top of each other, like putting a label on top of an image.

You’re welcome to nest multiple stacks to compose complex view hierarchies. The stack views will automatically provide platform-appropriate defaults and if you don’t like the defaults, you can provide your own alignment and spacing.

Lists

SwiftUI lists look like tableviews in UIKit. In SwiftUI, the framework does the heavy lifting, freeing you from data source boilerplate.

Setting up a list with hard-coded views is really easy. Setting it up is exactly like setting up a stack:

struct ListView : View {
    var body: some View {
        List { 
            Text("Look")
            Text("A")
            Text("TableView")
        }
    }
}

The views inside the closure of a list serve as static cells.

The List also has an optional data parameter (that conforms to the Identifiable protocol) where you can provide some data to drive the creation of the cells inside the table.

Here’s a struct of a contact:

struct Contact: Identifiable {
    let id = UUID()
    let name: String
}

Having a unique identifier in your identifiable data helps with list selection and cell manipulation.

And here’s a View that hardcodes a list of contacts:


struct ListView : View {
    let contacts = [Contact(name: "Arya"), Contact(name: "Bran"), Contact(name: "Jon"), Contact(name: "Sansa")]
    
    var body: some View {
        List(contacts) { contact in
            Text(contact.name)
        }
    }
}

This is the magic bit:

        List(contacts) { contact in
            Text(contact.name)
        }

This is telling the list: “for every Identifiable in this list of contacts, make a new Text view whose text contents are the contact’s name. You can add new folks to contacts, and the list will contain more rows.

State and Binding

Static hard-coded lists are great for initial prototyping of your ideas, but it’d be nice for the list to be dynamic, such as populating it with data from the internet. Or insert and
delete new rows, and to rearrange things.

WWDC 2019 session 226 Data Flow Through SwiftUI tells us:

In SwiftUI data is a first class citizen

To manage state within our app, SwiftUI provides stateful binding to data and controls using property wrappers. Check out Swift Evolution Proposal
258
for more about property wrappers.

Here we’re using the the @State property wrapper to bind an isEnabled property to a toggle control:

struct EnabledTogglingView : View {
    
    @State private var isEnabled: Bool = false

    var body: some View {
        Toggle(isOn: $isEnabled) {
            Text("Enabled")
        }
    }
}

Another quote from session 226, Data Flow Through SwiftUI:

SwiftUI manages the storage of any property you declare as a state. When the state value changes, the view invalidates its appearance and recomputes the body.

The state becomes the source of truth for a given view.

@State is great for prototyping, and for properties that are intrinsic to a view (such as this enabled state). To actually separate your data layer from your view layer, you need BindableObject.

So say we’re setting up a preference panel, or some kind of configuration panel with a sequence of toggle buttons. First, a model object for each individual setting:

class Settings: BindableObject {
    var didChange = PassthroughSubject<Bool, Never>()
    var isEnabled = false {
        didSet {
            didChange.send(self.isEnabled)
        }
    }
}

Notice that Settings is a reference type (a class), which conforms to the BindableObject protocol. The PassthroughSubject doesn’t maintain any state – just
passes through provided values. When a subscriber is connected and requests data, it will not receive any values until a .send() call is invoked.

struct BindableToggleView : View {
    @ObjectBinding var settings: Settings
    var body: some View {
        Toggle(isOn: $settings.isEnabled) {
            Text("Enabled")
        }
    }
}

take another look at this line of code:

    @ObjectBinding var settings: Settings

That’s weird. It’s not an optional then what’s it initialized to? Well, because settings is declared with the @ObjectBinding property wrapper, this tells the framework that it will be receiving data from some bindable object outside of the view. This object binding doesn’t need to be initialized.

You inject a BindableObject into the View that should use it. Here’s how our Settings gets hooked up to this BindableToggleView:

    BindableToggleView(settings: Settings())

Your view does not care where the data lives or how it is populated disk, cache, network, osmosis. The view simply knows that it will be binding to some object when available.

The @ObjectBinding property wrapper creates a two-way binding. Whenever a mutation occurs the view automatically re-renders.

@ObjectBinding requires dependency injection to provide the data object. That can be inconvenient.

To make this object available anywhere within your app without dependency injection you can use the environment. New with SwiftUI you have an environment object that contains all kinds of information like screen size, orientation, locale, date format, etc. If you stick an object into the environment then that object can be accessed anywhere within your app. Think of it as a dictionary that is globally scoped within your app.

So how do you use it? The @EnvironmentObject property can help you with this.

struct BindableToggleView : View {
    @EnvironmentObject var settings: Settings
    var body: some View {
        Toggle(isOn: $settings.isEnabled) {
            Text("Enabled")
        }
    }
}

In the above example, everything remains the same as seen previously, except you replace @ObjectBinding with @EnvironmentObject.

To inject an object into the environment you call the .environmentObject method.

    ContentView().environmentObject(Settings())

An instance of Settings is now injected into the environment and is available to you anywhere within your app. When the object mutates then every view bound to it will receive the update and re-render its view. You no longer have to maintain a mental model of all the dependency-injections to keep your user interface in sync.

Animation

Animations in SwiftUI work like any view modifier. Basic animations can be achieved using the withAnimation method.

Here’s a View with two text labels:

struct ContentView: View {
    @State var scale: CGFloat = 1
    var body: some View {
        VStack {
            Text("This text grows and shrinks")
                .scaleEffect(scale)
            Text("Scale it")
                .tapAction {
                    self.scale = self.scale == 1.0 ? 5.0 : 1.0
        				}
        }
    }
}

Tapping the second text label alters the size of the first text label, alternating its size between normal and five-times larger. The scaling happens instantly without any animation.

It’s a one-liner to make it animate smoothly.

Text("Scale it")
    .tapAction {
        withAnimation {
            self.scale = self.scale == 1.0 ? 5.0 : 1.0
        }
    }	

withAnimation can take an argument that lets you customize the animation. You can call it with a .basic animation, or you can have more fun with .spring animation that gives you control to make a beautiful spring effect.

Conclusion

SwiftUI is a delight to use. The focus is on creating an app because SwiftUI gets out of your way, taking care of a lot of annoying details. Design rich, interactive user interfaces by composing reusable views. Ensure data is coming into the system and not worry about keeping the view and model layers in sync. Finally, easily add animations to make views come alive.

Not Happy with Your Current App, or Digital Product?

Submit your event

Let's Discuss Your Project

Let's Discuss Your Project