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!



All Articles