
Apple recently updated the documentation for State and now describes it as a macro. The public declaration looks verbose: @attached(accessor, names: named(init), named(get), named(set)) @attached(peer, names: prefixed(`_`), prefixed(__), prefixed(`$`)) macro State() \ At first glance, it seems like a big change, but in everyday SwiftUI code, almost nothing changes. You still write @State on a property to hold data that belongs only to the current view or screen. You initialize it where you declare it, keep it private , and let SwiftUI handle storage. Why a Macro? @State has always been “magic.” We access the value ( count ), get a binding via $count , and there is a hidden _count for the backing storage. What has changed is the implementation. Instead of being a special kind of property wrapper, @State is now a macro that generates the familiar pieces: storage, accessors, and the projected binding. For us as developers, the mental model remains the same — the compiler simply generates this code under the hood. Lazy Initialization The most noticeable practical change is how default values are created. In the old implementation, if you stored an object in @State (like a view model) its initializer could run every time the parent view was recreated, even though the state itself persisted. The macro version evaluates the initial value lazily: it calls the initializer once, when the state storage is created, and holds onto that instance. \ Here’s the example from the original article. We have an @Observable class and a view that stores it in @State : @Observable final class ViewModel { var index = 0 init() { print("Init") } } struct MyView: View { @State private var viewModel = ViewModel() var body: some View { Button("Index: \(viewModel.index)") { viewModel.index += 1 } } } \ Before this change, you would see “Init” printed every time MyView was recreated by its parent. Now, the initializer runs once when SwiftUI creates the state storage, and the same ViewModel instance is reused. No More Optional Workaround Developers sometimes worked around repeated initialization by storing an optional and setting it later: @State private var viewModel: ViewModel? var body: some View { MyView(viewModel: viewModel) .task { viewModel = ViewModel() } } With the new macro, this pattern is unnecessary when all you want is lazy initialization. The state will construct the object once, saving you the ceremony. Caution: Assigning in init() One thing hasn’t changed: assigning to @State inside your view’s init() is different from providing a default value. The view’s initializer runs each time the view is recreated, and so will any side effects in it. For example: struct MyView: View { @State private var viewModel: ViewModel init(id: Item.ID) { self.viewModel = ViewModel(id: id) } var body: some View { // ... } } \ If id changes, this code does not automatically update the state model. When a model depends on parent input, you should pass the dependency through the view properties or recreate it with .task(id:) . The macro improves the common case, but @State is still not a general dependency‑injection mechanism. Working With @Observable , @Binding and @Bindable Apple shows the same recommended patterns. If your view creates and owns an @Observable model, store the reference in @State and pass the object reference down to child views: @Observable class Library { var name = "My library of books" } struct ContentView: View { @State private var library = Library() var body: some View { LibraryView(library: library) // pass the object directly } } struct LibraryView: View { var library: Library var body: some View { Text(library.name) // access properties directly } } \ A binding to the object itself ( @Binding ) is only needed when a child view needs to replace the reference or set it to nil : struct ContentView: View { @State private var book: Book? var body: some View { DeleteBookView(book: $book) .task { book = Book() // perhaps loaded asynchronously } } } struct DeleteBookView: View { @Binding var book: Book? var body: some View { Button("Delete book") { book = nil } } } \ If a child view just needs to mutate the properties of an observable object and not replace it, use @Bindable to get bindings to properties of the @Observable object: @Observable class Book { var title = "Default" } struct BookEditorView: View { @Bindable var book: Book var body: some View { TextField("Title", text: $book.title) } } \ To remember these patterns: @State var count = 0 — a local value owned by this view. @State var book = Book() — the view owns a reference to an observable object. @Binding var book: Book? — a child can replace or clear the parent’s reference. @Bindable var book: Book — a child binds to properties of the observable object. Macro Isn’t “Free” Macros can reduce boilerplate, but they also have costs: they affect compile times and debugging. Apple’s choice to express something as fundamental as @State through a macro means they trust this mechanism, but the syntax remains deliberately “boring.” That’s a good thing: for something as pervasive as state, the simpler the API looks, the easier it is to write and maintain code. \
View original source — Hacker Noon ↗


