0

I’m looking for the proper pattern and syntax to address my goal of having an ObservableObject instance that I can share amongst multiple views, but while keeping logic associated with it contained to another class. I’m looking to do this to allow different ‘controller’ classes to manipulate the properties of the state without the view needing to know which controller is acting on it (injected).

Here is a simplification that illustrates the issue:

import SwiftUI

class State: ObservableObject {
    @Published var text = "foo"
}

class Controller {
    var state : State
    init(_ state: State) {
        self.state = state
    }
    
    func changeState() {
        state.text = "bar"
    }
}

struct ContentView: View {
    @StateObject var state = State()
    var controller: Controller!
    
    init() {
        controller = Controller(state)
    }
    
    var body: some View {
        VStack {
            Text(controller.state.text) // always shows 'foo'
            Button("Press Me") {
                print(controller.state.text) // prints 'foo'
                controller.changeState()
                print(controller.state.text) // prints 'bar'
            }
        }
    }
}

I know that I can use my ObservableObject directly and manipulate its properties such that the UI is updated in response, but in my case, doing so prevents me from having different ‘controller’ instances depending on what needs to happen. Please advise with the best way to accomplish this type of scenario in SwiftUI

Shackleford
  • 485
  • 1
  • 5
  • 16
  • 1
    SwiftUI is designed to use MVVM pattern (ObservableObject is view model here), and you try to use MVC which is foreign here. I will not ask *why*, just don't do that - it is anti-pattern. – Asperi Mar 24 '22 at 06:08
  • Take a look at protocols/generics instead of using objects. – malhal Mar 24 '22 at 08:24

1 Answers1

2

To make SwiftUI view follow state updates, your controller needs to be ObservableObject.

SwiftUI view will update when objectWillChange is triggered - it's done automatically for properties annotated with Published, but you can trigger it manually too.

Using same publisher of your state, you can sync two observable objects, for example like this:

class Controller: ObservableObject {
    let state: State
    private var cancellable: AnyCancellable?
    init(_ state: State) {
        self.state = state

        cancellable = state.objectWillChange.sink {
            self.objectWillChange.send()
        }
    }

    func changeState() {
        state.text = "bar"
    }
}

struct ContentView: View {
    @StateObject var controller = Controller(State())
Phil Dukhov
  • 67,741
  • 15
  • 184
  • 220
  • Seems to me this is reimplementing what SwiftUI does automatically for us when we use structs – malhal Mar 24 '22 at 08:11
  • @malhal yes, struct is much different from class. with struct it can work automatically, because changing struct property actually creates a new "object", but with class it won't work out of the box. – Phil Dukhov Mar 24 '22 at 08:14