In this article, we will explore several property wrappers that are frequently used and crucial in SwiftUI development. This article aims to provide an overview of the main functions and usage considerations of these property wrappers, rather than a detailed usage guide.
This article is written at the request of several friends, intending to help developers who are familiar with general programming but relatively new to SwiftUI, to quickly understand the core functions and applicable scenarios of these property wrappers.
- @AppStorage, @SceneStorage, @FocusState, @GestureState, @ScaledMetric
- @FetchRequest, @SectionedFetchRequest, @Query, @Namespace, @Bindable
- @UIApplicationDelegateAdaptor, @AccessibilityFocusState, @FocusedObject, @FocusedValue, @FocusedBinding
1 @State
@State
is one of the most commonly used property wrappers in SwiftUI, primarily used for managing private data within a view. It’s particularly suited for storing value-type data such as strings, integers, enums, or struct instances.
@State
is used to manage a view’s private state.- It’s mainly used for storing value-type data (lifespan consistent with the view).
1.1 Typical Use Cases
@State
is the ideal choice when a view update is triggered by changes in data within the view.- It’s commonly used for simple UI component state management, such as switch states, text input, etc.
- If data does not require complex cross-view sharing,
@State
can simplify state management.
1.2 Considerations
-
Try to use
@State
only within the view, and consider it as a private property of the view, even if not explicitly marked asprivate
. -
@State
provides a two-way data binding pipeline for wrapped data, accessible using the$
prefix. -
@State
is not suitable for storing large amounts of data or complex data models; in these cases,@StateObject
or other state management solutions are more appropriate. -
Property wrappers are essentially structs. The
@
prefix is used to wrap other data; without@
, it represents its own type. For more details, refer to John Sundell and Antoine van der Lee, or read @State Research in SwiftUI. -
When assigning values in the constructor, access the raw value of
@State
using an underscore_
for assignment.
@State var name: String
init(text: String) {
// Assign to the underscore version, wrapping it with the State type itself
_name = State(wrappedValue: text)
}
-
@State
variables can only be assigned once in the view’s constructor, and subsequent adjustments should be made within the view’sbody
. See How to Avoid Repeating SwiftUI View Updates. -
If there’s no need to modify the value in the current view or in child views (through
@Binding
), there’s no need to use@State
. -
In some cases,
@State
is also used to store non-value types, such as reference types, to ensure their uniqueness and lifespan.
@State var textField: UITextField?
TextField("", text: $text)
.introspect(.textField, on: .iOS(.v17)) {
// Holding the UITextField instance
self.textField = $0
}
@State
in the Observation framework ensures that@Observable
instances have a lifespan no shorter than the view itself. For detailed information, see A Deep Dive Into Observation: A New Way to Boost SwiftUI Performance.@State
is thread-safe and can be modified on non-main threads.
@State var text: String = ""
Button("Change") {
// No need to switch back to the main thread
Task.detached {
text = "hi"
}
}
2 @Binding
@Binding
is a property wrapper in SwiftUI used for implementing two-way data binding. It creates a two-way connection between a value (like a Bool) and the UI elements that display and modify these values.
@Binding
does not hold the data directly but provides a wrapper for read and write access to other data sources.- It allows UI elements to directly modify the data and reflect these changes.
2.1 Typical Use Cases
@Binding
is mainly used with UI components that support two-way data binding, such as in combination withTextField
,Stepper
,Sheet
, andSlider
.- It is suitable for situations where you need to directly modify the data in a parent view from a child view.
2.2 Considerations
-
Use
@Binding
cautiously; it’s unnecessary if a child view only needs to respond to data changes without modifying them. -
In complex view hierarchies, passing
@Binding
through multiple levels can make data flow hard to track, in which case other state management methods should be considered. -
Ensure the data source for
@Binding
is reliable, as an incorrect data source can lead to inconsistencies or application crashes. Since@Binding
is just a conduit, it doesn’t guarantee that the corresponding data source will exist when called. -
Developers can customize Binding by providing
get
andset
methods.
let binding = Binding<String>(
get: { text },
// Limit the length of the string
set: { text = String($0.prefix(10)) }
)
- Creating extensions for
Binding
type can greatly enhance development efficiency and flexibility. For more, read: SwiftUI Binding Extensions.
// Convert a Binding<V?> to a Binding<Bool>
extension Binding {
static func isPresented<V>(_ value: Binding<V?>) -> Binding<Bool> {
Binding<Bool>(
get: { value.wrappedValue != nil },
set: {
if !$0 { value.wrappedValue = nil }
}
)
}
}
-
In the Observation framework,
@Bindable
can be used to create a correspondingBinding
interface for@Observable
instances. For details, see A Deep Dive Into Observation: A New Way to Boost SwiftUI Performance. -
When declaring constructor parameters, the wrapped value type of
Binding
(return type of theget
method) needs to be explicitly specified, likeBinding<String>
. -
@Binding
is not an independent data source. It’s merely a reference to already existing data. The view is only updated when theget
method reads values that can trigger a view update (like @State, @StateObject), which is crucial for customBinding
.
struct Test: View {
let a = A()
var body: some View {
let binding = Binding<String>(
get: { a.name },
set: { a.name = $0 }
)
// Although A conforms to the ObservableObject protocol, since it's not associated with the view using StateObject, the Binding created for its properties will also not trigger a view update
Text(binding.wrappedValue)
TextField("input:", text: binding)
}
class A: ObservableObject {
@Published var name: String = ""
}
}
3 @StateObject
@StateObject
is a property wrapper in SwiftUI for managing instances of objects conforming to the ObservableObject
protocol. It ensures these instances have a lifecycle that is at least as long as the current view’s lifecycle.
@StateObject
is specifically used for managing instances that conform to theObservableObject
protocol.- The annotated object instance remains unique throughout the entire lifecycle of the view, meaning it won’t be recreated even if the view updates.
3.1 Typical Use Cases
@StateObject
is typically used at the top of the view hierarchy to create and maintainObservableObject
instances.- It’s commonly used for data models or business logic that need to persist throughout the entire lifecycle of the view.
- Compared to
@State
,@StateObject
is more suitable for managing complex data models and their associated logic.
3.2 Considerations
-
The conditions triggering a view update with
@StateObject
include assigning values to properties marked with@Published
(regardless of whether the new value is different from the old) and invoking theobjectWillChange
publisher. -
Use
@StateObject
only in views that must respond to changes in instance properties. If you only need to read data without observing changes, consider other options. -
Introducing
@StateObject
implies that all related operations occur on the main thread (as SwiftUI implicitly adds@MainActor
to the view), including asynchronous operations. Code that needs to run on a non-main thread should be separated from the view code.
struct B: View {
// Using StateObject is equivalent to adding @MainActor to the current view
@StateObject var store = Store()
var body: some View {
Button("Main Thread") {
Task.detached {
await printThreadName()
// output <_NSMainThread: 0x60000170c000>{number = 1, name = main}
}
}
}
func printThreadName() async {
print(Thread.current)
}
}
- If an instance is created in a context where the view’s lifespan is guaranteed (such as at the app level), and there is no need to respond to changes in that instance’s properties at the current level,
@StateObject
may not be necessary.
struct DemoApp: App {
// Since the lifespan of the view at this level is consistent with the application, and if there's no need to respond to changes in 'store', StateObject is not required
let store = Store()
var body: some Scene {
WindowGroup {
Test()
.environmentObject(store)
}
}
}
4 @ObservedObject
@ObservedObject
is a property wrapper in SwiftUI used to create a connection between a view and instances of ObservableObject
, mainly for introducing external ObservableObject
instances during the view’s lifespan.
@ObservedObject
does not own the observed instance and does not guarantee its lifespan.@ObservedObject
can switch its associated instance during the view’s lifespan.
4.1 Typical Use Cases
- Often used in conjunction with
@StateObject
, where a parent view creates an instance using@StateObject
, and a child view introduces this instance through@ObservedObject
, responding to changes in the instance.
- Suitable for scenarios where dynamic switching of instances is needed. For example, in a
NavigationSplitView
, selecting different instances in the sidebar dynamically changes the data source in the detail view. For more details, please read StateObject and ObservedObject.
// Define a data model conforming to the ObservableObject protocol
class DataModel: ObservableObject, Identifiable {
let id = UUID()
}
struct MyView: View {
@State private var items = [DataModel(), DataModel()]
var body: some View {
VStack {
// Switch the DataModel instance associated with MySubView
Button("Replace Model") {
items.reverse()
}
MySubView(model: items.first!)
}
}
}
// Subview
struct MySubView: View {
// Introduce an external ObservableObject instance with @ObservedObject
@ObservedObject var model: DataModel
var body: some View {
VStack {
// Display the UUID of the current DataModel instance
// When the 'items' array in MyView changes, the displayed UUID here will update, showcasing the dynamic switching capability of @ObservedObject
Text(model.id.uuidString)
}
}
}
- Used in views to introduce
ObservableObject
instances whose lifespans are ensured by external frameworks or code, such as introducingNSManagedObject
instances from Core Data.
4.2 Considerations
- In iOS 13, due to the absence of
@StateObject
,@ObservedObject
was the only choice, which could lead to unexpected results due to the inability to guarantee the lifespan of the instance. To avoid such issues, one could hold the instance using@State
in a higher-level view (where stability isn’t a concern), and then introduce it in the view where it’s used via@ObservedObject
. - When introducing instances of
ObservableObject
provided by third parties, it’s crucial to ensure that the object referenced by@ObservedObject
is available throughout the entire lifespan of the view. Otherwise, it might lead to runtime errors.
5 @EnvironmentObject
@EnvironmentObject
is a property wrapper used in SwiftUI to create a connection between the current view and an ObservableObject
instance passed down through the environment from a higher-level view. It provides a convenient way to introduce shared data across different view hierarchies without explicitly passing it through each view’s constructor.
5.1 Typical Use Cases
- Ideal for sharing the same data model across multiple views, such as user settings, themes, or application states.
- Suitable for building complex view hierarchies where multiple views need access to the same
ObservableObject
instance.
5.2 Considerations
- Before using
@EnvironmentObject
, ensure that the corresponding instance has been provided upstream in the view hierarchy (using the.environmentObject
modifier). Failure to do so will result in a runtime error. - The conditions that trigger view updates for
@EnvironmentObject
are the same as those for@StateObject
and@ObservedObject
.
- Like
@ObservedObject
,@EnvironmentObject
supports dynamically switching the associated instance.
struct MyView: View {
@State private var items = [DataModel(), DataModel()]
var body: some View {
VStack {
Button("Replace Model") {
// Switch the instance associated with the child view MySubView
items.reverse()
}
MySubView()
.environmentObject(items.first!)
}
}
}
struct MySubView: View {
@EnvironmentObject var model: DataModel // Dynamically switch associated instance
var body: some View {
VStack {
Text(model.id.uuidString)
}
}
}
- Only introduce
@EnvironmentObject
when necessary, as it can trigger unnecessary view updates. Often, multiple views from different levels observe and respond to the same instance, and proper optimization is required to avoid performance degradation in the application. This is a reason why many developers are wary of@EnvironmentObject
. - In a view hierarchy, only one instance of the same type of environment object is effective.
@StateObject var a = DataModel()
@StateObject var b = DataModel()
MySubView()
.environmentObject(a) // The one closer to the view is effective
.environmentObject(b)
6 @Environment
@Environment
is a property wrapper used by views to read, respond to, and invoke specific values from the environment. It allows views to access data, instances, or methods provided by SwiftUI or the app environment.
6.1 Typical Use Cases
- When needing to access and respond to environment values provided by the system or higher-level views, such as interface style (dark/light mode), device orientation, or font size (typically corresponding to value types).
- When accessing and invoking SwiftUI’s ModelContext (corresponding to reference types).
- When using methods provided by the system, such as
dismiss
oropenURL
(encapsulated via the struct’scallAsFunction
method).
6.2 Considerations
- Compared to the complex logic handled by instances provided by
@EnvironmentObject
, data introduced by@Environment
typically has more specific functionality. - Developers can create custom environment values by defining a custom
EnvironmentKey
. Like system-provided environment values, they can define various types (value types, Bindings, reference types, methods). For more details, see Custom SwiftUI Environment Values Cheatsheet.
public struct ContainerEnvironmentKey: EnvironmentKey {
// Default value for the example environment key
public static var defaultValue = ContainerEnvironment(containerName: "Default")
}
public extension EnvironmentValues {
var overlayContainer: ContainerEnvironment {
get { self[ContainerEnvironmentKey.self] }
set { self[ContainerEnvironmentKey.self] = newValue }
}
}
- In SwiftUI, the definition style similar to
EnvironmentKey
is used in many ways. Once mastered, it’s easy to grasp others, such asPreferenceKey
(for child-to-parent view communication),FocusedValueKey
(for values based on focus), andLayoutValueKey
(for child-to-layout container communication). - Due to the presence of default values,
@Environment
won’t cause an app crash due to missing values, but this can also lead to developers forgetting to inject values. - Unlike
@EnvironmentObject
, lower-level views cannot modify theEnvironmentValue
values passed down from ancestor views. - Multiple properties of the same type but with different names can be created in
EnvironmentValue
by defining differentEnvironmentKeys
.
Summary
@StateObject
,@ObservedObject
, and@EnvironmentObject
are specifically used for associating instances that conform to theObservableObject
protocol.- While
@StateObject
can sometimes replace@ObservedObject
and offer similar functionality, they each have unique use cases.@StateObject
is typically used for creating and maintaining instances, whereas@ObservedObject
is for introducing and responding to already existing instances. - In environments with iOS 17+ where applications primarily rely on the Observation and SwiftData frameworks, the use of these three property wrappers might be relatively less frequent.
@State
and@Environment
are not limited to storing value types but can also be used for other types.@Environment
provides a relatively safer method to introduce environmental data because it can offer default values throughEnvironmentValue
. This reduces the risk of application crashes due to missing data injections.- In the context of the Observation framework,
@State
and@Environment
become the primary property wrappers. Whether it’s value types or@Observable
instances, both can be introduced into views through these wrappers. - Custom Bindings offer great flexibility, allowing developers to implement complex logic between data sources and UI components that depend on Bindings with concise code.
Each property wrapper has its unique use cases and advantages. Choosing the right tool is crucial for building efficient and maintainable SwiftUI applications. As often mentioned in software development, no single tool is a panacea, but using them appropriately can greatly enhance our development efficiency and application quality.