Advanced Data Flow in SwiftUI: Beyond @State and @Binding

Evangelist Apps
Evangelist Apps Blog
3 min readJun 27, 2024

--

Understanding the Limitations of @State and @Binding

@State and @Binding are important tools for managing local state in SwiftUI views. However, they have some limitations that become apparent in more complex data management situations, such as -

Limited to a Single View Hierarchy

While @State and @Binding work well within a single view hierarchy, they can be cumbersome when data needs to be shared across multiple unrelated views or when the view hierarchy becomes more complex.

Immutable State

@State makes the state immutable within a view, which means that its value cannot be directly modified. This immutability ensures data consistency but can be restrictive in situations where mutable state is required.

Limited to Value Types

@State and @Binding are designed to work with value types. When dealing with reference types, such as classes @ObservedObject must be used instead, which can lead to inconsistencies in data management.

Introduction to @ObservedObject and @EnvironmentObject

To make handling data easier in SwiftUI, besides using @State and @Binding, there are two more property wrappers available called @ObservedObject and @EnvironmentObject. These wrappers allow for more advanced ways to manage data.

When to use ObservedObject?

In SwiftUI, @ObservedObject allows views to observe and respond to changes in external data models that conform to the ObservableObject protocol. This means your views will automatically update whenever the observed object’s properties, marked with @Published, change. It's perfect for managing state in a more decoupled and reusable way compared to @State and @Binding.

Let’s take a look at the following example -

import SwiftUI
import Combine

// Model class conforming to ObservableObject
class UserSettings: ObservableObject {
@Published var username: String = "Guest"
@Published var notificationsEnabled: Bool = true
}

struct ContentView: View {
// Observing an instance of UserSettings
@ObservedObject var settings = UserSettings()

var body: some View {
VStack {
TextField("Username", text: $settings.username)
.padding()
.textFieldStyle(RoundedBorderTextFieldStyle())

Toggle(isOn: $settings.notificationsEnabled) {
Text("Enable Notifications")
}
.padding()

Text("Current Username: \(settings.username)")
Text("Notifications: \(settings.notificationsEnabled ? "On" : "Off")")
}
.padding()
}
}

Here UserSettings class conforms to the ObservableObject protocol. It has two @Published properties, username and notificationsEnabled. This will notify any observing views when their values change.

Next, in ContentView an instance of UserSettings is created and marked with @ObservedObject. This allows SwiftUI to monitor changes to the settings object and update the UI accordingly.

Using this approach, we can observe and manage state changes in a more complex and reusable way compared to @State and @Binding

Sharing Data with @EnvironmentObject

In SwiftUI, @EnvironmentObject is a powerful way to share data across many views in your app without needing to pass it manually through each view. This makes it easier to manage shared state.

You start by creating a data model that conforms to the ObservableObject protocol. Then, you inject this model into the environment using the .environmentObject(_:) modifier. After that, any view in the hierarchy can access the data by declaring it as an @EnvironmentObject.

import SwiftUI
import Combine

class AppSettings: ObservableObject {
@Published var darkModeEnabled: Bool = false
}

struct ParentView: View {
@StateObject private var settings = AppSettings()

var body: some View {
NavigationView {
VStack {
Toggle(isOn: $settings.darkModeEnabled) {
Text("Enable Dark Mode")
}
.padding()

NavigationLink(destination: ChildView()) {
Text("Go to Child View")
}
.padding()
}
.navigationTitle("Parent View")
}
.environmentObject(settings)
}
}

struct ChildView: View {
@EnvironmentObject var settings: AppSettings

var body: some View {
VStack {
Text("Child View")
.font(.largeTitle)
.padding()

Text("Dark Mode is \(settings.darkModeEnabled ? "Enabled" : "Disabled")")
}
.padding()
}
}

Here AppSettings class conforms to ObservableObject and has a single published property, darkModeEnabled, that notifies observing views when its value changes.

Next in ParentView, we create an instance of AppSettings using @StateObject and inject it into the environment with .environmentObject(settings). This allows any child views to access the AppSettings instance.

Later in ChildView we declare an @EnvironmentObject property to access the shared AppSettings instance. It can now update its UI reactively based on the darkModeEnabled property.

Using @EnvironmentObject, you can easily share data across your SwiftUI app without needing to pass it explicitly through each view. This keeps your views clean and organized.

--

--

Evangelist Apps
Evangelist Apps Blog

Evangelist Software is UK based mobile apps development agency that specializes in developing and testing iOS apps.