SwiftUI Property Wrappers
- 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
@EnvironmentObject property wrappers.
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:
This is a workaround for a
navigationTitle bug. To find the right place to add the modifier, fold
ThingStore has the property
things, which is an array of
You’ll first manage state changes to the
ThingStore structure using
@Binding, then convert it to an
ObservableObject and manage state changes with
Finally, you’ll extend the app to create a reason to access
ThingStore as an
@EnvironmentObject. You’ll instantiate
ThingStore when you create
TILApp. As an
ThingStore object will be available to any view that needs to access it.
Tools for Managing Data
@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 wrap a value or object in a structure with two properties:
wrappedValueis the underlying value or object.
projectedValueis 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
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
@Bindingfor values like
showAddThingthat affect the view’s appearance. The underlying type must be a value type like
@Stateto create a source of truth in one view, then pass a
@Bindingto this property to subviews. A view can access built-in
@Environmentproperties or with the
Data model objects: For objects like
ThingStorethat model your app’s data, use
@EnvironmentObject. The underlying object type must be a reference type — a class — that conforms to
ObservableObject, and it should publish at least one value. Then, either use
@ObservedObjector declare an
@EnvironmentObjectwith the same type as the environment object created by the
While prototyping your app, you can model your data with structures and use
@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
This is what you’ll do in this tutorial to consolidate your understanding of how to use these property wrappers.
UserDefaultsvalues and you can use
@SceneStorageto save and restore the state of a scene.
Managing UI State Values
@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
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
@State private var myThings = ThingStore()
You’ll add items to
myThings must be a wrapped property. In this case, it’s
ContentView owns it and initializes it.
AddThingView needs to modify
myThings, so you need a
In AddThingView.swift, add this property to
@Binding var someThings: ThingStore
You’ll soon pass this binding from
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:
You append a specific string to the array.
Fix this view’s
You create a binding for the constant initial value of
Now, go back to ContentView.swift and fix the call to
You pass a binding to the
@State property to the subview
ThingStore. In this case,
ThingStorehas only the
thingsarray but, if it had more properties and you wanted to restrict write access to its
thingsarray, you could pass
$myThings.things— a binding to only the
thingsarray. You’d need to initialize an array of
Stringfor the preview of
Start live preview, tap + then tap Done:
Great, you’ve got data flowing from
Now to get input from your user, you’ll add a
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.