Home > front end >  How to make a non-observable class's methods available to SwiftUI views?
How to make a non-observable class's methods available to SwiftUI views?

Time:04-18

I have a pretty complicated UI so I broke my view models into multiple observable object classes that manage each part of the UI.

The 2 view model instances are created inside a "main" class called Manager. Manager contains methods that manipulate the published properties inside the view models.

I want those methods to be available to be used from my views but since the Manager class does not conform to ObservableObject protocol as it doesn't have any published properties, where do I create an instance of the Manager class so that I can use its methods from MULTIPLE views?

Important secondary question: According to Paul Hudson from HackingWithSwift, he recommended to mark all classes that are ObservableObjects with @MainActor attribute. But if I do that, the Manager class' methods won't be able to manipulate the published properties of the view models since not all methods from Manager have to run on the main queue. What is the solution?

import SwiftUI

class ViewModel1: ObservableObject {
    @Published var duration = 0.0
}

class ViewModel2: ObservableObject {
    @Published var currentTime = 0.0
}

class Manager {
    var vm1 = ViewModel1()
    var vm2 = ViewModel2()
    
    func play() {
        //Code that changes the published properties of the 2 view models
    }
    
    func pause() {
        //Code that changes the published properties of the 2 view models
    }
}

struct ContentView: View {
    @EnvironmentObject var manager: Manager //Won't work since Manager doesn't conform to ObservableObject

    var body: some View {
        //View code
    }
}

CodePudding user response:

A possible approach is to create manager as regular property, but inject view models as environment, like

struct ContentView: View {
    private var manager = Manager()

    var body: some View {
        SomeRootView()
           .environmentObject(manager.vm1)
           .environmentObject(manager.vm2)
    }
}

or in-place of each subview depending in which subview hierarchy corresponding view model is needed.

If manager is needed somewhere in deep child view, then it possible to transfer it there by injecting Environment value, like in https://stackoverflow.com/a/61847419/12299030. Or to make it singleton and use via Manager.shared.

Complete findings and variants code

CodePudding user response:

When updating child classes' properties you'll need to trigger a change event manually using objectWillChange.send.

class ViewModel1: ObservableObject {
    @Published var duration = 0.0
}

class Manager: ObservableObject {
    var vm1 = ViewModel1()

    func play() {
        vm1.duration  = 10
        
        objectWillChange.send()
    }
}

struct ContentView: View {
    @ObservedObject var manager = Manager() // init here only for testing
    
    var body: some View {
        Button {
            manager.play()
        } label: {
            Text (String(manager.vm1.duration))
        }

    }
}
  • Related