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.
TIL in action
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:
Fold NavigationView to add the modifier.
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:
Managing UI values and model objects
- 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:
-
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 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
@Stateand@Bindingfor values likeshowAddThingthat affect the view’s appearance. The underlying type must be a value type likeBool,Int,StringorThing. Use@Stateto create a source of truth in one view, then pass a@Bindingto this property to subviews. A view can access built-in@Environmentvalues as@Environmentproperties or with the.environment(_:_:)view modifier. -
Data model objects: For objects like
ThingStorethat model your app’s data, use@StateObjectwith@ObservedObjector.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@StateObjectand@ObservedObjector declare an@EnvironmentObjectwith 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:
Starter TIL
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:
Adding a string works.
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.




