Should you access @State properties from an NSViewController (AppKit / SwiftUI Integration)?

I'm currently working on a project to integrate some SwiftUI components into an existing AppKit application. The application makes extensive use of NSViewControllers. I can easily bridge between AppKit and SwiftUI using a view model that conforms to ObservableObject and is shared between the NSViewController and the SwiftUI View. But it's kind of tedious creating a view model for every view.

Is it "safe" and "acceptable" for the NSViewController to "hold on" to the SwiftUI View that it creates and then access its @State or @StateObject properties?

The lifecycle of DetailsView, a SwiftUI View, isn't clear to me when viewed through the lens of an NSViewController. Consider the following:

import AppKit
import SwiftUI

struct DetailsView: View {
  @State var details: String = ""
  
  var body: some View {
    Text(details)
  }
}

final class ViewController: NSViewController {
  
  private let detailsView: DetailsView
  
  init() {
    self.detailsView = DetailsView()
    super.init(nibName: nil, bundle: nil)
  }
  
  required init?(coder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
  }
 
  override func viewDidLoad() {
    view.addSubview(NSHostingView(rootView: detailsView))
  }
  
  func updateDetails(_ details: String) {
    // Is this 'safe' and 'acceptable'?
    self.detailsView.details = details
  }
}

Is the view controller guaranteed to always be updating the correct @State property or is there a chance that the view controller's reference to it somehow becomes stale because of a SwiftUI update?

Answered by DTS Engineer in 834701022

Your best approach is to use a shared source of truth, such as an Observable or a model object.

SwiftUI views are data driven, your View gets re-rendered whenever the data it depend on changes. You should declare state as private to prevent setting it in a memberwise initializer, which can conflict with the storage management that SwiftUI provides. When the value changes, SwiftUI updates the parts of the view hierarchy that depend on the value.

Accepted Answer

Your best approach is to use a shared source of truth, such as an Observable or a model object.

SwiftUI views are data driven, your View gets re-rendered whenever the data it depend on changes. You should declare state as private to prevent setting it in a memberwise initializer, which can conflict with the storage management that SwiftUI provides. When the value changes, SwiftUI updates the parts of the view hierarchy that depend on the value.

Great, that's kind of what I was expecting. So a shared ViewModel like in the implementation below is the recommended way to marshal data between an NSViewController and a SwiftUI View?

import AppKit
import SwiftUI

final class ViewModel: ObservableObject { 
  @Published var details: String = ""
} 

struct DetailsView: View {
  @ObservedObject var viewModel: ViewModel
  
  var body: some View {
    Text(viewModel.details)
  }
}
 
final class ViewController: NSViewController {
  
  private let viewModel: ViewModel
  
  init() {
    self.viewModel = ViewModel()
    super.init(nibName: nil, bundle: nil)
  }
 
  override func viewDidLoad() {
    let detailsView = DetailsView(viewModel: viewModel)
    view.addSubview(NSHostingView(rootView:detailsView))
  }
  
  func updateDetails(_ details: String) {
    viewModel.details = details
  }
}
Should you access @State properties from an NSViewController (AppKit / SwiftUI Integration)?
 
 
Q