SwiftUI’s @State is now a macro


Greetings, traveler!

Apple’s documentation for State has a new shape. It now shows State() as an attached macro:

@attached(accessor, names: named(init), named(get), named(set))
@attached(peer, names: prefixed(`_`), prefixed(__), prefixed(`$`))
macro State()

That looks like a much bigger change than it feels in actual SwiftUI code.

You still write this:

struct PlayButton: View {
    @State private var isPlaying = false

    var body: some View {
        Button(isPlaying ? "Pause" : "Play") {
            isPlaying.toggle()
        }
    }
}

The call site is the same. The rule is also the same: use @State for local state owned by a view, scene, or app. Initialize it where you declare it. Keep it private. Let SwiftUI manage the storage.

That last part is still the part people get wrong. A SwiftUI view is a value. SwiftUI can recreate that value many times. The @State value is different. SwiftUI manages its storage and reconnects the view to that storage when it needs to evaluate the body again. So @State private var count = 0 is not just a stored property inside a struct. It is a request to SwiftUI: keep this value alive for this view identity.

The new macro declaration makes that relationship more visible. The accessor role can provide init, get, and set. The peer role can create related declarations with names prefixed by _, __, and $.

That should look familiar. With property wrappers, we already had the idea of backing storage and projected values:

@State private var count = 0

// mental model:
count   // wrapped value
$count  // binding
_count  // backing storage

SwiftUI can now express State through the macro system while preserving the syntax developers already use.

There is also an important compatibility note in the docs. When you build with Xcode 26 or earlier, SwiftUI uses the State property wrapper instead.

Lazy initialization for observable state

There is one practical change that is easy to miss if you only look at the syntax.

This still looks the same:

@State private var store = StickerStore()

But with the new macro implementation, SwiftUI can initialize the default value lazily. For an observable class stored in @State, the initializer runs when SwiftUI creates the state storage for the view. It does not run again just because the view struct was recreated by its parent.

Before this change, this pattern could do more work than it seemed to:

@Observable
final class ViewModel {
    var index = .zero

    init() {
        print("Init")
    }
}

struct MyView: View {
    @State private var viewModel = ViewModel()

    var body: some View {
        Button("Index: \(viewModel.index)") {
            viewModel.index += 1
        }
    }
}

The state value itself was preserved by SwiftUI, but the default value expression could still run when SwiftUI recreated the view struct. If the initializer did real work, created subscriptions, started observation, allocated caches, or touched disk, that work could happen more often than expected.

With the macro version of @State, this case is much cleaner. The model is created once for the lifetime of that view’s state storage.

This also means that the old optional-state workaround is no longer needed just to avoid repeated default initialization:

@State private var viewModel: ViewModel?

var body: some View {
    MyView(viewModel: viewModel)
        .task {
            viewModel = ViewModel()
        }
}

That workaround still has its place when the model really needs to be created from async work or refreshed from changing input. But if the only reason was avoiding repeated allocations for a default @Observable model, the new @State behavior removes that extra ceremony.

One caveat remains: assigning to @State inside a view initializer is still different from using a default value.

struct MyView: View {
    @State private var viewModel: ViewModel

    init() {
        viewModel = ViewModel()
    }

    var body: some View {
        // ...
    }
}

The view initializer can still run many times. So the model initializer can still run many times too. SwiftUI may keep the original state storage and ignore later assignments for the purpose of preserving state, but your initializer side effects have already happened.

This becomes more dangerous when the model depends on input from the parent:

struct MyView: View {
    @State private var viewModel: ViewModel

    init(id: Item.ID) {
        viewModel = ViewModel(id: id)
    }

    var body: some View {
        // ...
    }
}

If id changes, this code does not automatically mean the state model is now in sync with the new input. For dependency-driven models, you still need to think in SwiftUI terms: pass the dependency directly, derive state from it, or use a pattern such as task(id:) when the model has to be recreated for a new identity.

So the rule becomes a little sharper:

// Good for a model owned by this view.
@State private var store = Store()

// Still suspicious if parent input can change.
init(id: ID) {
    self.store = Store(id: id)
}

The macro makes the common case better. It does not make @State a general dependency injection mechanism.

What the macro shows under the hood

If you expand the new @State macro, the generated code may look less dramatic than the release note sounds.

For a property like this:

@State private var title = ""

the expansion still contains familiar pieces: backing storage, wrappedValue, projectedValue, and the $title binding projection.

The macro does not make the old @State programming model disappear. It preserves the surface model SwiftUI developers already use:

title   // read and write the stored value
$title  // get a Binding to the stored value

The difference is where this behavior comes from. Before this change, @State was expressed as a property wrapper. With the macro version, Swift can generate the accessors and the related peer declarations around the property. The public declaration tells us that directly:

@attached(accessor, names: named(init), named(get), named(set))
@attached(peer, names: prefixed(`_`), prefixed(__), prefixed(`$`))
macro State()

The accessor part explains how reading and writing the property can be routed through generated get and set logic. The peer part explains why the compiler can still synthesize related declarations for the backing storage and the projected value.

So when macro expansion shows something like wrappedValue and projectedValue, I would read it as “Apple kept the existing mental model while moving the implementation behind it.”

@State is now macro-based at the declaration level, but it still behaves like the @State developers already know at the call site.

The macro can control how the initial value is stored and when it is evaluated. For observable class instances, that means SwiftUI can now create the state value lazily, once for the lifetime of the view’s state storage.

The macro is a new implementation path for the old spelling, with one important payoff: default initialization of class-based observable state no longer has to run every time the view struct is recreated.

Here is a simplified version of the macro expansion. The real output contains mangled generated names and internal attributes, but the important pieces are visible here: SwiftUI still generates backing storage, a projected value for $title, and accessors that route reads and writes through wrappedValue.

@State private var title = ""

// Macro-generated accessor for `title`
{
    get {
        __title.wrappedValue
    }

    nonmutating set {
        __title.wrappedValue = newValue
    }
}

// Macro-generated storage
private var __title = SwiftUICore.State._makeStorage {
    ""
}

@SwiftUICore._StatePropertyWrapperStorage(initialValue: "...")
private var _title: SwiftUICore.State<_>! = SwiftUICore._stateNil {
    SwiftUICore.State(initialValue: "")
}

@SwiftUICore._StateProjectedValue
private var $title = SwiftUICore.State(initialValue: "").projectedValue

@SwiftUICore._StateInitialStoredValue("...")
private static var _initialStoredValue =
    SwiftUICore.State._makeStorage(initialValue: "")

// Accessors for the property-wrapper-like storage
{
    @storageRestrictions(initializes: __title)
    init(initialValue) {
        if initialValue == nil {
            __title = Self._initialStoredValue
        } else {
            __title = SwiftUICore.State._makeStorage(
                initialValue: initialValue.wrappedValue
            )
        }
    }

    get {
        SwiftUICore.State(initialValue: __title.wrappedValue)
    }

    set {
        if newValue != nil {
            __title = SwiftUICore.State._makeStorage(
                initialValue: newValue.wrappedValue
            )
        }
    }
}

// Accessor for `$title`
{
    get {
        __title.projectedValue
    }
}

// Lazy initial stored value
{
    get {
        SwiftUICore.State._makeStorage {
            ""
        }
    }
}

@Observable

Apple shows storing an @Observable object in @State like it was before:

@Observable
class Library {
    var name = "My library of books"
}

struct ContentView: View {
    @State private var library = Library()

    var body: some View {
        LibraryView(library: library)
    }
}

Iif the view creates an observable model and owns its lifetime, store the reference in @State.

Then pass the object reference to child views:

struct LibraryView: View {
    var library: Library

    var body: some View {
        Text(library.name)
    }
}

You do not need a binding just to mutate properties on the object. If a child view receives an observable object reference, it can mutate its properties directly:

struct BookCheckoutView: View {
    var book: Book

    var body: some View {
        Button(book.isAvailable ? "Check out book" : "Return book") {
            book.isAvailable.toggle()
        }
    }
}

A binding to the object is useful when the child needs to change the reference stored in state in some other subview or replace the reference itself:

struct ContentView: View {
    @State private var book: Book?

    var body: some View {
        DeleteBookView(book: $book)
            .task {
                book = Book()
            }
    }
}

struct DeleteBookView: View {
    @Binding var book: Book?

    var body: some View {
        Button("Delete book") {
            book = nil
        }
    }
}

That is a different operation. Mutating book.title and setting book = nil are not the same kind of state change.

If a child view needs a binding to a specific property of an observable object, use @Bindable:

struct BookEditorView: View {
    @Bindable var book: Book

    var body: some View {
        TextField("Title", text: $book.title)
    }
}

That is probably the cleanest rule to remember:

// This view owns local value state.
@State private var count = 0

// This view owns an Observable reference.
@State private var book = Book()

// This child can replace the parent's value or reference.
@Binding var book: Book?

// This child needs bindings to properties of an Observable object.
@Bindable var book: Book

Conclusion

Macros also have their own cost. Anyone who has used custom macros in a real project knows that tooling and compile times are part of the story. A macro can remove boilerplate from source code and still move complexity into the build. So I would not describe this as a free win.

But Apple moving State to a macro is still worth paying attention to. @State is not a niche API. It sits inside a huge amount of SwiftUI code. If Apple is comfortable expressing something this central through macros, that tells us something about where SwiftUI and Swift are going.

The interesting part is that SwiftUI’s state system is becoming more openly tied to Swift’s macro infrastructure. The syntax stays boring. Good. Boring is exactly what you want from a state API that sits inside almost every SwiftUI view you write.