
Build 10x products in minutes by chatting with AI - beyond just a prototype.
What are property wrappers in Swift?
What is the state property wrapper in Swift?
How to create a property wrapper in SwiftUI?
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:
@propertyWrapper struct ZeroToOne<Value: Comparable & Numeric> { private var value: Value /// A helper to clamp the input between 0 and 1. private static func clamped(_ input: Value) -> Value { // For many numeric types you might define a custom clamping function. // Here we assume Value supports literal 0 and 1. return min(max(input, 0), 1) } init(wrappedValue: Value) { self.value = Self.clamped(wrappedValue) } var wrappedValue: Value { get { value } set { value = Self.clamped(newValue) } } }
Now, you can declare a property that’s automatically clamped:
struct Color { @ZeroToOne var red: Double @ZeroToOne var green: Double @ZeroToOne var blue: Double } var superRed = Color(red: 2, green: 0, blue: 0) print(superRed.red) // Prints: 1.0 superRed.blue = -2 print(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:
private var _red: ZeroToOne<Double> = ZeroToOne(wrappedValue: initialValue) var red: Double { get { _red.wrappedValue } set { _red.wrappedValue = newValue } }
wrappedValue.projectedValue property on your wrapper, Swift synthesizes a corresponding $property accessor.For example, updating our wrapper to expose the raw (unclamped) value:
@propertyWrapper struct ZeroToOneV2<Value: Comparable & Numeric> { private var value: Value init(wrappedValue: Value) { self.value = wrappedValue } var wrappedValue: Value { get { min(max(value, 0), 1) } set { value = newValue } } // Expose the original value via the projected value. var projectedValue: Value { value } }
You can then access both the “clamped” value and the original value:
struct ColorV2 { @ZeroToOneV2 var red: Double } var color = ColorV2(red: 1.5) print(color.red) // Clamped: 1.0 print(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.
Now you can use the wrapper to define static properties in your app settings:
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:
This updated syntax enables you to inject custom logic directly into function parameters.
SwiftUI makes extensive use of property wrappers such as , , and . Recent updates help clarify the difference between owned state and externally provided bindings. For example:
If you create custom wrappers intended for use in views, be cautious about nesting multiple wrappers (e.g. using 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:
Then use it in your view:
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 ), 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:
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.
@State@Binding@StateObject@State@State@State@propertyWrapper
struct UserDefaultBacked<Value> {
private let key: String
private let defaultValue: Value
private var storage: UserDefaults
var wrappedValue: Value {
get {
return storage.object(forKey: key) as? Value ?? defaultValue
}
set {
// For optionals, remove the object if nil is assigned.
if let optional = newValue as? AnyOptional, optional.isNil {
storage.removeObject(forKey: key)
} else {
storage.set(newValue, forKey: key)
}
}
}
// Provide a projected value as the wrapper instance itself.
var projectedValue: UserDefaultBacked<Value> {
self
}
init(wrappedValue: Value, key: String, storage: UserDefaults = .standard) {
self.defaultValue = wrappedValue
self.key = key
self.storage = storage
}
}
/// A helper protocol to detect optionals.
private protocol AnyOptional {
var isNil: Bool { get }
}
extension Optional: AnyOptional {
var isNil: Bool { self == nil }
}extension UserDefaults {
@UserDefaultBacked(key: "has_seen_app_introduction")
static var hasSeenAppIntroduction: Bool = false
@UserDefaultBacked(key: "username")
static var username: String = "Default User"
}@propertyWrapper
struct Debuggable<Value> {
private var value: Value
private let description: String
init(wrappedValue: Value, description: String = "") {
print("Initialized '\(description)' with value \(wrappedValue)")
self.value = wrappedValue
self.description = description
}
var wrappedValue: Value {
get {
print("Accessing '\(description)', returning: \(value)")
return value
}
set {
print("Updating '\(description)' to \(newValue)")
value = newValue
}
}
}
func runAnimation(@Debuggable(description: "Duration") withDuration duration: Double) {
// Example: Call an animation with the debugged duration
print("Animating for \(duration) seconds")
}
runAnimation(withDuration: 2.0)
// Output will show initialization and access logs.struct ContentView: View {
@State private var counter = 0
var body: some View {
VStack {
Text("Counter: \(counter)")
Button("Increment") {
counter += 1
}
}
}
}@propertyWrapper
struct UppercasedState: DynamicProperty {
@State private var value: String
var wrappedValue: String {
get { value }
nonmutating set { value = newValue.uppercased() }
}
init(wrappedValue: String) {
_value = State(initialValue: wrappedValue.uppercased())
}
}struct UppercaseView: View {
@UppercasedState private var text: String = "hello world"
var body: some View {
VStack {
Text(text)
Button("Change Text") {
text = "swift is awesome"
}
}
}
}