Chapters

Hide chapters

RxSwift: Reactive Programming with Swift

Fourth Edition · iOS 13 · Swift 5.1 · Xcode 11

21. RxGesture
Written by Florent Pillet

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now

Gesture processing is a good candidate for reactive extensions. Gestures can be viewed as a stream of events, either discrete or continuous. Working with gestures normally involves using the target-action pattern, where you set some object as the gesture target and create a function to receive updates.

At this point, you can appreciate the value of turning as much as of your data and event sources as possible into observable sequences. Enter RxGesture, https://github.com/RxSwiftCommunity/RxGesture, a project living under the RxSwiftCommunity banner at https://github.com/RxSwiftCommunity. It’s cross-platform, working on both iOS and macOS.

In this chapter, you’ll focus on the iOS implementation of RxGesture.

Attaching gestures

RxGesture makes it dead simple to attach a gesture to a view:

view.rx.tapGesture()
  .when(.recognized)
  .subscribe(onNext: { _ in
    print("view tapped")
  })
  .disposed(by: disposeBag)

In this example, RxGesture creates a UITapGestureRecognizer, attaches it to the view and emits an event every time the gesture is recognized. When you want to get rid of the recognizer, simply call dispose() on the Disposable object returned by the subscription.

You can also attach multiple gestures at once:

view.rx.anyGesture(.tap(), .longPress())
  .when(.recognized)
  .subscribe(onNext: { [weak view] gesture in
    if let tap = gesture as? UITapGestureRecognizer {
      print("view was tapped at \(tap.location(in: view!))")
    } else {
      print("view was long pressed")
    }
  })
  .disposed(by: disposeBag)

The event the subscription emits is the gesture recognizer object which changed state. The when(_:...) operator above lets you filter events based on the recognizer state to avoid processing events you’re not interested in.

Supported gestures

RxGesture works with all iOS and macOS built-in gesture recognizers. You can use it with your own gesture recognizers, but that’s beyond the scope of this chapter.

view.rx.screenEdgePanGesture(edges: [.top, .bottom])
  .when(.recognized)
  .subscribe(onNext: { recognizer in
    // gesture was recognized
  })
  .disposed(by: disposeBag)
let observable = view.rx.swipeGesture(.left, configuration: { recognizer in
  recognizer.allowedTouchTypes = [NSNumber(value: UITouchType.stylus.rawValue)]
})

Current location

Any gesture observable can be transformed to an observable of the location in the view of your choice with asLocation(in:), saving you from doing it manually:

view.rx.tapGesture()
  .when(.recognized)
  .asLocation(in: .window)
  .subscribe(onNext: { location in
    // you now directly get the tap location in the window
  })
  .disposed(by: disposeBag)

Pan gestures

When creating a pan gesture observable with the rx.panGesture() reactive extension, use the asTranslation(in:) operator to transform events and obtain a tuple of current translation and velocity. The operator lets you specify which of the gestured view, superview, window or any other views you want to obtain the relative translation for. You’ll get an Observable<(translation: CGPoint, velocity: CGPoint)> in return:

view.rx.panGesture()
  .asTranslation(in: .superview)
  .subscribe(onNext: { translation, velocity in
    print("Translation=\(translation), velocity=\(velocity)")
  })
  .disposed(by: disposeBag)

Rotation gestures

Similarly to pan gestures, rotation gestures created with the rx.rotationGesture() extension can be further transformed with the asRotation() operator. It creates an Observable<(rotation: CGFloat, velocity: CGFloat)>.

view.rx.rotationGesture()
  .asRotation()
  .subscribe(onNext: { rotation, velocity in
    print("Rotation=\(rotation), velocity=\(velocity)")
  })
  .disposed(by: disposeBag)

Automated view transform

More complex interactions, such as the pan/pinch/rotate combination gesture in MapView, can be fully automated with the help of the transformGestures() reactive extension of UIView:

view.rx.transformGestures()
  .asTransform()
  .subscribe(onNext: { [unowned view] transform, velocity in
    view.transform = transform
  })
  .disposed(by: disposeBag)
view.rx.transformGestures(configuration: { (recognizers, delegate) in
  recognizers.pinchGesture.isEnabled = false
})

Advanced usage

You’ll sometimes need to use the observable for the same gesture at multiple places. Since subscribing to the observable creates and attaches the gesture recognizer, you only want to do this once.

let panGesture = view.rx.panGesture()
  .share(replay: 1)

panGesture
  .when(.changed)
  .asTranslation()
  .subscribe(onNext: { [unowned view] translation, _ in
    view.transform = CGAffineTransform(translationX: translation.x,
      y: translation.y)
  })
  .disposed(by: disposeBag)

panGesture
  .when(.ended)
  .subscribe(onNext: { _ in
    print("Done panning")
  })
  .disposed(by: disposeBag)
Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2024 Kodeco Inc.

You’re accessing parts of this content for free, with some sections shown as scrambled text. Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now