Home iOS & Swift Books SwiftUI by Tutorials

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

You can unlock the rest of this book, and our entire catalogue of books and videos, with a raywenderlich.com Professional subscription.

In the previous chapter you learned how to use @State and @Binding, and the power that they brought to you in a transparent and easy to use way.

In this chapter you’ll learn about other tools that allows you to make your own types efficiently reactive, or reactively efficient. :]

Before diving into it, while you’re still dry, a word about the project. You can use the starter project that comes with this chapter, but since it is an exact copy of the final project from the previous chapter, you can also reuse what you’ve worked on, if you prefer — no change needed.

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!

Consider that you have a model made up of several properties and you want to use it as a state variable. If you implement the model as a value type, like a struct, it works properly, but it’s not efficient.

In fact, if you have an instance of a struct and you modify one of its properties, you actually replace the entire instance by a copy of it with the updated property. In other words, the entire instance mutates.

When you change a property of your model, you’d expect that only the UI that references that property should refresh. In reality, you’ve modified the whole struct instance, so the update will trigger a refresh in all places that reference the struct.

Depending on the use case, this could have a low impact or it could affect performance considerably.

That doesn’t mean you shouldn’t use structs, just that you should avoid putting unrelated properties in the same model. This prevents cases where updating a property value triggers a UI update that doesn’t use that property.

If you implement your model as a reference type instead — that is, a class — it won’t actually work. If a property is a reference type, it mutates only if you assign a new reference. Any change made to the actual instance doesn’t change the property itself, which means it won’t trigger any UI refresh.

Making an Object Obsevable

The good news is that you have four new types that come to your rescue. Given the considerations expressed above, your custom model should:

// 1
final class UserManager: ObservableObject {
  // 2
  @Published
  var profile: Profile = Profile()

  @Published
  var settings: Settings = Settings()

  @Published
  var isRegistered: Bool
  ...
}

Observing an Object

As mentioned earlier, there’s another observable class in the project, in Practice/ChallengesViewModel.swift. Its purpose is to define and serve challenges, which consist of a Japanese word, its English translation and a list of potential answers. Only one answer is correct.

@Published var currentChallenge: ChallengeTest?
@ObservedObject var challengesViewModel = ChallengesViewModel()
QuestionView(question:
  challengesViewModel.currentChallenge!.challenge.question)
ChoicesView(
  challengeTest: challengesViewModel.currentChallenge!)
Button(action: {
  self.showAnswers.toggle()
  // 1
  self.challengesViewModel.generateRandomChallenge()
}) {
  QuestionView(question:
    challengesViewModel.currentChallenge!.challenge.question
  )
    .frame(height: 300)
}
Current selection
Peftegr logaplueh

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

Environment and Objects

SwiftUI provides a way to achieve that. It’s not a dependency injection, just a way to put an object into something like a bag and retrieve it whenever you need it. The bag is called the environment and the object, an environment object.

var body: some Scene {
  WindowGroup {
    StarterView()
      .environmentObject(userManager)
      // 1
      .environmentObject(ChallengesViewModel())
  }
}
@ObservedObject var challengesViewModel = ChallengesViewModel()
@EnvironmentObject var challengesViewModel: ChallengesViewModel
Challenge sequence
Mzuwrevre biziixqi

Congrats view
Qinwqatp feud

Environment and duplicates (to avoid)

So earlier you left the app with two issues that you’re going to get rid of now.

@EnvironmentObject var challengesViewModel: ChallengesViewModel
@Binding var numberOfAnswered: Int
ScoreView(
  numberOfQuestions: 5,
  numberOfAnswered: $numberOfAnswered
)
@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
)
numberOfAnswered: .constant(challengesViewModel.numberOfAnswered)
Score Working
Vsoyo Dupledj

Object Ownership

In the previous sections you’ve seen that there are three different ways a view can obtain an observable object:

print("New KeyboardFollower instance created")
Keyboard follower
Hilxeidq xuqkexof

Keyboard other followers
Vampoold omhic xoxtadodq

@ObservedObject var keyboardHandler: KeyboardFollower

init(keyboardHandler: KeyboardFollower) {
  self.keyboardHandler = keyboardHandler
}
#if os(iOS)
RegisterView(keyboardHandler: KeyboardFollower())
#endif
let keyboardFollower = KeyboardFollower()
#if os(iOS)
RegisterView(keyboardHandler: keyboardFollower)
#endif
Keyboard follower single instance
Lojxoijt mejtoyar mosmfu aqmjidgo

let keyboardFollower = KeyboardFollower()
#if os(iOS)
RegisterView()
#endif
@ObservedObject var keyboardHandler: KeyboardFollower
@StateObject var keyboardHandler = KeyboardFollower()
init(keyboardHandler: KeyboardFollower) {
  self.keyboardHandler = keyboardHandler
}
State object
Xravo ijpisy

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.

Challenge view in landscape
Fzawtibvu heab oy dozttfije

@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()
      }
    }
  }
}
Challenge view in landscape
Kfutkalda cail ek xovqbhima

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

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
)
Custom environment property
Wupweq acvubiglatz zraxexlw

Key points

This was another intense chapter. But in the end, as with the previous one, concepts are simple, once you understand how they work.

Where to go from here?

This chapter completes the state and data flow topic — whereas in the previous chapter you learned how to use observable properties in your views, and how to pass them around, in this chapter you looked at defining and using your own observable types, as well as getting your hands on environment properties.

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.

Have feedback to share about the online reading experience? If you have feedback about the UI, UX, highlighting, or other features of our online readers, you can send them to the design team with the form below:

© 2021 Razeware LLC

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 raywenderlich.com Professional subscription.

Unlock Now

To highlight or take notes, you’ll need to own this book in a subscription or purchased by itself.