Understanding Property Wrappers in SwiftUI

The translation of the article was prepared specifically for students of the course “iOS Developer. Advanced Course v 2.0. ”




Last week we started a new series of posts about the SwiftUI framework. Today I want to continue this topic by talking about Property Wrappers in SwiftUI. SwiftUI provides us with wrappers for the @State , @Binding , @ObservedObject , @EnvironmentObject and @Environment . So, let's try to understand the difference between them and when, why and which one we should use.

Property wrappers


Property Wrappers (hereinafter referred to as “property wrappers”) are described in SE-0258 . The main idea is to wrap properties with logic, which can be extracted into a separate structure for reuse in the code base.

State


@State is a wrapper that we can use to indicate the state of a View . SwiftUI will store it in a special internal memory outside the View structure. Only a linked View can access it. Once the value of the @State property changes, SwiftUI rebuilds the View to account for state changes. Here is a simple example.

 struct ProductsView: View { let products: [Product] @State private var showFavorited: Bool = false var body: some View { List { Button( action: { self.showFavorited.toggle() }, label: { Text("Change filter") } ) ForEach(products) { product in if !self.showFavorited || product.isFavorited { Text(product.title) } } } } } 

In the above example, we have a simple screen with a button and a list of products. As soon as we click on the button, it changes the value of the state property, and SwiftUI rebuilds the View .

@Binding


@Binding provides reference access for value type. Sometimes we need to make the state of our View accessible to his children. But we can’t just take and pass this value, because it is a value type, and Swift will pass a copy of this value. This is where the wrapping of the @Binding property comes to the rescue.

 struct FilterView: View { @Binding var showFavorited: Bool var body: some View { Toggle(isOn: $showFavorited) { Text("Change filter") } } } struct ProductsView: View { let products: [Product] @State private var showFavorited: Bool = false var body: some View { List { FilterView(showFavorited: $showFavorited) ForEach(products) { product in if !self.showFavorited || product.isFavorited { Text(product.title) } } } } } 

We use @Binding to mark the showFavorited property inside the FilterView . We also use the special $ character to pass the anchor link, because without $ Swift it will pass a copy of the value instead of passing the anchor link itself. FilterView can read and write the value of the showFavorited property in a ProductsView , but cannot track changes using this binding. As soon as the FilterView changes the value of the showFavorited property, SwiftUI recreates the ProductsView and FilterView as its child.

@ObservedObject


@ObservedObject works similarly to @State , but the main difference is that we can split it between several independent View , which can subscribe and watch the changes of this object, and as soon as the changes appear, SwiftUI rebuilds all the views associated with this object . Let's look at an example.

 import Combine final class PodcastPlayer: ObservableObject { @Published private(set) var isPlaying: Bool = false func play() { isPlaying = true } func pause() { isPlaying = false } } 

Here we have the PodcastPlayer class, which is shared by the screens of our application. Each screen should display a floating pause button when the application is playing a podcast episode. SwiftUI tracks changes to an ObservableObject using the @Published wrapper, and as soon as the property marked as @Published changes, SwiftUI rebuilds all the SwiftUI associated with this PodcastPlayer . Here we use the @ObservedObject wrapper to bind our EpisodesView to the PodcastPlayer class

 struct EpisodesView: View { @ObservedObject var player: PodcastPlayer let episodes: [Episode] var body: some View { List { Button( action: { if self.player.isPlaying { self.player.pause() } else { self.player.play() } }, label: { Text(player.isPlaying ? "Pause": "Play") } ) ForEach(episodes) { episode in Text(episode.title) } } } } 

@EnvironmentObject


Instead of passing an ObservableObject through the init method of our View , we can implicitly embed it in the Environment our View hierarchy. By doing this, we make it possible for all child views of the current Environment to access this ObservableObject .

 class SceneDelegate: UIResponder, UIWindowSceneDelegate { var window: UIWindow? func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { let window = UIWindow(frame: UIScreen.main.bounds) let episodes = [ Episode(id: 1, title: "First episode"), Episode(id: 2, title: "Second episode") ] let player = PodcastPlayer() window.rootViewController = UIHostingController( rootView: EpisodesView(episodes: episodes) .environmentObject(player) ) self.window = window window.makeKeyAndVisible() } } struct EpisodesView: View { @EnvironmentObject var player: PodcastPlayer let episodes: [Episode] var body: some View { List { Button( action: { if self.player.isPlaying { self.player.pause() } else { self.player.play() } }, label: { Text(player.isPlaying ? "Pause": "Play") } ) ForEach(episodes) { episode in Text(episode.title) } } } } 

As you can see, we must pass the PodcastPlayer through the environmentObject modifier of our View . By doing this, we can easily access the PodcastPlayer by defining it using the @EnvironmentObject wrapper. @EnvironmentObject uses the dynamic member search function to find an instance of the PodcastPlayer class in Environment , so you don’t need to pass it through the EpisodesView init method. Environment is the right way to inject dependencies into SwiftUI .

@Environment


As we said in the previous chapter, we can transfer custom objects to the Environment View hierarchy inside SwiftUI . But SwiftUI already has an Environment filled with system-wide settings. We can easily access them using the @Environment wrapper.

 struct CalendarView: View { @Environment(\.calendar) var calendar: Calendar @Environment(\.locale) var locale: Locale @Environment(\.colorScheme) var colorScheme: ColorScheme var body: some View { return Text(locale.identifier) } } 

By marking our properties with the @Environment wrapper, we gain access and subscribe to changes to system-wide settings. As soon as Locale , Calendar or ColorScheme systems change, SwiftUI recreates our CalendarView .

Conclusion


Today we talked about the Property Wrappers provided by SwiftUI . @State , @Binding , @EnvironmentObject and @ObservedObject play a huge role in SwiftUI development. Thanks for attention!

Source: https://habr.com/ru/post/475078/


All Articles