SwiftUI Property Wrappers (With Examples)

If you understand property wrappers well, SwiftUI suddenly becomes predictable, testable, and easy to reason about.
This blog explains:
What property wrappers are
The most commonly used SwiftUI property wrappers
When to use which one
What’s new or improved in recent iOS versions
How to create your own custom property wrapper
What Is a Property Wrapper?
A property wrapper is a Swift feature that adds behavior to a variable.
In SwiftUI, property wrappers:
Manage state
Control data flow
Trigger UI updates automatically
Instead of manually updating the UI, SwiftUI watches these wrappers and re-renders the view when needed.
Core SwiftUI Property Wrappers (Most Used)
@State – Local View State
Use @State when the value:
Belongs only to the current view
Can change over time
Should trigger a UI update
Example:
struct CounterView: View {
@State private var count = 0
var body: some View {
VStack {
Text("Count: \(count)")
Button("Increase") {
count += 1
}
}
}
}
Important points:
SwiftUI owns the storage
The view is re-rendered when
countchangesNever pass
@Statedirectly to another view
@Binding – Two-Way Data Connection
@Binding lets a child view read and modify state owned by a parent view.
Parent view:
struct ParentView: View {
@State private var isOn = false
var body: some View {
ToggleView(isOn: $isOn)
}
}
Child view:
struct ToggleView: View {
@Binding var isOn: Bool
var body: some View {
Toggle("Enable", isOn: $isOn)
}
}
Key idea:
Parent owns the data
Child edits it safely
@ObservedObject – Observing External Objects
Use this when:
The data lives outside the view
The view does NOT own the object’s lifecycle
class UserViewModel: ObservableObject {
@Published var name = "Amar"
}
struct ProfileView: View {
@ObservedObject var viewModel: UserViewModel
var body: some View {
Text(viewModel.name)
}
}
Important:
The object may be recreated if the view reloads
Not suitable for ownership
@StateObject – Owning an Observable Object
Use @StateObject when:
The view creates and owns the observable object
The object must survive view re-renders
struct ProfileView: View {
@StateObject private var viewModel = UserViewModel()
var body: some View {
Text(viewModel.name)
}
}
Rule of thumb:
Create →
@StateObjectInject →
@ObservedObject
@EnvironmentObject – Shared App-Wide Data
Use this for shared data like:
User session
App settings
Theme configuration
Setup:
@main
struct MyApp: App {
@StateObject var settings = AppSettings()
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(settings)
}
}
}
Usage anywhere:
@EnvironmentObject var settings: AppSettings
Warning:
App crashes if the object is not injected
Best for truly global state
@Environment – System Values
Use this to read system-provided values.
Example:
@Environment(\.colorScheme) var colorScheme
@Environment(\.scenePhase) var scenePhase
Good for:
Dark / light mode
App lifecycle handling
Accessibility settings
Persistence Property Wrappers
@AppStorage – UserDefaults Made Easy
Automatically syncs data with UserDefaults.
@AppStorage("isLoggedIn") var isLoggedIn = false
Use for:
User preferences
Feature flags
Small pieces of data
@SceneStorage – Per-Window State
Stores state per scene or window.
@SceneStorage("selectedTab") var selectedTab = 0
Useful for:
iPad multi-window apps
Restoring UI state after app relaunch
Interaction & UI Helpers
@FocusState – Manage Keyboard Focus
@FocusState private var isFocused: Bool
TextField("Name", text: $name)
.focused($isFocused)
Useful for:
Forms
Automatically focusing fields
Dismissing keyboard
@GestureState – Temporary Gesture Values
Used for values that should reset automatically.
@GestureState private var dragOffset = CGSize.zero
Perfect for:
Drag gestures
Temporary UI movement
@ScaledMetric – Accessibility-Friendly Sizes
Automatically scales values based on user’s text size.
@ScaledMetric var padding: CGFloat = 16
Helps build:
Accessible layouts
Dynamic spacing
Data Fetching
@FetchRequest – Core Data Integration
Fetches Core Data objects and updates the UI automatically.
@FetchRequest(
sortDescriptors: [NSSortDescriptor(keyPath: \Item.name, ascending: true)]
)
var items: FetchedResults<Item>
Modern SwiftUI Wrappers (iOS 17+)
@Observable – Simpler Observable Models
Removes the need for ObservableObject and @Published.
@Observable
class AppSettings {
var username = ""
}
SwiftUI tracks changes automatically.
@Bindable – Bind to Observable Models
Allows binding to properties inside @Observable models.
@Bindable var settings: AppSettings
Without this, $settings.property won’t work.
What’s New or Improved in Recent iOS Versions (Including iOS 26)
While recent iOS versions focus more on performance, stability, and tooling, there are some important improvements worth noting:
Better performance for state updates in large SwiftUI views
Improved diagnostics for unnecessary view re-renders
Deeper integration of
@Observableand@BindableStronger compile-time safety around data flow
Rather than adding many new wrappers, Apple is doubling down on making existing ones faster and safer.
How to Create a Custom Property Wrapper
Sometimes you want custom behavior around a property.
That’s when you create your own property wrapper.
Step 1: Define the Wrapper
@propertyWrapper
struct Clamped {
private var value: Int
private let range: ClosedRange<Int>
init(wrappedValue: Int, _ range: ClosedRange<Int>) {
self.range = range
self.value = min(max(wrappedValue, range.lowerBound), range.upperBound)
}
var wrappedValue: Int {
get { value }
set {
value = min(max(newValue, range.lowerBound), range.upperBound)
}
}
}
Step 2: Use the Wrapper
struct PlayerView {
@Clamped(0...100) var health: Int = 120
}
Result:
healthwill never go below0healthwill never exceed100
Why This Is Powerful
Property wrappers:
Encapsulate logic
Reduce repetition
Improve readability
Work beautifully with SwiftUI
SwiftUI itself is built using this same concept.
SwiftUI property wrappers are not just syntax sugar.
They define how data moves, who owns it, and when UI updates happen.
If you master:
@State@Binding@StateObject@ObservedObject@EnvironmentObject@Observable
You’ll avoid:
Random UI bugs
State resets
Performance issues
Architecture confusion



