Initializing @Observable classes with the @State macro in Xcode 27
Xcode 27 beta is out, and it comes with a range of SwiftUI and Swift updates and improvements. The first thing that caught my eye is a change to @State. Up until now it was a property wrapper conforming to the DynamicProperty protocol, but in Xcode 27 it becomes a Swift macro. In this post we will look at what the change means for @Observable models stored in @State.
# Lazy evaluation of the initial value
Before Xcode 27, we had to be careful when initializing an @Observable class as a default @State value. The @State property wrapper did not support lazy initialization, which meant that the observable's initializer ran every time the view struct was recreated when its parent reevaluated. For frequently changing hierarchies, or when the initialization of the object was not cheap, this affected app performance. Even though SwiftUI discarded redundant instances of the observable straight away and only kept the initial one to preserve view state, the initialization logic of the model still ran. I covered this behavior in Initializing @Observable classes within the SwiftUI hierarchy, where I noted that observable classes with non-trivial initialization assigned as default values for @State could cause repeated, unnecessary work.
With @State becoming a macro, the initial value expression is now evaluated lazily. The observable's initializer runs only once, when SwiftUI sets up the state storage for the view. Subsequent recreations of the view struct do not cause the model's initialization logic to run again.
We can observe the behavior change in the following example, where we add a print statement to CounterViewModel's initializer and trigger CounterView struct recreation by changing the tint color in ContentView.
@Observable
class CounterViewModel {
var count = 0
init() {
print("CounterViewModel initialized")
}
func increment() {
count += 1
}
}
struct ContentView: View {
@State private var tint: Color = .accentColor
var body: some View {
NavigationStack {
CounterView()
.tint(tint)
.toolbar {
ColorPicker("Tint Color", selection: $tint)
.labelsHidden()
}
}
}
}
struct CounterView: View {
@State private var viewModel = CounterViewModel()
var body: some View {
VStack {
Text("Count: \(viewModel.count)")
Button("Increment") {
viewModel.increment()
}
}
}
}
Before Xcode 27, changing the tint color would print "CounterViewModel initialized" on every recreation of the CounterView struct, even though SwiftUI preserved its state. With Xcode 27, the print runs only once, when CounterView is first inserted into the hierarchy.
The workaround of setting up the @State property as an optional and assigning the model instance inside a task(priority:_:) modifier to avoid repeated allocations is no longer necessary.
The new @State behavior is back-deployed to the OS versions where the Observation framework was first introduced: iOS 17, macOS 14, tvOS 17, watchOS 10, and visionOS 1. But to benefit from this change our apps have to be compiled with Xcode 27.
# Initializing @Observable models in a view's init()
We should still avoid the common anti-pattern of assigning an observable instance to @State in the view's initializer, especially if the view's parent reevaluates frequently. In the example below, every time the CounterView initializer runs, CounterViewModel's initializer runs too, even though SwiftUI immediately discards all instances except the first one created.
struct CounterView: View {
@State private var viewModel: CounterViewModel
init() {
viewModel = CounterViewModel()
}
var body: some View {
VStack {
Text("Count: \(viewModel.count)")
Button("Increment") {
viewModel.increment()
}
}
}
}
The pattern is particularly important to avoid when the model depends on a value passed in from a parent view. SwiftUI only uses the assignment in init() to create the state storage the first time the view is inserted into the hierarchy. On subsequent recreations, the assignment in init() runs but SwiftUI ignores it, keeping the existing state value. The model can then hold stale data relative to what the parent is passing in.
To keep the model reactive to changes in its dependencies, we should initialize it outside of init() and use the task(id:priority:_:) pattern instead, which I describe in detail in Managing state with observable models in "The SwiftUI Way" book.
If you are looking to build a strong foundation in SwiftUI, my book SwiftUI Fundamentals takes a deep dive into the framework's core principles and APIs to help you understand how it works under the hood and how to use it effectively in your projects. And my new book The SwiftUI Way helps you adopt recommended patterns, avoid common pitfalls, and use SwiftUI's native tools appropriately to work with the framework rather than against it.
For more resources on Swift and SwiftUI, check out my other books and book bundles.
I’m currently running a WWDC 2026 promotion with 30% off my books, plus additional savings when purchased as a bundle. Visit the books page to learn more.



