How to make simple pan to dismiss view in iOS

Issue #301

Make it more composable using UIViewController subclass and ThroughView to pass hit events to underlying views.

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
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
class PanViewController: UIViewController {
var animator = UIViewPropertyAnimator(duration: 0, curve: .easeOut)
lazy var panGR = UIPanGestureRecognizer(target: self, action: #selector(handlePan(_: )))
let slideView = UIView()
let gripView = UIView()
var options: Options = Options()
var didHide: (() -> Void)?
let pullDownVelocity: CGFloat = 70

class Options {
var contentView: UIView = UIView()
var percentHeight: CGFloat = 0.24
}

override func loadView() {
view = ThroughView()
view.translatesAutoresizingMaskIntoConstraints = false
}

override func viewDidLoad() {
super.viewDidLoad()

setup()
}

func setup() {
slideView.layer.cornerRadius = 10
slideView.clipsToBounds = true

gripView.backgroundColor = UIColor.yellow
gripView.layer.cornerRadius = 1

view.addSubview(slideView)
slideView.addSubview(gripView)
slideView.addGestureRecognizer(panGR)

NSLayoutConstraint.on([
slideView.leftAnchor.constraint(equalTo: view.leftAnchor),
slideView.rightAnchor.constraint(equalTo: view.rightAnchor),
slideView.heightAnchor.constraint(equalTo: view.heightAnchor),
slideView.topAnchor.constraint(equalTo: view.bottomAnchor)
])

NSLayoutConstraint.on([
gripView.centerXAnchor.constraint(equalTo: slideView.centerXAnchor),
gripView.topAnchor.constraint(equalTo: slideView.topAnchor, constant: 16),
gripView.widthAnchor.constraint(equalToConstant: 30),
gripView.heightAnchor.constraint(equalToConstant: 2)
])
}

func apply(options: Options) {
self.options.contentView.removeFromSuperview()
slideView.insertSubview(options.contentView, at: 0)

NSLayoutConstraint.on([
options.contentView.leftAnchor.constraint(equalTo: slideView.leftAnchor),
options.contentView.rightAnchor.constraint(equalTo: slideView.rightAnchor),
options.contentView.topAnchor.constraint(equalTo: slideView.topAnchor),
options.contentView.heightAnchor.constraint(equalTo: slideView.heightAnchor, multiplier: options.percentHeight)
])

self.options = options
}

@objc func handlePan(_ gr: UIPanGestureRecognizer) {
switch gr.state {
case .began:
break
case .changed:
break
case .ended:
let velocity = gr.velocity(in: slideView)
if velocity.y > pullDownVelocity {
hide()
}
default:
break
}
}

func show() {
guard let parentView = view.superview else {
return
}

animator = self.makeAnimator()
animator.addAnimations {
self.slideView.transform = CGAffineTransform(
translationX: 0,
y: -parentView.bounds.height * self.options.percentHeight - parentView.safeAreaInsets.bottom
)
}

animator.startAnimation()
}

func hide() {
animator = self.makeAnimator()
animator.addAnimations {
self.slideView.transform = CGAffineTransform.identity
}

animator.addCompletion({ _ in
self.didHide?()
})

animator.startAnimation()
}

func makeAnimator() -> UIViewPropertyAnimator {
return UIViewPropertyAnimator(duration: 0.25, dampingRatio: 1.0)
}
}

class ThroughView: UIView {
override func didMoveToSuperview() {
super.didMoveToSuperview()

guard let superview = superview else {
return
}

NSLayoutConstraint.on([pinEdges(view: superview)])
}

override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
guard let slideView = subviews.first else {
return false
}

return slideView.hitTest(convert(point, to: slideView), with: event) != nil
}
}

Comments