Combineたのしそう
ということで、Target-ActionをCombineで処理出来るようにしてみました。
for macOSですが、ちょっと変えればiOSとかでも出来るのではないでしょうか?
使い方
NSControlなどがPublisherを返せるようにしたので、以下のようになります。
let b = NSButton(frame: NSRect(x: 0, y: 0, width: 200, height: 120))
let cancel = b.actionPublisher().sink { print($0) }
b.performClick(nil)
// prints <NSButton: 0xXXXXXXX>
b.performClick(nil)
// prints <NSButton: 0xXXXXXXX>
Publisher
のOutput
をTarget-ActionでいうところのSenderにしていますので、あまり違和を感じずに移行できると思います。
実装
ActionPublisher
利用者が気にするべきなのは通常このActionPublisherだけです。
public struct ActionPublisher: Publisher {
public typealias Output = ActionPerfomer
public typealias Failure = Never
private let actionReceiver: ActionReceiver
init(actionPerfomer: Output) {
self.actionReceiver = .init(actionPerfomer: actionPerfomer)
}
public func receive<S: Subscriber>(subscriber: S)
where Failure == S.Failure, Output == S.Input {
actionReceiver.handler = { performer in _ = subscriber.receive(performer) }
subscriber.receive(subscription: ActionSubscription(actionReceiver: actionReceiver))
}
}
ActionSubscription
Handlingをキャンセルする時のためのAnyCancellable
として現れますが、通常はその具象型であるActionSubscription
を気にかける必要はありません。
public struct ActionSubscription: Subscription {
public let combineIdentifier = CombineIdentifier()
let actionReceiver: ActionReceiver
public func request(_ demand: Subscribers.Demand) {}
public func cancel() {
actionReceiver.handler = nil
}
}
ActionReceiver
実際にTarget-Actionをハンドリングするヘルパークラスです。
このクラスは外部からは隠蔽されています。
internal final class ActionReceiver: NSObject {
private(set) weak var actionPerfomer: ActionPerfomer!
var handler: ((ActionPerfomer) -> Void)?
init(actionPerfomer: ActionPerfomer) {
self.actionPerfomer = actionPerfomer
super.init()
actionPerfomer.target = self
actionPerfomer.action = #selector(action)
}
@IBAction private func action(_ sender: Any) {
handler?(actionPerfomer)
}
}
ActionPerfomer
Target-ActionにおけるSenderとなるクラスが準拠するべきプロトコルです。
Target-Actionがかなり緩い制約の下で動いているので必要最低限のみが宣言されています。
ここでは一般的にSenderとなりうるNSControl
とNSMenuItem
を準拠させています。
また、ActionPublisher
を取り出すメソッドをextentionとして実装しています。
public protocol ActionPerfomer: AnyObject {
var target: AnyObject? { get set }
var action: Selector? { get set }
}
extension NSControl: ActionPerfomer {}
extension NSMenuItem: ActionPerfomer {}
extension ActionPerfomer {
func actionPublisher() -> ActionPublisher {
.init(actionPerfomer: self)
}
}