A SwiftUI Introduction #2: Data Flow in SwiftUI

Perceval Archimbaud
Shadow Tech Blog
Published in
8 min readMay 17, 2023

The previous article of this series has shown us how to create simple UIs. But you will probably be feeling frustrated when realising that all we built remains despairingly static. Don’t lose hope! We start manipulating data now! You will soon be able to create components that will be a bit more reusable.

If you didn’t read the previous part of the series, you can find it here:
A SwiftUI Introduction #1: What’s SwiftUI?.

Properties

Without losing any time, let’s dive into properties. Properties are data passed to a component initialiser. They will allow us to customise the behaviour or the look of it.

Characteristics of properties

  • Properties are passed as arguments of the component initialiser.
  • Properties should be immutable.
    If they are not, mutations will not trigger a re-render of our view anyway.
  • Properties can be optional.
  • Properties can have a default value.
  • Properties can be propagated as properties of child components.

Now that you’ve those characteristics in mind, you are possibly remembering some components that we used in the last article and that seem to have properties?

Text("My text")

That’s right, Text is taking a property that tells it what text to display.

If the immutable part raised some questions in your head, such as “If properties are immutable, can I not pass a variable to Text?”, be reassured: you can. The whole component will just be recreated and re-rendered, and the SwiftUI engine will be able to do that in an efficient way.

Using a property gives the indication to the component’s user (maybe yourself in an other part of your code) that this value will not be updated by the component itself, only read.

Using properties: examples

First, let’s have a look at a simple property that will be displayed by our component.

// Init as: MyAwesomeView(myProperty: 12)

struct MyAwesomeView: View {
let myProperty: Int

var body: some View {
Text("My property: \(myProperty)")
}
}

My property is immutable, and passed down to a Text component after being wrapped into a String.

But we could provide a default value, allowing our user to omit it when using our component:

// Init as: MyAwesomeView()
// Or as: MyAwesomeView(myProperty: 24)

struct MyAwesomeView: View {
let myProperty: Int

init(myProperty: Int = 12) {
self.myProperty = myProperty
}

var body: some View {
Text("My property: \(myProperty)")
}
}

As you can see here, we need to define an initialiser for our View structure.

Here, we cannot specify the default value directly after the property declaration because it’s a constant.

The Swift compiler automatically infers initialiser for structure that don’t declare one. This prevents us to be forced to declare an initialiser when we only have declared a few variables or constants that are not initialised inline.

In our case, we want to use a constant (since it’s a property and it should not be mutated), but also provide a default value (to allow the user to not provide us one). So we need to explicitly define our initialiser, to include our default value in it.

Let’s take a look at a final example, with an optional property:

// Init as: MyAwesomeView()
// Or as: MyAwesomeView(myProperty: 12)

struct MyAwesomeView: View {
let myProperty: Int?

init(myProperty: Int? = nil) {
self.myProperty = myProperty
}

var body: some View {
if let myProperty {
Text("My property: \(myProperty)")
} else {
Text("No property 🤷‍♂️")
}
}
}

As you can see, even for an optional falling back to nil, we need to define an initialiser. That’s because, here, nil is our default value.

While defining a variable with an optional type in Swift can be made without an explicit default value (nil will be used in this case), it is not possible to call a method without an argument that as no explicit default value.

State

A tree graph representing the SwiftUI data flow.

When working on UIs, we want to make things dynamic. We are not building up a collection of images, we are building interactive interfaces.

We will not present here the multitude of components provided by Apple to handle interaction (Button, TextField, etc.). This kind of component is usually called a control. We will, instead, be interested by the way data can be managed by those components.

As I said before, properties mutations do not trigger re-render of our view, that is why we uses State to store data that can be mutated, while triggering a re-render as expected.

Characteristics of states

  • States initial value can be passed as arguments of the component initialiser.
  • States are mutable.
    And any mutation will trigger a re-render of our view.
  • States can be optional.
  • States can have a default value.
  • States should always represent a single source of truth.
  • States’ current value can be propagated as properties of child components.
  • States can be propagated and remains mutable, while preserving the single source of truth, by using Bindings.

We introduced the notion of Single source of truth in the list above. You absolutely need to fully understand the principle itself and the importance of respecting it. When working with mutable data, you always want to be sure that the data is the same everywhere it is used. If it is not, you will end-up in situation where the UI is lacking of consistency. That will be confusing for the user, and might introduce bugs into you app.

Imagine that you built an app where you provide a way to your users to create notes. The first view is a list of all your notes. At the top of the screen, you are displaying the count of created notes. The user touches the button to create a note, and your app is indeed creating a new note for him to fill, that will inserted at the bottom of the list. If you are using multiple source of data to display the number of created note and the list itself, you might end up with a counter showing “4” notes created, even if the list is showing 5 notes. And this can happen quicker that you think. Always ensure that you are in fact using a single source of truth.

For the following example, I will use one of the controls provided by Apple: the Button. The initialiser of the Button we will use takes a label and a callback (a function to be called by the component at a certain point). The label lets us define a text to display, and the callback is executed when we click on the Button.

// Init as: MyAwesomeView()
// Or as: MyAwesomeView(myState: 1)
// WARNING: NEVER USE THE LAST ONE, EXCEPT WITH A RAW VALUE
struct MyAwesomeView: View {
@State var myState = 0

var body: some View {
VStack {
Text("My myState: \(myState)")
Button("Add") {
myState += 1
}
}
}
}

As you can see here, we used the decorator @State to indicate to SwiftUI that we are using a state here. This way, it will be aware that the view will need to be re-rendered each time we mutate this variable. The user of the component can provide an initial value for this state, and if not, a default value will be used.

I have added a warning that you will understand it later in this article.

We do not need to specify an initialiser here since we use a variable. So defining a default value inline with the declaration will not prevent the variable to be re-assigned by the compiler-generated initialiser.

Bindings

Now, let’s see how we can pass our state to an other component and still keep both the mutability and the single source of truth.

Here are our options to propagate a state’s current value:

  • Propagate a state’s current value as a property.
    If you were passing a state as a property of an other component, it would be immutable. The single source of truth is maintained, but not the mutability.
  • Propagate a state’s current value as the initial value of a component state.
    DO NOT DO THAT, EVER, IN ANY CASE. PLEASE.
    By passing the state’s current value as the initial value of an other component state, you are creating a second source of truth, and now, there
    isn’t any source of truth anymore. Since it’s an initial value, the state value may change in the child component and our component will never know about it.
  • Propagate a state by using a Binding.
    This is the solution to maintain mutability and a single source of truth.

As you can see in the list above, there really are two ways to propagate a state. The second one is not an option (it’s a trap option in reality), I will write it one more time: do not use it.

If you only care about reading the value (maybe to display it), pass it as a property to child’s components. But if you want to allow the child component to mutate your state, pass a Binding to it.

A Binding, as its name suggest, is a link to an original state. It can be used like any standard state, with the particularity to always remain synchronised with the state it has been created from. If the child component mutate a binding value, it will update the original state at the same time.

Here is an example of a binding usage.

struct MyAwesomeView: View {
@State var myState: Int // Single source of truth

var body: some View {
VStack {
Text("My myState: \(myState)")
MyAwesomeChildView(myBinding: $myState)
}
}
}

struct MyAwesomeChildView: View {
@Binding var myBinding: Int

var body: some View {
Button("Add") {
myBinding += 1 // Updating the binding updates the original state
}
}
}

You can see that creating a Binding from a State only costs you a single key stroke. Most of the work must be done by the child component (not so much as you can see). Every component that wants to offer a way to update a value (a control mainly), has to declare a Binding instead of a State. The type of the variable passed to the initialiser will be Binding<BaseType>, meaning that the component user will not be able to pass anything else, and by doing so, break the rule of the single source of truth.

When the Binding is updated by MyAwesomeChildView, the State of MyAwesomeView is also updated, and as a consequence of that, the Text component will be re-rendered with the correct text.

We have reviewed the basics data flow in SwiftUI and you are now able to make your app a bit more interactive. I encourage you to play with SwiftUI and test all the things we have learned in this article, and the previous one. The Swift Playground, available on macOS and iPadOS, is your ally here.

I hope you will have as much fun as I do when using SwiftUI, and that you will be hungry for more!

Want to learn more?

Apple provides some interactive articles to help you find your way with SwiftUI: Apple’s SwiftUI Tutorials.

I also recommend the great YouTube channel Swiftful Thinking, that deep dives into SwiftUI, with hundreds of well made videos.

All the articles of the series:

--

--