Home Using SwiftUI Property Wrappers to ReloadData
Post
Cancel

Using SwiftUI Property Wrappers to ReloadData

Intro

One of the most remarkable things that SwiftUI really ushered into the iOS ecosystem is the ability to reactively refresh data, alongside its prowess in building exceptional UIs. At the heart of this simplicity and abstraction lies the concept of property wrappers, which, when combined with the Combine framework, makes SwiftUI a true pleasure to work with.

Each property wrapper in SwiftUI abstracts a distinct set of operations that previously required manual binding or intricate setups in the days of UIKit. As a developer transitioning from UIKit to SwiftUI, one persistent question I had was, “How do I reload data?” Well, the answer is quite intriguing—instead of explicitly triggering data reloads, SwiftUI leverages the cheap nature of it’s views (structs aka value types) and property wrappers to seamlessly reload data in a more reactive manner. This approach is so elegant that even seasoned UIKit developers might find themselves envious, I mean we had to do so much with RxSwift to accomplish similar reactive data reloads.

The goal of this article, is for us to explore the various property wrappers available in SwiftUI and how they enable us to reload data within a SwiftUI view. By understanding their functionality and application, we can unlock the true potential of reactive data updates in our SwiftUI projects.

Why Property Wrappers?

As we have established in the intro the concept of property wrappers plays an important role in achieving reactive data binding and effortless data reloads in SwiftUI. They enable us to encapsulate and manage the state of our data, ensuring that changes are automatically reflected in our views. Whether it’s local, external, global, core data or even user default data changes, property wrappers handle them all for us in SwiftUI. Beyond that, it also minimize boilerplate code and provides a clean and declarative approach to handling data updates.

Ok, Let’s take a closer look at the different property wrappers and how they contribute to the reactive binding of data.

@State: Managing Local State

The @State property wrapper is one of the fundamental property wrappers in SwiftUI. It allows us to declare a mutable state within a view, indicating that the value can change over time. When the value assigned to a @State property changes, SwiftUI automatically updates the corresponding view, ensuring a reactive UI.

For example, consider a scenario where we have a simple counter view. By using the @State property wrapper on an integer variable representing the count, any changes to the count value will trigger an automatic update of the view, seamlessly reflecting the updated count.

1
2
3
4
5
6
7
8
9
10
11
12
13
struct CounterView: View {
    @State private var count: Int = 0
    
    var body: some View {
        VStack {
            Text("Count: \(count)")
            Button("Increment") {
                count += 1
            }
        }
    }
}

As you can see, SwiftUI handles the synchronization between the count value and the UI, allowing us to focus on the logic without worrying about explicitly reloading the data.

@Binding: Two-Way Data Binding

The @Binding property wrapper helps us to establish a two-way connection between a property defined in a child view and a property defined in a parent view. It allows us to pass data from a parent view to a child view and automatically reflect any changes made in the child view back to the parent view.

By using @Binding, SwiftUI ensures that both the parent and child views stay in sync, effectively achieving two-way data binding. When the bounded value is modified in the child view, SwiftUI automatically updates the parent view and propagates the changes through the entire view hierarchy.

This property wrapper is particularly useful when building interactive and reusable components, where the state is shared between different views.

To grasp the significance of @Binding property wrapper, let’s consider a practical example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
struct ParentView: View {
    @State private var text = ""
    
    var body: some View {
        VStack {
            TextField("Enter text", text: $text)
            
            ChildView(text: $text)
        }
    }
}

struct ChildView: View {
    @Binding var text: String
    
    var body: some View {
        Text("Entered text: \(text)")
            .foregroundColor(.blue)
    }
}

In our example above, we have a parent view with a @State property text, representing the user-entered text. Inside ParentView, we have a VStack containing a TextField where the user can enter text. The text property is bound to the TextField using the $ prefix, establishing a two-way connection. We then pass the text binding to the ChildView using @Binding. The ChildView displays the entered text using a Text view, and the text value is updated whenever the user types in the TextField. By leveraging @Binding, any changes made to the text property in ChildView will also update the value in ParentView, ensuring synchronization between the two views.

@StateObject: Managing External State

Sometimes we need to manage data from external sources, such as our network request managers or shared data models. This is where the @StateObject, @ObservedObject and @EnvironmentObject property wrappers come into play.

The @StateObject property wrapper is one of the tools we use for managing such external state within a view. It is particularly useful when working with reference types that conform to the ObservableObject protocol. The @StateObject property wrapper ensures that the observed object persists across view rerenders, maintaining a consistent state.

Unlike @ObservedObject property wrapper, which recreates the observed object each time the view rerenders itself, @StateObject retains the same instance across multiple updates. This behavior is essential when managing shared state or interacting with objects that have complex initialization requirements. A simplified way to think of it is, use @StateObject when the view is the owner of that observed object and use @ObservedObject when injecting observable objects into a view. I know this particular property wrapper can be tricky to understand at first, so i have written another article specifically dedicated to it.

To grasp the significance of @StateObject property wrapper, let’s consider a practical example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class WeatherViewModel: ObservableObject {
    @Published var temperature: Double = 0.0
    @Published var city: String = ""
    
    func fetchWeather() async {
        // Simulated API call to fetch weather data
        // Update temperature and city properties
    }
}

struct WeatherView: View {
    @StateObject private var viewModel = WeatherViewModel()
    
    var body: some View {
        VStack {
            Text("Temperature: \(viewModel.temperature)")
            Text("City: \(viewModel.city)")
            
            Button("Fetch Weather") {
                Task {
                   await viewModel.fetchWeather()
                }
            }
        }
    }
}

In this example, we have a WeatherViewModel that encapsulates the logic and data for fetching weather information. The view model has @Published properties temperature and city, representing the current temperature and the city name.

Inside the WeatherView, we use the @StateObject property wrapper to create an instance of the WeatherViewModel. This ensures that the view model is retained throughout the view’s lifecycle and automatically updates the view when its published properties change.

The view displays the temperature and city information using Text views that bind to the corresponding properties of the view model. Whenever the view model’s properties update, SwiftUI automatically reflects those changes in the UI.

We also have a Button that triggers the fetchWeather() method on the view model when pressed. This method can perform asynchronous tasks, such as making API calls to fetch weather data. As a result, the temperature and city properties will update, triggering UI updates accordingly.

By utilizing @StateObject, we seamlessly integrate the view model into the view hierarchy while ensuring proper lifecycle management. SwiftUI takes care of creating and disposing of the view model instance, which makes for an efficient memory usage.

@ObservedObject: Observing External State

The @ObservedObject property wrapper allows us to observe changes to an object’s state, typically a reference type conforming to the ObservableObject protocol. When the observed object’s published properties change, SwiftUI will automatically updates the view that’s using the observed object.

By using @ObservedObject, we establish a reactive link between the observed object and the view that relies on its data. Any changes made to the observed object trigger an automatic reload of the view. Check out this article for more in depth explanation and difference between @StateObject and @ObservedObject property wrappers.

To grasp how @ObservedObject property wrapper helps us reload data, let’s consider a practical example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class DataManager: ObservableObject {
    @Published var data: [String] = []
    
    func fetchData() async {
        // Simulated data fetching
        // Update data property with fetched data
    }
}

struct ContentView: View {
    @ObservedObject var dataManager: DataManager
    
    var body: some View {
        VStack {
            Button("Fetch Data") {
                dataManager.fetchData()
            }
            
            List(dataManager.data, id: \.self) { item in
                Text(item)
            }
        }
    }
}

In our example above, we are using the @ObservedObject property wrapper to inject the DataManager instance into the view, this allows us to establish a connection that allows the view to observe and react to changes in the observed object’s state, that is the DataManager.

TLDR: Ideal property wrapper for Injecting an ObservableObject from a parent view to a childview, do not use it to instantiate an ObservableObject unless you are sure that’s the behavior you’re looking for, since it will recreate the object every single time the view redraws itself.

@EnvironmentObject: Sharing Data Across Views

The @EnvironmentObject property wrapper is what we use in SwiftUI to share data across multiple views in our SwiftUI views hierarchy. It provides a convenient way to access and observe shared data without the need to explicitly pass the data through the view’s parameters.

TLDR: @EnvironmentObject property wrapper prevents prop drilling.

By leveraging @EnvironmentObject property wrapper, we can inject shared data at the root of our SwiftUI hierarchy and access it from any child views within that parent view’s hierarchy. SwiftUI will ensure that the views observe changes to the shared data and refresh accordingly, enabling seamless data updates across the application.

@Environment: Responding to Global Changes

The environment in SwiftUI represents a collection of key-value pairs that define the global settings and properties for our app. These settings can be accessed using the @Environment property wrapper within our views. When an environment value changes, SwiftUI automatically updates the associated views to reflect the updated values. This provides us a convenient and centralized approach for retrieving and utilizing global configuration and settings.

With the @Environment property wrapper we can access values such as color schemes, layout directions, font settings, and more.

For instance, let’s imagine a scenario where the user can switch between light and dark mode within our app. By accessing the colorScheme environment value using @Environment property wrapper, we can dynamically adjust the appearance of our views based on the current color scheme. This allows for a seamless and reactive refreshing of view within our UI whenever the color scheme changes.

Another good example is the layoutDirection. Let’s consider a situation where our app’s layout needs to adjust based on the current layout direction. By accessing the layoutDirection environment value using @Environment property wrapper, we can dynamically modify the view’s layout, repositioning elements or altering their appearance as needed.

In addition to the different built-in environment values provided by SwiftUI, we can also create and utilize our own custom environment values. All we need is an EnvironmentKey and EnvironmentValues, which allows us to define and propagate specific settings or data throughout our app.

@FetchRequest: Seamless Integration with Core Data

The @FetchRequest property wrapper provides seamless integration with Core Data, by enabling us to fetch and display data from a Core Data persistent store while automatically handling data reloads when the underlying data changes.

To utilize @FetchRequest, we need to set up our Core Data stack, define data models, and establish relationships between entities. SwiftUI provides the @Environment property wrapper to access the managed object context, making it accessible throughout our app.

Fetching Data with @FetchRequest

The @FetchRequest property wrapper allows us to declare a fetch request directly within a SwiftUI view. By specifying the entity we want to fetch, the sort descriptors, and any necessary predicates, we can seamlessly fetch and display data within our view.

Here’s an example of how to use @FetchRequest to fetch and display a list of books from a Core Data store:

1
2
3
4
5
6
7
8
9
10
11
struct BookListView: View {
    @FetchRequest(entity: Book.entity(), sortDescriptors: [
        NSSortDescriptor(keyPath: \Book.title, ascending: true)
    ]) var books: FetchedResults<Book>
    
    var body: some View {
        List(books) { book in
            Text(book.title ?? "")
        }
    }
}

For our example, the @FetchRequest property wrapper is used to fetch Book entities sorted by title in ascending order. The resulting fetched objects are stored in the books property, which is of type FetchedResults. Any changes to the underlying data that match the fetch request will automatically trigger a reload of the view, keeping the displayed data up to date.

@AppStorage: Simplifying UserDefaults Integration

In SwiftUI, the @AppStorage property wrapper provides a seamless integration with UserDefaults, enabling reactive data binding and effortless persistence of user settings and preferences. By leveraging @AppStorage, we can establish a direct connection between the stored value and the view, ensuring automatic updates whenever the data changes.

  1. Using @AppStorage for Reactive Data Binding

Similar to @State, @AppStorage allows us to create a reactive link between the stored value and the view. By annotating a property with @AppStorage, SwiftUI automatically tracks changes to the value, triggering immediate updates in the associated view.

Here is an example to understand how @AppStorage simplifies UserDefaults integration:

1
2
3
4
5
6
7
8
struct SettingsView: View {
    @AppStorage("themePreference") var themePreference: String = "light"
    
    var body: some View {
        // View content
    }
}

in this our example, the themePreference property is annotated with @AppStorage(“themePreference”). Any modifications to the themePreference value, such as changing the theme from light to dark, automatically update the view.

With @AppStorage, integrating UserDefaults becomes a streamlined process. The property wrapper abstracts the complexities of reading from and writing to UserDefaults, allowing us to focus on the logic and presentation of our views.

@SceneStorage: Saving Screen-Specific Data for Seamless State Restoration

In SwiftUI, the @SceneStorage property wrapper comes in handy when we need to save unique data for each screen within our app. It allows for effortless state restoration, particularly in complex multi-scene setups commonly seen in iPadOS. Similar to @AppStorage, @SceneStorage simplifies data persistence by associating a name and default value with the stored data.

For instance, let’s consider a text editor where we want to preserve the user’s input. Here’s how you can utilize @SceneStorage to achieve this:

1
2
3
4
5
6
7
struct TextEditorView: View {
    @SceneStorage("userInput") var userInput: String = ""
    
    var body: some View {
        // View content
    }
}

In this example, the userInput property is annotated with @SceneStorage("userInput"). This ensures that the text the user enters will be automatically saved and restored for the individual screen.

TLDR: @SceneStorage is like AppStorage except it’s ideal for screen specific data storage.

@Published: Unleash the power of combine to handle data streams

The @Published property wrapper plays a vital role in managing observable data and facilitating reactive updates in SwiftUI. By annotating a property with @Published, you establish a publisher-subscriber relationship, allowing automatic notifications and updates to propagate through our app’s data flow. This means that when an Observable object with a @Published property is modified, all views relying on that object will be automatically reloaded to reflect the changes.

The pattern is that we mark properties as @Published inside classes that conform to the ObservableObject protocol.

To grasp the significance of @Published, let’s consider a practical example:

1
2
3
class UserData: ObservableObject {
    @Published var username: String = ""
}
1
2
3
4
5
6
7
struct ProfileView: View {
    @StateObject var userData = UserData()
    
    var body: some View {
        Text("Username: \(userData.username)")
    }
}

In this example, the UserData class adopts the ObservableObject protocol, and the username property is annotated with @Published. Any changes made to username property automatically triggers the view to rerender, ensuring that the subscribers are informed and any dependent views are updated accordingly. This allows for a reactive UI that reflects the latest state of the observable object.

The @Published property wrapper’s underlying implementation relies on Combine’s publishers and subscribers, which provides us a powerful mechanism for handling asynchronous events and data streams.

Conclusion

These property wrappers - @State, @StateObject, @ObservedObject, and @EnvironmentObject etc - collectively empower us to manage both local and external state, observe changes, and share data effectively. By abstracting the complexities of data management and incorporating a reactive programming approach, SwiftUI enables us to create dynamic and responsive user interfaces with minimal effort.

This post is licensed under CC BY 4.0 by the author.