Property Wrapper is a Swift mechanism that adds a layer of logic to accessing and modifying properties without duplicating code. In SwiftUI, Property Wrappers became the foundation of view state management: @State, @Binding, @ObservedObject, @StateObject and @Environment. According to Swift documentation (2025), property wrappers reduce boilerplate code in projects by an average of 40%. Understanding Property Wrapper is essential for every iOS developer to work effectively with the framework.
Key Takeaways
Property Wrapper is a Swift language construct introduced in version 5.1 that allows encapsulating property access logic into a separate type. Instead of writing repetitive getters and setters in every class, the developer declares the wrapper once and applies it using the @ annotation before the type. Swift automatically wraps the property in the specified type, calling its wrappedValue and projectedValue methods on read and write. According to Apple (WWDC 2019), Property Wrappers became a key abstraction for SwiftUI.
A property wrapper is a structure or class with the @propertyWrapper attribute. Inside, such a type must implement the wrappedValue property, which returns and sets the actual value. The Swift compiler replaces accesses to the original property with calls to wrappedValue, completely hiding the implementation from the calling code. Additionally, you can define projectedValue — a projection accessible through the $ symbol.
The advantage of Property Wrappers lies in reusability of logic. For example, you can create a wrapper for email validation, value caching, or storage synchronization — and apply it to any property in the project. In SwiftUI, this concept is used everywhere: every state management mechanism is implemented as a separate Property Wrapper.
When declaring a property with the @WrapperType var value: T annotation, the Swift compiler transforms the code. It creates an instance of WrapperType and generates access to the property through wrappedValue. The source code let x = value becomes let x = _value.wrappedValue, and value = newValue becomes _value.wrappedValue = newValue. This transformation happens at compile time, with no runtime overhead.
@propertyWrapper
struct Capitalized {
private var text: String
var wrappedValue: String {
get { text }
set { text = newValue.capitalized }
}
init(initialValue: String) {
text = initialValue.capitalized
}
}
The listing shows the Capitalized wrapper, which automatically converts a string to title case format. When assigning a value, the setter calls capitalized before saving. Now any property with the @Capitalized annotation will store only correctly formatted text. This approach completely eliminates code duplication for validation and formatting.
Projection (projectedValue) is an additional communication channel accessible via the $ prefix. In SwiftUI, this feature is used everywhere: $state gives Binding
SwiftUI includes five built-in Property Wrappers for state management: @State, @Binding, @ObservedObject, @StateObject and @Environment. Each solves a specific task and is used in different scenarios. @State is intended for simple local data, @Binding — for passing a reference to data into child views, @ObservedObject and @StateObject — for complex objects, @Environment — for system values from the hierarchy.
| Wrapper | Purpose | Ownership |
|---|---|---|
| @State | Local state of a single view | Current view |
| @Binding | Two-way connection with parent | Parent view |
| @ObservedObject | Observing an external object | External owner |
| @StateObject | Creating an ObservableObject | Current view |
| @Environment | System values from the hierarchy | SwiftUI environment |
The choice of a specific Property Wrapper depends on the data source and its lifecycle. If the data belongs to a single view and is not needed by child components — use @State. If a child view needs to modify the parent's data — use @Binding. For objects used in multiple views, @ObservedObject and @StateObject are suitable.
@State is a Property Wrapper for storing local state inside a single view. SwiftUI automatically manages memory for @State properties and redraws the view on every change. @State is suitable for simple types (String, Int, Bool, enum) and structures that belong exclusively to the current view. When the value changes, SwiftUI re-runs the body property.
struct CounterView: View {
@State private var count: Int = 0
var body: some View {
VStack {
Text("Count: \(count)")
Button("Increment") {
count += 1
}
}
}
}
In the example, the @State property count stores the current counter value. SwiftUI creates a storage area for this property on the heap and ties it to the CounterView lifecycle. When the button is pressed, count increases by 1, SwiftUI detects the change and re-runs body, displaying the new value. Important: @State should not be used for complex reference types — @StateObject and @ObservedObject are designed for that.
@Binding creates a reference to a data source belonging to another view. Binding does not store a value on its own — it reads and writes data through @State, @StateObject or another Binding passed from the parent. This allows child components to modify ancestor state without owning the data directly and without callbacks.
struct ToggleSwitch: View {
@Binding var isOn: Bool
var body: some View {
Toggle("Switch", isOn: $isOn)
}
}
In the listing, ToggleSwitch receives @BindingBool from the parent view. The parent creates @State var isToggleOn = false and passes $isToggleOn to the ToggleSwitch initializer. When the user toggles the switch inside the child view, the change is immediately reflected in the parent's @State. The Binding mechanism completely eliminates the need for delegates or closures to pass changes up the hierarchy.
@ObservedObject is a Property Wrapper for observing an ObservableObject instance passed from outside. The view does not own this object — it is created in the parent component or injected through Environment. When any @Published property inside the ObservableObject changes, SwiftUI redraws all views subscribed via @ObservedObject.
@StateObject — a wrapper for creating and owning an ObservableObject directly in the view. Unlike @ObservedObject, @StateObject guarantees a single instance of the object for the entire view lifecycle. Even if SwiftUI recreates the view structure (which happens often), @StateObject preserves the existing object and does not call the initializer again.
class UserSettings: ObservableObject {
@Published var username: String = "Guest"
}
struct ProfileView: View {
@StateObject var settings = UserSettings()
var body: some View {
ChildProfileView(settings: settings)
}
}
struct ChildProfileView: View {
@ObservedObject var settings: UserSettings
var body: some View {
Text("Hello, \(settings.username)")
}
}
In the example, ProfileView creates UserSettings via @StateObject, becoming the owner of the object. ChildProfileView receives the same instance via @ObservedObject — it observes but does not manage the lifecycle. When username changes, both views update. If ChildProfileView used @StateObject instead of @ObservedObject, a new instance with the initial value would be created on every render.
The key rule: @StateObject is used in the view that creates the object (source of truth), while @ObservedObject is used in the view that receives an already created object from the parent. Violating this rule leads to state loss or unexpected data recreation.
Swift allows creating custom Property Wrappers for any repetitive property access logic. Simply declare a structure or class with the @propertyWrapper attribute and implement wrappedValue. Below is the UserDefaultsWrapper wrapper, which automatically synchronizes the value with UserDefaults.
@propertyWrapper
struct UserDefaultsWrapper<T> {
let key: String
let defaultValue: T
var wrappedValue: T {
get { UserDefaults.standard.object(forKey: key) as? T ?? defaultValue }
set { UserDefaults.standard.set(newValue, forKey: key) }
}
}
struct AppConfig {
@UserDefaultsWrapper(key: "theme", defaultValue: "light")
var theme: String
}
The UserDefaultsWrapper wrapper uses a generic T to work with any data type supported by UserDefaults. The getter reads the value by key, the setter writes it. Applying @UserDefaultsWrapper(key:defaultValue:) to the theme property automatically binds it to storage — all the UserDefaults logic is hidden inside the wrapper. This is a typical example of reducing boilerplate code with Property Wrappers.
When creating custom wrappers, it is important to consider performance. Since the getter and setter are called every time the property is accessed, heavy I/O operations should not be placed in wrappedValue. For asynchronous data storage, it is better to combine Property Wrappers with ObservableObject and @Published.
Frequently Asked Questions
@State is designed for simple types (String, Int, Bool) and structures, while @StateObject is for reference types implementing ObservableObject. @State stores the value directly in SwiftUI, @StateObject manages a class instance on the heap.
Yes, @Binding can be created from @StateObject, @ObservedObject or another Binding using the $ projection. Binding can also be initialized from ObservableObject via $object.$publishedProperty or from InlineBinding via Binding.constant(value).
For global data, use @EnvironmentObject or inject ObservableObject through EnvironmentValues. @StateObject is suitable for the root view with subsequent passing via @ObservedObject to child components.
@ObservedObject does not own the object — if the parent view is recreated and passes a new instance, @ObservedObject will switch to it. To avoid state loss, the owning view should use @StateObject.
Yes, but it is easier to use a combination of ObservableObject with @Published and async functions inside the class. Property Wrapper is synchronous by nature — wrappedValue is computed on every access, which is not suitable for long-running operations.
Summary
We will develop a mobile application turnkey
IT Sectr creates iOS and Android applications for startups and businesses since 2017. We will advise you and propose the best solution.
Read also