on
Combine — Handling UIKit’s gestures
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
First, let’s create a custom publisher by conforming to the Publisher protocol which is composed of two associated types namely Output
and Failure
.
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.
- The Subscriber subscribes to a Publisher
- The Publisher gives Subscription to the Subscriber
- The Subscriber request values
- The Publisher sends values
- 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
Wrapping it all up by using a method in a UIView
extension which will create a custom publisher using the provided gesture type.
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
Combine enables to interact quite easily with UIKit
by customizing a publisher and a subscription to handle the user gestures in a reactive way.