Quick reference
This draft by Chris Eidhof from objc.io nicely summarizes the rules and decisions outlined on this page. (original tweet)
@State
The @State
property wrapper is used inside of View
objects and allows your view to respond to any changes made to @State
. You use @State
for properties that are owned by the view that it's contained in. In other words, a view initializes its @State
properties itself. It does not receive its @State
properties from another object.
struct StateExample: View {
@State private var intValue = 0
var body: some View {
VStack {
Text("intValue equals \(intValue)")
Button("Increment") {
intValue += 1
}
}
}
}
Internally, SwiftUI will store your @State
property's value and persist its value throughout re-renders of your view. This makes it a good fit for state that is managed by the view itself and should be persisted when SwiftUI must discard and recreate your view instance during a refresh.
Note that you should mark your @State
properties private
as a best-practice. No external sources should modify your @State
properties.
Deciding if you should use @State
You should use @State
if:
- The view itself creates (and owns) the instance you want to wrap.
- You need to respond to changes that occur within the wrapped property.
- You're wrapping a value type (
struct
orenum
).
@Binding
The @Binding
property wrapper is used for properties that are passed by another view. The view that receives the binding is able to read the bound property, respond to changes made by external sources (like the parent view), and it has write access to the property. Meaning that updating a @Binding
updates the corresponding property on the view that provided the @Binding
.
struct BindingView: View {
@Binding var intValue: Int
var body: some View {
VStack {
Button("Increment") {
intValue += 1
}
}
}
}
This is an example of a view that receives a Binding
and modifies it what a user taps a button. You would use this view as follows:
struct StateView: View {
@State private var intValue = 0
var body: some View {
VStack {
Text("intValue equals \(intValue)")
BindingView(intValue: $intValue)
}
}
}
struct BindingView: View {
@Binding var intValue: Int
var body: some View {
Button("Increment") {
intValue += 1
}
}
}
Notice that I pass a binding to the @State
wrapped intValue
to BindingView
by prefixing it with a $
: $intValue
. The projected value of a @State
property is a Binding<T>
that you can pass to child views so they can modify @State
properties through the binding rather than directly.
Internally, SwiftUI will not keep a @Binding
around when your view is discarded. It doesn't need to because the @Binding
is always passed by an external source. Unlike @State
where SwiftUI keeps the property around so its value persists when a view is discarded and recreated for a fresh render.
Deciding if you should use @Binding
You should use @Binding
if:
- You need read- and write access to a property that's owned by a parent view.
- The wrapped property is a value type (
struct
orenum
). - You don't own the wrapped property (it's provided by a parent view).
@StateObject
Only available in iOS 14+, iPadOS 14+, watchOS 7+, macOS 10.16+ etc.
The @StateObject
property is used for similar reasons as @State
, except it's applied to ObservableObject
s. An ObservableObject
is always a reference type (class
) and informs SwiftUI whenever one of its @Published
properties will change.
class DataProvider: ObservableObject {
@Published var currentValue = "a value"
}
struct DataOwnerView: View {
@StateObject private var provider = DataProvider()
var body: some View {
Text("provider value: \(provider.currentValue)")
}
}
Notice that DataOwnerView
creates an instance of DataProvider
. This means that DataOwnerView
owns the DataProvider
. Whenever the value of DataProvider.currentValue
changes, DataOwnerView
will rerender.
Internally, SwiftUI will keep the initially created instance of DataProvider
around whenever SwiftUI decides to discard and recreate DataOwnerView
for a fresh render. This means that a @StateObject
for any given view is initialized only once.
SwiftUI sets the instance associated with your @StateObject
aside and reuses it when the view that owns the @StateObject
is initialized again. This means that your new view instance does not get a new instance of the property marked as @StateObject
since it's reused.
In other words, a property marked as @StateObject
will keep its initially assigned ObservedObject
instance as long as the view is needed, even when the struct gets recreated by SwiftUI.
This is the same behavior you see in @State
, except it's applied to an ObservableObject
rather than a value type like a struct
.
Deciding if you should use @StateObject
You should use @StateObject
if:
- You want to respond to changes or updates in an
ObservableObject
. - The view you're using
@StateObject
in creates the instance of theObservableObject
itself.
@ObservedObject
An @ObservedObject
is used to wrap ObservableObject
instances that are not created or owned by the view that's used in. It's applied to the same types of objects as @StateObject
, and it provides similar features, except a view doesn't create its own @ObservedObject
instances. Instead, they are passed down to views like this:
struct DataOwnerView: View {
@StateObject private var provider = DataProvider()
var body: some View {
VStack {
Text("provider value: \(provider.currentValue)")
DataUserView(provider: provider)
}
}
}
struct DataUserView: View {
@ObservedObject var provider: DataProvider
var body: some View {
// create body and use / modify `provider`
}
}
The DataOwnerView
passes a reference to its @StateObject
down to DataUserView
, where the DataProvider
is used as an @ObservedObject
.
Internally, SwiftUI will not keep an @ObservedObject
around when it discards and recreates a view if this is needed for a fresh render.
Instead, SwiftUI knows that the parent view will pass down an ObservedObject
(which could be either a @StateObject
if the parent owns the property, or an @ObservedObject
if the parent doesn't own the property) that's used as the value for the property marked as @ObservedObject
.
Deciding if you should use @ObservedObject
You should use @ObservedObject
if:
- You want to respond to changes or updates in an
ObservedObject
. - The view does not create the instance of the
ObservedObject
itself. (if it does, you need a@StateObject
)
@EnvironmentObject
Sometimes you have objects that are needed in various places in your app, and you might not want to pass these objects down to the initializer of each view you create. In those cases, you might want to make a dependency available to all children of a view, your App
or a Scene
.
You can achieve this with @EnvironmentObject
:
struct EnvironmentUsingView: View {
@EnvironmentObject var dependency: DataProvider
var body: some View {
Text(dependency.currentValue)
}
}
Properties that are marked as @EnvironmentObject
must conform to ObservableObject
. They are configured by a parent object of the object that uses the @EnvironmentObject
. So for example, we can inject an environment object from the App
struct to make it available for us in all views that we create:
struct MyApp: App {
@StateObject var dataProvider = DataProvider()
var body: some Scene {
WindowGroup {
EnvironmentUsingView()
.environmentObject(dataProvider)
}
}
}
An @EnvironmentObject
shared the same functionality that an @ObservedObject
has. Your view will re-render when one of the @EnvironmentObject
's properties changes. The main difference is that @EnvironmentObject
properties are made available on a much larger scale than @ObservedObject
. In same cases even to your whole app.
Deciding if you should use @EnvironmentObject
You should use @EnvironmentObject
if:
- You would normally use an
@ObservedObject
but you would have to pass theObservableObject
through several view's initializers before it reaches the view where it's needed.
Want to learn more?
- The WWDC 2020 session called Data Essentials in SwiftUI goes in-depth on the topic of property wrappers.
- Chris Eidhof from objc.io captured the essence of choosing which property wrapper to use nicely in this decision chart.