I've encountered an issue where using @Observable
in SwiftUI causes extra initializations and deinitializations when a reference type is included as a property inside a struct. Specifically, when I include a reference type (a simple class Empty {}
) inside a struct (Test
), DetailsViewModel
is initialized and deinitialized twice instead of once. If I remove the reference type, the behavior is correct.
This issue does not occur when using @StateObject
instead of @Observable
. Additionally, I've submitted a feedback report: FB16631081.
Steps to Reproduce
- Run the provided SwiftUI sample code (tested on iOS 18.2 & iOS 18.3 using Xcode 16.2).
- Observe the console logs when navigating to
DetailsView
. - Comment out
var empty = Empty()
in theTest
struct. - Run again and compare console logs.
- Change
@Observable
inDetailsViewModel
to@StateObject
and observe that the issue no longer occurs.
Expected Behavior
The DetailsViewModel
should initialize once and deinitialize once, regardless of whether Test
contains a reference type.
Actual Behavior
With var empty = Empty()
present, DetailsViewModel
initializes and deinitializes twice. However, if the reference type is removed, or when using @StateObject
, the behavior is correct (one initialization, one deinitialization).
Code Sample
import SwiftUI
enum Route {
case details
}
@MainActor
@Observable
final class NavigationManager {
var path = NavigationPath()
}
struct ContentView: View {
@State private var navigationManager = NavigationManager()
var body: some View {
NavigationStack(path: $navigationManager.path) {
HomeView()
.environment(navigationManager)
}
}
}
final class Empty { }
struct Test {
var empty = Empty() // Comment this out to make it work
}
struct HomeView: View {
private let test = Test()
@Environment(NavigationManager.self) private var navigationManager
var body: some View {
Form {
Button("Go To Details View") {
navigationManager.path.append(Route.details)
}
}
.navigationTitle("Home View")
.navigationDestination(for: Route.self) { route in
switch route {
case .details:
DetailsView()
.environment(navigationManager)
}
}
}
}
@MainActor
@Observable
final class DetailsViewModel {
var fullScreenItem: Item?
init() {
print("DetailsViewModel Init")
}
deinit {
print("DetailsViewModel Deinit")
}
}
struct Item: Identifiable {
let id = UUID()
let value: Int
}
struct DetailsView: View {
@State private var viewModel = DetailsViewModel()
@Environment(NavigationManager.self) private var navigationManager
var body: some View {
ZStack {
Color.green
Button("Show Full Screen Cover") {
viewModel.fullScreenItem = .init(value: 4)
}
}
.navigationTitle("Details View")
.fullScreenCover(item: $viewModel.fullScreenItem) { item in
NavigationStack {
FullScreenView(item: item)
.navigationTitle("Full Screen Item: \(item.value)")
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button("Cancel") {
withAnimation(completionCriteria: .logicallyComplete) {
viewModel.fullScreenItem = nil
} completion: {
var transaction = Transaction()
transaction.disablesAnimations = true
withTransaction(transaction) {
navigationManager.path.removeLast()
}
}
}
}
}
}
}
}
}
struct FullScreenView: View {
@Environment(\.dismiss) var dismiss
let item: Item
var body: some View {
ZStack {
Color.red
Text("Full Screen View \(item.value)")
.navigationTitle("Full Screen View")
}
}
}
Console Output
With var empty = Empty() in Test
DetailsViewModel Init
DetailsViewModel Init
DetailsViewModel Deinit
DetailsViewModel Deinit
Without var empty = Empty() in Test
DetailsViewModel Init
DetailsViewModel Deinit
Using @StateObject Instead of @Observable
DetailsViewModel Init
DetailsViewModel Deinit
Additional Notes
This issue occurs only when using @Observable
. Switching to @StateObject
prevents it. This behavior suggests a possible issue with how SwiftUI handles reference-type properties inside structs when using @Observable
.
Using a struct-only approach (removing Empty
class) avoids the issue, but that’s not always a practical solution.
Questions for Discussion
- Is this expected behavior with
@Observable
? - Could this be an unintended side effect of SwiftUI’s state management?
- Are there any recommended workarounds apart from switching to
@StateObject
?
Would love to hear if anyone else has run into this or if Apple has provided any guidance!