Design Converter
Education
Last updated on Mar 10, 2025
•8 mins read
Last updated on Oct 1, 2024
•8 mins read
Since their introduction in Swift 5.1, property wrappers have become an essential tool for reducing boilerplate and encapsulating property logic. In recent Swift releases (Swift 5.8 through 6.0), enhancements include:
@State
and @Binding
) have been refined and new patterns for composing custom wrappers have emerged.In this article, we’ll review the fundamentals, walk through updated custom wrapper examples, and explore advanced topics—all with working code that reflects the latest Swift best practices.
A property wrapper is simply a type (usually a struct or class) marked with the @propertyWrapper
attribute. Its main purpose is to “wrap” an underlying value (the wrapped value) so that you can encapsulate common logic (such as validation, transformation, or persistence) without cluttering your model or view code.
Every property wrapper must expose a property called wrappedValue
. Optionally, it may also define a projectedValue
—this is made available using the dollar-sign syntax (e.g. $myProperty
).
For example, here’s a simple property wrapper that always clamps a numeric value between zero and one:
1@propertyWrapper 2struct ZeroToOne<Value: Comparable & Numeric> { 3 private var value: Value 4 5 /// A helper to clamp the input between 0 and 1. 6 private static func clamped(_ input: Value) -> Value { 7 // For many numeric types you might define a custom clamping function. 8 // Here we assume Value supports literal 0 and 1. 9 return min(max(input, 0), 1) 10 } 11 12 init(wrappedValue: Value) { 13 self.value = Self.clamped(wrappedValue) 14 } 15 16 var wrappedValue: Value { 17 get { value } 18 set { value = Self.clamped(newValue) } 19 } 20}
Now, you can declare a property that’s automatically clamped:
1struct Color { 2 @ZeroToOne var red: Double 3 @ZeroToOne var green: Double 4 @ZeroToOne var blue: Double 5} 6 7var superRed = Color(red: 2, green: 0, blue: 0) 8print(superRed.red) // Prints: 1.0 9 10superRed.blue = -2 11print(superRed.blue) // Prints: 0.0
When you apply a property wrapper to a property, the Swift compiler automatically does three key things:
@ZeroToOne var red: Double
is compiled roughly into:
1private var _red: ZeroToOne<Double> = ZeroToOne(wrappedValue: initialValue) 2var red: Double { 3 get { _red.wrappedValue } 4 set { _red.wrappedValue = newValue } 5}
wrappedValue
.projectedValue
property on your wrapper, Swift synthesizes a corresponding $property
accessor.For example, updating our wrapper to expose the raw (unclamped) value:
1@propertyWrapper 2struct ZeroToOneV2<Value: Comparable & Numeric> { 3 private var value: Value 4 5 init(wrappedValue: Value) { 6 self.value = wrappedValue 7 } 8 9 var wrappedValue: Value { 10 get { min(max(value, 0), 1) } 11 set { value = newValue } 12 } 13 14 // Expose the original value via the projected value. 15 var projectedValue: Value { 16 value 17 } 18}
You can then access both the “clamped” value and the original value:
1struct ColorV2 { 2 @ZeroToOneV2 var red: Double 3} 4 5var color = ColorV2(red: 1.5) 6print(color.red) // Clamped: 1.0 7print(color.$red) // Original stored value: 1.5
This example demonstrates a generic property wrapper that automatically synchronizes a value with UserDefaults. The code below reflects updated initializer naming and working code for Swift 5.9/6.0.
1@propertyWrapper 2struct UserDefaultBacked<Value> { 3 private let key: String 4 private let defaultValue: Value 5 private var storage: UserDefaults 6 7 var wrappedValue: Value { 8 get { 9 return storage.object(forKey: key) as? Value ?? defaultValue 10 } 11 set { 12 // For optionals, remove the object if nil is assigned. 13 if let optional = newValue as? AnyOptional, optional.isNil { 14 storage.removeObject(forKey: key) 15 } else { 16 storage.set(newValue, forKey: key) 17 } 18 } 19 } 20 21 // Provide a projected value as the wrapper instance itself. 22 var projectedValue: UserDefaultBacked<Value> { 23 self 24 } 25 26 init(wrappedValue: Value, key: String, storage: UserDefaults = .standard) { 27 self.defaultValue = wrappedValue 28 self.key = key 29 self.storage = storage 30 } 31} 32 33/// A helper protocol to detect optionals. 34private protocol AnyOptional { 35 var isNil: Bool { get } 36} 37extension Optional: AnyOptional { 38 var isNil: Bool { self == nil } 39}
Now you can use the wrapper to define static properties in your app settings:
1extension UserDefaults { 2 @UserDefaultBacked(key: "has_seen_app_introduction") 3 static var hasSeenAppIntroduction: Bool = false 4 5 @UserDefaultBacked(key: "username") 6 static var username: String = "Default User" 7}
This updated wrapper automatically provides a default value and synchronizes with UserDefaults.
Swift now supports attaching property wrappers to function parameters. For instance, you might create a debugging wrapper:
1@propertyWrapper 2struct Debuggable<Value> { 3 private var value: Value 4 private let description: String 5 6 init(wrappedValue: Value, description: String = "") { 7 print("Initialized '\(description)' with value \(wrappedValue)") 8 self.value = wrappedValue 9 self.description = description 10 } 11 12 var wrappedValue: Value { 13 get { 14 print("Accessing '\(description)', returning: \(value)") 15 return value 16 } 17 set { 18 print("Updating '\(description)' to \(newValue)") 19 value = newValue 20 } 21 } 22} 23 24func runAnimation(@Debuggable(description: "Duration") withDuration duration: Double) { 25 // Example: Call an animation with the debugged duration 26 print("Animating for \(duration) seconds") 27} 28 29runAnimation(withDuration: 2.0) 30// Output will show initialization and access logs.
This updated syntax enables you to inject custom logic directly into function parameters.
SwiftUI makes extensive use of property wrappers such as @State
, @Binding
, and @StateObject
. Recent updates help clarify the difference between owned state and externally provided bindings. For example:
1struct ContentView: View { 2 @State private var counter = 0 3 4 var body: some View { 5 VStack { 6 Text("Counter: \(counter)") 7 Button("Increment") { 8 counter += 1 9 } 10 } 11 } 12}
If you create custom wrappers intended for use in views, be cautious about nesting multiple wrappers (e.g. using @State
inside another wrapper) because the view’s re-rendering is triggered only by changes to the outermost (observed) state. A common pattern is to build a “DynamicProperty” version of your wrapper if you want it to work naturally inside SwiftUI views. For example, here’s how you might update an uppercase wrapper for SwiftUI:
1@propertyWrapper 2struct UppercasedState: DynamicProperty { 3 @State private var value: String 4 5 var wrappedValue: String { 6 get { value } 7 nonmutating set { value = newValue.uppercased() } 8 } 9 10 init(wrappedValue: String) { 11 _value = State(initialValue: wrappedValue.uppercased()) 12 } 13}
Then use it in your view:
1struct UppercaseView: View { 2 @UppercasedState private var text: String = "hello world" 3 4 var body: some View { 5 VStack { 6 Text(text) 7 Button("Change Text") { 8 text = "swift is awesome" 9 } 10 } 11 } 12}
Note that if you nest wrappers (for example, having a model that itself uses a dynamic property wrapper and then storing that model in an outer @State
), the outer wrapper must observe all changes for the view to update properly.
Recent discussions and proposals in the Swift community have focused on several advanced topics:
For now, the best practice is to design your wrappers around clear, single-responsibility tasks (like state transformation, persistence, or validation) and avoid over-nesting wrappers that might obscure the data flow.
Even with recent improvements, keep in mind:
@State
(or other observation mechanisms) may result in the outer wrapper not noticing changes inside the inner one.Property wrappers remain one of Swift’s most powerful features for reducing boilerplate and centralizing property logic. With the latest Swift updates, you now have more robust compiler synthesis, improved support for function parameters, and better integration with SwiftUI. Whether you’re building user defaults managers, debugging tools, or custom SwiftUI state management systems, property wrappers continue to evolve—providing you with flexible and reusable solutions for modern Swift development.
Tired of manually designing screens, coding on weekends, and technical debt? Let DhiWise handle it for you!
You can build an e-commerce store, healthcare app, portfolio, blogging website, social media or admin panel right away. Use our library of 40+ pre-built free templates to create your first application using DhiWise.