Skip to content

Commit 4ec158d

Browse files
authored
Merge pull request #35 from tomaskraina/enhancement/resizePanelByPointerScroll
Allow pointer scroll to resize/move panel
2 parents 0d24522 + 7dd2681 commit 4ec158d

File tree

5 files changed

+290
-122
lines changed

5 files changed

+290
-122
lines changed

Aiolos/Aiolos/Sources/HorizontalPanGestureRecognizer.swift

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,15 @@
55
import UIKit
66

77
/// UIPanGestureRecognizer that's being used for moving the panel horizontally
8-
public final class HorizontalPanGestureRecognizer: UIPanGestureRecognizer { }
8+
public final class HorizontalPanGestureRecognizer: UIPanGestureRecognizer {
9+
10+
// MARK: - Properties
11+
12+
public var detectsPointerScrolling: Bool = false {
13+
didSet {
14+
guard #available(iOS 13.4, *), NSClassFromString("UIPointerInteraction") != nil else { return }
15+
16+
self.allowedScrollTypesMask = self.detectsPointerScrolling ? .continuous : []
17+
}
18+
}
19+
}

Aiolos/Aiolos/Sources/PanGestureRecognizer.swift

Lines changed: 65 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -9,28 +9,74 @@
99
import UIKit
1010

1111

12-
/// GestureRecognizer that recognizes a pan gesture without any delay
13-
public final class PanGestureRecognizer: UIGestureRecognizer {
12+
/// Set of methods implemented by both UIPanGestureRecognizer and our own NoDelayPanGestureRecognizer
13+
@objc
14+
protocol PanGestureRecognizer: AnyObject {
15+
16+
// We need the @objc annotations because of `typealias PanOrScrollGestureRecognizer = UIGestureRecognizer & PanGestureRecognizer` in PanelGestures
17+
// which makes the methods in our `NoDelayPanGestureRecognizer` to be called via objc bridging. If we don't have these annotations, we get crashes in runtime.
18+
@objc(translationInView:)
19+
func translation(in view: UIView?) -> CGPoint
20+
@objc(setTranslation:inView:)
21+
func setTranslation(_ translation: CGPoint, in view: UIView?)
22+
@objc(velocityInView:)
23+
func velocity(in view: UIView?) -> CGPoint
24+
}
25+
26+
extension UIPanGestureRecognizer: PanGestureRecognizer { }
27+
28+
29+
// MARK: - PointerScrollGestureRecognizer
30+
31+
/// A UIPanGestureRecognizer subclass that recognizes only pointer scroll gestures
32+
/// We won't need this after UIGestureRecognizer subclasses will be able to detect pointer scrolls (FB7733482)
33+
public final class PointerScrollGestureRecognizer: UIPanGestureRecognizer {
34+
35+
// MARK: - Lifecycle
1436

15-
enum StartMode: Equatable {
16-
case onFixedArea
17-
case onVerticallyScrollableArea(competingScrollView: UIScrollView)
37+
/// As a non-failable initializer cannot be overwritten by a failable initializer, we mark this initializer as 'private' and use make() factory method from outside
38+
private override init(target: Any?, action: Selector?) {
39+
super.init(target: target, action: action)
1840
}
41+
}
42+
43+
public extension PointerScrollGestureRecognizer {
44+
45+
static func make(withTarget target: Any?, action: Selector?) -> PointerScrollGestureRecognizer? {
46+
guard #available(iOS 13.4, *), NSClassFromString("UIPointerInteraction") != nil else { return nil }
47+
48+
let gesture = PointerScrollGestureRecognizer(target: target, action: action)
49+
gesture.allowedScrollTypesMask = .continuous
50+
return gesture
51+
}
52+
}
53+
54+
// MARK: - UIGestureRecognizer+Subclass
55+
56+
public extension PointerScrollGestureRecognizer {
57+
58+
@available(iOS 13.4, *)
59+
override func shouldReceive(_ event: UIEvent) -> Bool {
60+
guard event.type == .scroll else { return false }
61+
62+
return super.shouldReceive(event)
63+
}
64+
}
65+
66+
67+
// MARK: - NoDelayPanGestureRecognizer
68+
69+
/// A UIGestureRecognizer subclass that recognizes pan gestures without any delay but doesn't recognize scrolling with pointer
70+
public final class NoDelayPanGestureRecognizer: UIGestureRecognizer {
1971

2072
private lazy var panForVelocity: UIPanGestureRecognizer = self.makeVelocityPan()
21-
private var initialPoint: CGPoint?
2273
private var lastPoint: CGPoint?
2374
private var currentPoint: CGPoint?
24-
25-
// MARK: Properties
26-
27-
private(set) var didPan: Bool = false
28-
var startMode: StartMode = .onFixedArea
2975
}
3076

3177
// MARK: - UIGestureRecognizer+Subclass
3278

33-
public extension PanGestureRecognizer {
79+
public extension NoDelayPanGestureRecognizer {
3480

3581
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
3682
super.touchesBegan(touches, with: event)
@@ -50,11 +96,9 @@ public extension PanGestureRecognizer {
5096
guard let touch = touches.first else { return }
5197

5298
let location = touch.location(in: self.view?.window)
53-
self.initialPoint = location
5499
self.lastPoint = location
55100
self.currentPoint = location
56101
self.state = .began
57-
self.didPan = false
58102
}
59103

60104
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent) {
@@ -65,17 +109,6 @@ public extension PanGestureRecognizer {
65109

66110
let currentPoint = touch.location(in: self.view?.window)
67111
self.currentPoint = currentPoint
68-
69-
if self.totalTranslation.hypotenuse() > Constants.minTranslation && self.didPan == false {
70-
self.didPan = true
71-
72-
// if we recognized a pan, make sure it can be considered a vertical pan
73-
guard self.totalTranslation.direction() == .vertical else {
74-
self.state = .cancelled
75-
return
76-
}
77-
}
78-
79112
self.state = .changed
80113
}
81114

@@ -96,41 +129,36 @@ public extension PanGestureRecognizer {
96129
override func reset() {
97130
super.reset()
98131

99-
self.didPan = false
100-
self.startMode = .onFixedArea
101132
self.panForVelocity = self.makeVelocityPan()
102133
}
103134
}
104135

105136
// MARK: - PanGestureRecognizer
106137

107-
extension PanGestureRecognizer {
138+
extension NoDelayPanGestureRecognizer: PanGestureRecognizer {
108139

109-
func translation(in view: UIView) -> CGPoint {
140+
func translation(in view: UIView?) -> CGPoint {
110141
guard let lastPoint = self.lastPoint else { return .zero }
111142
guard let currentPoint = self.currentPoint else { return .zero }
143+
guard let view = view ?? self.view?.window else { return .zero }
112144

113145
return self.translation(from: lastPoint, to: currentPoint, in: view)
114146
}
115147

116-
func setTranslation(_ translation: CGPoint, in view: UIView) {
148+
func setTranslation(_ translation: CGPoint, in view: UIView?) {
117149
guard let currentPoint = self.currentPoint else { return }
118150

119151
self.lastPoint = currentPoint.applying(.init(translationX: -translation.x, y: -translation.y))
120152
}
121153

122-
func velocity(in view: UIView) -> CGPoint {
154+
func velocity(in view: UIView?) -> CGPoint {
123155
return self.panForVelocity.velocity(in: view)
124156
}
125157
}
126158

127159
// MARK: - Private
128160

129-
private extension PanGestureRecognizer {
130-
131-
struct Constants {
132-
static let minTranslation: CGFloat = 5.0
133-
}
161+
private extension NoDelayPanGestureRecognizer {
134162

135163
func makeVelocityPan() -> UIPanGestureRecognizer {
136164
let pan = UIPanGestureRecognizer(target: nil, action: nil)
@@ -140,39 +168,12 @@ private extension PanGestureRecognizer {
140168
return pan
141169
}
142170

143-
var totalTranslation: CGPoint {
144-
guard let view = self.view else { return .zero }
145-
guard let initialPoint = self.initialPoint else { return .zero }
146-
guard let currentPoint = self.currentPoint else { return .zero }
147-
148-
return self.translation(from: initialPoint, to: currentPoint, in: view)
149-
}
150-
151171
func translation(from startPoint: CGPoint, to endPoint: CGPoint, in view: UIView) -> CGPoint {
152-
guard let window = self.view?.window else { return .zero }
172+
guard let window = view.window else { return .zero }
153173

154174
let startPointInView = window.convert(startPoint, to: view)
155175
let endPointInView = window.convert(endPoint, to: view)
156176

157177
return CGPoint(x: endPointInView.x - startPointInView.x, y: endPointInView.y - startPointInView.y)
158178
}
159179
}
160-
161-
private extension CGPoint {
162-
163-
enum Direction {
164-
case horizontal
165-
case vertical
166-
}
167-
168-
func hypotenuse() -> CGFloat {
169-
return sqrt(self.x * self.x + self.y * self.y)
170-
}
171-
172-
func direction() -> Direction {
173-
let horizontalDiff = self.x * self.x
174-
let verticalDiff = self.y * self.y
175-
176-
return horizontalDiff > verticalDiff ? .horizontal : .vertical
177-
}
178-
}

Aiolos/Aiolos/Sources/PanelConfiguration.swift

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,19 @@ public extension Panel {
1717
public typealias Edge = _PanelEdge
1818
public typealias Position = _PanelPosition
1919
public typealias PositionLogic = _PanelPositionLogic
20-
public typealias GestureResizingMode = _PanelGestureResizingMode
20+
21+
public struct GestureResizingMode: OptionSet {
22+
public let rawValue: Int
23+
24+
public init(rawValue: Int) {
25+
self.rawValue = rawValue
26+
}
27+
28+
public static let handle = GestureResizingMode(rawValue: 1 << 0)
29+
public static let content = GestureResizingMode(rawValue: 1 << 1)
30+
public static let byTouch = GestureResizingMode(rawValue: 1 << 2)
31+
public static let byPointerScroll = GestureResizingMode(rawValue: 1 << 3)
32+
}
2133

2234
public enum ResizeHandleMode {
2335
case hidden
@@ -156,13 +168,6 @@ public enum _PanelPositionLogic: Int {
156168
}
157169
}
158170

159-
@objc(PanelGestureResizingMode)
160-
public enum _PanelGestureResizingMode: Int {
161-
case disabled
162-
case excludingContent
163-
case includingContent
164-
}
165-
166171
extension _PanelPositionLogic {
167172

168173
func applyingInsets(of view: UIView, to insets: NSDirectionalEdgeInsets, edge: Panel.Configuration.Edge) -> NSDirectionalEdgeInsets {
@@ -185,3 +190,11 @@ extension _PanelPositionLogic {
185190
return insets
186191
}
187192
}
193+
194+
extension Panel.Configuration.GestureResizingMode {
195+
196+
public static let includingContent: Self = [.handle, .content, .byTouch, .byPointerScroll]
197+
public static let excludingContent: Self = [.handle, .byTouch, .byPointerScroll]
198+
var isPanningByTouchEnabled: Bool { self.contains(.byTouch) && (self.contains(.content) || self.contains(.handle)) }
199+
var isScrollingByPointerEnabled: Bool { self.contains(.byPointerScroll) && (self.contains(.content) || self.contains(.handle)) }
200+
}

0 commit comments

Comments
 (0)