Chapters

Hide chapters

SwiftUI by Tutorials

Fifth Edition · iOS 16, macOS 13 · Swift 5.8 · Xcode 14.2

Before You Begin

Section 0: 4 chapters
Show chapters Hide chapters

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.

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 Observable

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

// 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. 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: {
  showAnswers.toggle()
  // 1
  challengesViewModel.generateRandomChallenge()
}) {
  QuestionView(question:
    challengesViewModel.currentChallenge!.challenge.question
  )
    .frame(height: 300)
}
Current selection
Yamvugs duqepliac

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.

let userManager = UserManager()
// Add this line
let challengesViewModel = ChallengesViewModel()
var body: some Scene {
  WindowGroup {
    StarterView()
      .environmentObject(userManager)
      // Add this line
      .environmentObject(challengesViewModel)
  }
}
@ObservedObject var challengesViewModel = ChallengesViewModel()
@EnvironmentObject var challengesViewModel: ChallengesViewModel
Challenge sequence
Gjemsuydo joyaenka

Congrats view
Gumjtofs tios

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
)
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
Rsuro Nacnemh

Object Ownership

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

struct SomeView: View {
  @ObservedObject var userManager = UserManager()
  ...
}
struct SomeView: View {
  @ObservedObject var userManager: UserManager

  init(userManager: UserManager) {
    self.userManager = userManager
  }
}
struct SomeOtherView: View {
  var body: some View {
    SomeView(userManager: UserManager())
  }
}
struct SomeOtherView: View {
  let userManager = UserManager()

  var body: some View {
    SomeView(userManager: userManager)
  }
}
struct SomeView: View {
  @StateObject var userManager = UserManager()
  ...
}

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
Fsirnazdo beeb it wijxgvotu

@Environment(\.verticalSizeClass) var verticalSizeClass
var body: some View {
  // 1
  if verticalSizeClass == .compact {
    // 2
    VStack {
      // 3
      HStack {
        Button(action: {
          showAnswers = !showAnswers
        }) {
          QuestionView(
            question: challengeTest.challenge.question)
        }
        if showAnswers {
          Divider()
          ChoicesView(challengeTest: challengeTest)
        }
      }
      ScoreView(
        numberOfQuestions: 5,
        numberOfAnswered: $numberOfAnswered
      )
    }
  } else {
    // 4
    VStack {
      Button(action: {
        showAnswers = !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
Ngawfavfi jieb an qemwbjitu

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

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
Peynoq ebpulapnotf vqeqehbr

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.
© 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