Search code examples
swiftuistateviewmodelcombinedeinit

How to deinit ViewModel with combine framework when views disappears


I use Combine in viewModels to update the views. But if I store the AnyCancellable objects into a set of AnyCancellable, the deinit method is never called. I use the deinit to cancel all cancellables objects.

struct View1: View {

    @ObservedObject var viewModel:ViewTextModel = ViewTextModel()
    @Injected var appActions:AppActions

    var body: some View {
        VStack {
            Text(self.viewModel.viewText)

            Button(action: {
                self.appActions.goToView2()
            }) {
                Text("Go to view \(self.viewModel.viewText)")
            }
        }
    }
}
class ViewTextModel: ObservableObject {
    @Published var viewText: String

    private var cancellables = Set<AnyCancellable>()

    init(state:AppState) {
        // initial state
        viewText = "view  \(state.view)"
        // updated state
        state.$view.removeDuplicates().map{ "view \($0)"}.assign(to: \.viewText, on: self).store(in: &cancellables)
    }

    deinit {
        cancellables.forEach { $0.cancel() }
    } 
}

Each time the view is rebuilt, a new viewmodel is instantiated but the old one is not destroyed. viewText attribute is updated on each instance with state.$view.removeDuplicates().map{ "view \($0)"}.assign(to: \.viewText, on: self).store(in: &cancellables)

If I don't store the cancellable object in the set, deinit is called but viewText is not updated if the state's changed for the current view.

Do you have an idea of ​​how to manage the update of the state without multiplying the instances of the viewmodel ?

Thanks


Solution

  • You could use sink instead of assign:

    state.$view
        .removeDuplicates()
        .sink { [weak self] in self?.viewText = $0 }
        .store(in: &cancellables)
    

    But I question the need for Combine here at all. Just use a computed property:

    class ViewTextModel: ObservableObject {
        @Published var state: AppState
    
        var viewText: String { "view \(state.view)" }
    }
    

    UPDATE

    If your deployment target is iOS 14 (or macOS 11) or later:

    Because you are storing to an @Published, you can use the assign(to:) operator instead. It manages the subscription for you without returning an AnyCancellable.

    state.$view
        .removeDuplicates()
        .map { "view \($0)" }
        .assign(to: &$viewText)
        // returns Void, so nothing to store