除非保留,否则自定义 Combine Publisher 包装器 class 不起作用

Custom Combine Publisher wrapper class does not work unless retained

我为一些使用委托的旧代码写了一个 Combine Publisher 包装器 class。

TLDR;有人可以改进我管理自定义发布者生命周期的方式吗?最好使它的行为像普通发布者一样,您可以在其中沉迷于它而不必担心保留该实例。

详情 我遇到了一个问题,我必须保留对 Publisher 包装器的引用才能使其工作。自定义发布者的每个示例都没有此要求,尽管他们的发布者是结构并且与我的有很大不同。

这是我遇到的问题的简化版本。注意 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: \([=11=])")
        }.store(in: &self.cancellables)
    }
}

print("start")
let t = Test()
t.doSomething()

有没有一种方法可以避免像这样保留发布者?我问是因为在使用 flatMap 时这会变得非常难看。

一个解决方案是实现自定义 Subscription 对象。

我会实施一个简单的协议,您可以为所有具有委托的 类 遵守该协议。

protocol Provider {
    associatedtype Output
    func start(provide: @escaping (Output) -> Void)
}

我在这里实现了一个 Publisher,我可以为 Provider 提供数据。发布者真正做的就是创建一个 Subscription 对象并将其连接到传递给 receive(subscriber:) 方法的 Subscriber。自定义 Subscription 对象完成所有繁重的工作。因此,我们可以将 Publisher 定义为结构。

Subscription 对象从 Provider 接收数据并将其传递给 Subscriber。请注意,Subscription 对象需要保存对 Provider 的引用,因此它不会被释放。

extension Publishers {

    struct Providable<ProviderType: Provider>: Publisher {
        typealias Output = ProviderType.Output
        typealias Failure = Never

        private class Subscription<SubscriberType: Subscriber>: Combine.Subscription {

            private let provider: ProviderType

            init(
                provider: ProviderType,
                subscriber: SubscriberType
            ) where SubscriberType.Input == ProviderType.Output {
                self.provider = provider
                provider.start { value in
                    _ = subscriber.receive(value)
                    subscriber.receive(completion: .finished)
                }
            }

            deinit {
                Swift.print("provided subscription deinit")
            }

            func request(_ demand: Subscribers.Demand) {}
            func cancel() {}
        }

        private let provider: ProviderType

        init(provider: ProviderType) {
            self.provider = provider
        }

        func receive<S>(subscriber: S) where S : Subscriber, Failure == S.Failure, Output == S.Input {
            subscriber.receive(subscription: Subscription(provider: provider, subscriber: subscriber))
        }
    }
}

缺点是您需要为每个要用 Combine 包装的委托对象实现自定义 Provider 对象:

final class ThingOutputProvider: Provider, ThingDelegate {
    private let thing: Thing

    private var provide: (Int) -> Void = { _ in }

    init(thing: Thing) {
        self.thing = thing
        self.thing.delegate = self
    }

    func start(provide: @escaping (Int) -> Void) {
        self.provide = provide
        self.thing.start()
    }

    func delegateCall(number: Int) {
        provide(number)
    }
}

这是一个方便的小协议扩展,因此我们可以为我们的 Provider 创建发布者:

extension Provider {
    var publisher: Publishers.Providable<Self> {
        Publishers.Providable(provider: self)
    }
}

用法如下:

class Test {
    var cancellables = Set<AnyCancellable>()
    func doSomething() {
        ThingOutputProvider(thing: Thing())
            .publisher
            .sink { [weak self] completion in
                print("completion: \(completion)")
                self?.cancellables.removeAll()
            } receiveValue: {
                print("value: \([=14=])")
            }.store(in: &self.cancellables)
    }
}

无需维护对 Publisher 的引用即可工作的原因是 Subscription 对象在 Combine 管道的生命周期内保持活动状态。

希望这对您有所帮助。