So, I tried to make Target-Action processable with Combine.
It's for macOS, but if you change it a little, you can do it with iOS.
Now that NSControl etc. can return Publisher, it looks like this:
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>
Since ʻOutput of
Publisher` is set to Sender in Target-Action, I think that you can migrate without feeling too uncomfortable.
ActionPublisher This Action Publisher is usually the only thing users should care about.
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
It appears as ʻAnyCancellable for when canceling Handling, but usually you don't have to worry about its concrete type, ʻAction Subscription
.
public struct ActionSubscription: Subscription {
public let combineIdentifier = CombineIdentifier()
let actionReceiver: ActionReceiver
public func request(_ demand: Subscribers.Demand) {}
public func cancel() {
actionReceiver.handler = nil
}
}
ActionReceiver A helper class that actually handles Target-Action. This class is hidden from the outside.
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 This is the protocol that the Sender class in Target-Action should comply with. Since Target-Action works under fairly loose constraints, only the minimum required is declared.
Here, NSControl
and NSMenuItem
, which can be Sender in general, are compliant.
Also, the method to retrieve ʻActionPublisher` is implemented as extension.
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)
}
}