How to flick using UIKit Dynamic in iOS

Issue #475

For a snack bar or image viewing, it’s handy to be able to just flick or toss to dismiss

We can use UIKit Dynamic, which was introduced in iOS 7, to make this happen.

Use UIPanGestureRecognizer to drag view around, UISnapBehavior to make view snap back to center if velocity is low, and UIPushBehavior to throw view in the direction of the gesture.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
import UIKit

final class FlickHandler {
private let viewToMove: UIView
private let referenceView: UIView
private var panGR: UIPanGestureRecognizer!
private let animator: UIDynamicAnimator

private var snapBehavior: UISnapBehavior?
private var pushBehavior: UIPushBehavior?
private let debouncer = Debouncer(delay: 0.5)

var onFlick: () -> Void = {}

init(viewToMove: UIView, referenceView: UIView) {
self.viewToMove = viewToMove
self.referenceView = referenceView
self.animator = UIDynamicAnimator(referenceView: referenceView)
self.panGR = UIPanGestureRecognizer(target: self, action: #selector(handleGesture(_:)))
viewToMove.addGestureRecognizer(panGR)
}

@objc private func handleGesture(_ gr: UIPanGestureRecognizer) {
switch gr.state {
case .began:
handleBegin()
case .changed:
handleChange(gr)
default:
handleEnd(gr)
}
}

private func handleBegin() {
animator.removeAllBehaviors()
}

private func handleChange(_ gr: UIPanGestureRecognizer) {
let translation = panGR.translation(in: referenceView)
viewToMove.transform = CGAffineTransform(
translationX: translation.x,
y: translation.y
)
}

private func handleEnd(_ gr: UIPanGestureRecognizer) {
let velocity = gr.velocity(in: gr.view)
let magnitude = sqrt((velocity.x * velocity.x) + (velocity.y * velocity.y))
if magnitude > 1000 {
animator.removeAllBehaviors()

let pushBehavior = UIPushBehavior(items: [viewToMove], mode: .instantaneous)
pushBehavior.pushDirection = CGVector(dx: velocity.x, dy: velocity.y)
pushBehavior.magnitude = magnitude / 35

self.pushBehavior = pushBehavior
animator.addBehavior(pushBehavior)

onFlick()
debouncer.run { [weak self] in
self?.animator.removeAllBehaviors()
}
} else {
let snapBehavior = UISnapBehavior(
item: viewToMove,
snapTo: viewToMove.center
)

self.snapBehavior = snapBehavior
animator.addBehavior(snapBehavior)
}
}
}

Comments