Property wrappers are key to SwiftUI’s magic, they add capabilities to properties and automate common UI state behaviors, such as updating views, without the need for manual boilerplate code.
In this quick tutorial, we’ll walk through some of the most commonly used SwiftUI property wrappers, with simple examples of how each one is used and what sets them apart.
@State
As stated in the official documentation:
A property wrapper type that can read and write a value managed by SwiftUI.
When used, @State
acts as the source of truth for a value type in the view hierarchy. Once applied to a property, the @State
attribute creates a state value in the app.
Let’s take this example of a simple screen with a toggle button. Create a SwiftUI project and replace the code in ContentView.swift
with the following:
@State private var isPlaying: Bool = false var body: some View { Button(isPlaying ? "Pause" : "Play") { isPlaying.toggle() } }
Why is the property annotated with @State
marked as private?
SwiftUI designed @State
to work locally and be tied to the view in which it is declared. Otherwise, nothing would prevent other objects from mutating its state by passing it as a dependency to another object. Marking it as private
is a convention that helps encapsulate the property within its view and allows SwiftUI to manage the storage reliably.
@Binding
But what if I need to mutate an @State
property from outside the view in which it is declared?
Here’s where @Binding
comes in handy. Let’s take the previous example and modify it to showcase the importance of @Binding
.
struct ContentView: View { @State private var isPlaying: Bool = false var body: some View { PlayerButton(isPlaying: $isPlaying) } }
The button is currently part of the main view on the screen. Let’s say we want to extract it into its own view for better control of the code:
struct PlayerButton: View { @Binding var isPlaying: Bool var body: some View { VStack { Button(isPlaying ? "Pause" : "Play") { isPlaying.toggle() } } } }
Now, the isPlaying
property declared in the PlayerButton
view is pointing to the one declared in ContentView
as @State
. In other words, we now have a reference to the source of truth.
@Published
Let’s spice this up by moving the isPlaying
property to a view model for a cleaner approach. The view model will be a class, like the following:
import Combine class ViewModel: ObservableObject { @Published var isPlaying: Bool = false }
Now, the ViewModel
owns the isPlaying
property, and a Combine
publisher will emit its new value to any subscriber whenever the value changes.
Let’s tweak the ContentView
a little bit to leverage the new use of the Combine publisher.
struct ContentView: View { @StateObject private var viewModel = ViewModel() var body: some View { VStack { Button(viewModel.isPlaying ? "Pause" : "Play") { viewModel.isPlaying.toggle() } } } }
Marking the view model with the @StateObject
property wrapper tells SwiftUI that the ContentView
owns and should retain the ObservableObject
(the view model, in this case).
You may notice that both approaches (@State + @Binding
vs @StateObject + @Published
) lead to the same result: reactive updates when the isPlaying
value changes. However, it’s important to highlight a key difference between them. While you would use @State + @Binding
for simple local state that lives in a struct (value type), @StateObject + @Published
allows for more complex state that can be shared and reused across views, and which lives in a class (reference type).
When is it best to use one or the other?
As a simple rule of thumb, if your state logic is simple, tied to a single view, and you’re working with structs, @State
–@Binding
is your best bet. However, if you’re dealing with complex logic and need to share the state across multiple views, you would want to use @StateObject
–@Published
instead.
Thanks for reading – see you in the next digest!
Discover more from SweetTutos
Subscribe to get the latest posts sent to your email.