swiftswiftuicombineobservableobjectdata-communication

Data communication between 2 ObservableObjects


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?


Solution

  • 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.