Understanding Data Updating

Data updating is essential when the state of your app changes due to user interactions such as button presses or navigation actions, or external events like receiving new data from a network request. You want the UI to reflect these state changes.

Declarative UI

SwiftUI is a declarative UI framework. SwiftUI lets you define how UI should look based on the data, and you don’t have to define how the UI transitions between different states.

The Role of State Management

SwiftUI uses state management to update data. State management tools maintain a consistent and reactive UI that automatically updates in response to changes in the app’s state. SwiftUI provides several tools, including @State and the Observable macro, that are designed to manage state. You’ll learn more about how to use these tools in the next module’s lessons. For now, you’ll focus on exploring why these tools are necessary.

How SwiftUI Updates the UI

In SwiftUI, the app’s state is based on the data. To update the UI, you must first update the data. To update the data, you must use state management tools. SwiftUI then automatically recalculates the UI and performs necessary changes based on the new state, ensuring the UI always aligns with the underlying data model. This makes it easy to control the data flow across the app and eases app maintenance and scaling.

Up next are some scenarios that require state management to change the data in order to change the UI.

Scenarios Requiring UI Updates

The following scenarios require UI updates:

  • User interactions: Tapping buttons, toggling switches, or submitting forms.
  • External updates: Fetching data from a server or responding to notifications.
  • Navigation: Navigating to a new view.
  • Environment changes: Adjusting UI elements when the app resumes from the background or reacts to accessibility settings.
  • Device orientation changes: Adapting the layout for changes between portrait and landscape modes.
  • Theme changes: Switching appearances between light and dark modes.

These are just some examples, but essentially, any scenario where the UI needs to reflect updated data after the initial display requires the use of SwiftUI state management tools.

Demonstrating the Need for State Management

Effective state management ensures that UI updates occur when underlying data changes. Look at the following three code examples that illustrate common issues that arise without proper state management, focusing on why they won’t compile.

Example 1: Simple Counter

Consider this simple counter example demonstrating a typical issue when implementing data updating for the first time:

struct ContentView: View {
  var count = 0

  var body: some View {
    Text("Count: \(count)")

    Button("Increment") {
      // This attempt won't update the view:
      self.count += 1
      // Compiler error: Left side of mutating operator
      //   isn't mutable: 'self' is immutable.
    }
  }
}

This code seems straightforward. Pressing the Increment button should modify self.count and update the Text view, but it won’t compile. The compiler error, “Left side of mutating operator isn’t mutable: ‘self’ is immutable”, occurs because SwiftUI defines views using structs, which are immutable by default. You can’t modify their properties directly once they’re set. This immutability is a safety feature to prevent unexpected behaviors and bugs. State management tools are required to manage changes within a view.

Example 2: Fetching Data

Here’s another example, this time trying to fetch and display data from the network:

struct ContentView: View {
  var postTitle = "Loading..."

  var body: some View {
    Text(postTitle)
      .task {
        // This update won't be reflected in the UI:
        self.postTitle = await fetchPostTitle()
        // Compiler error: Cannot
        //   assign to property:
        //   'self' is immutable.
      }
  }

  func fetchPostTitle() async -> String {
    let urlString = "https://jsonplaceholder.typicode.com/posts/1"
    guard let url = URL(string: urlString) else {
      return "Invalid URL"
    }

    do {
      let (data, _) = try await URLSession.shared.data(from: url)
      let post = try JSONDecoder().decode(Post.self, from: data)
      return post.title
    } catch {
      return "Failed to load post"
    }
  }
}

struct Post: Codable {
  var title: String
}

In this case, the task modifier is supposed to update self.postTitle when the data loads from the network. As with the first example, this code won’t compile due to the same kind of compiler error. The code doesn’t compile without using state management tools because of the immutable nature of the view.

Example 3: Responding to App Backgrounding

Last, examine this example monitoring app state changes:

struct AppStateObserverView: View {
  @Environment(\.scenePhase)
  var scenePhase

  var appState = "Active"

  var body: some View {
    Text("App State: \(appState)")
      .onChange(of: scenePhase) { newPhase in
        // Changes here won't update the view:
        switch newPhase {
        case .active:
          self.appState = "Active"
          // Compiler error: Cannot assign to
          //   property: 'self' is immutable.
        case .background:
          self.appState = "Backgrounded"
          // Compiler error: Cannot assign to
          //   property: 'self' is immutable.
        case .inactive:
          self.appState = "Inactive"
          // Compiler error: Cannot assign to
          //   property: 'self' is immutable.
        @unknown default:
          self.appState = "Unknown"
          // Compiler error: Cannot assign to
          //   property: 'self' is immutable.
        }
      }
  }
}

In this example, AppStateObserverView monitors the application’s lifecycle state using the onChange view modifier on the @Environment(\.scenePhase) property. This snippet also fails to compile with the same error when attempting to update self.appState.

Upcoming Video Demo: Observing the Limitations of Static Data Management

To reinforce these concepts, the upcoming video demo will revisit the simple counter example. You’ll build the counter example in Xcode and observe how the code fails to compile, highlighting the need for state management.

See forum comments
Download course materials from Github
Previous: Introduction Next: Demo: Observing the Limitations of Static Data Management