Delegateを複数オブジェクトに通知(配信)します。
完全に全自動でとは行かないため、コード記述量を出来るだけ少なくする使い回しの効く部品を作ってみます。
delegate
以下のprotocolの呼び出し複数箇所で受け取ります。
protocol Output {
func didFinish(error: Error?)
}
class UseCase {
let output: Output
init(output: Output) {
self.output = output
}
}
複数に通知する
このままでは1箇所でしか受け取れません。
OutputPublisher
を定義しUseCaseからの呼び出しをOutputPubliser
で一度受け取ります。
そしてOutputPubliser
が複数箇所に通知するようにします。(Publisher
については後述)
class OutputPublisher: Publisher<Output> {}
extension OutputPublisher: Output {
func finish(error: Error?) {
publish { $0.finish(error: error) }
}
}
let outputPublisher = OutputPublisher()
let useCase = UseCase(output: outputPublisher)
通知を受け取る
class Something {
var subscriver: AnyObject?
init() {
subscriver = outputPublisher.addSubscriber(self)
}
}
extension Something: Output {
func finish(error: Error?) {
print(error)
}
}
通知を受け取りたいオブジェクトでaddSubscriber(_:)
します。
戻り値は必ず保持してください。
通知を受け取るのを止めるにはsubscriverをnilにします。
明示的に止める必要がない場合Something
が消える時にsubscriverも消え、通知が自動で止まります。
Publisher
複数のSubscriber(Delegateを受け取るオブジェクト)を登録する機能です。
オブジェクトの登録と解除を行うだけですが、循環参照を回避したりするために少し面倒なことになっています。
- NSMapTable
Subscriberをweakで保持する。 - Disposer
Disposerが消える時に登録解除を行う。 - Box
weakで保持できるのはAnyObjectのみ。
SubscriberがAnyObjectでない場合もNSMapTableに格納するため、Boxに包むようにする。
public class Publisher<Subscriber> {
private class Disposer {
private let disposing: () -> Void
fileprivate init(disposing: @escaping () -> Void) {
self.disposing = disposing
}
deinit {
disposing()
}
}
private class Box {
fileprivate var content: Subscriber
fileprivate init(_ content: Subscriber) {
self.content = content
}
}
private var subscribers_: NSMapTable<AnyObject, Box> = .strongToWeakObjects()
private var subscribers: [Subscriber] {
return subscribers_.dictionaryRepresentation().map { $0.value.content }
}
public init() {}
public func addSubscriber(_ subscriber: Subscriber) -> AnyObject {
let subscriberKey = UUID().uuidString as NSString
let box = Box(subscriber)
subscribers_.setObject(box, forKey: subscriberKey)
let disposable = Disposer { [weak self, box] in
_ = box // NSMapTableにweakで参照されるだけだとBoxがすぐ消えるため、Disposerでキャプチャする
self?.removeSubscriber(subscriberKey)
}
return disposable
}
private func removeSubscriber(_ subscriberKey: AnyObject) {
subscribers_.removeObject(forKey: subscriberKey)
}
public func publish(_ handler: (Subscriber) -> Void) {
subscribers.forEach(handler)
}
}
補足その1
Delegate用のプロパティはweakで定義されていることがよくあります。
Publisherを生成しweakのdelegateにそのまま設定すると、いきなりPublisherが消えます😇
そのためどこかでPublisherを保持し続ける必要があります。
補足その2
Disposer生成時、上記サンプルではself(Publisher)をweakでキャプチャしています。
このselfを普通にキャプチャすると、PuplisherはSubscriberの登録があれば残り続け、登録がなくなると消えるようになります。
Publisherの参照をnilにしても残り続けてよければ、そのように変更します。