I wrote a Combine Publisher wrapper class for some old code that used delegation.
TLDR; Can someone improve how I manage the lifetime of my custom publisher. Preferrable by making it behave like normal publishers, where you can just sink to it and not have to worry about retaining that instance.
Details I encountered a problem where I have to keep a reference to my Publisher wrapper for it to work. Every example of a custom publisher doesn't have this requirement, though their publishers were structs and were fairly different from mine.
Here's a simplified version of the problem that I'm having. Note the commented out section in doSomething()
import Foundation
import Combine
// Old code that uses delegate
protocol ThingDelegate: AnyObject {
func delegateCall(number: Int)
}
class Thing {
weak var delegate: ThingDelegate?
var name: String = "Stuff"
init() {
Swift.print("thing init")
}
deinit {
Swift.print("☠️☠️☠️☠️☠️☠️ thing deinit")
}
func start() {
Swift.print("Thing.start()")
DispatchQueue.main.async {
self.delegate?.delegateCall(number: 99)
}
}
}
// Combine Publisher Wrapper
class PublisherWrapper: Publisher {
typealias Output = Int
typealias Failure = Error
private let subject = PassthroughSubject<Int, Failure>()
var thing: Thing
init(thing: Thing) {
Swift.print("wrapper init")
self.thing = thing
self.thing.delegate = self
}
deinit {
Swift.print("☠️☠️☠️☠️☠️☠️ wrapper deinit")
}
func receive<S>(subscriber: S) where S : Subscriber, Failure == S.Failure, Int == S.Input {
self.subject.subscribe(subscriber)
self.thing.start()
}
}
extension PublisherWrapper: ThingDelegate {
func delegateCall(number: Int) {
Swift.print("publisher delegate call: \(number)")
self.subject.send(number)
self.subject.send(completion: .finished)
}
}
class Test {
var cancellables = Set<AnyCancellable>()
var wrapper: PublisherWrapper?
func doSomething() {
Swift.print("doSomething()")
let thing = Thing()
let wrapper = PublisherWrapper(thing: thing)
self.wrapper = wrapper
// Take a look over here
//
// if you comment out the line above where I set self.wrapper = wrapper
// it prints out the following
//
//start
//doSomething()
//thing init
//wrapper init
//Thing.start()
//☠️☠️☠️☠️☠️☠️ wrapper deinit
//☠️☠️☠️☠️☠️☠️ thing deinit
//
// But if you uncomment the line and retain it and you'll get the following
//start
//doSomething()
//thing init
//wrapper init
//Thing.start()
//publisher delegate call: 99
//value: 99
//finished
//release wrapper: nil
//☠️☠️☠️☠️☠️☠️ wrapper deinit
//☠️☠️☠️☠️☠️☠️ thing deinit
// we get the value and everything works as it should
wrapper.sink { [weak self] completion in
print(completion)
self?.wrapper = nil
print("release wrapper: \(self?.wrapper)")
} receiveValue: {
print("value: \($0)")
}.store(in: &self.cancellables)
}
}
print("start")
let t = Test()
t.doSomething()
Is there an approach that avoids retaining the publisher like this? I ask because this can get pretty ugly when using flatMap.