Chapters

Hide chapters

SwiftUI by Tutorials

Second Edition · iOS 13 · Swift 5.2 · Xcode 11

Before You Begin

Section 0: 3 chapters
Show chapters Hide chapters

Section II: Building Blocks of SwiftUI

Section 2: 6 chapters
Show chapters Hide chapters

9. State & Data Flow
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 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.

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()
  }
}

Button(action: {
  self.showAnswers = !self.showAnswers
}) {
  QuestionView(question: challengeTest.challenge.question)
    .frame(height: 300)
}
// -Insert this-
ScoreView()

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

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
}

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.wrappedValue)")
    }) {
      HStack {
        Text("\(_numberOfAnswered.wrappedValue)/\(numberOfQuestions)")
          .font(.caption)
          .padding(4)
        Spacer()
      }
    }
  }
}

@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 var name: String = ""
TextField("Type your name...", text: $name)
Text(name)

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 numberOfAnswers: Int = 0
  
  static var previews: some View {
    // 2
    ScoreView(
      numberOfQuestions: 5, 
      numberOfAnswered: numberOfAnswers
    )
  }
}
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
)

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)

The art of observation

So, you use a binding to pass data that a source of truth owns, and a state to additionally own the data itself. You have everything you need to create an awesome user interface, right? Wrong!

// 1
final class UserManager: ObservableObject {
  // 2
  @Published
  var profile: Profile = Profile()
  
  @Published
  var settings: Settings = Settings()
  
  // 3
  var isRegistered: Bool {
    return profile.name.isEmpty == false
  }
  ...
}
@Published var currentChallenge: ChallengeTest?
@ObservedObject var challengesViewModel = ChallengesViewModel()
QuestionView(question: 
  challengesViewModel.currentChallenge!.challenge.question)
ChoicesView(
  challengeTest: challengesViewModel.currentChallenge!)
Button(action: {
  self.showAnswers = !self.showAnswers
  // 1
  self.challengesViewModel.generateRandomChallenge()
}) {
  QuestionView(question:
    challengesViewModel.currentChallenge!.challenge.question
  )
    .frame(height: 300)
}

Sharing in the environment

You’ve already played with the app in this chapter, so you’ve probably noticed that the game lacks progress.

func saveCorrectAnswer(for challenge: Challenge) {
  correctAnswers.append(challenge)
}

func saveWrongAnswer(for challenge: Challenge) {
  wrongAnswers.append(challenge)
}
window.rootViewController = UIHostingController(
  rootView: StarterView()
    .environmentObject(userManager)
    // 1
    .environmentObject(ChallengesViewModel())
)
@ObservedObject var challengesViewModel = ChallengesViewModel()
@EnvironmentObject var challengesViewModel: ChallengesViewModel

@EnvironmentObject var challengesViewModel: ChallengesViewModel
@Binding var numberOfAnswered: Int
ScoreView(
  numberOfQuestions: 5, 
  numberOfAnswered: $numberOfAnswers
)
@Binding var numberOfAnswered: Int
ScoreView(
  numberOfQuestions: 5, 
  numberOfAnswered: $numberOfAnswered
)
@State static var numberOfAnswered: Int = 0
return ChallengeView(
  challengeTest: challengeTest, 
  numberOfAnswered: $numberOfAnswered
)
@Binding var numberOfAnswered: Int
ChallengeView(
  challengeTest: challengeTest!, 
  numberOfAnswered: $numberOfAnswered
)
@State static var numberOfAnswered: Int = 0
return PracticeView(
  challengeTest: .constant(challengeTest), 
  userName: .constant("Johnny Swift"), 
  numberOfAnswered: $numberOfAnswered
)
var numberOfAnswered: Int { return correctAnswers.count }
PracticeView(
  challengeTest: $challengesViewModel.currentChallenge,
  userName: $userManager.profile.name,
  // Add this
  numberOfAnswered: $challengesViewModel.numberOfAnswered
)
.constant(challengesViewModel.numberOfAnswered)

Understanding environment properties

SwiftUI provides another interesting and useful way to put the environment to work. Earlier in this chapter, you used it to inject environmental objects that can be pulled from any view down through the view hierarchy.

@Environment(\.verticalSizeClass) var verticalSizeClass
// 1
@ViewBuilder
var body: some View {
  // 2
  if verticalSizeClass == .compact {
    // 3
    VStack {
      // 4
      HStack {
        Button(action: {
          self.showAnswers = !self.showAnswers
        }) {
          QuestionView(
            question: challengeTest.challenge.question)
        }
        if showAnswers {
          Divider()
          ChoicesView(challengeTest: challengeTest)
        }
      }
      ScoreView(
        numberOfQuestions: 5, 
        numberOfAnswered: $numberOfAnswered
      )
    }
  } else {
    // 5
    VStack {
      Button(action: {
        self.showAnswers = !self.showAnswers
      }) {
        QuestionView(
          question: challengeTest.challenge.question)
          .frame(height: 300)
      }
      ScoreView(
        numberOfQuestions: 5, 
        numberOfAnswered: $numberOfAnswered
      )
      if showAnswers {
        Divider()
        ChoicesView(challengeTest: challengeTest)
          .frame(height: 300)
          .padding()
      }
    }
  }
}

PracticeView(
  challengeTest: $challengesViewModel.currentChallenge,
  userName: $userManager.profile.name,
  numberOfAnswered: 
    .constant(challengesViewModel.numberOfAnswered)
)
  // Add this modifier
  .environment(\.verticalSizeClass, .compact)

Creating custom environment properties

Environment properties are so useful and versatile that it would be great if you could create your own. Well, as it turns out, you can!

struct QuestionsPerSessionKey: EnvironmentKey {
  static var defaultValue: Int = 5
}
// 1
extension EnvironmentValues {
  // 2
  var questionsPerSession: Int {
    // 3
    get { self[QuestionsPerSessionKey.self] }
    set { self[QuestionsPerSessionKey.self] = newValue }
  }
}
private(set) var numberOfQuestions = 6
func generateRandomChallenge() {
  if correctAnswers.count < numberOfQuestions {
    currentChallenge = getRandomChallenge()
  } else {
    currentChallenge = nil
  }
}
PracticeView(
  challengeTest: $challengesViewModel.currentChallenge,
  userName: $userManager.profile.name,
  numberOfAnswered: 
    .constant(challengesViewModel.numberOfAnswered)
)
  // Add this
  .environment(
    \.questionsPerSession, 
    challengesViewModel.numberOfQuestions
  )
@Environment(\.questionsPerSession) var questionsPerSession
ScoreView(
  numberOfQuestions: questionsPerSession, 
  numberOfAnswered: $numberOfAnswered
)

Key points

This was an intense and long 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 the basics of state so far. To get the most out of state with SwiftUI, there’s a wealth of material that continues to grow and evolve. These include:

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