LoginSignup
1
2

More than 3 years have passed since last update.

Delegateを複数に通知する

Posted at

Delegateを複数オブジェクトに通知(配信)します。
完全に全自動でとは行かないため、コード記述量を出来るだけ少なくする使い回しの効く部品を作ってみます。

delegate

以下のprotocolの呼び出し複数箇所で受け取ります。

Output.swift
protocol Output {
    func didFinish(error: Error?)
}

class UseCase {
    let output: Output
    init(output: Output) {
        self.output = output
    }
}

複数に通知する

このままでは1箇所でしか受け取れません。

OutputPublisherを定義しUseCaseからの呼び出しをOutputPubliserで一度受け取ります。
そしてOutputPubliserが複数箇所に通知するようにします。(Publisher については後述)

OutputPublisher.swift
class OutputPublisher: Publisher<Output> {}

extension OutputPublisher: Output {
    func finish(error: Error?) {
        publish { $0.finish(error: error) }
    }
}
let outputPublisher = OutputPublisher()
let useCase = UseCase(output: outputPublisher)

通知を受け取る

Something.swift
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に包むようにする。
Publisher.swift
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にしても残り続けてよければ、そのように変更します。

1
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
2