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
You are currently viewing page 4 of 4 of this article. Click here to view the first page.

Wrapping Up Property Wrappers

Here’s a summary to help you wrap your head around property wrappers.

First, decide whether you’re managing the state of a value or the state of an object. Values are mainly used to describe the state of your app’s user interface. If you can model your app’s data with value data types, you’re in luck because you have a lot more property wrapper options for working with values. But at some level, most apps need reference types to model their data, often to add or remove items from a collection.

Property wrappers for values and objects

Property wrappers for values and objects

Wrapping Values

@State and @Binding are the workhorses of value property wrappers. A view owns the value if it doesn’t receive it from any parent views. In this case, it’s a @State property — the single source of truth. When a view is first created, it initializes its @State properties. When a @State value changes, the view redraws itself, resetting everything except its @State properties.

The owning view can pass a @State value to a subview as an ordinary read-only value or as a read-write @Binding.

When you’re prototyping an app and trying out a subview, you might write it as a stand-alone view with only @State properties. Later, when you fit it into your app, you just change @State to @Binding for values that come from a parent view.

Your app can access the built-in @Environment values. An environment value persists within the subtree of the view you attach it to. Often, this is just a container like VStack, where you use an environment value to set a default like font size.

Note: You can also define your own custom environment value, for example to expose a view’s property to ancestor views. This is beyond the scope of this tutorial.

You can store a few values in the @AppStorage or @SceneStorage dictionary. @AppStorage values are in UserDefaults, so they persist after the app closes. You use a @SceneStorage value to restore the state of a scene when the app reopens. In an iOS context, scenes are easiest to see as multiple windows on an iPad.

Wrapping Objects

When your app needs to change and respond to changes in a reference type, you create a class that conforms to ObservableObject and publishes the appropriate properties. In this case, you use @StateObject and @ObservedObject in much the same way as @State and @Binding for values. You instantiate your publisher class in a view as a @StateObject then pass it to subviews as an @ObservedObject. When the owning view redraws itself, it doesn’t reset its @StateObject properties.

If your app’s views need more flexible access to the object, you can lift it into the environment of a view’s subtree, still as a @StateObject. You must instantiate it here. Your app will crash if you forget to create it. Then you use the .environmentObject(_:) modifier to attach it to a view. Any view in the view’s subtree can then subscribe to the publisher object by declaring an @EnvironmentObject of that type.

To make an environment object available to every view in your app, attach it to the root view when the App creates its WindowGroup.

One More Thing

There are two small issues with the text fields. Fixing them would improve your users’ experience, mainly because they pretty much expect this behavior:

  1. When AddThingView appears, the focus should be in the first text field and the keyboard should be active.
  2. Tapping return after typing the long form of the acronym should perform the same action as tapping Done.

For the first issue, there’s no native SwiftUI way to programmatically make a TextField first responder. Solutions involve third party packages or a custom UIViewRepresentable text field that conforms to UITextFieldDelegate.

For the second issue, there’s a long-version TextField initializer.

First, in AddThingView.swift, extract the Done button action into a method:

private func saveAndExit() {
  if !short.isEmpty {
    someThings.things.append(
      Thing(short: short, long: long))
  }
  presentationMode.wrappedValue.dismiss()
}

// ...

Button("Done") { saveAndExit() }

You’ll use this method a second time in this file in the TextField action.

Replace TextField("Thing I Learned", text: $long) with the following:

TextField(
  "Thing I Learned",
  text: $long,
  onEditingChanged: { _ in },
  onCommit: { saveAndExit() }
)

Tapping return triggers the onCommit action.

Live-preview ContentView and run it through its paces.

Tapping return saves and exits.

Tapping return saves and exits.

Where to Go From Here?

You can download the final project by using the Download Materials button at the top or bottom of this page.

You’ve learned a lot about managing mutable data values and objects in a SwiftUI app with the @State, @Binding, @Environment, @StateObject, @ObservedObject and @EnvironmentObject property wrappers. This includes:

  • Use @State and @Binding to manage changes to user interface values.
  • Access @Environment values as @Environment view properties or by using the environment view modifier.
  • Use @StateObject and @ObservedObject to manage changes to data model objects. The object type must conform to ObservableObject and should publish at least one value.
  • For more flexible access to an ObservableObject, instantiate it as a @StateObject then pass it in the environmentObject view modifier. Declare an @EnvironmentObject property in any subviews that need access to it.
  • When prototyping your app, you can use @State and @Binding with structures that model your app’s data. 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.

If you have any questions or comments, join the forum below!