Skip to content

Commit 548e82e

Browse files
author
Vilém Kurz
authored
Merge pull request #6 from inloop/feature/add-third-position
Optional middle position
2 parents 75e33b5 + 52bf01c commit 548e82e

File tree

4 files changed

+141
-49
lines changed

4 files changed

+141
-49
lines changed

Example/UIViewController-DisplayInDrawer/ContentViewController.swift

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -68,21 +68,26 @@ class ContentViewController: UIViewController {
6868
}
6969

7070
extension ContentViewController: DrawerConfiguration {
71-
func topPositionY(in parentFrame: CGRect) -> CGFloat {
71+
func topPositionY(for parentHeight: CGFloat) -> CGFloat {
7272
guard isViewLoaded else { return 0 }
7373
/*
7474
You can return constant, but this is a more dynamic example:
7575
How to avoid making the drawer unneccessarily larger than its content
7676
*/
7777
let minimalTopPosition: CGFloat = 50
78-
let idealTopPosition = parentFrame.height - contentDerivedHeight()
78+
let idealTopPosition = parentHeight - contentDerivedHeight()
7979
let result = fmax(minimalTopPosition, idealTopPosition)
8080
return result
8181
}
8282

83-
var bottomPositionHeight: CGFloat {
83+
func middlePositionY(for parentHeight: CGFloat) -> CGFloat? {
84+
guard isViewLoaded else { return nil }
85+
return parentHeight - separatorView.frame.minY
86+
}
87+
88+
func bottomPositionY(for parentHeight: CGFloat) -> CGFloat {
8489
guard isViewLoaded else { return 0 }
85-
return 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: 66 additions & 28 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,9 +25,9 @@ final class PanGestureTarget {
2425
private weak var drawerConfiguration: DrawerConfiguration?
2526
private weak var drawerPositionDelegate: DrawerPositionDelegate?
2627
private var initialDrawerCenterLocation: CGPoint = .zero
27-
private var bottomPositionHeight: CGFloat!
28-
var basePositionY: CGFloat { return canvasView.frame.height - bottomPositionHeight }
2928
var topPositionY: CGFloat!
29+
var middlePositionY: CGFloat?
30+
var bottomPositionY: CGFloat!
3031

3132
init(canvasView: UIView,
3233
drawerContainerView: UIView,
@@ -42,8 +43,9 @@ final class PanGestureTarget {
4243
}
4344

4445
internal func refreshDrawerPositions() {
45-
bottomPositionHeight = drawerConfiguration?.bottomPositionHeight ?? 0
46-
topPositionY = drawerConfiguration?.topPositionY(in: canvasView.bounds) ?? 0
46+
bottomPositionY = drawerConfiguration?.bottomPositionY(for: canvasView.bounds.height) ?? 0
47+
topPositionY = drawerConfiguration?.topPositionY(for: canvasView.bounds.height) ?? 0
48+
middlePositionY = drawerConfiguration?.middlePositionY(for: canvasView.bounds.height)
4749
}
4850

4951
deinit {
@@ -57,16 +59,13 @@ final class PanGestureTarget {
5759
performDrag(recognizer: recognizer)
5860
} else if recognizer.state == .ended || recognizer.state == .cancelled {
5961
let velocity = recognizer.velocity(in: canvasView)
60-
if shouldFinishUp(recognizer: recognizer) {
61-
animate(to: topPositionY, velocity: velocity)
62-
} else {
63-
animate(to: basePositionY, velocity: velocity)
64-
}
62+
let targetPositionY = self.targetPositionY(for: velocity)
63+
animate(to: targetPositionY, velocity: velocity)
6564
}
6665
}
6766

6867
private var isDraggedViewWithinAllowedArea: Bool {
69-
return drawerContainerView.frame.minY > topPositionY && drawerContainerView.frame.minY < basePositionY
68+
return drawerContainerView.frame.minY > topPositionY && drawerContainerView.frame.minY < bottomPositionY
7069
}
7170

7271
private func performDrag(recognizer: UIPanGestureRecognizer) {
@@ -85,16 +84,20 @@ final class PanGestureTarget {
8584

8685
private func updateDimming() {
8786
guard overDragAmount(drawerContainerView.center) == 0 else { return }
88-
let currentDistanceToBasePosition = basePositionY - drawerContainerView.frame.minY
89-
let totalDraggableDistance = basePositionY - topPositionY
90-
let movementPercentage = currentDistanceToBasePosition / totalDraggableDistance
87+
let currentDistanceToDimStartPosition = dimStartPosition - drawerContainerView.frame.minY
88+
let totalDraggableDistance = dimStartPosition - topPositionY
89+
let movementPercentage = currentDistanceToDimStartPosition / totalDraggableDistance
9190
dimmingView.alpha = movementPercentage * targetDimmingViewAlpha
9291
}
9392

93+
private var dimStartPosition: CGFloat {
94+
return middlePositionY ?? bottomPositionY
95+
}
96+
9497
private func overDragAmount(_ newCenter: CGPoint) -> CGFloat {
9598
let newMinY = newCenter.y - drawerContainerView.frame.height/2
9699
let aboveTop = newMinY - topPositionY
97-
let underBottom = newMinY - basePositionY
100+
let underBottom = newMinY - bottomPositionY
98101
if aboveTop < 0 {
99102
return aboveTop
100103
} else if underBottom > 0 {
@@ -104,23 +107,46 @@ final class PanGestureTarget {
104107
}
105108
}
106109

107-
private func shouldFinishUp(recognizer: UIPanGestureRecognizer) -> Bool {
108-
let velocity = recognizer.velocity(in: canvasView)
110+
private func targetPositionY(for velocity: CGPoint) -> CGFloat {
111+
let isDraggingQuickly = abs(velocity.y) > skipMiddleVelocityThreshold
109112
let isDraggingSlowly = abs(velocity.y) < bounceVelocityThreshold
110-
if isDraggingSlowly {
111-
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)
112124
} else {
113-
let isDraggingUp = velocity.y < 0
114-
return isDraggingUp
125+
fatalError()
115126
}
116127
}
117128

118-
private func isInUpperHalfOfMovement() -> Bool {
119-
let allowedMovementMinY = topPositionY!
120-
let allowedMovementMaxY = basePositionY
121-
let halfMovement = (allowedMovementMaxY - allowedMovementMinY) / 2
122-
let movementMidY = allowedMovementMinY + halfMovement
123-
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]
124150
}
125151

126152
private func animate(to minY: CGFloat, velocity: CGPoint) {
@@ -136,6 +162,8 @@ final class PanGestureTarget {
136162
guard finished else { return }
137163
if self.isOnTopPosition {
138164
self.drawerPositionDelegate?.didMoveDrawerToTopPosition()
165+
} else if self.isOnMiddlePosition {
166+
self.drawerPositionDelegate?.didMoveDrawerToMiddlePosition()
139167
} else if self.isOnBasePosition {
140168
self.drawerPositionDelegate?.didMoveDrawerToBasePosition()
141169
}
@@ -159,6 +187,16 @@ final class PanGestureTarget {
159187
}
160188
}
161189

162-
private var isOnTopPosition: Bool { return Int(drawerContainerView.frame.minY) == Int(topPositionY) }
163-
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+
}
164202
}

UIViewController-DisplayInDrawer/Classes/UIViewController+PresentDrawerController.swift

Lines changed: 42 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@
33
import UIKit
44

55
public protocol DrawerConfiguration: class {
6-
func topPositionY(in parentFrame: CGRect) -> CGFloat
7-
var bottomPositionHeight: CGFloat { get }
6+
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?
9+
func bottomPositionY(for parentHeight: CGFloat) -> CGFloat
810
/**
911
drawerDismissClosure is injected by this lib.
1012
You should not change it, and you should call it when user presses dismiss button in your content view controller.
@@ -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,17 +99,39 @@ 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)
100-
if isOnOldTopPosition && newTopPositionIsDifferentThanOld {
101-
self?.updateWithNewYPosition(panGestureTarget.topPositionY,
102-
containerView: strongContainerView,
103-
inFlightAnimator: initialDisplayAnimator
114+
let newBottomPositionIsDifferentThanOld = Int(panGestureTarget.bottomPositionY) != Int(oldBasePositionY)
115+
116+
let isInitialDisplayAnimation = initialDisplayAnimator != nil
117+
118+
if !isInitialDisplayAnimation && isOnOldTopPosition && newTopPositionIsDifferentThanOld {
119+
self?.updateDrawerPosition(
120+
inFlightAnimator: initialDisplayAnimator,
121+
newTargetY: panGestureTarget.topPositionY,
122+
containerView: strongContainerView
123+
)
124+
} else if !isInitialDisplayAnimation && isOnOldMiddlePosition && newMiddlePositionIsDifferentThanOld {
125+
self?.updateDrawerPosition(
126+
inFlightAnimator: initialDisplayAnimator,
127+
newTargetY: panGestureTarget.middlePositionY ?? panGestureTarget.bottomPositionY,
128+
containerView: strongContainerView
104129
)
105130
} else if isOnOldBottomPosition && newBottomPositionIsDifferentThanOld {
106-
self?.updateWithNewYPosition(panGestureTarget.basePositionY,
107-
containerView: strongContainerView,
108-
inFlightAnimator: initialDisplayAnimator
131+
self?.updateDrawerPosition(
132+
inFlightAnimator: initialDisplayAnimator,
133+
newTargetY: panGestureTarget.bottomPositionY,
134+
containerView: strongContainerView
109135
)
110136
}
111137
}
@@ -122,10 +148,10 @@ extension UIViewController {
122148
controller.didMove(toParentViewController: self)
123149
}
124150

125-
private func updateWithNewYPosition(_ newY: CGFloat, containerView: UIView, inFlightAnimator: UIViewPropertyAnimator?) {
151+
private func updateDrawerPosition(inFlightAnimator: UIViewPropertyAnimator?, newTargetY: CGFloat, containerView: UIView) {
126152
dLog("inFlightAnimator: \(String(describing: inFlightAnimator))")
127153
var newFrame = containerView.frame
128-
newFrame.origin.y = newY
154+
newFrame.origin.y = newTargetY
129155
if let animator = inFlightAnimator {
130156
animator.addAnimations { containerView.frame = newFrame }
131157
} else {
@@ -137,7 +163,7 @@ extension UIViewController {
137163
containerView: UIView,
138164
positionDelegate: DrawerPositionDelegate?) -> UIViewPropertyAnimator {
139165
var endFrame = containerView.frame
140-
endFrame.origin.y = view.bounds.height - drawerConfiguration.bottomPositionHeight
166+
endFrame.origin.y = drawerConfiguration.bottomPositionY(for: view.bounds.height)
141167
let animator = UIViewPropertyAnimator(duration: 0.25, curve: .easeInOut, animations: {
142168
containerView.frame = endFrame
143169
})
@@ -184,7 +210,7 @@ extension UIViewController {
184210
private extension UIView {
185211
func addContainerView(for drawerConfiguration: DrawerConfiguration, cornerRadius: CGFloat, bottomPaddingHeight: CGFloat) -> UIView {
186212
var startFrame = bounds
187-
startFrame.size.height = bounds.height - drawerConfiguration.topPositionY(in: bounds) + bottomPaddingHeight
213+
startFrame.size.height = bounds.height - drawerConfiguration.topPositionY(for: bounds.height) + bottomPaddingHeight
188214
startFrame.origin.y += bounds.height
189215
/*
190216
OutsideBoundsHittableView is used here so that we capture taps on dimmed background.

0 commit comments

Comments
 (0)