CombineでUIViewのタップ等のジェスチャーを取得する


CombineにおいてUIViewのタップ等のジェスチャーを取得する方法です。

参考にした記事は
https://jllnmercier.medium.com/combine-handling-uikits-gestures-with-a-publisher-c9374de5a478
にあります。

以下のコードはほぼその記事からの抜粋です。

まず、Publisherを実装します、後ほど定義するGestureSubscriptionを使うところがポイントです。

struct GesturePublisher: Publisher {
    typealias Output = GestureType
    typealias Failure = Never
    private let view: UIView
    private let gestureType: GestureType
    init(view: UIView, gestureType: GestureType) {
        self.view = view
        self.gestureType = gestureType
    }

    func receive<S>(subscriber: S) where S: Subscriber,
        GesturePublisher.Failure == S.Failure, GesturePublisher.Output
        == S.Input
    {
        let subscription = GestureSubscription(
            subscriber: subscriber,
            view: view,
            gestureType: gestureType
        )
        subscriber.receive(subscription: subscription)
    }
}

ジェスチャーの種類をenumで定義します。

enum GestureType {
    case tap(UITapGestureRecognizer = .init())
    case swipe(UISwipeGestureRecognizer = .init())
    case longPress(UILongPressGestureRecognizer = .init())
    case pan(UIPanGestureRecognizer = .init())
    case pinch(UIPinchGestureRecognizer = .init())
    case edge(UIScreenEdgePanGestureRecognizer = .init())
    func get() -> UIGestureRecognizer {
        switch self {
        case let .tap(tapGesture):
            return tapGesture
        case let .swipe(swipeGesture):
            return swipeGesture
        case let .longPress(longPressGesture):
            return longPressGesture
        case let .pan(panGesture):
            return panGesture
        case let .pinch(pinchGesture):
            return pinchGesture
        case let .edge(edgePanGesture):
            return edgePanGesture
        }
    }
}

GestureSubscriptionにおいて従来ながらのUIKitのaddTargetを利用しSubscriptionを実装します。

class GestureSubscription<S: Subscriber>: Subscription where S.Input == GestureType, S.Failure == Never {
    private var subscriber: S?
    private var gestureType: GestureType
    private var view: UIView
    init(subscriber: S, view: UIView, gestureType: GestureType) {
        self.subscriber = subscriber
        self.view = view
        self.gestureType = gestureType
        configureGesture(gestureType)
    }

    private func configureGesture(_ gestureType: GestureType) {
        let gesture = gestureType.get()
        gesture.addTarget(self, action: #selector(handler))
        view.addGestureRecognizer(gesture)
    }

    func request(_ demand: Subscribers.Demand) {}
    func cancel() {
        subscriber = nil
    }

    @objc
    private func handler() {
        _ = subscriber?.receive(gestureType)
    }
}

最後にこれをUIViewの拡張メソッドとしてもたせます。

extension UIView {
    func gesturePublisher(_ gestureType: GestureType = .tap()) ->
        GesturePublisher
    {
        .init(view: self, gestureType: gestureType)
    }
}

これで簡単にジェスチャー扱えます。

       var cancellables = Set<AnyCancellable>()
       view.gesture().sink { recognizer in
           print("Tapped !")
       }.store(in: &cancellables)
       // prints "Tapped !"

       view.gesture(.swipe()).sink { recognizer in
           print("Swiped !")
       }.store(in: &cancellables)
       // prints "Swiped !"

以上となります。ありがとうございました。