[SWIFT] [macOS] Process Target-Action with Combine

Combine seems to be fun

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.

How to use

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.

Implementation

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)
    }
}

Recommended Posts

[macOS] Process Target-Action with Combine
Process validation messages with Decorator
URLSession with URLSession and Combine normally
Process Communication using AMQP with RabbitMQ