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.

4.9 (18) · 2 Reviews

Download materials
Save for later
Share

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.

Note: This tutorial assumes you’re comfortable with using Xcode to develop iOS apps and familiar with SwiftUI basics like those in SwiftUI Tutorial: Getting Started and SwiftUI Tutorial: Navigation.

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

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.

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

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:

  • 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 like showAddThing that affect the view’s appearance. The underlying type must be a value type like Bool, Int, String or Thing. 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 to ObservableObject, 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.

Note: There are two other property wrappers, which manage the state of the app or scene. @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

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.

Note: Passing a binding gives the subview write access to everything in 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.

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.