Skip to main content

Command Palette

Search for a command to run...

SwiftUI Property Wrappers (With Examples)

Updated
5 min read
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 count changes

  • Never pass @State directly 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 → @StateObject

  • Inject → @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 @Observable and @Bindable

  • Stronger 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:

  • health will never go below 0

  • health will never exceed 100


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