8.
State & Data Flow — Part I
Written by Antonio Bello
In the previous chapters, you’ve used some of the most common UI components to build up your user interface. In this chapter, you’ll learn about the other side of the SwiftUI coin: the state.
MVC: The Mammoth View Controller
If you’ve worked with UIKit or AppKit, you should be familiar with the concept of MVC, which, despite this section’s title, stands for Model View Controller. It’s vulgarly known as Massive View Controller.
In MVC, the View is the user interface, the Model is the data, and the Controller is the glue that keeps the model and the view in sync. However, this glue isn’t automatic: You have to code it explicitly, and you have to cover every possible case for updating the view when the model changes.
Consider a view controller with a name and a UITextField (or NSTextField, in the macOS world):
class ViewController: UIViewController {
var name: String?
@IBOutlet var nameTextField: UITextField!
}
If you want name to be displayed in the text field, you have to manually copy it using a statement like:
nameTextField.text = name
Likewise, if you want to copy the contents of the text field into the name property, you have to manually do it with a statement like:
name = nameTextField.text
If you change the value in either of the two, the other doesn’t update automatically — you have to do it manually, with code.
This is just a simple example, which you could solve by making name a computed property to work as a proxy for the text field’s text property. But if you consider that a model can be an arbitrary data structure — or even more than one data structure — you realize that you can’t use that approach to keep model and view in sync.
Besides the model, the UI also depends on a state. Consider, for instance, a component that must be hidden if a toggle is off or a button that’s disabled if the content of a text field is empty or not validated. Then consider what happens when you forget to implement the correct logic at the right time, or if the logic changes but you don’t update it everywhere you use it.
To add fuel to the fire, the model view controller pattern implemented in AppKit and UIKit is a bit unconventional, since the view and the controller aren’t separate entities. Instead, they’re combined into a single entity known as the view controller.
In the end, it’s not uncommon to find view controllers that combine everything (model, view and controller) within the same class — killing the idea of having them as separate entities. That’s what caused the “Model” term in Model View Controller to be replaced with “Massive”, making it a brand new fat pattern known as Massive View Controller.
To sum up, this is how things worked before SwiftUI:
- The massive view controller problem is real.
- Keeping the model and UI in sync is a manual process.
- The state is not always in sync with the UI.
- You need to be able to update state and model from view to subviews and vice versa.
- All this is error-prone and open to bugs.
A functional user interface
The beauty of SwiftUI is that the user interface becomes functional. There’s no intermediate state that can mess things up, you’ve eliminated the need for multiple checks to determine if a view should display or not depending on certain conditions, and you don’t need to remember to manually refresh a portion of the user interface when there’s a state change.
You’re also freed from the burden of having to remember to avoid circular references in closures by using [weak self]. Since views are value types, captures happen using copies rather than references.
Being functional, rendering now always produces the same result given the same input, and changing the input automatically triggers an update. Connecting the right wires pushes data to the user interface, rather than the user interface having to pull data.
That doesn’t mean that you can now look for a new job and change careers. :] You still control how you implement the user interface and how to link data to the UI. It’s just that it’s much simpler now, and much less error-prone. Not to mention that it’s more elegant.
SwiftUI has many positive aspects — among them is that it’s primarily:
- Declarative: You don’t implement the user interface — you declare it.
- Functional: Given the same state, the rendered UI is always the same. In other words, the UI is a function of the state.
- Reactive: When the state changes, SwiftUI automatically updates the UI.
This chapter focuses mostly on the last aspect: Managing the relationship between state and UI, and how to propagate state from a view to its subviews.
Now, open the starter project and build and run. You can use either the starter project that comes with this chapter or the copy of the project you developed in the previous chapter.
Depending on whether you’re using a new installation or the version from the previous chapter, the app starts with either the registration view or the welcome view.
Proceed until you reach the challenge view, the first view in the picture below, which displays a Japanese word. Tap it and it will display a list of three options for your answer, as in the second view. If you tap the wrong option, it will display an error message. Otherwise, you’ll see an alert that you’ve chosen the correct answer, shown in the third view.
And that’s it — there’s no option to move forward and try another challenge. You need to fix that… and guess what, you’re going to use @State to do it.
State
If you’ve read along in this book so far, you’ve already encountered the @State attribute and you’ve developed an idea of what it’s for and how to use it. But it’s been an acquaintance — it’s time to let it become a friend.
Note: Now, you’ll try a few things to understand some of the concepts of this chapter. Bear with it, the reason will be clear at the end.
The first thing you’ll do is add a couple of counters to keep track of:
- The number of answered questions.
- The total number of challenges.
Create a new SwiftUI file in the Practice group and name it ScoreView.swift.
Next, add two properties to keep track of the number of answers and questions:
var numberOfAnswered = 0
var numberOfQuestions = 5
Then replace the auto-generated body with this:
var body: some View {
HStack {
Text("\(numberOfAnswered)/\(numberOfQuestions)")
.font(.caption)
.padding(4)
Spacer()
}
}
In Xcode, resume the preview. This is what you should see:
Now, embed this new view into ChallengeView by adding it after the button:
Button(action: {
self.showAnswers.toggle()
}) {
QuestionView(question: challengeTest.challenge.question)
.frame(height: 300)
}
// -Insert this-
ScoreView()
The preview looks like this:
Go back to ScoreView, which will display the current progress calculated as the number of challenges compared to the total number of challenges. For now, you only want to simulate progress. To do this, you’ll add a button that increments the number of challenges when you tap it.
To achieve that, replace the body implementation with:
var body: some View {
// 1
Button(action: {
// 2
self.numberOfAnswered += 1
}) {
// 3
HStack {
Text("\(numberOfAnswered)/\(numberOfQuestions)")
.font(.caption)
.padding(4)
Spacer()
}
}
}
Here you’ve:
- Added a button.
- Incremented
numberOfAnsweredin its action handler. - Embedded the previous content in the button’s body.
Don’t waste time trying to resume the preview, because it won’t work; it doesn’t even compile.
Why is that? Simply, you can’t mutate the state of the view by modifying its properties from inside the body.
Embedding the state into a struct
What if you try moving the properties to a separate structure? Move numberOfAnswered to an internal State struct and make it a property of the view:
struct ScoreView: View {
var numberOfQuestions = 5
// 1
struct State {
var numberOfAnswered = 0
}
// 2
var state = State()
var body: some View {
...
}
}
As mentioned, here you:
- Encapsulate
numberOfAnsweredinto a struct. - Add a new property, an instance of that struct.
Next, update the text inside the HStack to reflect the property’s new location:
Text("\(state.numberOfAnswered)/\(numberOfQuestions)")
and the button’s action:
self.state.numberOfAnswered += 1
But when you try to compile, you get the same error. Unfortunately, this didn’t work, either. That’s not surprising, because the struct is a value type and you’re still trying to mutate the internal state of the view.
Embedding the state into a class
By replacing a value type with a reference type, however, things change considerably. Try making State a class:
class State {
var numberOfAnswered = 0
}
Now, the error disappears and you can restore the preview. Try enabling live preview:
Now, you can tap the view and it reacts visually, but the displayed text doesn’t change. It’s anchored to 0/5.
Add a print statement to the button’s action handler, after you increment numberOfAnswered:
self.state.numberOfAnswered += 1
print("Answered: \(self.state.numberOfAnswered)")
Run the app and tap the text and you’ll see the console displays a new value at every tap. This means the state updates, but the view doesn’t.
Note: For this step, you’ll need to run in the simulator or use Debug Preview to see the output of the
This is actually the expected behavior if you’re using UIKit. If the model changes, it’s your responsibility to update the relevant part of the user interface.
Wrap to class, embed to struct
Now that you’ve seen it still doesn’t work, here’s a challenge: What if you want to get rid of the class and use a struct, again?
If you’re wondering why you’d want to do that, it will become clear as you read through this unconventional section of the chapter.
If you remember, the reason why the struct didn’t work earlier is because a struct is a value type. Modifying a value type requires mutability, but the body cannot mutate the struct that contains it.
To update without mutating, you simply have to wrap the mutating property into a reference type — in other words, a class. So add this before ScoreView:
class Box<T> {
var wrappedValue: T
init(initialValue value: T) { self.wrappedValue = value }
}
This lets you wrap a value type (actually any type) inside a class. Now make State a struct again and make its property an instance of Box<Int>:
struct State {
var numberOfAnswered = Box<Int>(initialValue: 0)
}
Now, this will work because you can mutate the value contained in Box without modifying numberOfAnswered. You’d mutate it only if you make it point to another instance, but instead, you’re just going to update the instance that the property points to.
Xcode is still showing you two compilation errors because you now have to use the wrappedValue property of Box rather than the Box instance itself. You’ll fix those next. In the Button’s action closure, update the increment statement as follows:
self.state.numberOfAnswered.wrappedValue += 1
Here, you increment the wrappedValue of numberOfAnswered. Similarly, update the print statement that comes next:
print("Answered: \(self.state.numberOfAnswered.wrappedValue)")
And, finally, the Text inside HStack:
Text("\(state.numberOfAnswered.wrappedValue)/\(numberOfQuestions)")
The real State
Now, you can officially ask: What’s the point of all this discussion?
It’s time to replace State with a similar struct from SwiftUI. Delete the Box you added earlier, then replace the State struct and the state property with the following property:
var _numberOfAnswered = State<Int>(initialValue: 0)
Note that you renamed the property by prefixing it with an underscore. The reason why will be revealed soon.
Fix the compilation errors by renaming numberOfAnswered as _numberOfAnswered, and removing state. This is how ScoreView should look now:
struct ScoreView: View {
var numberOfQuestions = 5
var _numberOfAnswered = State<Int>(initialValue: 0)
var body: some View {
Button(action: {
self._numberOfAnswered.wrappedValue += 1
print("Answered: \(self._numberOfAnswered)")
}) {
HStack {
Text("\(_numberOfAnswered.wrappedValue)/\(numberOfQuestions)")
.font(.caption)
.padding(4)
Spacer()
}
}
}
}
Build and run, then navigate to ChallengeView. If you tap the score view… magic! The counter updates every time you tap it.
So, what’s State? From the official SwiftUI documentation at apple.co/2WrfKzk:
A property wrapper type that can read and write a value managed by SwiftUI.
It’s like the Box inside the State struct you created earlier, but with the additional capability that the view that contains it can monitor it.
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. Use the state as the single source of truth for a given view.
Remember the term, single source of truth — you’ll meet it again soon.
When the wrapped value changes, SwiftUI re-renders the portion of the view that uses that value.
You’ve used state variables in earlier chapters. Now, you might wonder: What’s the relationship between State<Value>, the @State attribute and the $ operator?
Replace _numberOfAnswered with the following:
@State var numberOfAnswered = 0
This looks more familiar. You can now compile and run, and you’ll see that it works.
So what’s happening? The property declared with the @State attribute is a property wrapper, and the compiler generates an actual implementation of State<Int> type, prefixing the name by an underscore, _numberOfAnswered.
You can prove this by noting that you’re still referencing this property in body:
var body: some View {
Button(action: {
// 1
self._numberOfAnswered.wrappedValue += 1
// 2
print("Answered: \(self._numberOfAnswered.wrappedValue)")
}) {
HStack {
// 3
Text("\(_numberOfAnswered.wrappedValue)/\(numberOfQuestions)")
.font(.caption)
.padding(4)
Spacer()
}
}
}
There are three places where you use _numberOfAnswered:
- In the button’s action handler, to increment the counter of answers.
- Still in the button’s action handler, to print that counter.
- In the button’s embedded view, to display the number of answers against the total number of questions.
You can now replace each of them with the actual property that you’ve declared, numberOfAnswered. Just reference the property as-is. In the first two cases, replace it with:
self.numberOfAnswered += 1
print("Answered: \(self.numberOfAnswered)")
The compiler will translate these into the actual statements, which increase and read the wrappedValue of numberOfAnswered.
In the third case you do the same, replacing it with:
Text("\(numberOfAnswered)/\(numberOfQuestions)")
Compile and run the app. Once you navigate to ChallengeView, you won’t notice any visual or behavioral change — which means that the replacement worked.
Now, you need to roll back the changes you added for testing purposes. Remove the button and leave only its body, which consists of the HStack:
var body: some View {
HStack {
Text("\(numberOfAnswered)/\(numberOfQuestions)")
.font(.caption)
.padding(4)
Spacer()
}
}
What have you learned? If you have a property in your view, and you use that property in the view’s body, when the property value changes, the view is unaffected.
If you make the property a state property by applying the @State attribute, thanks to some magic that SwiftUI and the compiler do under the hood, the view reacts to property changes, refreshing the relevant portion of the view hierarchy that references that property.
Not everything is reactive
The score view defines two properties. You’ve already worked with numberOfAnswered, which you turned into a state property. What about the other one, numberOfQuestions? Why isn’t it a state property as well?
numberOfAnswered is dynamic, meaning that its value changes over the life of the view. In fact, it increments every time the user provides a correct answer. On the other hand, numberOfQuestions is not dynamic: It represents the total number of questions.
Since its value never changes, you don’t need to make it a state variable. Moreover, you don’t even need it to be a var — you can turn it into an immutable and initialize it via an initializer.
Replace its declaration with:
let numberOfQuestions: Int
Next, you need to update the preview view by providing the new parameter, as follows:
ScoreView(numberOfQuestions: 5)
Also apply the same change to the other place where you reference the view, in ChallengeView. The compiler will help you find the exact line.
Using binding for two-way reactions
A state variable is not only useful to trigger a UI update when its value changes; it also works the other way around.
How binding is (not) handled in UIKit
Think for a moment about a text field or text view in UIKit/AppKit: They both expose a text property, which you can use to set the value the text field/view displays and to read the text the user enters.
You can say that the UI component owns the data that it displays, or that the user enters, in its text property.
To get a notification when that value changes, you have to use either a delegate (text view) or subscribe to be notified when an editing changed event occurs (text field).
If you want to implement validation as the user enters text, you have to provide a method that is called every time the text changes. Then you have to manually update the UI. For example, you might enable or disable a button, or you could show a validation error.
Owning the reference, not the data
SwiftUI makes this process simpler. It uses a declarative approach and leverages the reactive nature of state properties to automatically update the user interface when the state property changes.
In SwiftUI, components don’t own the data — instead, they hold a reference to data that’s stored elsewhere. This enables SwiftUI to automatically update the user interface when the model changes. Since it knows which components reference the model, it can figure out which portion of the user interface to update when the model changes.
To achieve this, it uses binding, which is a sophisticated way to handle references.
In Chapter 6: Controls & User Input, you played with a TextField in the Kuchi app. You used a state property to hold the user’s name, which you later replaced with an environment object.
Now, you’ll rework that form again, this time focusing exclusively on the text field.
Open RegisterView.swift in the Welcome folder and comment out RegisterView, including its extension, and RegisterView_Previews, so that you can resume them later. Then, add this simplified code:
struct RegisterView: View {
@ObservedObject var keyboardHandler: KeyboardFollower
var name: String = ""
init(keyboardHandler: KeyboardFollower) {
self.keyboardHandler = keyboardHandler
}
var body: some View {
VStack {
TextField("Type your name...", text: name)
.bordered()
}
.padding(.bottom, keyboardHandler.keyboardHeight)
.edgesIgnoringSafeArea(
keyboardHandler.isVisible ? .bottom : [])
.padding()
.background(WelcomeBackgroundImage())
}
}
struct RegisterView_Previews: PreviewProvider {
static var previews: some View {
RegisterView(keyboardHandler: KeyboardFollower())
}
}
As soon as you do that, the compiler will complain about name not being a Binding<String>. So, what’s a binding? According to the official documentation:
A binding is a two-way connection between a property that stores data, and a view that displays and changes the data. A binding connects a property to a source of truth stored elsewhere, instead of storing data directly.
You heard about this earlier, when you read that the component doesn’t own the data, it holds a reference to the data that’s stored elsewhere. You’ll find out what source of truth means soon.
So, a state property contains a binding in projectedValue. To fix that here, change the type of the name property to State<String>:
var name: State<String> = State(initialValue: "")
Next, reference this property in the text field:
TextField("Type your name...", text: name.projectedValue)
Great, the compilation error disappears now. Enable the live preview and you can interact with the text field and input some text.
However, you don’t have any proof that it actually works, so you’ll add a Text component that displays the name after TextField:
Text(name.wrappedValue)
You don’t need the binding here because you only need to display the text without modifying it, so you use wrappedValue.
Resume live preview. Now, when you type any text, it replicates in the Text component below TextField:
This means that:
- When the user modifies the text,
TextFieldupdates the underlying data using the binding of thenamestate property. - When the data changes, the
namestate property triggers an update to all UI components that reference the data. - The
Textview receives the update request and updates its content by reprinting the value that thename’swrappedValuecontains.
Now that you’ve seen what a binding is and where it belongs, it’s better to get rid of the State property declaration and use the more fascinating counterpart defined by the corresponding attribute.
Replace the name property declaration once again, this time with:
@State var name: String = ""
You access a binding by using the $ operator, so you can simply replace name.projectedValue in the text field with $name:
TextField("Type your name...", text: $name)
To reference the value only, use the raw property name instead as if it were the value instead of a wrapper.
Text(name)
Since you haven’t made any functional changes, just used a different syntax, you won’t notice any difference when you test the view in the live preview.
The beauty of SwiftUI doesn’t end there. You can use a state property to declaratively change the behavior or aspect of the user interface.
If you wanted, for example, to hide the text if the name length is less than three characters, you can just surround it with an if statement:
if name.count >= 3 {
Text(name)
}
That expression re-evaluates automatically when name changes. Besides declaring it, you don’t have to do anything else — no subscription to a changed event, no logic to manually execute. You simply declare it, and SwiftUI will take care of it for you.
Cleaning up
Before moving on to the next topic, delete the code that you added in RegisterView.swift and restore the code you commented out at the beginning of this section.
Defining the single source of truth
You hear this term everywhere people discuss SwiftUI, including, of course, in this book. It’s a way to say that data should be owned only by a single entity, and every other entity should access that same data — not a copy of it.
It’s natural to find similarities between value and reference types. When you pass a value type, you actually pass a copy of it, so any change made to it is limited to the lifetime of the copy. It doesn’t affect the original. Likewise, changes made to the original data don’t propagate and don’t affect the copy.
This is how you do not want to handle UI state because when you change the state, you want that change to automatically apply to the user interface. If the data is a reference type, every time you move data around, you’re actually passing a reference to the data. Any change made to the data is visible from anywhere you access the data, regardless of who made the actual change.
In SwiftUI, you can think of the single source of truth as a reference type with attached behavior.
Earlier, you created ScoreView, where you ended up using a state property named numberOfAnswered. The number of answered questions isn’t determined nor changed in this view. Those actions take place in its parent view, ChallengeView, even if indirectly.
Consider ScoreView as an independent component of its own, unaware of why it’s used and without a state. Here, you use it merely to display the number of completed answers versus the total number of answers.
Open ChallengeView.swift and add a new state property right after showAnswers:
@State var numberOfAnswered = 0
You might think that all you need to do now is to pass this property to ScoreView. You actually do need to do that, but that’s not the only thing.
Test what happens if you only pass the property. In ScoreView.swift, remove the inline initialization of numberOfAnswered so that you’re forced to use an initializer:
@State
var numberOfAnswered: Int
At the same time, you need to update the preview to provide that new parameter. Replace its implementation with:
struct ScoreView_Previews: PreviewProvider {
// 1
@State static var numberOfAnswered: Int = 0
static var previews: some View {
// 2
ScoreView(
numberOfQuestions: 5,
numberOfAnswered: numberOfAnswered
)
}
}
Here you’re:
- Creating a new state property.
- Passing the new property to the
ScoreView’s initializer.
Now, you need to update ChallengeView to pass the additional parameter as well. Replace the line that uses ScoreView with:
ScoreView(
numberOfQuestions: 5,
numberOfAnswered: numberOfAnswered
)
So far, you don’t have a way to test if this works — and it shouldn’t. ChallengeView has a button and an action handler in it. Add this line to temporarily increment the property to the button’s action section:
self.numberOfAnswered += 1
Next, after ScoreView, add a text view showing the counter value:
Text("ChallengeView Counter: \(numberOfAnswered)")
Do the same in ScoreView.swift, right before the spacer:
Text("ScoreView Counter: \(numberOfAnswered)")
Now, go back to ChallengeView and ensure that the live preview is active. Tap the upper half of the screen repeatedly and you’ll notice that the ChallengeView counter increments, but not the ScoreView counter.
Why is that? A property marked as @State has, in reality, a State<Value> type, which is a value type. When you pass it to a method, it actually passes a copy.
Since a state property owns the data, you’re also passing a copy of the data, so the original and the copy have different lives.
In SwiftUI terms, by copying a @State property, you end up having multiple sources of truth — or, if it helps you better understand the concept, multiple sources of untruth. Every state property has its relative truth, which, at some point, won’t match the other sources’ truth.
Here’s an example to clarify the concept. If you want to share the phone number of your favorite pizza delivery with the rest of your family, you can write it on some sticker notes and give one to each family member.
Here, you’re creating multiple sources of truth: If the phone number changes, not everyone will know.
Instead of writing the phone number down, you can write on the note: “The phone number is hanging on the fridge.” Now, the note on the fridge is a single source of truth because everyone can update it and everyone is sure that the number is up to date.
Back to your code. Instead of passing the data, you have to pass a reference to it. The binding is the reference that you need. So go to ScoreView and update the state property to be a binding instead:
@Binding
var numberOfAnswered: Int
Both ChallengeView and the preview now report errors because ScoreView expects a binding in its second parameter. You’ll handle ChallengeView first.
Just as you did in the previous example with the text field, you obtain a binding by prefixing the property name with the $ operator. So replace the statement with:
ScoreView(
numberOfQuestions: 5,
numberOfAnswered: $numberOfAnswered
)
You need to repeat that same change in ScoreView’s preview. Once that’s done, try ChallengeView using live preview. When you tap now, both counters update:
So what have you achieved?
- You used a state variable to store the counter that tracks the number of answered questions.
- You passed a binding to
ScoreViewso it can access the same underlying data. - When you change the data, either through the state property or the binding property, you made that change available to everyone who references that data.
Cleaning up
In the section above, you added some temporary code that you can now remove.
In ChallengeView:
- Remove
numberOfAnswered, which you’ll rework soon:
@State var numberOfAnswered = 0
- Remove the increment statement in the button’s action handler:
self.numberOfAnswered += 1
- Use again the single parameter initializer for
ScoreView:
ScoreView(numberOfQuestions: 5)
- Remove the text control that prints the value of
numberOfAnswered:
Text("ChallengeView Counter: \(numberOfAnswered)")
In ScoreView:
- Make
numberOfAnswereda state property again, instead of a binding:
@State var numberOfAnswered: Int = 0
- Remove the other text control, which prints
numberOfAnswered:
Text("ScoreView Counter: \(numberOfAnswered)")
- In the preview struct, remove the second parameter passed to
ScoreView’s initializer:
ScoreView(numberOfQuestions: 5)
And that’s all. You used this temporary code to better understand the differences between @State and @Binding, and how they relate with the concept of single source of truth.
Key points
This was an intense and theoretical chapter. But in the end, the concepts are simple, once you understand how they work. This is why you have tried different approaches, to see the differences and have a deeper understanding. Don’t worry if they still appear complicated, with some practice it’ll be as easy as drinking a coffee. :]
To summarize what you’ve learned:
- You use
@Stateto create a property with data owned by the view where you declare it. When the property value changes, the UI that uses this property automatically re-renders. - With
@Binding, you create a property similar to a state property, but with the data stored and owned elsewhere: in a state property or an observable object of an ancestor view.
This is just half of what concerns state and data flow. In the next chapter you’ll look at making your own reference types observable, and how to use the environment.
Where to go from here?
You’ve only covered a few of the basics of state so far. In the next chapter you’ll dive deeper into state and data management in SwiftUI.
To get the most out of state with SwiftUI, there’s a wealth of material that continues to grow and evolve. These include:
- SwiftUI documentation: apple.co/2MlBqJJ
- State and data flow reference documentation: apple.co/2YzOdyp
To become a power SwiftUI developer, you’d do well to check out the Combine documentation: apple.co/2L7kWTy
Last, the SwiftUI Attributes Cheat Sheet: bit.ly/35Xt7eU is a helpful reference.