SwiftUI Property Wrappers
Learn different ways to use SwiftUI property wrappers to manage changes to an app’s data values and objects. By Audrey Tam.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
SwiftUI Property Wrappers
35 mins
- Getting Started
- Tools for Managing Data
- Property Wrappers
- Managing UI State Values
- Managing ThingStore With @State and @Binding
- Using a TextField
- Accessing Environment Values
- Modifying Environment Values
- Managing Model Data Objects
- Class and Structure
- Managing ThingStore With @StateObject and @ObservedObject
- Refactoring TIL
- Using Thing Structure
- Navigating to ThingView
- Adding a New Thing From ThingView
- Using @EnvironmentObject
- Wrapping Up Property Wrappers
- Wrapping Values
- Wrapping Objects
- One More Thing
- Where to Go From Here?
In a SwiftUI app, every data value or object that can change needs a single source of truth and a mechanism to enable views to change it or to observe it. SwiftUI property wrappers enable you to declare how each view interacts with mutable data.
In this tutorial, you’ll build a simple app and learn different ways to use SwiftUI property wrappers to manage changes to its data values and objects with the @State
, @Binding
, @Environment
, @StateObject
, @ObservedObject
and @EnvironmentObject
property wrappers.
Getting Started
Download the project materials using the Download Materials button at the top or bottom of this tutorial.
Open the TIL project in the starter folder. The project name “TIL” is the acronym for “Today I Learned”. Or, you can think of it as “Things I Learned”. Here’s how the app should work: The user taps the + button to add acronyms like “YOLO” and “BTW”, and the main screen displays these.
This app embeds the VStack
in a NavigationView
. This gives you the navigation bar where you display the title and the + button.
Build and run the app. If you get a LayoutConstraints
message in the console, complaining about UIModernBarButton
, add this modifier to the NavigationView
in ContentView.swift:
.navigationViewStyle(StackNavigationViewStyle())
This is a workaround for a navigationTitle
bug. To find the right place to add the modifier, fold NavigationView
:
ThingStore
has the property things
, which is an array of String
values.
You’ll first manage state changes to the ThingStore
structure using @State
and @Binding
, then convert it to an ObservableObject
and manage state changes with @StateObject
and @ObservedObject
.
Finally, you’ll extend the app to create a reason to access ThingStore
as an @EnvironmentObject
. You’ll instantiate ThingStore
when you create ContentView
in TILApp
. As an @EnvironmentObject
, your ThingStore
object will be available to any view that needs to access it.
Tools for Managing Data
A @State
property is a source of truth. A view that owns a @State
property can pass either its value or its binding to its subviews. If it passes a binding to a subview, the subview now has a reference to the source of truth. This allows it to update that property’s value or redraw itself when that variable changes. When a @State
value changes, any view with a reference to it invalidates its appearance and redraws itself to display its new state.
Your app needs to manage changes to two kinds of data:
- User interface values, like Boolean flags to show or hide views, text field text, slider or picker values.
- Data model objects, often collections of objects that model the app’s data, like a collection of acronyms.
Property Wrappers
Property wrappers wrap a value or object in a structure with two properties:
-
wrappedValue
is the underlying value or object. -
projectedValue
is a binding to the wrapped value or a projection of the object that creates bindings to its properties.
Swift syntax lets you write just the name of the property, like showAddThing
, instead of showAddThing.wrappedValue
. And, its binding is $showAddThing
instead of showAddThing.projectedValue
.
SwiftUI provides property wrappers and other tools to create and modify the single source of truth for values and for objects:
-
User interface values: Use
@State
and@Binding
for values likeshowAddThing
that affect the view’s appearance. The underlying type must be a value type likeBool
,Int
,String
orThing
. Use@State
to create a source of truth in one view, then pass a@Binding
to this property to subviews. A view can access built-in@Environment
values as@Environment
properties or with the.environment(_:_:)
view modifier. -
Data model objects: For objects like
ThingStore
that model your app’s data, use@StateObject
with@ObservedObject
or.environmentObject(_:)
with@EnvironmentObject
. The underlying object type must be a reference type — a class — that conforms toObservableObject
, and it should publish at least one value. Then, either use@StateObject
and@ObservedObject
or declare an@EnvironmentObject
with the same type as the environment object created by the.environmentObject(_:)
view modifier.
While prototyping your app, you can model your data with structures and use @State
and @Binding
. When you’ve worked out how data needs to flow through your app, you can refactor your app to accommodate data types that need to conform to ObservableObject
.
This is what you’ll do in this tutorial to consolidate your understanding of how to use these property wrappers.
@AppStorage
wraps UserDefaults
values and you can use @SceneStorage
to save and restore the state of a scene.
Managing UI State Values
@State
and @Binding
value properties are mainly used to manage the state of your app’s user interface.
A view is a structure, so you can’t change a property value unless you wrap it as a @State
or @Binding
property.
The view that owns a @State
property is responsible for initializing it. The @State
property wrapper creates persistent storage for the value outside the view structure and preserves its value when the view redraws itself. This means initialization happens exactly once.
Managing ThingStore With @State and @Binding
TIL is a very simple app, making it easy to examine different ways to manage the app’s data. First, you’ll manage ThingStore
the same way as any other mutable value you share between your app’s views.
In ContentView.swift, run live preview and tap the + button:
MyThings
initializes with an empty things
array so, the first time your user launches your app, you display a message instead of a blank page. The message gives your users a hint of what they can do with your app. The text is grayed out so they know it’s just a placeholder until they add their own data.
TIL uses a Boolean flag showAddThing
to show or hide AddThingView
. It’s a @State
property because its value changes when you tap the + button, and ContentView
owns it.
In ContentView.swift, replace the myThings
property in ContentView
:
@State private var myThings = ThingStore()
You’ll add items to myThings.things
, so myThings
must be a wrapped property. In this case, it’s @State
because ContentView
owns it and initializes it.
AddThingView
needs to modify myThings
, so you need a @Binding
in AddThingView
.
In AddThingView.swift, add this property to AddThingView
:
@Binding var someThings: ThingStore
You’ll soon pass this binding from ContentView
.
You’ll also add a text field, but for now, just to have something happen when you tap Done, add this line to the button action, before you dismiss this sheet:
someThings.things.append("FOMO")
You append a specific string to the array.
Fix this view’s previews
:
AddThingView(someThings: .constant(ThingStore()))
You create a binding for the constant initial value of ThingStore
.
Now, go back to ContentView.swift and fix the call to AddThingView()
:
AddThingView(someThings: $myThings)
You pass a binding to the ContentView
@State
property to the subview AddThingView
.
ThingStore
. In this case, ThingStore
has only the things
array but, if it had more properties and you wanted to restrict write access to its things
array, you could pass $myThings.things
— a binding to only the things
array. You’d need to initialize an array of String
for the preview of AddThingView
.
Start live preview, tap + then tap Done:
Great, you’ve got data flowing from AddThingView
to ContentView
via ThingStore
!
Now to get input from your user, you’ll add a TextField
to AddThingView
.
First, pin the preview of ContentView
so it’s there when you’re ready to test your TextField
: Click the push-pin button in the canvas toolbar.