Property Wrappers in Swift: How `@State`, `@Binding`, and Custom Wrappers Work
If you’ve used SwiftUI, you’ve already seen @State, @Binding, and @AppStorage sprinkled throughout your views.
Those @ symbols aren’t magic — they’re property wrappers, a Swift feature that lets you attach reusable behavior
to any property.
You’ll learn what property wrappers are, how built-in wrappers like @State work under the hood, and how to build your
own. We won’t dive deep into SwiftUI’s state management system — that has its own dedicated post.
What You’ll Learn
- What Is a Property Wrapper?
- How Property Wrappers Work Under the Hood
- Built-In Property Wrappers
- Writing Your Own Property Wrapper
- Common Mistakes
- What’s Next?
What Is a Property Wrapper?
A property wrapper is a type that adds custom behavior every time a property is read or written. Instead of
repeating the same logic everywhere — like clamping a value between a minimum and maximum, or logging every change — you
write that logic once in a wrapper and apply it with an @ annotation.
Apple Docs: @propertyWrapper — Swift Standard Library
Think of Remy from Ratatouille hiding under Linguini’s chef hat. Linguini (the property) looks normal from the outside, but Remy (the wrapper) secretly controls what happens every time Linguini moves. The wrapper controls the property’s behavior without the outside world knowing.
How Property Wrappers Work Under the Hood
Every property wrapper is a struct (or class) marked with @propertyWrapper that has a special property called
wrappedValue. When you use the @ syntax, Swift rewrites your code behind the scenes.
Here’s a simple wrapper that ensures a string is always stored in uppercase:
@propertyWrapper
struct Uppercased {
private var stored: String = ""
var wrappedValue: String {
get { stored }
set { stored = newValue.uppercased() }
}
}
When you use it on a property, Swift transforms your code:
struct PixarCharacter {
@Uppercased var catchphrase: String
}
var buzz = PixarCharacter()
buzz.catchphrase = "To infinity and beyond"
print(buzz.catchphrase)
TO INFINITY AND BEYOND
Behind the scenes, Swift creates a hidden Uppercased instance and routes all reads and writes through its
wrappedValue. You write buzz.catchphrase = "..." — but the wrapper’s set block runs, converting it to uppercase
before storing it.
Tip: The
wrappedValueproperty is the only requirement of@propertyWrapper. Everything else — initializers, extra methods, projected values — is optional.
Built-In Property Wrappers
SwiftUI ships with several property wrappers you’ll use constantly. Here’s what the most common ones do at a high level:
@State
@State tells SwiftUI to manage a piece of data for a view. When the value changes, SwiftUI re-renders the view
automatically.
import SwiftUI
struct MovieRatingView: View {
@State private var rating = 0
var body: some View {
Text("Rating: \(rating) ⭐")
}
}
The @State wrapper stores rating outside the struct’s normal memory so it survives view re-creation. Without it, the
value would reset every time SwiftUI redraws the view.
@Binding
@Binding creates a two-way connection to a @State property owned by a parent view. It doesn’t own the data — it
references someone else’s data.
struct StarButton: View {
@Binding var rating: Int
var body: some View {
Button("Add Star") {
rating += 1
}
}
}
Think of it like the remote control for Buzz Lightyear: pressing buttons on the remote changes Buzz’s behavior directly, because the remote is bound to Buzz.
@AppStorage
@AppStorage reads and writes values to UserDefaults and triggers view updates when they change:
struct SettingsView: View {
@AppStorage("favoriteMovie") var favorite = "Toy Story"
var body: some View {
Text("Favorite: \(favorite)")
}
}
The string "favoriteMovie" is the UserDefaults key. The wrapper handles reading, writing, and notifying SwiftUI of
changes — all in one line.
Writing Your Own Property Wrapper
Let’s build a wrapper that clamps a number between a minimum and maximum value — perfect for things like movie ratings that must stay between 1 and 10.
First, define the wrapper using generics so it works with any Comparable type:
@propertyWrapper
struct Clamped<Value: Comparable> {
private var value: Value
let range: ClosedRange<Value>
var wrappedValue: Value {
get { value }
set { value = min(max(newValue, range.lowerBound), range.upperBound) }
}
init(wrappedValue: Value, _ range: ClosedRange<Value>) {
self.range = range
self.value = min(max(wrappedValue, range.lowerBound), range.upperBound)
}
}
Now use it to keep a movie rating in bounds:
struct Movie {
var title: String
@Clamped(1...10) var rating: Int = 5
}
var nemo = Movie(title: "Finding Nemo")
nemo.rating = 15
print("\(nemo.title): \(nemo.rating)")
nemo.rating = -3
print("\(nemo.title): \(nemo.rating)")
Finding Nemo: 10
Finding Nemo: 1
No matter what value you assign, the wrapper clamps it to the 1–10 range. The init(wrappedValue:_:) pattern lets you
use the natural = 5 syntax for the default value while also passing the range.
The projectedValue
Property wrappers can expose a projected value accessed with a $ prefix. This is how SwiftUI’s $rating creates a
Binding:
@propertyWrapper
struct Tracked<Value> {
private var value: Value
private(set) var changeCount = 0
var wrappedValue: Value {
get { value }
set { value = newValue; changeCount += 1 }
}
var projectedValue: Int { changeCount }
init(wrappedValue: Value) {
self.value = wrappedValue
}
}
struct Character {
@Tracked var mood: String = "Happy"
}
var joy = Character()
joy.mood = "Excited"
joy.mood = "Nervous"
print("Mood: \(joy.mood), changed \(joy.$mood) times")
Mood: Nervous, changed 2 times
Accessing joy.mood gives you the wrappedValue. Accessing joy.$mood gives you the projectedValue — in this case,
how many times the mood changed.
Common Mistakes
Forgetting wrappedValue in the Initializer
If your wrapper’s init doesn’t apply the same logic as the setter, the initial value can bypass your rules:
// ❌ Initial value skips clamping
@propertyWrapper
struct BadClamped {
private var value: Int
var wrappedValue: Int {
get { value }
set { value = min(max(newValue, 0), 100) }
}
init(wrappedValue: Int) {
self.value = wrappedValue // Not clamped!
}
}
// ✅ Apply the same logic in init
@propertyWrapper
struct GoodClamped {
private var value: Int
var wrappedValue: Int {
get { value }
set { value = min(max(newValue, 0), 100) }
}
init(wrappedValue: Int) {
self.value = min(max(wrappedValue, 0), 100)
}
}
Always clamp, validate, or transform in the initializer too — not just in the setter.
Using @State Outside of SwiftUI Views
@State is designed specifically for SwiftUI views. Using it in a regular struct or class won’t trigger any UI updates:
// ❌ @State does nothing useful here
class MovieManager {
@State var currentMovie = "Up"
}
// ✅ Use a regular property or @Published instead
class MovieManager: ObservableObject {
@Published var currentMovie = "Up"
}
@State only works inside a SwiftUI View. For observable classes, use @Published (or the newer @Observable macro
in iOS 17+).
What’s Next?
- Property wrappers attach reusable behavior to properties using
@propertyWrapper - The only requirement is a
wrappedValueproperty — everything else is optional @State,@Binding, and@AppStorageare built-in SwiftUI wrappers- You can build custom wrappers for validation, clamping, logging, and more
- The
$prefix accesses theprojectedValuefor extra capabilities
Ready to see how Swift handles operations that can succeed or fail? Head over to
The Result Type in Swift to learn about Result<Success, Failure>.