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!