Data communication between 2 ObservableObjects

772 Views Asked by At

I have 2 independent ObservableObjects called ViewModel1 and ViewModel2.

ViewModel2 has an array of strings:

@Published var strings: [String] = [].

Whenever that array is modified i want ViewModel1 to be informed.

What's the recommended approach to achieve this?

2

There are 2 best solutions below

1
jnpdx On BEST ANSWER

Clearly, there are a number of potential solutions to this, like the aforementioned NotificationCenter and singleton ideas.

To me, this seems like a scenario where Combine would be rather useful:

import SwiftUI
import Combine

class ViewModel1 : ObservableObject {
    var cancellable : AnyCancellable?
    
    func connect(_ publisher: AnyPublisher<[String],Never>) {
        cancellable = publisher.sink(receiveValue: { (newStrings) in
            print(newStrings)
        })
    }
}

class ViewModel2 : ObservableObject {
    @Published var strings: [String] = []
}


struct ContentView : View {
    @ObservedObject private var vm1 = ViewModel1()
    @ObservedObject private var vm2 = ViewModel2()
    
    var body: some View {
        VStack {
            Button("add item") {
                vm2.strings.append("\(UUID().uuidString)")
            }
            
            ChildView(connect: vm1.connect)
            
        }.onAppear {
            vm1.connect(vm2.$strings.eraseToAnyPublisher())
        }
    }
}

struct ChildView : View {
    var connect : (AnyPublisher<[String],Never>) -> Void
    @ObservedObject private var vm2 = ViewModel2()
    
    var body: some View {
        Button("Connect child publisher") {
            connect(vm2.$strings.eraseToAnyPublisher())
            vm2.strings = ["Other strings","From child view"]
        }
    }
}

To test this, first try pressing the "add item" button -- you'll see in the console that ViewModel1 receives the new values.

Then, try the Connect child publisher button -- now, the initial connection is cancelled and a new one is made to the child's iteration of ViewModel2.

In order for this scenario to work, you always have to have a reference to ViewModel1 and ViewModel2, or at the least, the connect method, as I demonstrated in ChildView. You could easily pass this via dependency injection or even through an EnvironmentObject

ViewModel1 could also be changed to instead of having 1 connection, having many by making cancellable a Set<AnyCancellable> and adding a connection each time if you needed a one->many scenario.

Using AnyPublisher decouples the idea of having a specific types for either side of the equation, so it would be just as easy to connect ViewModel4 to ViewModel1, etc.

0
ios coder On

I had same problem and I found this method working well, just using the idea of reference type and taking advantage of class like using shared one!

import SwiftUI

struct ContentView: View {
    
    @StateObject var viewModel2: ViewModel2 = ViewModel2.shared
    @State var index: Int = Int()
    
    var body: some View {

        Button("update strings array of ViewModel2") {

            viewModel2.strings.append("Hello" + index.description)
            index += 1
        }

        
    }
}


class ViewModel1: ObservableObject {
    
    static let shared: ViewModel1 = ViewModel1()
    
    @Published var onReceiveViewModel2: Bool = Bool() {
        
        didSet {
        
            print("strings array of ViewModel2 got an update!")
            print("new update is:", ViewModel2.shared.strings)

        }
        
    }
    
}


class ViewModel2: ObservableObject {
    
    static let shared: ViewModel2 = ViewModel2()
    
    @Published var strings: [String] = [String]() {
        
        didSet { ViewModel1.shared.onReceiveViewModel2.toggle() }
        
    }
    
}