@StateObject and view installation

I've come across some odd behavior with regards to classes annotated with the StateObject property wrapper. The body computed property of a View would be invoked prior to the StateObject being installed. This would result in the following warning in Xcode:

Accessing StateObject's object without being installed on a View. This will create a new instance each time.

Once this occurs for a particular View, any instance of that view will always render a new state object instance each time body is invoked regardless of wether or not the view is deallocated and re-created (i.e dismissing the screen that is hosting the view and returning to it).

The only time the error no longer occurs is when the app is restarted. This happens intermittently and I have been unable to pinpoint the reason. I've checked the code and I do not invoke the body property manually in any way. It seems to happen behind the scenes during a view's lifecycle

One common characteristic among affected views I noticed is this seems to happen to StateObjects whose views are nested within a VStack.

Is this is a known bug? I've been encountering this experience with Xcode 12.5 and below

Any insight would be great to help avoid this bad experience for users.

Best,

Can you show a complete code to reproduce the issue?

will update answer*

I cannot provide exact repro steps as the bug is intermittent. But I can provide some general code as to how the StateObjects are used:

import SwiftUI
import Combine

struct RootView: View {
  private let columns: [GridItem] = [
    GridItem(.flexible()),
    GridItem(.flexible())
  ]
  var body: some View {
    ScrollView {
      LazyVGrid(columns: columns) {
        ForEach(0..<20) { _ in
          ContentView()
        }
      }
    }
  }
}

struct ContentView: View {
  @StateObject
  private var viewModel = ViewModel()
   
  var body: some View {
    content(viewModel.viewState)
      .onAppear {
        viewModel.subscribe()
      }
  }
   
  @ViewBuilder
  private func content(_ viewState: ViewState) -> some View {
    switch viewState {
    case .loading:
      ProgressView()
    case .content(let counter):
      Text("\(counter)")
        .padding()
    }
  }
}

extension ContentView {
     
  enum ViewState: ObservableViewState {
    case loading
    case content(Int)
     
    static var initialViewState: ViewState = .loading
  }
   
  enum Input {
    case timerUpdated
  }
   
  enum Action {
    case increment
    case setState(ViewState)
  }
   
  class ViewModel: ObservableObject {
    @Published
    var viewState: ViewState = .loading
    private let inputSubject = PassthroughSubject<Input, Never>()
    private var subscribed = false
    var internalInputPublisher: AnyPublisher<Input, Never> {
      Timer.publish(every: 1, on: RunLoop.main, in: .common)
        .autoconnect()
        .eraseToAnyPublisher()
        .map({ _ in
          Input.timerUpdated
        })
        .eraseToAnyPublisher()
    }
     
    func subscribe() {
      if subscribed {
        return
      }
      subscribed = true
      Publishers.Merge(internalInputPublisher, inputSubject)
        .flatMap { input -> AnyPublisher<Action, Never> in
          switch input {
          case .timerUpdated:
            return Just(.increment)
              .eraseToAnyPublisher()
          }
        }
        .scan(viewState, { currentState, action in
          switch action {
          case .setState(let state):
            return state
          case .increment:
            if case .content(let counter) = currentState {
              return .content(counter + 1)
            } else {
              return .content(1)
            }
          }
        })
        .removeDuplicates()
        .assign(to: &$viewState)
    }
     
    func send(_ input: Input) {
      inputSubject.send(input)
    }

  }
}

pretty much all view models follow this reactive publisher pattern for channeling view state updates to the listening view. The bug can happen to random views throughout the app

@StateObject and view installation
 
 
Q