Search code examples
iosswiftswiftuipublish-subscribecombine

UIView as a subscriber to @EnvironmentObject


I've got an @EnvironmentObject that's updated in a worker thread, and several SwiftUI views subscribe to changes to the published values.

This all works quite nicely.

But I'm struggling to get a UIView to subscribe to changes in the @EnvironmentObject.

Given

@EnvironmentObject var settings: Settings 

where Settings is:

final class Settings {
    @Published var bar: Int = 0
    @Published var beat: Int = 1
    etc.
}

SwiftUI views update based on published value changes rather nicely.

But now, I want to declare a sink that receives the published values inside a UIView that conforms to UIViewRepresentable.

I've been working through the Combine book, and thought that I could declare a .sink closure with something like:

   func subscribeToBeatChanges() {
        settings.$bar
            .sink(receiveValue: {
                bar in
                self.bar = bar
                print("Bar = \(bar)")
            } )
        settings.$beat
            .sink(receiveValue: {
                beat in
                self.beat = beat
                print("Beat = \(beat)")
                self.setNeedsDisplay()
            } )
    }
 

Unfortunately, the closure is only called once, when subscribeToBeatChanges() is called. What I want is for the closure to be called every time a @Published property in the @EnvironmentObject value changes.

I've also tried to subscribe inside the UIViewRepresentable wrapper, with something inside the makeUIView method, but was unsuccessful.

I'm obviously doing some rather simple and fundamental wrong, and would sure appreciate a push in the right direction, because I'm getting cross-eyed trying to puzzle this out!

Thanks!


Solution

  • You have to store a reference to the AnyCancellable that you receive back from .sink:

    private var cancellables = Set<AnyCancellable>()
    
    func subscribeToBeatChanges() {
        settings.$bar
            .sink(receiveValue: {
                bar in
                self.bar = bar
                print("Bar = \(bar)")
            } )
            .store(in: &cancellables) // <-- store the instance
        settings.$beat
            .sink(receiveValue: {
                beat in
                self.beat = beat
                print("Beat = \(beat)")
                self.setNeedsDisplay()
            } )
            .store(in: &cancellables) // <-- store the instance
    }
    

    If you don't store it, the AnyCancellable instance will automatically cancel a subscription on deinit.