18

In the following code "Test" should be printed in the console when the Button is pressed, but it's not. The event is not send through the publisher. Any idea what happened with PassthroughSubject in Xcode 11 Beta 5 ? (in Xcode 11 Beta 4 it works well)

var body: some View {  

    let publisher = PassthroughSubject<String, Never>()

    publisher.sink { (str) in  
        print(str)  
    }  
    return Button("OK") {  
        publisher.send("Test")  
    }  
}

P.S. I know there are other ways to print a string when a button is pressed, I just wanna show a simple send-receive example

kontiki
  • 37,663
  • 13
  • 111
  • 125
Sorin Lica
  • 6,894
  • 10
  • 35
  • 68

2 Answers2

53

.sink() returns an AnyCancellable object. You should never ignored it. Never do this:

// never do this!
publisher.sink { ... }
// never do this!
let _ = publisher.sink { ... }

And if you assign it to a variable, make sure it is not short lived. As soon as the cancellable object gets deallocated, the subscription will get cancelled too.

// if cancellable is deallocated, the subscription will get cancelled
let cancellable = publisher.sink { ... }

Since you asked to use sink inside a view, I'll post a way of doing it. However, inside a view, you should probably use .onReceive() instead. It is way more simple.

Using sink:

When using it inside a view, you need to use a @State variable, to make sure it survives after the view body was generated.

The DispatchQueue.main.async is required, to avoid the state being modified while the view updates. You would get a runtime error if you didn't.

struct ContentView: View {
    @State var cancellable: AnyCancellable? = nil

    var body: some View {
        let publisher = PassthroughSubject<String, Never>()

        DispatchQueue.main.async {
            self.cancellable = publisher.sink { (str) in
                print(str)
            }
        }

        return Button("OK") {
            publisher.send("Test")
        }
    }
}

Using .onReceive()

struct ContentView: View {

    var body: some View {
        let publisher = PassthroughSubject<String, Never>()

        return Button("OK") {
            publisher.send("Test")
        }
        .onReceive(publisher) { str in
            print(str)
        }
    }
}
kontiki
  • 37,663
  • 13
  • 111
  • 125
  • In order to deallocate the subscription I used something like this : `self.cancellable = publisher.sink(receiveCompletion: { (completion) in self.cancellable = nil }, receiveValue: { (str) in print(str) })`. Is this the right solution or there is a better one ? – Sorin Lica Jul 31 '19 at 12:12
  • I'm not an expert on Combine, but it seems about right ;-) – kontiki Jul 31 '19 at 12:46
  • As a Combine and reactive beginner many of the examples around seem overly complicated for very simple tasks: `struct ContentView: View { @State var string: String = "" { didSet { print(string) } } var body: some View { return Button("OK") { self.string = "Test" } } }` Sorry for the inline code. Why would we use a Subject? I guess because it opens up the opportunity to use operators and such? – HelloTimo Jan 11 '20 at 10:59
15

You are missing .store when you subscribe to the sink. You can use .onReceive, but your code is not receiving values because you need to add .store(in: &subscription)

var body: some View {  
    var subscription = Set<AnyCancellable>()
    let publisher = PassthroughSubject<String, Never>()

    publisher.sink { (str) in  
        print(str)  
    }.store(in: &subscription)

    return Button("OK") {  
        publisher.send("Test")  
    }  
}
zdravko zdravkin
  • 2,090
  • 19
  • 21