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.
Using a TextField
Many UI controls work by binding a parameter to a @State
property of the view: These include Slider
, Toggle
, Picker
and TextField
.
To get user input via a TextField
, you need a mutable String
property to store the user’s input.
In AddThingView.swift, add this property to AddThingView
:
@State private var thing = ""
It’s a @State
property because it must persist when the view redraws itself. AddThingView
owns this property, so it’s responsible for initializing thing
. You initialize it to the empty string.
Now, add your TextField
in the VStack
, above the Done button:
TextField("Thing I Learned", text: $thing) // 1
.textFieldStyle(RoundedBorderTextFieldStyle()) // 2
.padding() // 3
- The label “Thing I Learned” is the placeholder text. It appears grayed out in the
TextField
as a hint to the user. You pass a binding tothing
soTextField
can set this value to what the user types. - You dress up this
TextField
with a rounded border. - You add padding so there’s some space from the top of the view and also to the button.
Then, edit what the button action appends:
if !thing.isEmpty {
someThings.things.append(thing)
}
Instead of "FOMO"
, you append the user’s text input to your things
array after checking it’s not the empty string.
Refresh live-preview in the ContentView
preview and tap +. Type an acronym like YOLO in the text field. It automatically capitalizes the first letter, but you must hold down the Shift key for the rest of the letters. Tap Done:
ContentView
displays your new acronym.
Sometimes the text field auto-corrects your acronym: FTW to GET or FOMO to DINO.
Add this modifier to TextField
:
.disableAutocorrection(true)
Accessing Environment Values
A view can access many environment values like accessibilityEnabled
, colorScheme
, lineSpacing
, font
and presentationMode
. Apple’s SwiftUI documentation has a full list of environment values.
A view’s environment is a kind of inheritance mechanism. A view inherits environment values from its ancestor views, and its subviews inherit its environment values.
To see this, open ContentView.swift and click anywhere in this line:
Text("Add acronyms you learn")
Now open the Attributes inspector:
Font, Weight, Line Limit, Padding and Frame Size are Inherited. Font Color would also be inherited if you hadn’t set it to Gray.
A view can override an inherited environment value. It’s common to set a default font for a stack then override it for the text in a subview of the stack.
Modifying Environment Values
AddThingView
already uses the presentationMode
environment value, declared as a view property. But, you can also set environment values by modifying a view.
Acronyms should appear as all caps but it’s easy to forget to hold down the Shift key. You can actually set an environment value to automatically convert text to upper case.
In TILApp.swift, add this modifier to ContentView()
:
.environment(\.textCase, .uppercase)
You set uppercase
as the default value of textCase
for ContentView
and all its subviews.
.textCase(.uppercase)
also works, but the .environment
syntax highlights the fact that textCase
is an environment value.
To see it in live preview, also add this modifier in ContentView.swift to ContentView()
in previews
.
Refresh live-preview, add acronyms without bothering to keep all the letters uppercase. Just type yolo or fomo. Tap DONE. Notice this label and the placeholder text are now all uppercase:
Your strings are automatically converted to upper case.
The environment value applies to all text in your app, which looks a little strange. No problem — you can override it.
In AddThingView
, add this modifier to the VStack
:
.textCase(nil)
You set the value to nil
, so none of the text displayed by this VStack
is converted to uppercase.
Refresh live-preview, tap +, type icymi then tap Done:
Now, the button label and placeholder text are back to normal. The uppercase
environment default still converts your strings to all caps on the main screen.
Managing Model Data Objects
@State
, @Binding
and @Environment
only work with value data types. Simple built-in data types like Int
, Bool
or String
are useful for defining the state of your app’s user interface.
You can use custom value data types like struct
or enum
to model your app’s data. And, you can use @State
and @Binding
to manage updates to these values, as you did earlier in this tutorial.
Most apps also use classes to model data. SwiftUI provides a different mechanism to manage changes to class objects: ObservableObject
, @StateObject
, @ObservedObject
and @EnvironmentObject
. To practice using @ObservedObject
, you’ll refactor TIL to use @StateObject
and @ObservedObject
to update ThingStore
, which conforms to ObservableObject
. You’ll see a lot of similarities, and a few differences, to using @State
and @Binding
.
Class and Structure
But, this section isn’t just to practice managing objects. ThingStore
actually should be a class, not a structure.
@State
and @Binding
work well enough to update the ThingStore
source of truth value in ContentView
from AddThingView
. But ThingStore
isn’t the most natural use of a structure. For the way your app uses ThingStore
, a class is a better fit.
A class is more suitable when you need shared mutable state like ThingStore
. A structure is more suitable when you need multiple independent states like the Thing
structures you’ll create later in this tutorial.
For a class object, change is normal. A class object expects its properties to change. For a structure instance, change is exceptional. A structure instance requires advance notice that a method might change a property.
A class object expects to be shared, and any reference can be used to change its properties. A structure instance lets itself be copied, but its copies change independently of it and of each other.
Managing ThingStore With @StateObject and @ObservedObject
To use ThingStore
as an @ObservedObject
, you’ll convert it from a structure to a class that conforms to ObservableObject
. Then, you’ll create it as a @StateObject
and pass it to a subview that uses it as an @ObservedObject
. Sounds a lot like “create a @State
property and pass its @Binding
“, doesn’t it?
@State
value or a @StateObject
to a subview as a @Binding
or @ObservedObject
property, even if that subview needs only read access. This enables the subview to redraw itself whenever the @State
value or ObservableObject
changes.
In ContentView.swift, replace the ThingStore
structure with the following:
final class ThingStore: ObservableObject {
@Published var things: [String] = []
}
You make ThingStore
a class instead of a structure, then make it conform to ObservableObject
. You mark this class final
to tell the compiler it doesn’t have to check for any subclasses overriding properties or methods.
ThingStore
publishes its array of data. A view subscribes to this publisher by declaring it as a @StateObject
, @ObservedObject
or @EnvironmentObject
. Any change to things
notifies subscriber views to redraw themselves.
In TIL, AddThingView
will use an @ObservedObject
, so you must instantiate the model object as a @StateObject
in an ancestor view, then pass it as a parameter to its subviews. The owning view creates the @StateObject
exactly once.
In ContentView
, replace @State private var myThings = ThingStore()
with this line:
@StateObject private var myThings = ThingStore()
ThingStore
is now a class, not a structure, so you can’t use the @State
property wrapper. Instead, you use @StateObject
.
@State
property, but its “value” is its address in memory, so dependent views will redraw themselves only when its address changes — for example, when the app reinitializes it.
The @StateObject
property wrapper ensures myThings
is instantiated only once. It persists when ContentView
redraws itself.
In the call to AddThingView(someThings:)
, remove the binding symbol $
:
AddThingView(someThings: myThings)
You don’t need to create a reference to myThings
. As a class object, it’s already a reference.
In AddThingView.swift, replace @Binding
in AddThingView
with @ObservedObject
:
@ObservedObject var someThings: ThingStore
ThingStore
had more properties and you wanted to restrict write access to its things
array, you could pass $myThings.things
to AddThingView
, which would have a @Binding someThings: [String]
property.
And fix its previews
:
AddThingView(someThings: ThingStore())
The argument isn’t a binding anymore.
Refresh live-preview, tap +, type yolo then tap Done:
No surprise: The app still works the same as before.
Refactoring TIL
You’ve managed the state of data in TIL with @State
and @Binding
for ThingStore
as a structure (value type), then with ObservableObject
, @StateObject
and @ObservedObject
for ThingStore
as a class (reference type).
TIL is a very simple app with a very simple view hierarchy. AddThingView
is a subview of ContentView
, so ContentView
just passes the ThingStore
value or object to AddThingView
.
Most apps have a more complex view hierarchy, where you might find yourself passing an object to a subview just so it can pass it on to one of its subviews. In this situation, you should consider using @EnvironmentObject
.
To try this out, you’ll need to make TIL a little less simple. Keeping a list of acronyms isn’t much use if you can’t remember what they mean. TIL really needs to store the long form of each acronym. So first, you’ll create a Thing
structure and refactor TIL to use this instead of String
. Then, you’ll create a detail view to display when the user taps an acronym in ContentView
. To give you a reason to use @EnvironmentObject
, ThingView
will have the same “Add New Thing” button as ContentView
.
Using Thing Structure
Move ThingStore
to its own file and add struct Thing
:
// ThingStore.swift
import SwiftUI
final class ThingStore: ObservableObject {
@Published var things: [Thing] = [] // 1
}
struct Thing: Identifiable {
let id = UUID() // 2
let short: String
let long: String
}
-
ThingStore
now publishes an array ofThing
values instead of an array ofString
values. - It’s possible to have the same acronym with different meanings, so
Thing
needs a uniqueid
value.
In ContentView.swift, modify ContentView
to use Thing
:
ForEach(myThings.things) { thing in // 1
Text(thing.short) // 2
}
-
ForEach
doesn’t need theid
parameter now that it’s iterating over anIdentifiable
type. - You display the short form of the acronym.
There’s a lot more work to do in AddThingView.swift.
Replace @State private var thing = ""
with two @State
properties:
@State private var short = ""
@State private var long = ""
Replace TextField("Thing I Learned", text: $thing)
with these two text fields:
TextField("TIL", text: $short) // 1
.disableAutocorrection(true)
.autocapitalization(.allCharacters) // 2
TextField("Thing I Learned", text: $long)
.autocapitalization(.words)
- The placeholder text “TIL” indicates this text field is for the acronym.
- Now that user input includes text with different capitalizations, you modify each text field to automatically capitalize either all letters or all words.
Now you don’t need or want to set the textCase
environment value for ContentView
.
Delete .environment(\.textCase, .uppercase)
in TILApp.swift and in struct ContentView_Previews
in ContentView.swift.
And, you don’t need to override .uppercase
in AddThingView
. Back in AddThingView.swift, delete .textCase(nil)
from the VStack
.
Continue to refactor AddThingView
to work with Thing
instead of String
:
Move the textFieldStyle
and padding
to modify the VStack
:
// VStack { ... }
.padding()
.textFieldStyle(RoundedBorderTextFieldStyle())
Now that you have two text fields, you modify their container instead of attaching the same modifiers to both text fields.
Next, modify the Done button action to create a Thing instance
:
if !short.isEmpty {
someThings.things.append(
Thing(short: short, long: long))
}
Live-preview ContentView
and add the short and long versions of an acronym like FTW:
I actually typed “ftw” and “for the win”. I didn’t touch the Shift key at all.
Now, you need a detail view to navigate to when the user taps an acronym in the list.
Navigating to ThingView
Create a new SwiftUI View file named ThingView.swift and replace its contents with the following:
import SwiftUI
struct ThingView: View {
let thing: Thing
var body: some View {
VStack {
Text(thing.short)
.font(.largeTitle)
Text(thing.long)
.font(.title)
Spacer()
}
.padding()
}
}
struct ThingView_Previews: PreviewProvider {
static var previews: some View {
ThingView(thing: Thing(short: "TIL", long: "Thing I Learned"))
}
}
You just display the acronym and its meaning.
In ContentView.swift, in the ForEach
closure, replace Text(thing.short)
with the following:
NavigationLink(destination: ThingView(thing: thing)) {
Text(thing.short)
}
You pass a Thing
value to ThingView
. To do its main job — display a Thing
value — ThingView
doesn’t need access to the ThingStore
object.
Live-preview ContentView
, add the short and long versions of an acronym, then check out its detail view:
Well yes, you could easily display both short and long texts in the main list. Here’s a possible use case for the detail view: Use the main list to quiz yourself, then display the detail view to check your answer.
Adding a New Thing From ThingView
Now, suppose you want to let the user add a new Thing
from ThingView
.
Copy the showAddThing
property and the .sheet
and .toolbar
modifiers from ContentView
to ThingView
:
@State private var showAddThing = false
// ...
// modify the VStack with these
.sheet(isPresented: $showAddThing) {
AddThingView(someThings: myThings)
}
.toolbar {
ToolbarItem {
Button(action: { showAddThing.toggle() }) {
Image(systemName: "plus.circle")
.font(.title)
}
}
}
And Xcode complains “Cannot find ‘myThings’ in scope”. So, it’s time to make a decision! Do you pass myThings
from ContentView
to ThingView
just so it can pass it on to AddThingView
?
TIL is still a small app so this isn’t a life-changing decision. But your own apps will grow and grow and, at some point, you’ll have to face this kind of decision in earnest. Here’s the other option.
Using @EnvironmentObject
An @EnvironmentObject
is available to every view in a subtree of the app’s view hierarchy. You don’t pass it as a parameter.
You don’t need to do anything to ThingStore
because you still instantiate an ObservableObject
as a @StateObject
before using it as an @EnvironmentObject
.
What does change is where you instantiate the @StateObject
you’re going to use as an @EnvironmentObject
.
If a view uses an @EnvironmentObject
, you must create the model object by calling the environmentObject(_:)
modifier on an ancestor view. ContentView
uses the ThingStore
object, so you create it in TILApp.swift when it creates ContentView
.
Move the declaration of ThingStore()
from ContentView.swift to TILApp.swift:
struct TILApp: App {
@StateObject private var store = ThingStore()
Then, in WindowGroup
, add this modifier to ContentView()
:
.environmentObject(store)
Now, any view in TIL can access this ThingStore
object directly.
In ContentView.swift, replace the myThings
property with this:
@EnvironmentObject private var myThings: ThingStore
ContentView()
doesn’t have to use the same variable name as TILApp
. The ThingStore
type is like a dictionary key, and Xcode matches up its value to myThings
.
Now, modify AddThingView
to use your environment object.
Delete the argument from the call to AddThingView
:
AddThingView()
Xcode complains, but you’re about to fix the error. Although ContentView
can perfectly well pass myThings
to AddThingView
, you don’t want to make ThingView
do the same. So AddThingView
needs to access the ThingStore
object as an @EnvironmentObject
.
Fix the preview by attaching a ThingStore
object:
ContentView()
.environmentObject(ThingStore())
ThingStore
to persist between view refreshes.
In AddThingView.swift, replace someThings
with this:
@EnvironmentObject var someThings: ThingStore
You just change @ObservedObject
, which must be passed in as a parameter, to EnvironmentObject
, which is just there in the environment.
Also fix the preview: Delete the argument and attach a ThingStore
object:
AddThingView()
.environmentObject(ThingStore())
If you don’t create a ThingStore
object for the preview, it crashes when you tap Done.
And, in ThingView.swift, delete the argument from the call to AddThingView
:
AddThingView()
That’s all you need to do! The two views that use the ThingStore
object access it as an @EnvironmentObject
, giving it any name they want. ThingView
doesn’t need to know anything about ThingStore
.
Live-preview ContentView
and run it through its paces.
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.
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.
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:
- When
AddThingView
appears, the focus should be in the first text field and the keyboard should be active. - 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.
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 theenvironment
view modifier. -
Use
@StateObject
and@ObservedObject
to manage changes to data model objects. The object type must conform toObservableObject
and should publish at least one value. -
For more flexible access to an
ObservableObject
, instantiate it as a@StateObject
then pass it in theenvironmentObject
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 toObservableObject
.
If you have any questions or comments, join the forum below!
All videos. All books.
One low price.
A Kodeco subscription is the best way to learn and master mobile development — plans start at just $19.99/month! Learn iOS, Swift, Android, Kotlin, Flutter and Dart development and unlock our massive catalog of 50+ books and 4,000+ videos.
Learn more