Chapters

Hide chapters

SwiftUI by Tutorials

Third Edition · iOS 14 · Swift 5.3 · Xcode 12

Before You Begin

Section 0: 3 chapters
Show chapters Hide chapters

8. State & Data Flow — Part I
Written by Antonio Bello

Heads up... You're reading this book for free, with parts of this chapter shown beyond this point as scrambled text.

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.

Initial Challenge View
Atesuan Zfojwujzi Peel

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.

var numberOfAnswered = 0
var numberOfQuestions = 5
var body: some View {
  HStack {
    Text("\(numberOfAnswered)/\(numberOfQuestions)")
      .font(.caption)
      .padding(4)
    Spacer()
  }
}
Score View
Djome Naep

Button(action: {
  self.showAnswers.toggle()
}) {
  QuestionView(question: challengeTest.challenge.question)
    .frame(height: 300)
}
// -Insert this-
ScoreView()
Challenge Score View
Sjadhuxmu Dlazi Duos

var body: some View {
  // 1
  Button(action: {
    // 2
    self.numberOfAnswered += 1
  }) {
    // 3
    HStack {
      Text("\(numberOfAnswered)/\(numberOfQuestions)")
        .font(.caption)
        .padding(4)
      Spacer()
    }
  }
}
Score View error
Bkazi Paet ukgoj

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 {
    ...
  }
}
Text("\(state.numberOfAnswered)/\(numberOfQuestions)")
self.state.numberOfAnswered += 1

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
}
Score view live preview
Gfufu toey pesa wsivaet

self.state.numberOfAnswered += 1
print("Answered: \(self.state.numberOfAnswered)")

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?

class Box<T> {
  var wrappedValue: T
  init(initialValue value: T) { self.wrappedValue = value }
}
struct State {
  var numberOfAnswered = Box<Int>(initialValue: 0)
}
self.state.numberOfAnswered.wrappedValue += 1
print("Answered: \(self.state.numberOfAnswered.wrappedValue)")
Text("\(state.numberOfAnswered.wrappedValue)/\(numberOfQuestions)")

The real State

Now, you can officially ask: What’s the point of all this discussion?

var _numberOfAnswered = State<Int>(initialValue: 0)
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()
      }
    }
  }
}
Score view updating
Qpalu riub oprowegv

@State var numberOfAnswered = 0
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()
    }
  }
}
self.numberOfAnswered += 1
print("Answered: \(self.numberOfAnswered)")
Text("\(numberOfAnswered)/\(numberOfQuestions)")
var body: some View {
  HStack {
    Text("\(numberOfAnswered)/\(numberOfQuestions)")
      .font(.caption)
      .padding(4)
    Spacer()
  }
}

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?

let numberOfQuestions: Int
ScoreView(numberOfQuestions: 5)

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.

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.

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())
  }
}
var name: State<String> = State(initialValue: "")
TextField("Type your name...", text: name.projectedValue)
Text(name.wrappedValue)
State and binding
Zkori elw noxruhr

@State var name: String = ""
TextField("Type your name...", text: $name)
Text(name)
State and binding
Dmaxe ozw fiswebv

if name.count >= 3 {
  Text(name)
}

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.

@State var numberOfAnswered = 0
@State
var numberOfAnswered: Int
struct ScoreView_Previews: PreviewProvider {
  // 1
  @State static var numberOfAnswered: Int = 0

  static var previews: some View {
    // 2
    ScoreView(
      numberOfQuestions: 5,
      numberOfAnswered: numberOfAnswered
    )
  }
}
ScoreView(
  numberOfQuestions: 5,
  numberOfAnswered: numberOfAnswered
)
self.numberOfAnswered += 1
Text("ChallengeView Counter: \(numberOfAnswered)")
Text("ScoreView Counter: \(numberOfAnswered)")

@Binding
var numberOfAnswered: Int
ScoreView(
  numberOfQuestions: 5,
  numberOfAnswered: $numberOfAnswered
)
State and Binding 2
Mhabo uxh Zozdidf 4

Cleaning up

In the section above, you added some temporary code that you can now remove.

@State var numberOfAnswered = 0
self.numberOfAnswered += 1
ScoreView(numberOfQuestions: 5)
Text("ChallengeView Counter: \(numberOfAnswered)")
@State var numberOfAnswered: Int = 0
Text("ScoreView Counter: \(numberOfAnswered)")
ScoreView(numberOfQuestions: 5)

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. :]

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.

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2024 Kodeco Inc.

You're reading for free, with parts of this chapter shown as scrambled text. Unlock this book, and our entire catalogue of books and videos, with a Kodeco Personal Plan.

Unlock now