Data Flow Through SwiftUI

This article may be considered a condensed version of the (highly-recommended) WWDC19 talk, “Data Flow Through SwiftUI”, along with a few thoughts and insights of my own. It represents my own, current understanding of data flow in SwiftUI, derived from experimentation over the past two months.

Let’s start with an important quote from the aforementioned talk:

Data is a first class citizen in SwiftUI.

This is the crux of what makes SwiftUI not only so beautifully elegant, but also extremely ergonomic.

The Two Key Principles #

SwiftUI is designed to be declarative and functional. As such, data flow in SwiftUI revolves around two key principles:

This all seems quite abstract, and so we shall revisit a popular definition for declarative programming:

“You know, imperative programming is like* how you do something, and declarative programming is more like what you do, or something.”

What does this mean, when contextualized in actual Cocoa/Cocoa Touch development? If you’re ever worked with UIKit (or AppKit), you know that the traditional method of handling events or receiving data input is by the way of delegates. These delegates inform an internal or external state, managed through view controllers, which also bear the responsibility of configuring and reconfiguring child views.

As your view controller grows in complexity, the number of manual synchronizations and invalidations that have to be made increase sharply. A switch may show/hide a view, a slider may set the value of a label, a picker may drive an image displayed above it, etc. It gets even worse when you have complex, compound predicate based logic, such as a condition satisfied by a certain permutation of toggles in a form, updates from a real-time database.

SwiftUI tackles this problem through some powerful design choices:

Consider the following example:

struct ContentView: View {
    let text: String

    var body: some View {
        Text(text)
    }
}

Our ContentView has no means of mutating Text, only a means of initializing it. In turn, the view initializing ContentView shall have no means of mutating ContentView, but only that of passing down a single variable text.

This is all well and good, but real world applications require views capable of mutating themselves on screen. The example above simply demonstrates the display of static data. How are SwiftUI views to mutate themselves, if they are value types and the body is a nonmutating getter? Let us progress to dynamic view properties.

Dynamic View Properties #

If you’ve ever seen or written SwiftUI code, you must have noticed by now that the framework offers various property wrappers to represent mutable data stored in your application’s view hierarchy:

These are what are known as dynamic view properties (or just dynamic properties, as of Xcode 11 beta 5. All these structures share a conformance to SwiftUI’s DynamicProperty protocol (formerly DynamicViewProperty).

Here is the official documentation on DynamicProperty:

A stored variable that updates an external property of a view.

The view gives values to these properties prior to recomputing the view’s body.

And here is the public interface from the SwiftUI module:

/// Represents a stored variable in a `View` type that is dynamically
/// updated from some external property of the view. These variables
/// will be given valid values immediately before `body()` is called.
@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
public protocol DynamicProperty {

    /// Called immediately before the view's body() function is
    /// executed, after updating the values of any dynamic properties
    /// stored in `self`.
    mutating func update()
}

As you can see, the current documentation is frustratingly sparse; even almost self-contradictory. One says that it is a stored variable that updates an external property of a view, and the other says that it represents a stored variable that is updated from some external property of the view. So what in the heck is a dynamic view property?

A dynamic property is a special type of property that stores its value indirectly in some interally managed memory, separate from the view’s memory. As you recall earlier in the article, we noted that SwiftUI views are simply models, and not actual the actual render tree. Dynamic view properties are what connect and describe the bridge between a View and its render tree at runtime.

The storage of a dynamic view property is tracked by the framework’s runtime, and whenever it changes, the Views dependent on it are recomputed and re-rendered as necessary. There is a lot of clever static and dynamic optimization that goes into making the re-rendering as efficient and conservative as possible.

The property wrapper feature recently accepted and implemented in Swift 5.1 is what makes them truly special. These structures have complex underlying mechanisms to interface with the SwiftUI runtime, but are accessible and usable in your View structure’s code as if they are normal Swift properties!

If you’re familiar with Combineor RxSwift, a useful analogy might be to think of a dynamic view property as Publisher or Observable, that hides its internal implementation and reactive nature.

Below is a demonstration of the most commonly used dynamic view property type, @State:

struct ContentView {
    @State var counter: Int = 0

    var body: some View {
        VStack {
            Button(action: increment) {
                Text("Increment")
            }
            Text("Count: \(counter)")
        }
    }

    func increment() {
        counter += 1
    }
}

If you run the code above and set a breakpoint anywhere inside body, you will see that it is triggered every time you press the “Increment” button. This is because everytime you tap it, your the increment() function is called, mutating the counter variable and triggering a re-render of ContentView.

This re-render is necessary as because Text("Count: \(counter)") is dependent on the dynamic view property counter. The framework is intelligent enough to re-render only the counting text, and not the Button.

This also brings us back to the first principle of data flow in SwiftUI:

Reading data creates dependencies.

If you were to add another property (dynamic or not) the dependency would have to be declared in the view structure itself. It is not possible to defer the dependencies of the view, as it is possible with UIView or NSView in AppKit, which expose a whole sleuth of mutable properties, which may be referenced and reassigned from any part of your code with a reference to the view.

This also highlights the functional aspect of SwiftUI, in that SwiftUI views are a function over their dependencies (as implemented in the body getter).

While now we understand how data is passed down a SwiftUI view hierarchy, we still don’t know how to pass it up. This is where Binding comes in.

What is a @Binding? #

Let us examine the documentation:

A manager for a value that provides a way to mutate it.

And now let us look at the public interface from the SwiftUI module:

/// A value and a means to mutate it.
@propertyWrapper public struct Binding<Value> {
    /// Initializes from functions to read and write the value.
    public init(get: @escaping () -> Value, set: @escaping (Value) -> Void)

    /// The value referenced by the binding. Assignments to the value
    /// will be immediately visible on reading (assuming the binding
    /// represents a mutable location), but the view changes they cause
    /// may be processed asynchronously to the assignment.
    public var value: Value { get nonmutating set }
}

A way to mutate what? #

A Binding, in its simplest form, is nothing more than a getter and a setter paired together. Note the following initializer:

    /// Initializes from functions to read and write the value.
    public init(get: @escaping () -> Value, set: @escaping (Value) -> Void)

… and that it simply accepts two parameters. A get closure, and a set closure. get returns a Value, set accepts one. But where are we getting it from, and where are we setting it to?

Dynamic view properties, up the view hierarchy.

Let us take the example of a simple text field:

struct ContentView {
    @State var someText: String = ""

    var body: some View {
        TextField("Placeholder", text: $someText)
    }
}

You’ve noticed that we’ve declared a dynamic property, text, and are passing it down to TextField. You’ve also noticed that we are passing it as $someText, instead of someText. What’s up with the dollar sign?

The dollar sign is an operator that converts any given SwiftUI dynamic property to a Binding.

If we take a look at the initializer of TextField:

    public init(_ titleKey: LocalizedStringKey, text: Binding<String>, onEditingChanged: @escaping (Bool) -> Void = { _ in }, onCommit: @escaping () -> Void = {})

We see that it accepts a Binding of a String, expressed simply as Binding<String>. This is what allows the TextField to pass user-input up the view hierarchy, through text.value (where text: Binding<String>).

When you, the user, type into the TextField, the state variable someText is updated through the binding that you passed down to the text field. This triggers a re-render of your view, also causing TextField to refresh the text in display. It is highly important to note that the update and re-render are sequential and not simultaneous (which would be disastrously confusing).

When someText is updated, the SwiftUI runtime queues a request to re-render the portion of the view hierarchy dependent on someText. When your screen refreshes (at a speed of 60Hz for iPhones and 120Hz for iPad Pros), it recomputes and re-renders the affected portion (in our case, just the TextField).

It is also important to note here that dynamic view properties cannot be mutated inside the view’s body property, as it will trigger a runtime exception. The functional aspect of SwiftUI has resurfaced again here: SwiftUI provides us with a runtime-enforced invariant that body is guaranteed to be side-effect free (as far as dynamic view properties are concerned).

And finally, the second principle:

Every piece of data has a source of truth.

What is “a source of truth”? It is nothing but the source of data that your views depend on.

The concept of source of truth is extremely useful in the context of complex views, where multiple views and/or subviews depend on a single source of data. In traditional UIKit or AppKit, if you had multiple views dependent on the same “truth”, an on/off switch for example, you would need to synchronize the changes from one of the views to apply across the rest, as each view would maintain its own internal state of being on/off.

SwiftUI makes it nigh impossible for synchronization errors, as it allows you to utilize powerful structures such as Binding to pass a reference to a source of truth down the view hierarchy. This is also where the concept of ancestry and composition become vital. In SwiftUI, data changes flow top-down (that is, top to bottom), and re-renders occur bottom-up. This is not to say that a view down in the hierarchy cannot trigger a change in data at the top of the hierarchy, it’s just that the change will flow from top to bottom.

The second principle of data flow in SwiftUI is perhaps best represented by the table below:

Source of Truth Derived Value
Read only Constant Property
Read-write @State, @ObservedObject etc. @Binding

A Binding may be derived from any dynamic property, and passed down the view hierarchy as a means to reference data without duplicating the source of it.

 
73
Kudos
 
73
Kudos

Now read this

3D Touch

3D Touch is a woefully underrated feature in today’s modern iPhones. There are now (what I consider to be) credible reports suggesting that Apple intends to drop this feature in their 2019 iPhone models. This is highly unfortunate if... Continue →