Skip to content

Commit 52bf01c

Browse files
committed
NEW FEATURE: Added optional middle position
1 parent 259dd7e commit 52bf01c

File tree

4 files changed

+122
-32
lines changed

4 files changed

+122
-32
lines changed

Example/UIViewController-DisplayInDrawer/ContentViewController.swift

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,9 +80,14 @@ extension ContentViewController: DrawerConfiguration {
8080
return result
8181
}
8282

83+
func middlePositionY(for parentHeight: CGFloat) -> CGFloat? {
84+
guard isViewLoaded else { return nil }
85+
return parentHeight - separatorView.frame.minY
86+
}
87+
8388
func bottomPositionY(for parentHeight: CGFloat) -> CGFloat {
8489
guard isViewLoaded else { return 0 }
85-
return parentHeight - separatorView.frame.minY
90+
return parentHeight - 18
8691
}
8792

8893
func setPanGestureTarget(_ target: Any, action: Selector) {

Example/UIViewController-DisplayInDrawer/MainViewController.swift

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Copyright © 2018 Inloop. All rights reserved.
22

33
import UIKit
4+
import UIViewController_DisplayInDrawer
45

56
class MainViewController: UIViewController {
67
@IBAction func push(_ sender: Any) {
@@ -12,7 +13,7 @@ class MainViewController: UIViewController {
1213
@IBAction func presentInDrawer(_ sender: Any) {
1314
let controller = makeContentViewController()
1415
controller.setup(for: .drawer)
15-
navigationController?.displayInDrawer(controller, drawerPositionDelegate: nil)
16+
navigationController?.displayInDrawer(controller, drawerPositionDelegate: self)
1617
}
1718

1819
private func makeContentViewController() -> ContentViewController {
@@ -21,3 +22,25 @@ class MainViewController: UIViewController {
2122
return result
2223
}
2324
}
25+
26+
extension MainViewController: DrawerPositionDelegate {
27+
func didMoveDrawerToTopPosition() {
28+
NSLog("did move drawer to top position")
29+
}
30+
31+
func didMoveDrawerToMiddlePosition() {
32+
NSLog("did move drawer to middle position")
33+
}
34+
35+
func didMoveDrawerToBasePosition() {
36+
NSLog("did move drawer to base position")
37+
}
38+
39+
func willDismissDrawer() {
40+
NSLog("will dismiss drawer")
41+
}
42+
43+
func didDismissDrawer() {
44+
NSLog("did dismiss drawer")
45+
}
46+
}

UIViewController-DisplayInDrawer/Classes/PanGestureTarget.swift

Lines changed: 65 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ final class PanGestureTarget {
88
when view lands at the target drawer position
99
*/
1010
private let bounceVelocityThreshold: CGFloat = 100
11+
private let skipMiddleVelocityThreshold: CGFloat = 1500
1112
private let animationDuration: TimeInterval = 0.25
1213
private let dampedAnimationDuration: TimeInterval = 0.4
1314
private let springAnimationDamping: CGFloat = 0.75
@@ -24,8 +25,9 @@ final class PanGestureTarget {
2425
private weak var drawerConfiguration: DrawerConfiguration?
2526
private weak var drawerPositionDelegate: DrawerPositionDelegate?
2627
private var initialDrawerCenterLocation: CGPoint = .zero
27-
var basePositionY: CGFloat!
2828
var topPositionY: CGFloat!
29+
var middlePositionY: CGFloat?
30+
var bottomPositionY: CGFloat!
2931

3032
init(canvasView: UIView,
3133
drawerContainerView: UIView,
@@ -41,8 +43,9 @@ final class PanGestureTarget {
4143
}
4244

4345
internal func refreshDrawerPositions() {
44-
basePositionY = drawerConfiguration?.bottomPositionY(for: canvasView.bounds.height) ?? 0
46+
bottomPositionY = drawerConfiguration?.bottomPositionY(for: canvasView.bounds.height) ?? 0
4547
topPositionY = drawerConfiguration?.topPositionY(for: canvasView.bounds.height) ?? 0
48+
middlePositionY = drawerConfiguration?.middlePositionY(for: canvasView.bounds.height)
4649
}
4750

4851
deinit {
@@ -56,16 +59,13 @@ final class PanGestureTarget {
5659
performDrag(recognizer: recognizer)
5760
} else if recognizer.state == .ended || recognizer.state == .cancelled {
5861
let velocity = recognizer.velocity(in: canvasView)
59-
if shouldFinishUp(recognizer: recognizer) {
60-
animate(to: topPositionY, velocity: velocity)
61-
} else {
62-
animate(to: basePositionY, velocity: velocity)
63-
}
62+
let targetPositionY = self.targetPositionY(for: velocity)
63+
animate(to: targetPositionY, velocity: velocity)
6464
}
6565
}
6666

6767
private var isDraggedViewWithinAllowedArea: Bool {
68-
return drawerContainerView.frame.minY > topPositionY && drawerContainerView.frame.minY < basePositionY
68+
return drawerContainerView.frame.minY > topPositionY && drawerContainerView.frame.minY < bottomPositionY
6969
}
7070

7171
private func performDrag(recognizer: UIPanGestureRecognizer) {
@@ -84,16 +84,20 @@ final class PanGestureTarget {
8484

8585
private func updateDimming() {
8686
guard overDragAmount(drawerContainerView.center) == 0 else { return }
87-
let currentDistanceToBasePosition = basePositionY - drawerContainerView.frame.minY
88-
let totalDraggableDistance = basePositionY - topPositionY
89-
let movementPercentage = currentDistanceToBasePosition / totalDraggableDistance
87+
let currentDistanceToDimStartPosition = dimStartPosition - drawerContainerView.frame.minY
88+
let totalDraggableDistance = dimStartPosition - topPositionY
89+
let movementPercentage = currentDistanceToDimStartPosition / totalDraggableDistance
9090
dimmingView.alpha = movementPercentage * targetDimmingViewAlpha
9191
}
9292

93+
private var dimStartPosition: CGFloat {
94+
return middlePositionY ?? bottomPositionY
95+
}
96+
9397
private func overDragAmount(_ newCenter: CGPoint) -> CGFloat {
9498
let newMinY = newCenter.y - drawerContainerView.frame.height/2
9599
let aboveTop = newMinY - topPositionY
96-
let underBottom = newMinY - basePositionY
100+
let underBottom = newMinY - bottomPositionY
97101
if aboveTop < 0 {
98102
return aboveTop
99103
} else if underBottom > 0 {
@@ -103,23 +107,46 @@ final class PanGestureTarget {
103107
}
104108
}
105109

106-
private func shouldFinishUp(recognizer: UIPanGestureRecognizer) -> Bool {
107-
let velocity = recognizer.velocity(in: canvasView)
110+
private func targetPositionY(for velocity: CGPoint) -> CGFloat {
111+
let isDraggingQuickly = abs(velocity.y) > skipMiddleVelocityThreshold
108112
let isDraggingSlowly = abs(velocity.y) < bounceVelocityThreshold
109-
if isDraggingSlowly {
110-
return isInUpperHalfOfMovement()
113+
let isDraggingUp = velocity.y < 0
114+
if isDraggingQuickly && isDraggingUp {
115+
return topPositionY
116+
} else if isDraggingQuickly && !isDraggingUp {
117+
return bottomPositionY
118+
} else if isDraggingSlowly {
119+
return nearestPosition(searchStyle: .nearest)
120+
} else if isDraggingUp {
121+
return nearestPosition(searchStyle: .higher)
122+
} else if !isDraggingUp {
123+
return nearestPosition(searchStyle: .lower)
111124
} else {
112-
let isDraggingUp = velocity.y < 0
113-
return isDraggingUp
125+
fatalError()
114126
}
115127
}
116128

117-
private func isInUpperHalfOfMovement() -> Bool {
118-
let allowedMovementMinY = topPositionY!
119-
let allowedMovementMaxY = basePositionY!
120-
let halfMovement = (allowedMovementMaxY - allowedMovementMinY) / 2
121-
let movementMidY = allowedMovementMinY + halfMovement
122-
return drawerContainerView.frame.minY < movementMidY
129+
private enum NearestPositionSearchStyle {
130+
case nearest, higher, lower
131+
}
132+
133+
private func nearestPosition(searchStyle: NearestPositionSearchStyle) -> CGFloat {
134+
let currentPosition = drawerContainerView.frame.minY
135+
switch searchStyle {
136+
case .nearest:
137+
return positionsArray.min(by: { abs(currentPosition - $0) < abs(currentPosition - $1) })!
138+
case .higher:
139+
/* Keep in mind, that higher position means lower Y in UIView coordinate system */
140+
return positionsArray.sorted(by: >).first(where: { currentPosition - $0 > 0 }) ?? topPositionY
141+
case .lower:
142+
/* Keep in mind, that lower position means higher Y in UIView coordinate system */
143+
return positionsArray.sorted(by: <).first(where: { $0 - currentPosition > 0 }) ?? bottomPositionY
144+
}
145+
}
146+
147+
private var positionsArray: [CGFloat] {
148+
guard let middlePositionY = middlePositionY else { return [bottomPositionY, topPositionY] }
149+
return [bottomPositionY, middlePositionY, topPositionY]
123150
}
124151

125152
private func animate(to minY: CGFloat, velocity: CGPoint) {
@@ -135,6 +162,8 @@ final class PanGestureTarget {
135162
guard finished else { return }
136163
if self.isOnTopPosition {
137164
self.drawerPositionDelegate?.didMoveDrawerToTopPosition()
165+
} else if self.isOnMiddlePosition {
166+
self.drawerPositionDelegate?.didMoveDrawerToMiddlePosition()
138167
} else if self.isOnBasePosition {
139168
self.drawerPositionDelegate?.didMoveDrawerToBasePosition()
140169
}
@@ -158,6 +187,16 @@ final class PanGestureTarget {
158187
}
159188
}
160189

161-
private var isOnTopPosition: Bool { return Int(drawerContainerView.frame.minY) == Int(topPositionY) }
162-
private var isOnBasePosition: Bool { return Int(drawerContainerView.frame.minY) == Int(basePositionY) }
190+
private var isOnTopPosition: Bool {
191+
return Int(drawerContainerView.frame.minY) == Int(topPositionY)
192+
}
193+
194+
private var isOnMiddlePosition: Bool {
195+
guard let middlePositionY = middlePositionY else { return false }
196+
return Int(drawerContainerView.frame.minY) == Int(middlePositionY)
197+
}
198+
199+
private var isOnBasePosition: Bool {
200+
return Int(drawerContainerView.frame.minY) == Int(bottomPositionY)
201+
}
163202
}

UIViewController-DisplayInDrawer/Classes/UIViewController+PresentDrawerController.swift

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import UIKit
44

55
public protocol DrawerConfiguration: class {
66
func topPositionY(for parentHeight: CGFloat) -> CGFloat
7+
///If you return nil, there will only be two positions, top and bottom
8+
func middlePositionY(for parentHeight: CGFloat) -> CGFloat?
79
func bottomPositionY(for parentHeight: CGFloat) -> CGFloat
810
/**
911
drawerDismissClosure is injected by this lib.
@@ -25,8 +27,9 @@ public protocol DrawerConfiguration: class {
2527
}
2628

2729
public protocol DrawerPositionDelegate: class {
28-
func didMoveDrawerToBasePosition()
2930
func didMoveDrawerToTopPosition()
31+
func didMoveDrawerToMiddlePosition()
32+
func didMoveDrawerToBasePosition()
3033
func willDismissDrawer()
3134
func didDismissDrawer()
3235
}
@@ -84,7 +87,8 @@ extension UIViewController {
8487
return { [weak self, weak containerView, weak initialDisplayAnimator] in
8588
guard let strongSelf = self, let strongContainerView = containerView else { return }
8689
let oldTopPositionY = panGestureTarget.topPositionY!
87-
let oldBasePositionY = panGestureTarget.basePositionY!
90+
let oldMiddlePositionY = panGestureTarget.middlePositionY
91+
let oldBasePositionY = panGestureTarget.bottomPositionY!
8892
panGestureTarget.refreshDrawerPositions()
8993
let newContainerHeight = strongSelf.view.bounds.height - panGestureTarget.topPositionY + bottomOverpullPaddingHeight
9094
if strongContainerView.frame.height != newContainerHeight {
@@ -95,19 +99,38 @@ extension UIViewController {
9599

96100
let isOnOldTopPosition = Int(strongContainerView.frame.minY) == Int(oldTopPositionY)
97101
let newTopPositionIsDifferentThanOld = Int(panGestureTarget.topPositionY) != Int(oldTopPositionY)
102+
103+
let isOnOldMiddlePosition: Bool
104+
let newMiddlePositionIsDifferentThanOld: Bool
105+
if let oldMiddlePositionY = oldMiddlePositionY, let newMiddlePositionY = panGestureTarget.middlePositionY {
106+
isOnOldMiddlePosition = Int(strongContainerView.frame.minY) == Int(oldMiddlePositionY)
107+
newMiddlePositionIsDifferentThanOld = Int(newMiddlePositionY) != Int(oldMiddlePositionY)
108+
} else {
109+
isOnOldMiddlePosition = false
110+
newMiddlePositionIsDifferentThanOld = false
111+
}
112+
98113
let isOnOldBottomPosition = Int(strongContainerView.frame.minY) == Int(oldBasePositionY)
99-
let newBottomPositionIsDifferentThanOld = Int(panGestureTarget.basePositionY) != Int(oldBasePositionY)
114+
let newBottomPositionIsDifferentThanOld = Int(panGestureTarget.bottomPositionY) != Int(oldBasePositionY)
115+
100116
let isInitialDisplayAnimation = initialDisplayAnimator != nil
117+
101118
if !isInitialDisplayAnimation && isOnOldTopPosition && newTopPositionIsDifferentThanOld {
102119
self?.updateDrawerPosition(
103120
inFlightAnimator: initialDisplayAnimator,
104121
newTargetY: panGestureTarget.topPositionY,
105122
containerView: strongContainerView
106123
)
124+
} else if !isInitialDisplayAnimation && isOnOldMiddlePosition && newMiddlePositionIsDifferentThanOld {
125+
self?.updateDrawerPosition(
126+
inFlightAnimator: initialDisplayAnimator,
127+
newTargetY: panGestureTarget.middlePositionY ?? panGestureTarget.bottomPositionY,
128+
containerView: strongContainerView
129+
)
107130
} else if isOnOldBottomPosition && newBottomPositionIsDifferentThanOld {
108131
self?.updateDrawerPosition(
109132
inFlightAnimator: initialDisplayAnimator,
110-
newTargetY: panGestureTarget.basePositionY,
133+
newTargetY: panGestureTarget.bottomPositionY,
111134
containerView: strongContainerView
112135
)
113136
}

0 commit comments

Comments
 (0)