@StateObject for view owning "viewModel" to use with @Observable observation framework object

I have an issue with a View that owns @Observable object (Instantize it, and is owner during the existence) in the same notion as @StateObject, however when @State is used the viewModel is re-created and deallocated on every view redraw.

In the previous models, @StateObject were used when the View is owner of the object (Owner of the viewModel as example)

When @Observable object is used in the same notion, @State var viewModel = ViewModel() the viewModel instance is recreated on views redraws.

@StateObject was maintaining the same instance during the View existence, that however is not happening when used @Observable with @State.

You can see the effect in following code.

There are two viewModels, one using ObservableObject and second using @Observable observation framework.

The tabView content views owns the viewModel, however when the parent TabView is redrawed, the ownership of @Observation is teared down and new object is recreated on every redraw of TabView

@StateObject with ObservableObject maintains its view ownership and the object is not deallocated or recreated as would be expected.

@State with @Observable do not follow the ownership, and new object is created every time the parent is redrawed.

Following is the code:


import Observation

import SwiftUI



/// ObservableObject, via @StateObject.

class ViewModelObservable: ObservableObject {

    @Published public var title: String



    deinit {

        print("deinit ViewModelObservable")

    }



    init(title: String) {

        self.title = title

        print("init ViewModelObservable")

    }

}



/// New Observation framework object.

@Observable

class ViewModelObseration {

    public var title: String



    deinit {

        print("deinit ViewModelObseration")

    }



    init(title: String) {

        self.title = title

        print("init ViewModelObseration")

    }

}



struct SecondTabContentView: View {

    /// View Owns object (Observation Framework)

    @State var viewModelObservation: ViewModelObseration



    /// View Owns object (Observable Object)

    @StateObject var viewModelObservable: ViewModelObservable



    init() {

        _viewModelObservable = StateObject(wrappedValue: ViewModelObservable(title: "SecondTabContentView Observable"))

        _viewModelObservation = State(wrappedValue: ViewModelObseration(title: "SecondTabContentView Observation"))

    }



    var body: some View {

        VStack {

            Text(viewModelObservable.title)

            Text(viewModelObservation.title)

        }

    }

}



struct FirstTabContentView: View {

    /// View Owns object (Observation Framework)

    @State var viewModelObservation: ViewModelObseration



    /// View Owns object (Observable Object)

    @StateObject var viewModelObservable: ViewModelObservable



    init() {

        _viewModelObservable = StateObject(wrappedValue: ViewModelObservable(title: "FirstTabContentView Observable"))

        _viewModelObservation = State(wrappedValue: ViewModelObseration(title: "FirstTabContentView Observation"))

    }



    var body: some View {

        VStack {

            Text(viewModelObservable.title)

            Text(viewModelObservation.title)

        }

    }

}



struct ContentView: View {

    @State var tabSelection: Int = 1

    @State var redrawTrigger: Bool = false

    var body: some View {

        TabView(selection: $tabSelection) {

            FirstTabContentView()

                .tag(0)

                .tabItem { Label("First \(redrawTrigger ? "true" : "false") ", systemImage: "pen") }

            SecondTabContentView()

                .tag(1)

                .tabItem { Label("Second \(redrawTrigger ? "true" : "false")", systemImage: "pen") }

        }

        .task {

            try? await Task.sleep(for: .seconds(3))

            self.redrawTrigger = true

            try? await Task.sleep(for: .seconds(3))

            self.redrawTrigger = false

        }

    }

}



#Preview {

    ContentView()

}

Even though Apple migration guide suggests to use @State I noticed this behavior too as I'm used to have a sort of MVVM in SwiftUI.

So I tried a mixed approach; I used a sort of wrapper which is both Observable and a @StateObject just for the sake of the lifetime, no @Published var within it.

@Observable
public final class Store<VM: Observable & AnyObject>: ObservableObject {
  private var _viewModel: VM
  
  public var viewModel: VM {
    get { _viewModel }
    set {} // set ignored
  }
  
  deinit {
    print("Deinit", self)
  }
  
  init(_ viewModel: @escaping () -> VM) {
    _viewModel = viewModel()
  }
}

import SwiftUI

extension StateObject {
  public init<ViewModel: Observable & AnyObject>(
    viewModel: @escaping @autoclosure () -> ViewModel
  ) where ObjectType == Store<ViewModel> {
    self.init(wrappedValue: .init(viewModel))
  }
}

Then in the view

@StateObject private var store: Store<MyViewModel>

init(someInitialValue: String) {
    _store = .init(viewModel: .init(value: someInitialValue))
}

var body: some View {
  Text(store.viewModel.value)
  TextField(text: $store.viewModel.editableText)
}

Did you find any other solution?

@StateObject for view owning "viewModel" to use with @Observable observation framework object
 
 
Q