SwiftUI @Observable Causes Extra Initializations When Using Reference Type Properties

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

  1. Run the provided SwiftUI sample code (tested on iOS 18.2 & iOS 18.3 using Xcode 16.2).
  2. Observe the console logs when navigating to DetailsView.
  3. Comment out var empty = Empty() in the Test struct.
  4. Run again and compare console logs.
  5. Change @Observable in DetailsViewModel 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!

SwiftUI @Observable Causes Extra Initializations When Using Reference Type Properties
 
 
Q