Combine — Handling UIKit’s gestures with a Publisher

While Combine doesn’t provide a built-in API to handle UIKit’s gestures yet, the Publisher and Subscription protocols give us the ability to create our own solution.

— Creating a custom publisher

`Output` represents the type of value the publisher will produce and `Failure` is the type of error it can throw.

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

Our custom `GesturePublisher` provides with a GestureType — an enum with default gesture recognizer values — and must never error out.

The Publisher protocol has one required method `receive<S>(subscriber: S)` which « attaches the specified subscriber to this publisher».

This method will be called once every time the publisher receives a new subscriber.

The `sink` method will be used later on to create our subscriber.

A quick reminder on how Publisher and Subscriber works together.

  1. The Subscriber subscribes to a Publisher
  2. The Publisher gives Subscription to the Subscriber
  3. The Subscriber request values
  4. The Publisher sends values
  5. The Publisher sends a completion event

Once a subscriber is created, it needs to receive a subscription.

— Creating a custom 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)
}
}

Our `GestureSubscription` object must conform to the Subscription protocol which has two required methods namely `request(_ demand: Subscribers.Demand)` and `cancel()`.

Since a publisher emits values to a downstream subscriber, its Output type must match the Subscriber’s Input type, same goes for Failure.

We’ll be using the cancel() method to cancel our subscription hence the use of optional on the subscriber property.

While our implementation is almost done, our subscribers won’t be able to receive any values yet since we didn’t really handle the user gesture.

Let’s fix it by adding the provided recognizer to the view and passing the gesture type to the subscriber from our selector.

— Finalizing the implementation

extension UIView {
func gesture(_ 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 !"

Let’s combine (:D) all of our gestures to leverage the full power of the framework.

let tap = view.gesture(.tap())
let swipe = view.gesture(.swipe())
let longPress = view.gesture(.longPress())
let pan = view.gesture(.pan())
let pinch = view.gesture(.pinch())
let edge = view.gesture(.edge())
Publishers.MergeMany(tap, swipe, longPress, pan, pinch, edge)
.sink(receiveValue: { gesture in
switch gesture {
case let .tap(tapRecognizer):
print("Tapped !")
case let .swipe(swipeRecognizer):
print("Swiped !")
case let .longPress(longPressRecognizer):
print("Long pressed !")
case let .pan(panRecognizer):
print("Panned !")
case let .pinch(pinchRecognizer):
print("Pinched !")
case let .edge(edgesRecognizer):
print("Panned edges !")
}
}).store(in: &cancellables)

We may now properly handle the user gesture and define a specific action using the provided associated recognizer.

Conclusion

Consider following me on Twitter, I post articles about Swift on a regular basis.

--

--

Senior iOS engineer. jullianmercier.com. @jullian_mercier.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store