Compare commits
64 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5d86bd5d02 | |||
| 3b6271c4f4 | |||
| 1671a3d50f | |||
| 0ab318e804 | |||
| 7df352a44b | |||
| 1443d377ad | |||
| e0bca25411 | |||
| e94d47b1a5 | |||
| 9d3a1674c4 | |||
| 24d81a4153 | |||
| 5723a8017b | |||
| 72055cd998 | |||
| 9cd8b4d960 | |||
| f39b368c1e | |||
| a4543351fe | |||
| 88ac013166 | |||
| 5d336b9090 | |||
| 45b3209b9b | |||
| da16cf6ada | |||
| 774a841fb5 | |||
| 020ffdaa84 | |||
| 2ef096b3a0 | |||
| 69bde3e80d | |||
| e6aa7db35a | |||
| 0124d98111 | |||
| c00a3836a5 | |||
| 66f9118e78 | |||
| f261b90a73 | |||
| a1602e0221 | |||
| b4e9ce8478 | |||
| 35d7cbb1d3 | |||
| 6ab678bb18 | |||
| 14ec9cf0a1 | |||
| a225bf2cf1 | |||
| 9b904cd895 | |||
| 11a16092a7 | |||
| b9b7f940b9 | |||
| e542728ff6 | |||
| 1eeb6e2d73 | |||
| cf9d53aca2 | |||
| 83463c792c | |||
| d5c7571a97 | |||
| 75c27bc232 | |||
| cbcc35268d | |||
| 11ba247ac4 | |||
| f411e81949 | |||
| 45d7cb7218 | |||
| 81f42d3951 | |||
| 2f7aed3e34 | |||
| 01f8261f0b | |||
| 489d7696cc | |||
| 0661f08a07 | |||
| 206475e6ab | |||
| a4a68e5b39 | |||
| de7ab0e0cb | |||
| 5f7b5ce81c | |||
| 36d7ea5100 | |||
| 33f8cf3802 | |||
| f6da876fdf | |||
| 96c5dc7b74 | |||
| a37931b62d | |||
| 5c848d9bf5 | |||
| 265b805fa9 | |||
| c4dfe33a5e |
+13
-13
@@ -36,6 +36,17 @@ jobs:
|
||||
osx_image: xcode10.2
|
||||
name: "iPhone X (iOS 12.2)"
|
||||
|
||||
- stage: Build examples
|
||||
osx_image: xcode10.2
|
||||
script: xcodebuild -scheme Maps -sdk iphonesimulator clean build
|
||||
name: "Maps"
|
||||
- script: xcodebuild -scheme Stocks -sdk iphonesimulator clean build
|
||||
osx_image: xcode10.2
|
||||
name: "Stocks"
|
||||
- script: xcodebuild -scheme Samples -sdk iphonesimulator clean build
|
||||
osx_image: xcode10.2
|
||||
name: "Samples"
|
||||
|
||||
- stage: Carthage
|
||||
osx_image: xcode10.2
|
||||
before_install:
|
||||
@@ -47,16 +58,5 @@ jobs:
|
||||
- stage: CocoaPods
|
||||
osx_image: xcode10.2
|
||||
script:
|
||||
- pod spec lint
|
||||
- pod lib lint
|
||||
|
||||
- stage: Build examples
|
||||
osx_image: xcode10.2
|
||||
script: xcodebuild -scheme Maps -sdk iphonesimulator clean build
|
||||
name: "Maps"
|
||||
- script: xcodebuild -scheme Stocks -sdk iphonesimulator clean build
|
||||
osx_image: xcode10.2
|
||||
name: "Stocks"
|
||||
- script: xcodebuild -scheme Samples -sdk iphonesimulator clean build
|
||||
osx_image: xcode10.2
|
||||
name: "Samples"
|
||||
- pod spec lint --allow-warnings
|
||||
- pod lib lint --allow-warnings
|
||||
|
||||
@@ -677,6 +677,10 @@ class DebugTableViewController: InspectableViewController {
|
||||
// Remove FloatingPanel from a view
|
||||
(self.parent as! FloatingPanelController).removePanelFromParent(animated: true, completion: nil)
|
||||
}
|
||||
|
||||
func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
||||
print("TableView --- ", scrollView.contentOffset, scrollView.contentInset)
|
||||
}
|
||||
}
|
||||
|
||||
extension DebugTableViewController: UITableViewDataSource {
|
||||
@@ -933,7 +937,7 @@ extension TabBarContentViewController: UITextViewDelegate {
|
||||
// Using KVO of `scrollView.contentOffset`). Because it can lead to an
|
||||
// infinite loop if a user also resets a content offset as below and,
|
||||
// in the situation, a user has to modify the library.
|
||||
if fpc.position != .full, fpc.surfaceView.frame.minY < fpc.originYOfSurface(for: .full) {
|
||||
if fpc.position != .full, fpc.surfaceView.frame.minY > fpc.originYOfSurface(for: .full) {
|
||||
scrollView.contentOffset = .zero
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
Pod::Spec.new do |s|
|
||||
|
||||
s.name = "FloatingPanel"
|
||||
s.version = "1.6.1"
|
||||
s.version = "1.6.4"
|
||||
s.summary = "FloatingPanel is a clean and easy-to-use UI component of a floating panel interface."
|
||||
s.description = <<-DESC
|
||||
FloatingPanel is a clean and easy-to-use UI component for a new interface introduced in Apple Maps, Shortcuts and Stocks app.
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
542753C622C49A6E00D17955 /* FloatingPanelLayoutTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 542753C522C49A6E00D17955 /* FloatingPanelLayoutTests.swift */; };
|
||||
542753C822C49A8F00D17955 /* Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 542753C722C49A8F00D17955 /* Utils.swift */; };
|
||||
54352E9621A51A2500CBCA08 /* FloatingPanelTransitioning.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54352E9521A51A2500CBCA08 /* FloatingPanelTransitioning.swift */; };
|
||||
54352E9821A521CA00CBCA08 /* FloatingPanelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54352E9721A521CA00CBCA08 /* FloatingPanelView.swift */; };
|
||||
5450EEE421646DF500135936 /* FloatingPanelBehavior.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5450EEE321646DF500135936 /* FloatingPanelBehavior.swift */; };
|
||||
@@ -16,9 +18,10 @@
|
||||
545DB9DE215118C800CA77B8 /* UIExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 545DB9DD215118C800CA77B8 /* UIExtensions.swift */; };
|
||||
545DB9E021511AC100CA77B8 /* FloatingPanelController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 545DB9DF21511AC100CA77B8 /* FloatingPanelController.swift */; };
|
||||
545DBA2B2152383100CA77B8 /* GrabberHandleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 545DBA2A2152383100CA77B8 /* GrabberHandleView.swift */; };
|
||||
549E944522CF295D0050AECF /* FloatingPanelPositionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 549E944422CF295D0050AECF /* FloatingPanelPositionTests.swift */; };
|
||||
54A6B6B122968B530077F348 /* FloatingPanelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54A6B6B022968B530077F348 /* FloatingPanelTests.swift */; };
|
||||
54A6B6B622968F710077F348 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 54A6B6B522968F710077F348 /* LaunchScreen.storyboard */; };
|
||||
54A6B6B82296A8520077F348 /* FloatingPanelViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54A6B6B72296A8520077F348 /* FloatingPanelViewTests.swift */; };
|
||||
54A6B6B82296A8520077F348 /* FloatingPanelSurfaceViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54A6B6B72296A8520077F348 /* FloatingPanelSurfaceViewTests.swift */; };
|
||||
54ABD7AF216CCFF7002E6C13 /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54ABD7AE216CCFF7002E6C13 /* Logger.swift */; };
|
||||
54CDC5D3215B6D5A007D205C /* FloatingPanelSurfaceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CDC5D2215B6D5A007D205C /* FloatingPanelSurfaceView.swift */; };
|
||||
54CDC5D5215B6D8D007D205C /* FloatingPanelBackdropView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CDC5D4215B6D8D007D205C /* FloatingPanelBackdropView.swift */; };
|
||||
@@ -45,6 +48,8 @@
|
||||
/* End PBXContainerItemProxy section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
542753C522C49A6E00D17955 /* FloatingPanelLayoutTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FloatingPanelLayoutTests.swift; sourceTree = "<group>"; };
|
||||
542753C722C49A8F00D17955 /* Utils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Utils.swift; sourceTree = "<group>"; };
|
||||
54352E9521A51A2500CBCA08 /* FloatingPanelTransitioning.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FloatingPanelTransitioning.swift; sourceTree = "<group>"; };
|
||||
54352E9721A521CA00CBCA08 /* FloatingPanelView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FloatingPanelView.swift; sourceTree = "<group>"; };
|
||||
5450EEE321646DF500135936 /* FloatingPanelBehavior.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FloatingPanelBehavior.swift; sourceTree = "<group>"; };
|
||||
@@ -57,9 +62,10 @@
|
||||
545DB9DD215118C800CA77B8 /* UIExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIExtensions.swift; sourceTree = "<group>"; };
|
||||
545DB9DF21511AC100CA77B8 /* FloatingPanelController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FloatingPanelController.swift; sourceTree = "<group>"; };
|
||||
545DBA2A2152383100CA77B8 /* GrabberHandleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GrabberHandleView.swift; sourceTree = "<group>"; };
|
||||
549E944422CF295D0050AECF /* FloatingPanelPositionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FloatingPanelPositionTests.swift; sourceTree = "<group>"; };
|
||||
54A6B6B022968B530077F348 /* FloatingPanelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FloatingPanelTests.swift; sourceTree = "<group>"; };
|
||||
54A6B6B522968F710077F348 /* LaunchScreen.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = LaunchScreen.storyboard; sourceTree = "<group>"; };
|
||||
54A6B6B72296A8520077F348 /* FloatingPanelViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FloatingPanelViewTests.swift; sourceTree = "<group>"; };
|
||||
54A6B6B72296A8520077F348 /* FloatingPanelSurfaceViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FloatingPanelSurfaceViewTests.swift; sourceTree = "<group>"; };
|
||||
54ABD7AE216CCFF7002E6C13 /* Logger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Logger.swift; sourceTree = "<group>"; };
|
||||
54CDC5D2215B6D5A007D205C /* FloatingPanelSurfaceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FloatingPanelSurfaceView.swift; sourceTree = "<group>"; };
|
||||
54CDC5D4215B6D8D007D205C /* FloatingPanelBackdropView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FloatingPanelBackdropView.swift; sourceTree = "<group>"; };
|
||||
@@ -140,8 +146,11 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
54A6B6B022968B530077F348 /* FloatingPanelTests.swift */,
|
||||
54A6B6B72296A8520077F348 /* FloatingPanelViewTests.swift */,
|
||||
545DB9CF2151169500CA77B8 /* FloatingPanelControllerTests.swift */,
|
||||
542753C522C49A6E00D17955 /* FloatingPanelLayoutTests.swift */,
|
||||
54A6B6B72296A8520077F348 /* FloatingPanelSurfaceViewTests.swift */,
|
||||
549E944422CF295D0050AECF /* FloatingPanelPositionTests.swift */,
|
||||
542753C722C49A8F00D17955 /* Utils.swift */,
|
||||
545DB9D12151169500CA77B8 /* Info.plist */,
|
||||
);
|
||||
path = Tests;
|
||||
@@ -309,6 +318,7 @@
|
||||
545DBA2B2152383100CA77B8 /* GrabberHandleView.swift in Sources */,
|
||||
54352E9621A51A2500CBCA08 /* FloatingPanelTransitioning.swift in Sources */,
|
||||
545DB9DE215118C800CA77B8 /* UIExtensions.swift in Sources */,
|
||||
542753C822C49A8F00D17955 /* Utils.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -318,7 +328,9 @@
|
||||
files = (
|
||||
54A6B6B122968B530077F348 /* FloatingPanelTests.swift in Sources */,
|
||||
545DB9D02151169500CA77B8 /* FloatingPanelControllerTests.swift in Sources */,
|
||||
54A6B6B82296A8520077F348 /* FloatingPanelViewTests.swift in Sources */,
|
||||
549E944522CF295D0050AECF /* FloatingPanelPositionTests.swift in Sources */,
|
||||
542753C622C49A6E00D17955 /* FloatingPanelLayoutTests.swift in Sources */,
|
||||
54A6B6B82296A8520077F348 /* FloatingPanelSurfaceViewTests.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
||||
@@ -29,7 +29,9 @@
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<Testables>
|
||||
<TestableReference
|
||||
skipped = "NO">
|
||||
skipped = "NO"
|
||||
parallelizable = "YES"
|
||||
testExecutionOrdering = "random">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "545DB9C92151169500CA77B8"
|
||||
|
||||
@@ -8,9 +8,9 @@ import UIKit.UIGestureRecognizerSubclass // For Xcode 9.4.1
|
||||
///
|
||||
/// FloatingPanel presentation model
|
||||
///
|
||||
class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate {
|
||||
class FloatingPanel: NSObject, UIGestureRecognizerDelegate {
|
||||
// MUST be a weak reference to prevent UI freeze on the presentation modally
|
||||
weak var viewcontroller: FloatingPanelController!
|
||||
weak var viewcontroller: FloatingPanelController?
|
||||
|
||||
let surfaceView: FloatingPanelSurfaceView
|
||||
let backdropView: FloatingPanelBackdropView
|
||||
@@ -25,7 +25,11 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
|
||||
}
|
||||
|
||||
private(set) var state: FloatingPanelPosition = .hidden {
|
||||
didSet { viewcontroller.delegate?.floatingPanelDidChangePosition(viewcontroller) }
|
||||
didSet {
|
||||
if let vc = viewcontroller {
|
||||
vc.delegate?.floatingPanelDidChangePosition(vc)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var isBottomState: Bool {
|
||||
@@ -47,13 +51,10 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
|
||||
|
||||
// Scroll handling
|
||||
private var initialScrollOffset: CGPoint = .zero
|
||||
private var initialScrollFrame: CGRect = .zero
|
||||
private var stopScrollDeceleration: Bool = false
|
||||
private var scrollBouncable = false
|
||||
private var scrollIndictorVisible = false
|
||||
|
||||
private var isScrollLocked: Bool = false
|
||||
|
||||
// MARK: - Interface
|
||||
|
||||
init(_ vc: FloatingPanelController, layout: FloatingPanelLayout, behavior: FloatingPanelBehavior) {
|
||||
@@ -91,6 +92,11 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
|
||||
}
|
||||
|
||||
private func move(from: FloatingPanelPosition, to: FloatingPanelPosition, animated: Bool, completion: (() -> Void)? = nil) {
|
||||
assert(layoutAdapter.isValid(to), "Can't move to '\(to)' position because it's not valid in the layout")
|
||||
guard let vc = viewcontroller else {
|
||||
completion?()
|
||||
return
|
||||
}
|
||||
if state != layoutAdapter.topMostState {
|
||||
lockScrollView()
|
||||
}
|
||||
@@ -100,11 +106,11 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
|
||||
let animator: UIViewPropertyAnimator
|
||||
switch (from, to) {
|
||||
case (.hidden, let to):
|
||||
animator = behavior.addAnimator(self.viewcontroller, to: to)
|
||||
animator = behavior.addAnimator(vc, to: to)
|
||||
case (let from, .hidden):
|
||||
animator = behavior.removeAnimator(self.viewcontroller, from: from)
|
||||
animator = behavior.removeAnimator(vc, from: from)
|
||||
case (let from, let to):
|
||||
animator = behavior.moveAnimator(self.viewcontroller, from: from, to: to)
|
||||
animator = behavior.moveAnimator(vc, from: from, to: to)
|
||||
}
|
||||
|
||||
animator.addAnimations { [weak self] in
|
||||
@@ -118,6 +124,8 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
|
||||
self.animator = nil
|
||||
if self.state == self.layoutAdapter.topMostState {
|
||||
self.unlockScrollView()
|
||||
} else {
|
||||
self.lockScrollView()
|
||||
}
|
||||
completion?()
|
||||
}
|
||||
@@ -128,6 +136,8 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
|
||||
self.updateLayout(to: to)
|
||||
if self.state == self.layoutAdapter.topMostState {
|
||||
self.unlockScrollView()
|
||||
} else {
|
||||
self.lockScrollView()
|
||||
}
|
||||
completion?()
|
||||
}
|
||||
@@ -139,11 +149,15 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
|
||||
self.layoutAdapter.activateLayout(of: target)
|
||||
}
|
||||
|
||||
private func getBackdropAlpha(with translation: CGPoint) -> CGFloat {
|
||||
let currentY = surfaceView.frame.minY
|
||||
func getBackdropAlpha(at currentY: CGFloat, with translation: CGPoint) -> CGFloat {
|
||||
let forwardY = (translation.y >= 0)
|
||||
let segment = layoutAdapter.segument(at: currentY, forward: forwardY)
|
||||
let lowerPos = segment.lower ?? layoutAdapter.topMostState
|
||||
let upperPos = segment.upper ?? layoutAdapter.bottomMostState
|
||||
|
||||
let pre = forwardY ? lowerPos : upperPos
|
||||
let next = forwardY ? upperPos : lowerPos
|
||||
|
||||
let next = directionalPosition(at: currentY, with: translation)
|
||||
let pre = redirectionalPosition(at: currentY, with: translation)
|
||||
let nextY = layoutAdapter.positionY(for: next)
|
||||
let preY = layoutAdapter.positionY(for: pre)
|
||||
|
||||
@@ -165,7 +179,8 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
|
||||
|
||||
/* log.debug("shouldRecognizeSimultaneouslyWith", otherGestureRecognizer) */
|
||||
|
||||
if viewcontroller.delegate?.floatingPanel(viewcontroller, shouldRecognizeSimultaneouslyWith: otherGestureRecognizer) ?? false {
|
||||
if let vc = viewcontroller,
|
||||
vc.delegate?.floatingPanel(vc, shouldRecognizeSimultaneouslyWith: otherGestureRecognizer) ?? false {
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -212,11 +227,11 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
|
||||
}
|
||||
}
|
||||
|
||||
if viewcontroller.delegate?.floatingPanel(viewcontroller, shouldRecognizeSimultaneouslyWith: otherGestureRecognizer) ?? false {
|
||||
if let vc = viewcontroller,
|
||||
vc.delegate?.floatingPanel(vc, shouldRecognizeSimultaneouslyWith: otherGestureRecognizer) ?? false {
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
switch otherGestureRecognizer {
|
||||
case is UIPanGestureRecognizer,
|
||||
is UISwipeGestureRecognizer,
|
||||
@@ -249,14 +264,16 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
|
||||
|
||||
let location = panGesture.location(in: surfaceView)
|
||||
|
||||
let belowTop = surfaceView.frame.minY > layoutAdapter.topY
|
||||
let belowTop = surfaceView.presentationFrame.minY > layoutAdapter.topY
|
||||
let offset = scrollView.contentOffset.y - scrollView.contentOffsetZero.y
|
||||
|
||||
log.debug("scroll gesture(\(state):\(panGesture.state)) --",
|
||||
"belowTop = \(belowTop),",
|
||||
"interactionInProgress = \(interactionInProgress),",
|
||||
"scroll offset = \(scrollView.contentOffset.y),",
|
||||
"scroll offset = \(offset),",
|
||||
"location = \(location.y), velocity = \(velocity.y)")
|
||||
|
||||
|
||||
if belowTop {
|
||||
// Scroll offset pinning
|
||||
if state == layoutAdapter.topMostState {
|
||||
@@ -273,22 +290,43 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
|
||||
scrollView.setContentOffset(initialScrollOffset, animated: false)
|
||||
}
|
||||
|
||||
// Always hide a scroll indicator at the non-top.
|
||||
// Hide a scroll indicator at the non-top in dragging.
|
||||
if interactionInProgress {
|
||||
lockScrollView()
|
||||
}
|
||||
} else {
|
||||
let offset = scrollView.contentOffset.y - scrollView.contentOffsetZero.y
|
||||
// Always show a scroll indicator at the top.
|
||||
if interactionInProgress {
|
||||
if offset > 0 {
|
||||
} else {
|
||||
if state == layoutAdapter.topMostState, self.animator == nil,
|
||||
offset > 0, velocity.y < 0 {
|
||||
unlockScrollView()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if interactionInProgress {
|
||||
// Show a scroll indicator at the top in dragging.
|
||||
if offset >= 0, velocity.y <= 0 {
|
||||
unlockScrollView()
|
||||
} else {
|
||||
if state == layoutAdapter.topMostState {
|
||||
// Adjust a small gap of the scroll offset just after swiping down starts in the grabber area.
|
||||
if grabberAreaFrame.contains(location), grabberAreaFrame.contains(initialLocation) {
|
||||
scrollView.setContentOffset(initialScrollOffset, animated: false)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Hide a scroll indicator just before starting an interaction by swiping a panel down.
|
||||
if state == layoutAdapter.topMostState,
|
||||
offset < 0, velocity.y > 0 {
|
||||
lockScrollView()
|
||||
if state == layoutAdapter.topMostState {
|
||||
// Hide a scroll indicator just before starting an interaction by swiping a panel down.
|
||||
if offset < 0, velocity.y > 0 {
|
||||
lockScrollView()
|
||||
}
|
||||
// Show a scroll indicator when an animation is interrupted at the top and content is scrolled up
|
||||
if offset > 0, velocity.y < 0 {
|
||||
unlockScrollView()
|
||||
}
|
||||
|
||||
// Adjust a small gap of the scroll offset just before swiping down starts in the grabber area,
|
||||
if grabberAreaFrame.contains(location), grabberAreaFrame.contains(initialLocation) {
|
||||
scrollView.setContentOffset(initialScrollOffset, animated: false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -299,23 +337,26 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
|
||||
log.debug("panel gesture(\(state):\(panGesture.state)) --",
|
||||
"translation = \(translation.y), location = \(location.y), velocity = \(velocity.y)")
|
||||
|
||||
if let animator = self.animator {
|
||||
log.debug("panel animation interrupted!!!")
|
||||
if animator.isInterruptible {
|
||||
animator.stopAnimation(false)
|
||||
animator.finishAnimation(at: .current)
|
||||
}
|
||||
self.animator = nil
|
||||
|
||||
// A user can stop a panel at the nearest Y of a target position
|
||||
if abs(surfaceView.frame.minY - layoutAdapter.topY) < 1.0 {
|
||||
surfaceView.frame.origin.y = layoutAdapter.topY
|
||||
}
|
||||
if interactionInProgress == false, isDecelerating == false,
|
||||
let vc = viewcontroller, vc.delegate?.floatingPanelShouldBeginDragging(vc) == false {
|
||||
return
|
||||
}
|
||||
|
||||
if interactionInProgress == false,
|
||||
viewcontroller.delegate?.floatingPanelShouldBeginDragging(viewcontroller) == false {
|
||||
return
|
||||
if let animator = self.animator {
|
||||
guard surfaceView.presentationFrame.minY >= layoutAdapter.topMaxY else { return }
|
||||
log.debug("panel animation(interruptible: \(animator.isInterruptible)) interrupted!!!")
|
||||
if animator.isInterruptible {
|
||||
animator.stopAnimation(false)
|
||||
// A user can stop a panel at the nearest Y of a target position so this fine-tunes
|
||||
// the a small gap between the presentation layer frame and model layer frame
|
||||
// to unlock scroll view properly at finishAnimation(at:)
|
||||
if abs(surfaceView.frame.minY - layoutAdapter.topY) <= 1.0 {
|
||||
surfaceView.frame.origin.y = layoutAdapter.topY
|
||||
}
|
||||
animator.finishAnimation(at: .current)
|
||||
} else {
|
||||
self.animator = nil
|
||||
}
|
||||
}
|
||||
|
||||
if panGesture.state == .began {
|
||||
@@ -334,6 +375,15 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
|
||||
}
|
||||
panningChange(with: translation)
|
||||
case .ended, .cancelled, .failed:
|
||||
if interactionInProgress == false {
|
||||
startInteraction(with: translation, at: location)
|
||||
// Workaround: Prevent stopping the surface view b/w anchors if the pan gesture
|
||||
// doesn't pass through .changed state after an interruptible animator is interrupted.
|
||||
let dy = translation.y - .leastNonzeroMagnitude
|
||||
layoutAdapter.updateInteractiveTopConstraint(diff: dy,
|
||||
allowsTopBuffer: true,
|
||||
with: behavior)
|
||||
}
|
||||
panningEnd(with: translation, velocity: velocity)
|
||||
default:
|
||||
break
|
||||
@@ -401,33 +451,36 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
|
||||
// So here just preserve the current state if needed.
|
||||
log.debug("panningBegan -- location = \(location.y)")
|
||||
initialLocation = location
|
||||
|
||||
guard let scrollView = scrollView else { return }
|
||||
if state == layoutAdapter.topMostState {
|
||||
if let scrollView = scrollView {
|
||||
initialScrollFrame = scrollView.frame
|
||||
}
|
||||
} else {
|
||||
if let scrollView = scrollView {
|
||||
if grabberAreaFrame.contains(location) {
|
||||
initialScrollOffset = scrollView.contentOffset
|
||||
}
|
||||
} else {
|
||||
initialScrollOffset = scrollView.contentOffset
|
||||
}
|
||||
}
|
||||
|
||||
private func panningChange(with translation: CGPoint) {
|
||||
log.debug("panningChange -- translation = \(translation.y)")
|
||||
let pre = surfaceView.frame.minY
|
||||
let preY = surfaceView.frame.minY
|
||||
let dy = translation.y - initialTranslationY
|
||||
|
||||
layoutAdapter.updateInteractiveTopConstraint(diff: dy,
|
||||
allowsTopBuffer: allowsTopBuffer(for: dy),
|
||||
with: behavior)
|
||||
|
||||
backdropView.alpha = getBackdropAlpha(with: translation)
|
||||
let currentY = surfaceView.frame.minY
|
||||
backdropView.alpha = getBackdropAlpha(at: currentY, with: translation)
|
||||
preserveContentVCLayoutIfNeeded()
|
||||
|
||||
let didMove = (pre != surfaceView.frame.minY)
|
||||
let didMove = (preY != currentY)
|
||||
guard didMove else { return }
|
||||
|
||||
viewcontroller.delegate?.floatingPanelDidMove(viewcontroller)
|
||||
if let vc = viewcontroller {
|
||||
vc.delegate?.floatingPanelDidMove(vc)
|
||||
}
|
||||
}
|
||||
|
||||
private func allowsTopBuffer(for translationY: CGFloat) -> Bool {
|
||||
@@ -442,20 +495,25 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
|
||||
}
|
||||
|
||||
private var disabledBottomAutoLayout = false
|
||||
private var disabledAutoLayoutItems: Set<NSLayoutConstraint> = []
|
||||
// Prevent stretching a view having a constraint to SafeArea.bottom in an overflow
|
||||
// from the full position because SafeArea is global in a screen.
|
||||
private func preserveContentVCLayoutIfNeeded() {
|
||||
guard let vc = viewcontroller else { return }
|
||||
// Must include topY
|
||||
if (surfaceView.frame.minY <= layoutAdapter.topY) {
|
||||
if !disabledBottomAutoLayout {
|
||||
viewcontroller.contentViewController?.view?.constraints.forEach({ (const) in
|
||||
switch viewcontroller.contentViewController?.layoutGuide.bottomAnchor {
|
||||
disabledAutoLayoutItems.removeAll()
|
||||
vc.contentViewController?.view?.constraints.forEach({ (const) in
|
||||
switch vc.contentViewController?.layoutGuide.bottomAnchor {
|
||||
case const.firstAnchor:
|
||||
(const.secondItem as? UIView)?.disableAutoLayout()
|
||||
const.isActive = false
|
||||
disabledAutoLayoutItems.insert(const)
|
||||
case const.secondAnchor:
|
||||
(const.firstItem as? UIView)?.disableAutoLayout()
|
||||
const.isActive = false
|
||||
disabledAutoLayoutItems.insert(const)
|
||||
default:
|
||||
break
|
||||
}
|
||||
@@ -464,8 +522,8 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
|
||||
disabledBottomAutoLayout = true
|
||||
} else {
|
||||
if disabledBottomAutoLayout {
|
||||
viewcontroller.contentViewController?.view?.constraints.forEach({ (const) in
|
||||
switch viewcontroller.contentViewController?.layoutGuide.bottomAnchor {
|
||||
disabledAutoLayoutItems.forEach({ (const) in
|
||||
switch vc.contentViewController?.layoutGuide.bottomAnchor {
|
||||
case const.firstAnchor:
|
||||
(const.secondItem as? UIView)?.enableAutoLayout()
|
||||
const.isActive = true
|
||||
@@ -476,6 +534,7 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
|
||||
break
|
||||
}
|
||||
})
|
||||
disabledAutoLayoutItems.removeAll()
|
||||
}
|
||||
disabledBottomAutoLayout = false
|
||||
}
|
||||
@@ -497,26 +556,37 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
|
||||
}
|
||||
}
|
||||
|
||||
let targetPosition = self.targetPosition(with: velocity)
|
||||
let currentY = surfaceView.frame.minY
|
||||
let targetPosition = self.targetPosition(from: currentY, with: velocity)
|
||||
let distance = self.distance(to: targetPosition)
|
||||
|
||||
endInteraction(for: targetPosition)
|
||||
|
||||
if isRemovalInteractionEnabled, isBottomState {
|
||||
let velocityVector = (distance != 0) ? CGVector(dx: 0,
|
||||
dy: min(abs(velocity.y)/distance, behavior.removalVelocity)) : .zero
|
||||
|
||||
if shouldStartRemovalAnimation(with: velocityVector) {
|
||||
|
||||
viewcontroller.delegate?.floatingPanelDidEndDraggingToRemove(viewcontroller, withVelocity: velocity)
|
||||
self.startRemovalAnimation(with: velocityVector) { [weak self] in
|
||||
let velocityVector = (distance != 0) ? CGVector(dx: 0, dy: min(velocity.y/distance, behavior.removalVelocity)) : .zero
|
||||
// `velocityVector` will be replaced by just a velocity(not vector) when FloatingPanelRemovalInteraction will be added.
|
||||
if shouldStartRemovalAnimation(with: velocityVector), let vc = viewcontroller {
|
||||
vc.delegate?.floatingPanelDidEndDraggingToRemove(vc, withVelocity: velocity)
|
||||
let animationVector = CGVector(dx: abs(velocityVector.dx), dy: abs(velocityVector.dy))
|
||||
startRemovalAnimation(vc, with: animationVector) { [weak self] in
|
||||
self?.finishRemovalAnimation()
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
viewcontroller.delegate?.floatingPanelDidEndDragging(viewcontroller, withVelocity: velocity, targetPosition: targetPosition)
|
||||
if let vc = viewcontroller {
|
||||
vc.delegate?.floatingPanelDidEndDragging(vc, withVelocity: velocity, targetPosition: targetPosition)
|
||||
}
|
||||
|
||||
if scrollView != nil, !stopScrollDeceleration,
|
||||
surfaceView.frame.minY == layoutAdapter.topY,
|
||||
targetPosition == layoutAdapter.topMostState {
|
||||
self.state = targetPosition
|
||||
self.updateLayout(to: targetPosition)
|
||||
self.unlockScrollView()
|
||||
return
|
||||
}
|
||||
|
||||
// Workaround: Disable a tracking scroll to prevent bouncing a scroll content in a panel animating
|
||||
let isScrollEnabled = scrollView?.isScrollEnabled
|
||||
@@ -536,12 +606,12 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
|
||||
private func shouldStartRemovalAnimation(with velocityVector: CGVector) -> Bool {
|
||||
let posY = layoutAdapter.positionY(for: state)
|
||||
let currentY = surfaceView.frame.minY
|
||||
let bottomMaxY = layoutAdapter.bottomMaxY
|
||||
let hiddenY = layoutAdapter.positionY(for: .hidden)
|
||||
let vth = behavior.removalVelocity
|
||||
let pth = max(min(behavior.removalProgress, 1.0), 0.0)
|
||||
|
||||
let num = (currentY - posY)
|
||||
let den = (bottomMaxY - posY)
|
||||
let den = (hiddenY - posY)
|
||||
|
||||
guard num >= 0, den != 0, (num / den >= pth || velocityVector.dy == vth)
|
||||
else { return false }
|
||||
@@ -549,8 +619,8 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
|
||||
return true
|
||||
}
|
||||
|
||||
private func startRemovalAnimation(with velocityVector: CGVector, completion: (() -> Void)?) {
|
||||
let animator = self.behavior.removalInteractionAnimator(self.viewcontroller, with: velocityVector)
|
||||
private func startRemovalAnimation(_ vc: FloatingPanelController, with velocityVector: CGVector, completion: (() -> Void)?) {
|
||||
let animator = behavior.removalInteractionAnimator(vc, with: velocityVector)
|
||||
|
||||
animator.addAnimations { [weak self] in
|
||||
self?.updateLayout(to: .hidden)
|
||||
@@ -575,13 +645,15 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
|
||||
log.debug("startInteraction -- translation = \(translation.y), location = \(location.y)")
|
||||
guard interactionInProgress == false else { return }
|
||||
|
||||
var offset: CGPoint = .zero
|
||||
|
||||
initialFrame = surfaceView.frame
|
||||
if state == layoutAdapter.topMostState, let scrollView = scrollView {
|
||||
if grabberAreaFrame.contains(location) {
|
||||
initialScrollOffset = scrollView.contentOffset
|
||||
} else {
|
||||
fitToBounds(scrollView: scrollView)
|
||||
settle(scrollView: scrollView)
|
||||
// Fit the surface bounds to a scroll offset content by startInteraction(at:offset:)
|
||||
offset = CGPoint(x: -scrollView.contentOffset.x, y: -scrollView.contentOffset.y)
|
||||
initialScrollOffset = scrollView.contentOffsetZero
|
||||
}
|
||||
log.debug("initial scroll offset --", initialScrollOffset)
|
||||
@@ -589,11 +661,15 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
|
||||
|
||||
initialTranslationY = translation.y
|
||||
|
||||
viewcontroller.delegate?.floatingPanelWillBeginDragging(viewcontroller)
|
||||
if let vc = viewcontroller {
|
||||
vc.delegate?.floatingPanelWillBeginDragging(vc)
|
||||
}
|
||||
|
||||
layoutAdapter.startInteraction(at: state)
|
||||
layoutAdapter.startInteraction(at: state, offset: offset)
|
||||
|
||||
interactionInProgress = true
|
||||
|
||||
lockScrollView()
|
||||
}
|
||||
|
||||
private func endInteraction(for targetPosition: FloatingPanelPosition) {
|
||||
@@ -606,7 +682,7 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
|
||||
interactionInProgress = false
|
||||
|
||||
// Prevent to keep a scroll view indicator visible at the half/tip position
|
||||
if state != layoutAdapter.topMostState {
|
||||
if targetPosition != layoutAdapter.topMostState {
|
||||
lockScrollView()
|
||||
}
|
||||
|
||||
@@ -621,19 +697,23 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
|
||||
|
||||
private func startAnimation(to targetPosition: FloatingPanelPosition, at distance: CGFloat, with velocity: CGPoint) {
|
||||
log.debug("startAnimation to \(targetPosition) -- distance = \(distance), velocity = \(velocity.y)")
|
||||
guard let vc = viewcontroller else { return }
|
||||
|
||||
isDecelerating = true
|
||||
viewcontroller.delegate?.floatingPanelWillBeginDecelerating(viewcontroller)
|
||||
|
||||
let velocityVector = (distance != 0) ? CGVector(dx: 0, dy: min(abs(velocity.y)/distance, 30.0)) : .zero
|
||||
let animator = behavior.interactionAnimator(self.viewcontroller, to: targetPosition, with: velocityVector)
|
||||
vc.delegate?.floatingPanelWillBeginDecelerating(vc)
|
||||
|
||||
let velocityVector = (distance != 0) ? CGVector(dx: 0, dy: abs(velocity.y)/distance) : .zero
|
||||
let animator = behavior.interactionAnimator(vc, to: targetPosition, with: velocityVector)
|
||||
animator.addAnimations { [weak self] in
|
||||
guard let `self` = self else { return }
|
||||
self.state = targetPosition
|
||||
self.updateLayout(to: targetPosition)
|
||||
}
|
||||
animator.addCompletion { [weak self] pos in
|
||||
guard let `self` = self else { return }
|
||||
// Prevent calling `finishAnimation(at:)` by the old animator whose `isInterruptive` is false
|
||||
// when a new animator has been started after the old one is interrupted.
|
||||
guard let `self` = self, self.animator == animator else { return }
|
||||
self.finishAnimation(at: targetPosition)
|
||||
}
|
||||
self.animator = animator
|
||||
@@ -646,66 +726,26 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
|
||||
self.isDecelerating = false
|
||||
self.animator = nil
|
||||
|
||||
self.viewcontroller.delegate?.floatingPanelDidEndDecelerating(self.viewcontroller)
|
||||
if let vc = viewcontroller {
|
||||
vc.delegate?.floatingPanelDidEndDecelerating(vc)
|
||||
}
|
||||
|
||||
if let scrollView = scrollView {
|
||||
log.debug("finishAnimation -- scroll offset = \(scrollView.contentOffset)")
|
||||
}
|
||||
|
||||
stopScrollDeceleration = false
|
||||
// Don't unlock scroll view in animating view when presentation layer != model layer
|
||||
if state == layoutAdapter.topMostState {
|
||||
|
||||
log.debug("finishAnimation -- state = \(state) surface.minY = \(surfaceView.presentationFrame.minY) topY = \(layoutAdapter.topY)")
|
||||
if state == layoutAdapter.topMostState, abs(surfaceView.presentationFrame.minY - layoutAdapter.topY) <= 1.0 {
|
||||
unlockScrollView()
|
||||
}
|
||||
}
|
||||
|
||||
private func distance(to targetPosition: FloatingPanelPosition) -> CGFloat {
|
||||
let topY = layoutAdapter.topY
|
||||
let middleY = layoutAdapter.middleY
|
||||
let bottomY = layoutAdapter.bottomY
|
||||
let currentY = surfaceView.frame.minY
|
||||
|
||||
switch targetPosition {
|
||||
case .full:
|
||||
return CGFloat(abs(currentY - topY))
|
||||
case .half:
|
||||
return CGFloat(abs(currentY - middleY))
|
||||
case .tip:
|
||||
return CGFloat(abs(currentY - bottomY))
|
||||
case .hidden:
|
||||
fatalError("Now .hidden must not be used for a user interaction")
|
||||
}
|
||||
}
|
||||
|
||||
private func directionalPosition(at currentY: CGFloat, with translation: CGPoint) -> FloatingPanelPosition {
|
||||
return getPosition(at: currentY, with: translation, directional: true)
|
||||
}
|
||||
|
||||
private func redirectionalPosition(at currentY: CGFloat, with translation: CGPoint) -> FloatingPanelPosition {
|
||||
return getPosition(at: currentY, with: translation, directional: false)
|
||||
}
|
||||
|
||||
private func getPosition(at currentY: CGFloat, with translation: CGPoint, directional: Bool) -> FloatingPanelPosition {
|
||||
let supportedPositions: Set = layoutAdapter.supportedPositions
|
||||
if supportedPositions.count == 1 {
|
||||
return state
|
||||
}
|
||||
let isForwardYAxis = (translation.y >= 0)
|
||||
switch supportedPositions {
|
||||
case [.full, .half]:
|
||||
return (isForwardYAxis == directional) ? .half : .full
|
||||
case [.half, .tip]:
|
||||
return (isForwardYAxis == directional) ? .tip : .half
|
||||
case [.full, .tip]:
|
||||
return (isForwardYAxis == directional) ? .tip : .full
|
||||
default:
|
||||
let middleY = layoutAdapter.middleY
|
||||
if currentY > middleY {
|
||||
return (isForwardYAxis == directional) ? .tip : .half
|
||||
} else {
|
||||
return (isForwardYAxis == directional) ? .half : .full
|
||||
}
|
||||
}
|
||||
let targetY = layoutAdapter.positionY(for: targetPosition)
|
||||
return CGFloat(abs(currentY - targetY))
|
||||
}
|
||||
|
||||
// Distance travelled after decelerating to zero velocity at a constant rate.
|
||||
@@ -714,148 +754,57 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
|
||||
return (initialVelocity / 1000.0) * decelerationRate / (1.0 - decelerationRate)
|
||||
}
|
||||
|
||||
private func targetPosition(with velocity: CGPoint) -> (FloatingPanelPosition) {
|
||||
let currentY = surfaceView.frame.minY
|
||||
func targetPosition(from currentY: CGFloat, with velocity: CGPoint) -> (FloatingPanelPosition) {
|
||||
guard let vc = viewcontroller else { return state }
|
||||
let supportedPositions = layoutAdapter.supportedPositions
|
||||
|
||||
if supportedPositions.count == 1 {
|
||||
guard supportedPositions.count > 1 else {
|
||||
return state
|
||||
}
|
||||
|
||||
switch supportedPositions {
|
||||
case [.full, .half]:
|
||||
return targetPosition(from: [.full, .half], at: currentY, velocity: velocity)
|
||||
case [.half, .tip]:
|
||||
return targetPosition(from: [.half, .tip], at: currentY, velocity: velocity)
|
||||
case [.full, .tip]:
|
||||
return targetPosition(from: [.full, .tip], at: currentY, velocity: velocity)
|
||||
default:
|
||||
/*
|
||||
[topY|full]---[th1]---[middleY|half]---[th2]---[bottomY|tip]
|
||||
*/
|
||||
let topY = layoutAdapter.topY
|
||||
let middleY = layoutAdapter.middleY
|
||||
let bottomY = layoutAdapter.bottomY
|
||||
let sortedPositions = Array(supportedPositions).sorted(by: { $0.rawValue < $1.rawValue })
|
||||
|
||||
let nextState: FloatingPanelPosition
|
||||
let forwardYDirection: Bool
|
||||
// Projection
|
||||
let decelerationRate = behavior.momentumProjectionRate(vc)
|
||||
let baseY = abs(layoutAdapter.positionY(for: layoutAdapter.bottomMostState) - layoutAdapter.positionY(for: layoutAdapter.topMostState))
|
||||
let vecY = velocity.y / baseY
|
||||
var pY = project(initialVelocity: vecY, decelerationRate: decelerationRate) * baseY + currentY
|
||||
|
||||
/*
|
||||
full <-> half <-> tip
|
||||
*/
|
||||
switch state {
|
||||
case .full:
|
||||
nextState = .half
|
||||
forwardYDirection = true
|
||||
case .half:
|
||||
nextState = (currentY > middleY) ? .tip : .full
|
||||
forwardYDirection = (currentY > middleY)
|
||||
case .tip:
|
||||
nextState = .half
|
||||
forwardYDirection = false
|
||||
case .hidden:
|
||||
fatalError("Now .hidden must not be used for a user interaction")
|
||||
let forwardY = velocity.y == 0 ? (currentY - layoutAdapter.positionY(for: state) > 0) : velocity.y > 0
|
||||
|
||||
let segment = layoutAdapter.segument(at: pY, forward: forwardY)
|
||||
|
||||
var fromPos: FloatingPanelPosition
|
||||
var toPos: FloatingPanelPosition
|
||||
|
||||
let (lowerPos, upperPos) = (segment.lower ?? sortedPositions.first!, segment.upper ?? sortedPositions.last!)
|
||||
(fromPos, toPos) = forwardY ? (lowerPos, upperPos) : (upperPos, lowerPos)
|
||||
|
||||
if behavior.shouldProjectMomentum(vc, for: toPos) == false {
|
||||
let segment = layoutAdapter.segument(at: currentY, forward: forwardY)
|
||||
var (lowerPos, upperPos) = (segment.lower ?? sortedPositions.first!, segment.upper ?? sortedPositions.last!)
|
||||
// Equate the segment out of {top,bottom} most state to the {top,bottom} most segment
|
||||
if lowerPos == upperPos {
|
||||
if forwardY {
|
||||
upperPos = lowerPos.next(in: sortedPositions)
|
||||
} else {
|
||||
lowerPos = upperPos.pre(in: sortedPositions)
|
||||
}
|
||||
}
|
||||
|
||||
let redirectionalProgress = max(min(behavior.redirectionalProgress(viewcontroller, from: state, to: nextState), 1.0), 0.0)
|
||||
|
||||
let th1: CGFloat
|
||||
let th2: CGFloat
|
||||
|
||||
if forwardYDirection {
|
||||
th1 = topY + (middleY - topY) * redirectionalProgress
|
||||
th2 = middleY + (bottomY - middleY) * redirectionalProgress
|
||||
(fromPos, toPos) = forwardY ? (lowerPos, upperPos) : (upperPos, lowerPos)
|
||||
// Block a projection to a segment over the next from the current segment
|
||||
// (= Trim pY with the current segment)
|
||||
if forwardY {
|
||||
pY = max(min(pY, layoutAdapter.positionY(for: toPos.next(in: sortedPositions))), layoutAdapter.positionY(for: fromPos))
|
||||
} else {
|
||||
th1 = middleY - (middleY - topY) * redirectionalProgress
|
||||
th2 = bottomY - (bottomY - middleY) * redirectionalProgress
|
||||
}
|
||||
|
||||
let decelerationRate = behavior.momentumProjectionRate(viewcontroller)
|
||||
|
||||
let baseY = abs(bottomY - topY)
|
||||
let vecY = velocity.y / baseY
|
||||
let pY = project(initialVelocity: vecY, decelerationRate: decelerationRate) * baseY + currentY
|
||||
|
||||
switch currentY {
|
||||
case ..<th1:
|
||||
switch pY {
|
||||
case bottomY...:
|
||||
return behavior.shouldProjectMomentum(viewcontroller, for: .tip) ? .tip : .half
|
||||
case middleY...:
|
||||
return .half
|
||||
case topY...:
|
||||
return .full
|
||||
default:
|
||||
return .full
|
||||
}
|
||||
case ...middleY:
|
||||
switch pY {
|
||||
case bottomY...:
|
||||
return behavior.shouldProjectMomentum(viewcontroller, for: .tip) ? .tip : .half
|
||||
case middleY...:
|
||||
return .half
|
||||
case topY...:
|
||||
return .half
|
||||
default:
|
||||
return .full
|
||||
}
|
||||
case ..<th2:
|
||||
switch pY {
|
||||
case bottomY...:
|
||||
return .tip
|
||||
case middleY...:
|
||||
return .half
|
||||
case topY...:
|
||||
return .half
|
||||
default:
|
||||
return behavior.shouldProjectMomentum(viewcontroller, for: .full) ? .full : .half
|
||||
}
|
||||
default:
|
||||
switch pY {
|
||||
case bottomY...:
|
||||
return .tip
|
||||
case middleY...:
|
||||
return .tip
|
||||
case topY...:
|
||||
return .half
|
||||
default:
|
||||
return behavior.shouldProjectMomentum(viewcontroller, for: .full) ? .full : .half
|
||||
}
|
||||
pY = max(min(pY, layoutAdapter.positionY(for: fromPos)), layoutAdapter.positionY(for: toPos.pre(in: sortedPositions)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func targetPosition(from positions: [FloatingPanelPosition], at currentY: CGFloat, velocity: CGPoint) -> FloatingPanelPosition {
|
||||
assert(positions.count == 2)
|
||||
|
||||
let top = positions[0]
|
||||
let bottom = positions[1]
|
||||
|
||||
let topY = layoutAdapter.positionY(for: top)
|
||||
let bottomY = layoutAdapter.positionY(for: bottom)
|
||||
|
||||
let target = top == state ? bottom : top
|
||||
let redirectionalProgress = max(min(behavior.redirectionalProgress(viewcontroller, from: state, to: target), 1.0), 0.0)
|
||||
|
||||
let th = topY + (bottomY - topY) * redirectionalProgress
|
||||
|
||||
let decelerationRate = behavior.momentumProjectionRate(viewcontroller)
|
||||
let pY = project(initialVelocity: velocity.y, decelerationRate: decelerationRate) + currentY
|
||||
|
||||
switch currentY {
|
||||
case ..<th:
|
||||
if pY >= bottomY {
|
||||
return bottom
|
||||
} else {
|
||||
return top
|
||||
}
|
||||
default:
|
||||
if pY <= topY {
|
||||
return top
|
||||
} else {
|
||||
return bottom
|
||||
}
|
||||
}
|
||||
// Redirection
|
||||
let redirectionalProgress = max(min(behavior.redirectionalProgress(vc, from: fromPos, to: toPos), 1.0), 0.0)
|
||||
let progress = abs(pY - layoutAdapter.positionY(for: fromPos)) / abs(layoutAdapter.positionY(for: fromPos) - layoutAdapter.positionY(for: toPos))
|
||||
return progress > redirectionalProgress ? toPos : fromPos
|
||||
}
|
||||
|
||||
// MARK: - ScrollView handling
|
||||
@@ -863,11 +812,11 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
|
||||
private func lockScrollView() {
|
||||
guard let scrollView = scrollView else { return }
|
||||
|
||||
if isScrollLocked {
|
||||
if scrollView.isLocked {
|
||||
log.debug("Already scroll locked.")
|
||||
return
|
||||
}
|
||||
isScrollLocked = true
|
||||
log.debug("lock scroll view")
|
||||
|
||||
scrollBouncable = scrollView.bounces
|
||||
scrollIndictorVisible = scrollView.showsVerticalScrollIndicator
|
||||
@@ -878,39 +827,14 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
|
||||
}
|
||||
|
||||
private func unlockScrollView() {
|
||||
guard let scrollView = scrollView, isScrollLocked else { return }
|
||||
|
||||
isScrollLocked = false
|
||||
guard let scrollView = scrollView, scrollView.isLocked else { return }
|
||||
log.debug("unlock scroll view")
|
||||
|
||||
scrollView.isDirectionalLockEnabled = false
|
||||
scrollView.bounces = scrollBouncable
|
||||
scrollView.showsVerticalScrollIndicator = scrollIndictorVisible
|
||||
}
|
||||
|
||||
private func fitToBounds(scrollView: UIScrollView) {
|
||||
log.debug("fit scroll view to bounds -- scroll offset =", scrollView.contentOffset.y)
|
||||
|
||||
surfaceView.frame.origin.y = layoutAdapter.topY - scrollView.contentOffset.y
|
||||
scrollView.transform = CGAffineTransform.identity.translatedBy(x: 0.0,
|
||||
y: scrollView.contentOffset.y)
|
||||
scrollView.scrollIndicatorInsets = UIEdgeInsets(top: -scrollView.contentOffset.y,
|
||||
left: 0.0,
|
||||
bottom: 0.0,
|
||||
right: 0.0)
|
||||
}
|
||||
|
||||
private func settle(scrollView: UIScrollView) {
|
||||
log.debug("settle scroll view")
|
||||
let frame = surfaceView.layer.presentation()?.frame ?? surfaceView.frame
|
||||
surfaceView.transform = .identity
|
||||
surfaceView.frame = frame
|
||||
scrollView.transform = .identity
|
||||
scrollView.frame = initialScrollFrame
|
||||
scrollView.contentOffset = scrollView.contentOffsetZero
|
||||
scrollView.scrollIndicatorInsets = .zero
|
||||
}
|
||||
|
||||
|
||||
private func stopScrollingWithDeceleration(at contentOffset: CGPoint) {
|
||||
// Must use setContentOffset(_:animated) to force-stop deceleration
|
||||
scrollView?.setContentOffset(contentOffset, animated: false)
|
||||
|
||||
@@ -23,6 +23,11 @@ public protocol FloatingPanelBehavior {
|
||||
func redirectionalProgress(_ fpc: FloatingPanelController, from: FloatingPanelPosition, to: FloatingPanelPosition) -> CGFloat
|
||||
|
||||
/// Returns a UIViewPropertyAnimator object to project a floating panel to a position on finger up if the user dragged.
|
||||
///
|
||||
/// - Attention:
|
||||
/// By default, it returns a non-interruptible animator to prevent a propagation of the animation to a content view.
|
||||
/// However returning an interruptible animator is working well depending on a content view and it can be better
|
||||
/// than using a non-interruptible one.
|
||||
func interactionAnimator(_ fpc: FloatingPanelController, to targetPosition: FloatingPanelPosition, with velocity: CGVector) -> UIViewPropertyAnimator
|
||||
|
||||
/// Returns a UIViewPropertyAnimator object to add a floating panel to a position.
|
||||
@@ -67,14 +72,7 @@ public protocol FloatingPanelBehavior {
|
||||
|
||||
public extension FloatingPanelBehavior {
|
||||
func shouldProjectMomentum(_ fpc: FloatingPanelController, for proposedTargetPosition: FloatingPanelPosition) -> Bool {
|
||||
switch (fpc.position, proposedTargetPosition) {
|
||||
case (.full, .tip):
|
||||
return false
|
||||
case (.tip, .full):
|
||||
return false
|
||||
default:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func momentumProjectionRate(_ fpc: FloatingPanelController) -> CGFloat {
|
||||
@@ -134,7 +132,7 @@ public class FloatingPanelDefaultBehavior: FloatingPanelBehavior {
|
||||
public func interactionAnimator(_ fpc: FloatingPanelController, to targetPosition: FloatingPanelPosition, with velocity: CGVector) -> UIViewPropertyAnimator {
|
||||
let timing = timeingCurve(with: velocity)
|
||||
let animator = UIViewPropertyAnimator(duration: 0, timingParameters: timing)
|
||||
animator.isInterruptible = false
|
||||
animator.isInterruptible = false // Prevent a propagation of the animation(spring etc) to a content view
|
||||
return animator
|
||||
}
|
||||
|
||||
|
||||
@@ -68,6 +68,40 @@ public enum FloatingPanelPosition: Int {
|
||||
case half
|
||||
case tip
|
||||
case hidden
|
||||
|
||||
static var allCases: [FloatingPanelPosition] {
|
||||
return [.full, .half, .tip, .hidden]
|
||||
}
|
||||
|
||||
func next(in positions: [FloatingPanelPosition]) -> FloatingPanelPosition {
|
||||
#if swift(>=4.2)
|
||||
guard
|
||||
let index = positions.firstIndex(of: self),
|
||||
positions.indices.contains(index + 1)
|
||||
else { return self }
|
||||
#else
|
||||
guard
|
||||
let index = positions.index(of: self),
|
||||
positions.indices.contains(index + 1)
|
||||
else { return self }
|
||||
#endif
|
||||
return positions[index + 1]
|
||||
}
|
||||
|
||||
func pre(in positions: [FloatingPanelPosition]) -> FloatingPanelPosition {
|
||||
#if swift(>=4.2)
|
||||
guard
|
||||
let index = positions.firstIndex(of: self),
|
||||
positions.indices.contains(index - 1)
|
||||
else { return self }
|
||||
#else
|
||||
guard
|
||||
let index = positions.index(of: self),
|
||||
positions.indices.contains(index - 1)
|
||||
else { return self }
|
||||
#endif
|
||||
return positions[index - 1]
|
||||
}
|
||||
}
|
||||
|
||||
///
|
||||
@@ -145,7 +179,7 @@ open class FloatingPanelController: UIViewController, UIScrollViewDelegate, UIGe
|
||||
}
|
||||
private var _contentViewController: UIViewController?
|
||||
|
||||
private var floatingPanel: FloatingPanel!
|
||||
private(set) var floatingPanel: FloatingPanel!
|
||||
private var preSafeAreaInsets: UIEdgeInsets = .zero // Capture the latest one
|
||||
private var safeAreaInsetsObservation: NSKeyValueObservation?
|
||||
private let modalTransition = FloatingPanelModalTransition()
|
||||
@@ -201,7 +235,8 @@ open class FloatingPanelController: UIViewController, UIScrollViewDelegate, UIGe
|
||||
if #available(iOS 11.0, *) {}
|
||||
else {
|
||||
// Because {top,bottom}LayoutGuide is managed as a view
|
||||
if preSafeAreaInsets != layoutInsets {
|
||||
if preSafeAreaInsets != layoutInsets,
|
||||
floatingPanel.isDecelerating == false {
|
||||
self.update(safeAreaInsets: layoutInsets)
|
||||
}
|
||||
}
|
||||
@@ -252,8 +287,7 @@ open class FloatingPanelController: UIViewController, UIScrollViewDelegate, UIGe
|
||||
|
||||
private func update(safeAreaInsets: UIEdgeInsets) {
|
||||
guard
|
||||
preSafeAreaInsets != safeAreaInsets,
|
||||
self.floatingPanel.isDecelerating == false
|
||||
preSafeAreaInsets != safeAreaInsets
|
||||
else { return }
|
||||
|
||||
log.debug("Update safeAreaInsets", safeAreaInsets)
|
||||
@@ -522,16 +556,7 @@ open class FloatingPanelController: UIViewController, UIScrollViewDelegate, UIGe
|
||||
|
||||
/// Returns the y-coordinate of the point at the origin of the surface view.
|
||||
public func originYOfSurface(for pos: FloatingPanelPosition) -> CGFloat {
|
||||
switch pos {
|
||||
case .full:
|
||||
return floatingPanel.layoutAdapter.topY
|
||||
case .half:
|
||||
return floatingPanel.layoutAdapter.middleY
|
||||
case .tip:
|
||||
return floatingPanel.layoutAdapter.bottomY
|
||||
case .hidden:
|
||||
return floatingPanel.layoutAdapter.hiddenY
|
||||
}
|
||||
return floatingPanel.layoutAdapter.positionY(for: pos)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -43,14 +43,16 @@ public protocol FloatingPanelLayout: class {
|
||||
/// Returns a set of FloatingPanelPosition objects to tell the applicable
|
||||
/// positions of the floating panel controller.
|
||||
///
|
||||
/// By default, it returns all position except for `hidden` position. Because
|
||||
/// it's always supported by `FloatingPanelController` so you don't need to return it.
|
||||
/// By default, it returns full, half and tip positions.
|
||||
var supportedPositions: Set<FloatingPanelPosition> { get }
|
||||
|
||||
/// Return the interaction buffer to the top from the top position. Default is 6.0.
|
||||
var topInteractionBuffer: CGFloat { get }
|
||||
|
||||
/// Return the interaction buffer to the bottom from the bottom position. Default is 6.0.
|
||||
///
|
||||
/// - Important:
|
||||
/// The specified buffer is ignored when `FloatingPanelController.isRemovalInteractionEnabled` is set to true.
|
||||
var bottomInteractionBuffer: CGFloat { get }
|
||||
|
||||
/// Returns a CGFloat value to determine a Y coordinate of a floating panel for each position(full, half, tip and hidden).
|
||||
@@ -130,9 +132,13 @@ public class FloatingPanelDefaultLandscapeLayout: FloatingPanelLayout {
|
||||
}
|
||||
}
|
||||
|
||||
struct LayoutSegment {
|
||||
let lower: FloatingPanelPosition?
|
||||
let upper: FloatingPanelPosition?
|
||||
}
|
||||
|
||||
class FloatingPanelLayoutAdapter {
|
||||
weak var vc: UIViewController!
|
||||
weak var vc: FloatingPanelController!
|
||||
private weak var surfaceView: FloatingPanelSurfaceView!
|
||||
private weak var backdropView: FloatingPanelBackdropView!
|
||||
|
||||
@@ -175,70 +181,31 @@ class FloatingPanelLayoutAdapter {
|
||||
}
|
||||
|
||||
var supportedPositions: Set<FloatingPanelPosition> {
|
||||
var supportedPositions = layout.supportedPositions
|
||||
supportedPositions.remove(.hidden)
|
||||
return supportedPositions
|
||||
return layout.supportedPositions
|
||||
}
|
||||
|
||||
var topMostState: FloatingPanelPosition {
|
||||
if supportedPositions.contains(.full) {
|
||||
return .full
|
||||
}
|
||||
if supportedPositions.contains(.half) {
|
||||
return .half
|
||||
}
|
||||
return .tip
|
||||
return supportedPositions.sorted(by: { $0.rawValue < $1.rawValue }).first ?? .hidden
|
||||
}
|
||||
|
||||
var bottomMostState: FloatingPanelPosition {
|
||||
return supportedPositions.sorted(by: { $0.rawValue < $1.rawValue }).last ?? .hidden
|
||||
}
|
||||
|
||||
var topY: CGFloat {
|
||||
if supportedPositions.contains(.full) {
|
||||
switch layout {
|
||||
case is FloatingPanelIntrinsicLayout:
|
||||
return surfaceView.superview!.bounds.height - surfaceView.bounds.height
|
||||
case is FloatingPanelFullScreenLayout:
|
||||
return fullInset
|
||||
default:
|
||||
return (safeAreaInsets.top + fullInset)
|
||||
}
|
||||
} else {
|
||||
return middleY
|
||||
}
|
||||
}
|
||||
|
||||
var middleY: CGFloat {
|
||||
if layout is FloatingPanelFullScreenLayout {
|
||||
return surfaceView.superview!.bounds.height - halfInset
|
||||
} else{
|
||||
return surfaceView.superview!.bounds.height - (safeAreaInsets.bottom + halfInset)
|
||||
}
|
||||
return positionY(for: topMostState)
|
||||
}
|
||||
|
||||
var bottomY: CGFloat {
|
||||
if supportedPositions.contains(.tip) {
|
||||
if layout is FloatingPanelFullScreenLayout {
|
||||
return surfaceView.superview!.bounds.height - tipInset
|
||||
} else{
|
||||
return surfaceView.superview!.bounds.height - (safeAreaInsets.bottom + tipInset)
|
||||
}
|
||||
} else {
|
||||
return middleY
|
||||
}
|
||||
}
|
||||
|
||||
var hiddenY: CGFloat {
|
||||
return surfaceView.superview!.bounds.height
|
||||
return positionY(for: bottomMostState)
|
||||
}
|
||||
|
||||
var topMaxY: CGFloat {
|
||||
return layout is FloatingPanelFullScreenLayout ? 0.0 : safeAreaInsets.top
|
||||
return topY - layout.topInteractionBuffer
|
||||
}
|
||||
|
||||
var bottomMaxY: CGFloat {
|
||||
if layout is FloatingPanelFullScreenLayout{
|
||||
return surfaceView.superview!.bounds.height - hiddenInset
|
||||
} else {
|
||||
return surfaceView.superview!.bounds.height - (safeAreaInsets.bottom + hiddenInset)
|
||||
}
|
||||
return bottomY + layout.bottomInteractionBuffer
|
||||
}
|
||||
|
||||
var adjustedContentInsets: UIEdgeInsets {
|
||||
@@ -251,13 +218,30 @@ class FloatingPanelLayoutAdapter {
|
||||
func positionY(for pos: FloatingPanelPosition) -> CGFloat {
|
||||
switch pos {
|
||||
case .full:
|
||||
return topY
|
||||
switch layout {
|
||||
case is FloatingPanelIntrinsicLayout:
|
||||
return surfaceView.superview!.bounds.height - surfaceView.bounds.height
|
||||
case is FloatingPanelFullScreenLayout:
|
||||
return fullInset
|
||||
default:
|
||||
return (safeAreaInsets.top + fullInset)
|
||||
}
|
||||
case .half:
|
||||
return middleY
|
||||
switch layout {
|
||||
case is FloatingPanelFullScreenLayout:
|
||||
return surfaceView.superview!.bounds.height - halfInset
|
||||
default:
|
||||
return surfaceView.superview!.bounds.height - (safeAreaInsets.bottom + halfInset)
|
||||
}
|
||||
case .tip:
|
||||
return bottomY
|
||||
switch layout {
|
||||
case is FloatingPanelFullScreenLayout:
|
||||
return surfaceView.superview!.bounds.height - tipInset
|
||||
default:
|
||||
return surfaceView.superview!.bounds.height - (safeAreaInsets.bottom + tipInset)
|
||||
}
|
||||
case .hidden:
|
||||
return hiddenY
|
||||
return surfaceView.superview!.bounds.height - hiddenInset
|
||||
}
|
||||
}
|
||||
|
||||
@@ -291,7 +275,7 @@ class FloatingPanelLayoutAdapter {
|
||||
", content safe area(bottom) =", safeAreaBottom)
|
||||
}
|
||||
|
||||
func prepareLayout(in vc: UIViewController) {
|
||||
func prepareLayout(in vc: FloatingPanelController) {
|
||||
self.vc = vc
|
||||
|
||||
NSLayoutConstraint.deactivate(fixedConstraints + fullConstraints + halfConstraints + tipConstraints + offConstraints)
|
||||
@@ -353,18 +337,19 @@ class FloatingPanelLayoutAdapter {
|
||||
]
|
||||
}
|
||||
|
||||
func startInteraction(at state: FloatingPanelPosition) {
|
||||
func startInteraction(at state: FloatingPanelPosition, offset: CGPoint = .zero) {
|
||||
guard self.interactiveTopConstraint == nil else { return }
|
||||
NSLayoutConstraint.deactivate(fullConstraints + halfConstraints + tipConstraints + offConstraints)
|
||||
|
||||
let interactiveTopConstraint: NSLayoutConstraint
|
||||
switch layout {
|
||||
case is FloatingPanelIntrinsicLayout,
|
||||
is FloatingPanelFullScreenLayout:
|
||||
initialConst = surfaceView.frame.minY
|
||||
initialConst = surfaceView.frame.minY + offset.y
|
||||
interactiveTopConstraint = surfaceView.topAnchor.constraint(equalTo: vc.view.topAnchor,
|
||||
constant: initialConst)
|
||||
default:
|
||||
initialConst = surfaceView.frame.minY - safeAreaInsets.top
|
||||
initialConst = surfaceView.frame.minY - safeAreaInsets.top + offset.y
|
||||
interactiveTopConstraint = surfaceView.topAnchor.constraint(equalTo: vc.layoutGuide.topAnchor,
|
||||
constant: initialConst)
|
||||
}
|
||||
@@ -429,13 +414,14 @@ class FloatingPanelLayoutAdapter {
|
||||
}()
|
||||
let bottomMostConst: CGFloat = {
|
||||
var ret: CGFloat = 0.0
|
||||
let _bottomY = vc.isRemovalInteractionEnabled ? positionY(for: .hidden) : bottomY
|
||||
switch layout {
|
||||
case is FloatingPanelIntrinsicLayout, is FloatingPanelFullScreenLayout:
|
||||
ret = bottomY
|
||||
ret = _bottomY
|
||||
default:
|
||||
ret = bottomY - safeAreaInsets.top
|
||||
ret = _bottomY - safeAreaInsets.top
|
||||
}
|
||||
return min(ret, bottomMaxY)
|
||||
return min(ret, surfaceView.superview!.bounds.height)
|
||||
}()
|
||||
let minConst = allowsTopBuffer ? topMostConst - layout.topInteractionBuffer : topMostConst
|
||||
let maxConst = bottomMostConst + layout.bottomInteractionBuffer
|
||||
@@ -468,6 +454,7 @@ class FloatingPanelLayoutAdapter {
|
||||
func activateLayout(of state: FloatingPanelPosition) {
|
||||
defer {
|
||||
surfaceView.superview!.layoutIfNeeded()
|
||||
log.debug("activateLayout -- surface.presentation = \(self.surfaceView.presentationFrame) surface.frame = \(self.surfaceView.frame)")
|
||||
}
|
||||
|
||||
var state = state
|
||||
@@ -481,7 +468,7 @@ class FloatingPanelLayoutAdapter {
|
||||
}
|
||||
NSLayoutConstraint.activate(fixedConstraints)
|
||||
|
||||
if supportedPositions.union([.hidden]).contains(state) == false {
|
||||
if isValid(state) == false {
|
||||
state = layout.initialPosition
|
||||
}
|
||||
|
||||
@@ -498,6 +485,10 @@ class FloatingPanelLayoutAdapter {
|
||||
}
|
||||
}
|
||||
|
||||
func isValid(_ state: FloatingPanelPosition) -> Bool {
|
||||
return supportedPositions.union([.hidden]).contains(state)
|
||||
}
|
||||
|
||||
private func setBackdropAlpha(of target: FloatingPanelPosition) {
|
||||
if target == .hidden {
|
||||
self.backdropView.alpha = 0.0
|
||||
@@ -509,7 +500,7 @@ class FloatingPanelLayoutAdapter {
|
||||
private func checkLayoutConsistance() {
|
||||
// Verify layout configurations
|
||||
assert(supportedPositions.count > 0)
|
||||
assert(supportedPositions.union([.hidden]).contains(layout.initialPosition),
|
||||
assert(supportedPositions.contains(layout.initialPosition),
|
||||
"Does not include an initial position (\(layout.initialPosition)) in supportedPositions (\(supportedPositions))")
|
||||
|
||||
if layout is FloatingPanelIntrinsicLayout {
|
||||
@@ -528,4 +519,38 @@ class FloatingPanelLayoutAdapter {
|
||||
assert(bottomY > topY, "Invalid insets { topY: \(topY), bottomY: \(bottomY) }")
|
||||
}*/
|
||||
}
|
||||
|
||||
func segument(at posY: CGFloat, forward: Bool) -> LayoutSegment {
|
||||
/// ----------------------->Y
|
||||
/// --> forward <-- backward
|
||||
/// |-------|===o===|-------| |-------|-------|===o===|
|
||||
/// |-------|-------x=======| |-------|=======x-------|
|
||||
/// |-------|-------|===o===| |-------|===o===|-------|
|
||||
/// pos: o/x, seguement: =
|
||||
let sortedPositions = supportedPositions.sorted(by: { $0.rawValue < $1.rawValue })
|
||||
|
||||
let upperIndex: Int?
|
||||
if forward {
|
||||
#if swift(>=4.2)
|
||||
upperIndex = sortedPositions.firstIndex(where: { posY < positionY(for: $0) })
|
||||
#else
|
||||
upperIndex = sortedPositions.index(where: { posY < positionY(for: $0) })
|
||||
#endif
|
||||
} else {
|
||||
#if swift(>=4.2)
|
||||
upperIndex = sortedPositions.firstIndex(where: { posY <= positionY(for: $0) })
|
||||
#else
|
||||
upperIndex = sortedPositions.index(where: { posY <= positionY(for: $0) })
|
||||
#endif
|
||||
}
|
||||
|
||||
switch upperIndex {
|
||||
case 0:
|
||||
return LayoutSegment(lower: nil, upper: sortedPositions.first)
|
||||
case let upperIndex?:
|
||||
return LayoutSegment(lower: sortedPositions[upperIndex - 1], upper: sortedPositions[upperIndex])
|
||||
default:
|
||||
return LayoutSegment(lower: sortedPositions[sortedPositions.endIndex - 1], upper: nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>FMWK</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.6.1</string>
|
||||
<string>1.6.4</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||
</dict>
|
||||
|
||||
@@ -60,6 +60,10 @@ extension UIView {
|
||||
return self
|
||||
}
|
||||
}
|
||||
|
||||
var presentationFrame: CGRect {
|
||||
return layer.presentation()?.frame ?? frame
|
||||
}
|
||||
}
|
||||
|
||||
extension UIView {
|
||||
@@ -107,6 +111,9 @@ extension UIScrollView {
|
||||
var contentOffsetZero: CGPoint {
|
||||
return CGPoint(x: 0.0, y: 0.0 - contentInset.top)
|
||||
}
|
||||
var isLocked: Bool {
|
||||
return !showsVerticalScrollIndicator && !bounces && isDirectionalLockEnabled
|
||||
}
|
||||
}
|
||||
|
||||
extension UISpringTimingParameters {
|
||||
|
||||
@@ -7,9 +7,7 @@ import XCTest
|
||||
@testable import FloatingPanel
|
||||
|
||||
class FloatingPanelControllerTests: XCTestCase {
|
||||
|
||||
override func setUp() {}
|
||||
|
||||
override func tearDown() {}
|
||||
|
||||
func test_warningRetainCycle() {
|
||||
@@ -30,12 +28,8 @@ class FloatingPanelControllerTests: XCTestCase {
|
||||
guard let rootVC = UIApplication.shared.keyWindow?.rootViewController else { fatalError() }
|
||||
let fpc = FloatingPanelController()
|
||||
fpc.addPanel(toParent: rootVC)
|
||||
|
||||
waitRunLoop(secs: 1.0)
|
||||
XCTAssert(fpc.surfaceView.frame.minY == (fpc.view.bounds.height - fpc.layoutInsets.bottom) - fpc.layout.insetFor(position: .half)!)
|
||||
|
||||
fpc.move(to: .tip, animated: true)
|
||||
waitRunLoop(secs: 1.0)
|
||||
fpc.move(to: .tip, animated: false)
|
||||
XCTAssert(fpc.surfaceView.frame.minY == (fpc.view.bounds.height - fpc.layoutInsets.bottom) - fpc.layout.insetFor(position: .tip)!)
|
||||
}
|
||||
|
||||
@@ -56,13 +50,66 @@ class FloatingPanelControllerTests: XCTestCase {
|
||||
XCTAssertEqual(traitCollection.userInterfaceStyle, .dark)
|
||||
fpc.prepare(for: traitCollection)
|
||||
}
|
||||
|
||||
func test_moveTo() {
|
||||
let fpc = FloatingPanelController(delegate: nil)
|
||||
fpc.showForTest()
|
||||
fpc.move(to: .full, animated: false)
|
||||
XCTAssertEqual(fpc.position, .full)
|
||||
XCTAssertEqual(fpc.surfaceView.frame.minY, fpc.originYOfSurface(for: .full))
|
||||
|
||||
fpc.move(to: .half, animated: false)
|
||||
XCTAssertEqual(fpc.position, .half)
|
||||
XCTAssertEqual(fpc.surfaceView.frame.minY, fpc.originYOfSurface(for: .half))
|
||||
|
||||
fpc.move(to: .tip, animated: false)
|
||||
XCTAssertEqual(fpc.position, .tip)
|
||||
XCTAssertEqual(fpc.surfaceView.frame.minY, fpc.originYOfSurface(for: .tip))
|
||||
|
||||
fpc.move(to: .hidden, animated: false)
|
||||
XCTAssertEqual(fpc.position, .hidden)
|
||||
XCTAssertEqual(fpc.surfaceView.frame.minY, fpc.originYOfSurface(for: .hidden))
|
||||
|
||||
fpc.move(to: .full, animated: true)
|
||||
waitRunLoop(secs: 0.3)
|
||||
XCTAssertEqual(fpc.position, .full)
|
||||
XCTAssertEqual(fpc.surfaceView.frame.minY, fpc.originYOfSurface(for: .full))
|
||||
|
||||
fpc.move(to: .half, animated: true)
|
||||
waitRunLoop(secs: 0.3)
|
||||
XCTAssertEqual(fpc.position, .half)
|
||||
XCTAssertEqual(fpc.surfaceView.frame.minY, fpc.originYOfSurface(for: .half))
|
||||
|
||||
fpc.move(to: .tip, animated: true)
|
||||
waitRunLoop(secs: 0.3)
|
||||
XCTAssertEqual(fpc.position, .tip)
|
||||
XCTAssertEqual(fpc.surfaceView.frame.minY, fpc.originYOfSurface(for: .tip))
|
||||
|
||||
fpc.move(to: .hidden, animated: true)
|
||||
waitRunLoop(secs: 0.3)
|
||||
XCTAssertEqual(fpc.position, .hidden)
|
||||
XCTAssertEqual(fpc.surfaceView.frame.minY, fpc.originYOfSurface(for: .hidden))
|
||||
|
||||
}
|
||||
|
||||
func test_originSurfaceY() {
|
||||
let fpc = FloatingPanelController(delegate: nil)
|
||||
fpc.loadViewIfNeeded()
|
||||
fpc.view.frame = CGRect(x: 0, y: 0, width: 375, height: 667)
|
||||
fpc.show(animated: false, completion: nil)
|
||||
|
||||
fpc.move(to: .full, animated: false)
|
||||
XCTAssertEqual(fpc.surfaceView.frame.minY, fpc.originYOfSurface(for: .full))
|
||||
fpc.move(to: .half, animated: false)
|
||||
XCTAssertEqual(fpc.surfaceView.frame.minY, fpc.originYOfSurface(for: .half))
|
||||
fpc.move(to: .tip, animated: false)
|
||||
XCTAssertEqual(fpc.surfaceView.frame.minY, fpc.originYOfSurface(for: .tip))
|
||||
fpc.move(to: .hidden, animated: false)
|
||||
XCTAssertEqual(fpc.surfaceView.frame.minY, fpc.originYOfSurface(for: .hidden))
|
||||
}
|
||||
}
|
||||
|
||||
func waitRunLoop(secs: TimeInterval = 0) {
|
||||
RunLoop.main.run(until: Date(timeIntervalSinceNow: secs))
|
||||
}
|
||||
|
||||
class MyZombieViewController: UIViewController, FloatingPanelLayout, FloatingPanelBehavior, FloatingPanelControllerDelegate {
|
||||
private class MyZombieViewController: UIViewController, FloatingPanelLayout, FloatingPanelBehavior, FloatingPanelControllerDelegate {
|
||||
var fpc: FloatingPanelController?
|
||||
override func viewDidLoad() {
|
||||
fpc = FloatingPanelController(delegate: self)
|
||||
|
||||
@@ -0,0 +1,206 @@
|
||||
//
|
||||
// Created by Shin Yamamoto on 2019/06/27.
|
||||
// Copyright © 2019 scenee. All rights reserved.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
@testable import FloatingPanel
|
||||
|
||||
class FloatingPanelLayoutTests: XCTestCase {
|
||||
var fpc: FloatingPanelController!
|
||||
override func setUp() {
|
||||
fpc = FloatingPanelController(delegate: nil)
|
||||
fpc.loadViewIfNeeded()
|
||||
fpc.view.frame = CGRect(x: 0, y: 0, width: 375, height: 667)
|
||||
}
|
||||
override func tearDown() {}
|
||||
|
||||
func test_layoutAdapter_topAndBottomMostState() {
|
||||
XCTAssertEqual(fpc.floatingPanel.layoutAdapter.topMostState, .full)
|
||||
XCTAssertEqual(fpc.floatingPanel.layoutAdapter.bottomMostState, .tip)
|
||||
|
||||
class FloatingPanelLayoutWithHidden: FloatingPanelLayout {
|
||||
func insetFor(position: FloatingPanelPosition) -> CGFloat? { return nil }
|
||||
let initialPosition: FloatingPanelPosition = .hidden
|
||||
let supportedPositions: Set<FloatingPanelPosition> = [.hidden, .half, .full]
|
||||
}
|
||||
class FloatingPanelLayout2Positions: FloatingPanelLayout {
|
||||
func insetFor(position: FloatingPanelPosition) -> CGFloat? { return nil }
|
||||
let initialPosition: FloatingPanelPosition = .tip
|
||||
let supportedPositions: Set<FloatingPanelPosition> = [.tip, .half]
|
||||
}
|
||||
let delegate = FloatingPanelTestDelegate()
|
||||
delegate.layout = FloatingPanelLayoutWithHidden()
|
||||
fpc.delegate = delegate
|
||||
XCTAssertEqual(fpc.floatingPanel.layoutAdapter.topMostState, .full)
|
||||
XCTAssertEqual(fpc.floatingPanel.layoutAdapter.bottomMostState, .hidden)
|
||||
|
||||
delegate.layout = FloatingPanelLayout2Positions()
|
||||
fpc.delegate = delegate
|
||||
XCTAssertEqual(fpc.floatingPanel.layoutAdapter.topMostState, .half)
|
||||
XCTAssertEqual(fpc.floatingPanel.layoutAdapter.bottomMostState, .tip)
|
||||
}
|
||||
|
||||
func test_layoutSegment_3position() {
|
||||
class FloatingPanelLayout3Positions: FloatingPanelTestLayout {
|
||||
let initialPosition: FloatingPanelPosition = .half
|
||||
let supportedPositions: Set<FloatingPanelPosition> = [.tip, .half, .full]
|
||||
}
|
||||
|
||||
let delegate = FloatingPanelTestDelegate()
|
||||
delegate.layout = FloatingPanelLayout3Positions()
|
||||
fpc.delegate = delegate
|
||||
|
||||
let fullPos = fpc.originYOfSurface(for: .full)
|
||||
let halfPos = fpc.originYOfSurface(for: .half)
|
||||
let tipPos = fpc.originYOfSurface(for: .tip)
|
||||
|
||||
let minPos = CGFloat.leastNormalMagnitude
|
||||
let maxPos = CGFloat.greatestFiniteMagnitude
|
||||
|
||||
assertLayoutSegment(fpc.floatingPanel, with: [
|
||||
(#line, pos: minPos, forwardY: true, lower: nil, upper: .full),
|
||||
(#line, pos: minPos, forwardY: false, lower: nil, upper: .full),
|
||||
(#line, pos: fullPos, forwardY: true, lower: .full, upper: .half),
|
||||
(#line, pos: fullPos, forwardY: false, lower: nil, upper: .full),
|
||||
(#line, pos: halfPos, forwardY: true, lower: .half, upper: .tip),
|
||||
(#line, pos: halfPos, forwardY: false, lower: .full, upper: .half),
|
||||
(#line, pos: tipPos, forwardY: true, lower: .tip, upper: nil),
|
||||
(#line, pos: tipPos, forwardY: false, lower: .half, upper: .tip),
|
||||
(#line, pos: maxPos, forwardY: true, lower: .tip, upper: nil),
|
||||
(#line, pos: maxPos, forwardY: false, lower: .tip, upper: nil),
|
||||
])
|
||||
}
|
||||
|
||||
func test_layoutSegment_2positions() {
|
||||
class FloatingPanelLayout2Positions: FloatingPanelTestLayout {
|
||||
let initialPosition: FloatingPanelPosition = .half
|
||||
let supportedPositions: Set<FloatingPanelPosition> = [.half, .full]
|
||||
}
|
||||
|
||||
let delegate = FloatingPanelTestDelegate()
|
||||
delegate.layout = FloatingPanelLayout2Positions()
|
||||
fpc.delegate = delegate
|
||||
|
||||
let fullPos = fpc.originYOfSurface(for: .full)
|
||||
let halfPos = fpc.originYOfSurface(for: .half)
|
||||
|
||||
let minPos = CGFloat.leastNormalMagnitude
|
||||
let maxPos = CGFloat.greatestFiniteMagnitude
|
||||
|
||||
assertLayoutSegment(fpc.floatingPanel, with: [
|
||||
(#line, pos: minPos, forwardY: true, lower: nil, upper: .full),
|
||||
(#line, pos: minPos, forwardY: false, lower: nil, upper: .full),
|
||||
(#line, pos: fullPos, forwardY: true, lower: .full, upper: .half),
|
||||
(#line, pos: fullPos, forwardY: false, lower: nil, upper: .full),
|
||||
(#line, pos: halfPos, forwardY: true, lower: .half, upper: nil),
|
||||
(#line, pos: halfPos, forwardY: false, lower: .full, upper: .half),
|
||||
(#line, pos: maxPos, forwardY: true, lower: .half, upper: nil),
|
||||
(#line, pos: maxPos, forwardY: false, lower: .half, upper: nil),
|
||||
])
|
||||
}
|
||||
|
||||
func test_layoutSegment_1positions() {
|
||||
class FloatingPanelLayout1Positions: FloatingPanelTestLayout {
|
||||
let initialPosition: FloatingPanelPosition = .full
|
||||
let supportedPositions: Set<FloatingPanelPosition> = [.full]
|
||||
}
|
||||
|
||||
let delegate = FloatingPanelTestDelegate()
|
||||
delegate.layout = FloatingPanelLayout1Positions()
|
||||
fpc.delegate = delegate
|
||||
|
||||
let fullPos = fpc.originYOfSurface(for: .full)
|
||||
|
||||
let minPos = CGFloat.leastNormalMagnitude
|
||||
let maxPos = CGFloat.greatestFiniteMagnitude
|
||||
|
||||
assertLayoutSegment(fpc.floatingPanel, with: [
|
||||
(#line, pos: minPos, forwardY: true, lower: nil, upper: .full),
|
||||
(#line, pos: minPos, forwardY: false, lower: nil, upper: .full),
|
||||
(#line, pos: fullPos, forwardY: true, lower: .full, upper: nil),
|
||||
(#line, pos: fullPos, forwardY: false, lower: nil, upper: .full),
|
||||
(#line, pos: maxPos, forwardY: true, lower: .full, upper: nil),
|
||||
(#line, pos: maxPos, forwardY: false, lower: .full, upper: nil),
|
||||
])
|
||||
}
|
||||
|
||||
func test_updateInteractiveTopConstraint() {
|
||||
fpc.showForTest()
|
||||
fpc.move(to: .full, animated: false)
|
||||
|
||||
fpc.floatingPanel.layoutAdapter.startInteraction(at: fpc.position)
|
||||
fpc.floatingPanel.layoutAdapter.startInteraction(at: fpc.position) // Should be ignore
|
||||
|
||||
let fullPos = fpc.originYOfSurface(for: .full)
|
||||
let tipPos = fpc.originYOfSurface(for: .tip)
|
||||
|
||||
var pre: CGFloat
|
||||
var next: CGFloat
|
||||
pre = fpc.surfaceView.frame.minY
|
||||
fpc.floatingPanel.layoutAdapter.updateInteractiveTopConstraint(diff: -100.0, allowsTopBuffer: false, with: fpc.behavior)
|
||||
next = fpc.surfaceView.frame.minY
|
||||
XCTAssertEqual(next, pre)
|
||||
|
||||
fpc.floatingPanel.layoutAdapter.updateInteractiveTopConstraint(diff: -100.0, allowsTopBuffer: true, with: fpc.behavior)
|
||||
next = fpc.surfaceView.frame.minY
|
||||
XCTAssertEqual(next, fullPos - fpc.layout.topInteractionBuffer)
|
||||
|
||||
fpc.floatingPanel.layoutAdapter.updateInteractiveTopConstraint(diff: 100.0, allowsTopBuffer: true, with: fpc.behavior)
|
||||
next = fpc.surfaceView.frame.minY
|
||||
XCTAssertEqual(next, fullPos + 100.0)
|
||||
|
||||
fpc.floatingPanel.layoutAdapter.updateInteractiveTopConstraint(diff: tipPos - fullPos, allowsTopBuffer: true, with: fpc.behavior)
|
||||
next = fpc.surfaceView.frame.minY
|
||||
XCTAssertEqual(next, tipPos)
|
||||
|
||||
fpc.floatingPanel.layoutAdapter.updateInteractiveTopConstraint(diff: tipPos - fullPos + 100.0, allowsTopBuffer: true, with: fpc.behavior)
|
||||
next = fpc.surfaceView.frame.minY
|
||||
XCTAssertEqual(next, tipPos + fpc.layout.bottomInteractionBuffer)
|
||||
|
||||
fpc.floatingPanel.layoutAdapter.endInteraction(at: fpc.position)
|
||||
}
|
||||
|
||||
func test_updateInteractiveTopConstraintWithHidden() {
|
||||
class FloatingPanelLayout2Positions: FloatingPanelTestLayout {
|
||||
let initialPosition: FloatingPanelPosition = .hidden
|
||||
let supportedPositions: Set<FloatingPanelPosition> = [.hidden, .full]
|
||||
}
|
||||
let delegate = FloatingPanelTestDelegate()
|
||||
delegate.layout = FloatingPanelLayout2Positions()
|
||||
fpc.delegate = delegate
|
||||
fpc.showForTest()
|
||||
fpc.move(to: .full, animated: false)
|
||||
|
||||
fpc.floatingPanel.layoutAdapter.startInteraction(at: fpc.position)
|
||||
|
||||
let fullPos = fpc.originYOfSurface(for: .full)
|
||||
let hiddenPos = fpc.originYOfSurface(for: .hidden)
|
||||
|
||||
var pre: CGFloat
|
||||
var next: CGFloat
|
||||
pre = fpc.surfaceView.frame.minY
|
||||
fpc.floatingPanel.layoutAdapter.updateInteractiveTopConstraint(diff: -100.0, allowsTopBuffer: false, with: fpc.behavior)
|
||||
next = fpc.surfaceView.frame.minY
|
||||
XCTAssertEqual(next, pre)
|
||||
|
||||
fpc.floatingPanel.layoutAdapter.updateInteractiveTopConstraint(diff: -100.0, allowsTopBuffer: true, with: fpc.behavior)
|
||||
next = fpc.surfaceView.frame.minY
|
||||
XCTAssertEqual(next, fullPos - fpc.layout.topInteractionBuffer)
|
||||
|
||||
fpc.floatingPanel.layoutAdapter.updateInteractiveTopConstraint(diff: hiddenPos - fullPos + 100.0, allowsTopBuffer: true, with: fpc.behavior)
|
||||
next = fpc.surfaceView.frame.minY
|
||||
XCTAssertEqual(next, hiddenPos + fpc.layout.bottomInteractionBuffer)
|
||||
|
||||
fpc.floatingPanel.layoutAdapter.endInteraction(at: fpc.position)
|
||||
}
|
||||
}
|
||||
|
||||
private typealias LayoutSegmentTestParameter = (UInt, pos: CGFloat, forwardY: Bool, lower: FloatingPanelPosition?, upper: FloatingPanelPosition?)
|
||||
private func assertLayoutSegment(_ floatingPanel: FloatingPanel, with params: [LayoutSegmentTestParameter]) {
|
||||
params.forEach { (line, pos, forwardY, lowr, upper) in
|
||||
let segument = floatingPanel.layoutAdapter.segument(at: pos, forward: forwardY)
|
||||
XCTAssertEqual(segument.lower, lowr, line: line)
|
||||
XCTAssertEqual(segument.upper, upper, line: line)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
//
|
||||
// Created by Shin Yamamoto on 2019/07/05.
|
||||
// Copyright © 2019 scenee. All rights reserved.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
@testable import FloatingPanel
|
||||
|
||||
class FloatingPanelPositionTests: XCTestCase {
|
||||
override func setUp() { }
|
||||
override func tearDown() { }
|
||||
|
||||
func test_nextAndPre() {
|
||||
var positions: [FloatingPanelPosition]
|
||||
positions = [.full, .half, .tip, .hidden]
|
||||
XCTAssertEqual(FloatingPanelPosition.full.next(in: positions), .half)
|
||||
XCTAssertEqual(FloatingPanelPosition.full.pre(in: positions), .full)
|
||||
XCTAssertEqual(FloatingPanelPosition.hidden.next(in: positions), .hidden)
|
||||
XCTAssertEqual(FloatingPanelPosition.hidden.pre(in: positions), .tip)
|
||||
|
||||
positions = [.full, .hidden]
|
||||
XCTAssertEqual(FloatingPanelPosition.full.next(in: positions), .hidden)
|
||||
XCTAssertEqual(FloatingPanelPosition.full.pre(in: positions), .full)
|
||||
XCTAssertEqual(FloatingPanelPosition.hidden.next(in: positions), .hidden)
|
||||
XCTAssertEqual(FloatingPanelPosition.hidden.pre(in: positions), .full)
|
||||
}
|
||||
}
|
||||
+21
-3
@@ -6,10 +6,8 @@
|
||||
import XCTest
|
||||
@testable import FloatingPanel
|
||||
|
||||
class FloatingPanelViewTests: XCTestCase {
|
||||
|
||||
class FloatingPanelSurfaceViewTests: XCTestCase {
|
||||
override func setUp() {}
|
||||
|
||||
override func tearDown() {}
|
||||
|
||||
func test_surfaceView() {
|
||||
@@ -24,6 +22,26 @@ class FloatingPanelViewTests: XCTestCase {
|
||||
XCTAssert(surface.backgroundColor == surface.containerView.backgroundColor)
|
||||
}
|
||||
|
||||
func test_surfaceView_constraintsUpdate() {
|
||||
let window = UIWindow()
|
||||
let surface = FloatingPanelSurfaceView(frame: CGRect(x: 0.0, y: 0.0, width: 320.0, height: 480.0))
|
||||
window.addSubview(surface)
|
||||
window.makeKeyAndVisible()
|
||||
XCTAssert(surface.contentView == nil)
|
||||
surface.layoutIfNeeded()
|
||||
XCTAssert(surface.grabberHandle.frame.minY == 6.0)
|
||||
XCTAssert(surface.grabberHandle.frame.width == surface.grabberHandleWidth)
|
||||
XCTAssert(surface.grabberHandle.frame.height == surface.grabberHandleHeight)
|
||||
|
||||
surface.grabberHandleWidth = 44.0
|
||||
surface.grabberHandleHeight = 12.0
|
||||
surface.layoutIfNeeded()
|
||||
waitRunLoop(secs: 0.000_001)
|
||||
XCTAssert(surface.grabberHandle.frame.width == surface.grabberHandleWidth, "\(surface.grabberHandle.frame.width) == \(surface.grabberHandleWidth)")
|
||||
XCTAssert(surface.grabberHandle.frame.height == surface.grabberHandleHeight, "\(surface.grabberHandle.frame.height) == \(surface.grabberHandleHeight)")
|
||||
window.resignKey()
|
||||
}
|
||||
|
||||
func test_surfaceView_cornderRaduis() {
|
||||
let surface = FloatingPanelSurfaceView(frame: CGRect(x: 0.0, y: 0.0, width: 320.0, height: 480.0))
|
||||
XCTAssert(surface.cornerRadius == 0.0)
|
||||
@@ -4,11 +4,534 @@
|
||||
//
|
||||
|
||||
import XCTest
|
||||
@testable import FloatingPanel
|
||||
|
||||
class FloatingPanelTests: XCTestCase {
|
||||
|
||||
override func setUp() {}
|
||||
|
||||
override func tearDown() {}
|
||||
|
||||
func test_scrolllock() {
|
||||
let fpc = FloatingPanelController()
|
||||
|
||||
let contentVC1 = UITableViewController(nibName: nil, bundle: nil)
|
||||
XCTAssertEqual(contentVC1.tableView.showsVerticalScrollIndicator, true)
|
||||
XCTAssertEqual(contentVC1.tableView.bounces, true)
|
||||
fpc.set(contentViewController: contentVC1)
|
||||
fpc.track(scrollView: contentVC1.tableView)
|
||||
fpc.showForTest()
|
||||
|
||||
XCTAssertEqual(fpc.position, .half)
|
||||
XCTAssertEqual(contentVC1.tableView.showsVerticalScrollIndicator, false)
|
||||
XCTAssertEqual(contentVC1.tableView.bounces, false)
|
||||
|
||||
fpc.move(to: .full, animated: false)
|
||||
XCTAssertEqual(contentVC1.tableView.showsVerticalScrollIndicator, true)
|
||||
XCTAssertEqual(contentVC1.tableView.bounces, true)
|
||||
|
||||
fpc.move(to: .tip, animated: false)
|
||||
XCTAssertEqual(contentVC1.tableView.showsVerticalScrollIndicator, false)
|
||||
XCTAssertEqual(contentVC1.tableView.bounces, false)
|
||||
|
||||
let exp1 = expectation(description: "move to full with animation")
|
||||
fpc.move(to: .full, animated: true) {
|
||||
XCTAssertEqual(contentVC1.tableView.showsVerticalScrollIndicator, true)
|
||||
XCTAssertEqual(contentVC1.tableView.bounces, true)
|
||||
exp1.fulfill()
|
||||
}
|
||||
wait(for: [exp1], timeout: 1.0)
|
||||
|
||||
let exp2 = expectation(description: "move to tip with animation")
|
||||
fpc.move(to: .tip, animated: false) {
|
||||
XCTAssertEqual(contentVC1.tableView.showsVerticalScrollIndicator, false)
|
||||
XCTAssertEqual(contentVC1.tableView.bounces, false)
|
||||
exp2.fulfill()
|
||||
}
|
||||
wait(for: [exp2], timeout: 1.0)
|
||||
|
||||
// Reset the content vc
|
||||
let contentVC2 = UITableViewController(nibName: nil, bundle: nil)
|
||||
XCTAssertEqual(contentVC2.tableView.showsVerticalScrollIndicator, true)
|
||||
XCTAssertEqual(contentVC2.tableView.bounces, true)
|
||||
fpc.set(contentViewController: contentVC2)
|
||||
fpc.track(scrollView: contentVC2.tableView)
|
||||
fpc.show(animated: false, completion: nil)
|
||||
XCTAssertEqual(fpc.position, .half)
|
||||
XCTAssertEqual(contentVC2.tableView.showsVerticalScrollIndicator, false)
|
||||
XCTAssertEqual(contentVC2.tableView.bounces, false)
|
||||
}
|
||||
|
||||
func test_getBackdropAlpha_1positions() {
|
||||
class FloatingPanelLayout1Positions: FloatingPanelTestLayout {
|
||||
let initialPosition: FloatingPanelPosition = .full
|
||||
let supportedPositions: Set<FloatingPanelPosition> = [.full]
|
||||
}
|
||||
let delegate = FloatingPanelTestDelegate()
|
||||
delegate.layout = FloatingPanelLayout1Positions()
|
||||
|
||||
let fpc = FloatingPanelController(delegate: delegate)
|
||||
fpc.showForTest()
|
||||
|
||||
let fullPos = fpc.originYOfSurface(for: .full)
|
||||
|
||||
XCTAssertEqual(fpc.floatingPanel.getBackdropAlpha(at: fullPos - 100.0, with: CGPoint(x: 0.0, y: -100.0)), 0.3)
|
||||
XCTAssertEqual(fpc.floatingPanel.getBackdropAlpha(at: fullPos, with: CGPoint(x: 0.0, y: 0.0)), 0.3)
|
||||
XCTAssertEqual(fpc.floatingPanel.getBackdropAlpha(at: fullPos + 100.0, with: CGPoint(x: 0.0, y: 100.0)), 0.3) // ok??
|
||||
}
|
||||
|
||||
func test_getBackdropAlpha_2positions() {
|
||||
class FloatingPanelLayout2Positions: FloatingPanelTestLayout {
|
||||
let initialPosition: FloatingPanelPosition = .half
|
||||
let supportedPositions: Set<FloatingPanelPosition> = [.half, .full]
|
||||
}
|
||||
let delegate = FloatingPanelTestDelegate()
|
||||
delegate.layout = FloatingPanelLayout2Positions()
|
||||
|
||||
let fpc = FloatingPanelController(delegate: delegate)
|
||||
fpc.showForTest()
|
||||
|
||||
let fullPos = fpc.originYOfSurface(for: .full)
|
||||
let halfPos = fpc.originYOfSurface(for: .half)
|
||||
let distance1 = abs(halfPos - fullPos)
|
||||
|
||||
fpc.move(to: .full, animated: false)
|
||||
XCTAssertEqual(fpc.floatingPanel.getBackdropAlpha(at: fullPos, with: CGPoint(x: 0.0, y: 0.0)), 0.3)
|
||||
XCTAssertEqual(fpc.floatingPanel.getBackdropAlpha(at: fullPos + distance1 * 0.5, with: CGPoint(x: 0.0, y: distance1 * 0.5)), 0.3 * 0.5)
|
||||
XCTAssertEqual(fpc.floatingPanel.getBackdropAlpha(at: halfPos, with: CGPoint(x: 0.0, y: distance1)), 0.0)
|
||||
|
||||
fpc.move(to: .half, animated: false)
|
||||
XCTAssertEqual(fpc.floatingPanel.getBackdropAlpha(at: halfPos, with: CGPoint(x: 0.0, y: 0.0)), 0.0)
|
||||
XCTAssertEqual(fpc.floatingPanel.getBackdropAlpha(at: fullPos + distance1 * 0.5, with: CGPoint(x: 0.0, y: -0.5 * distance1)), 0.3 * 0.5)
|
||||
XCTAssertEqual(fpc.floatingPanel.getBackdropAlpha(at: fullPos, with: CGPoint(x: 0.0, y: -1 * distance1)), 0.3)
|
||||
}
|
||||
|
||||
func test_getBackdropAlpha_2positionsWithHidden() {
|
||||
class FloatingPanelLayout2Positions: FloatingPanelTestLayout {
|
||||
let initialPosition: FloatingPanelPosition = .hidden
|
||||
let supportedPositions: Set<FloatingPanelPosition> = [.hidden, .full]
|
||||
}
|
||||
let delegate = FloatingPanelTestDelegate()
|
||||
delegate.layout = FloatingPanelLayout2Positions()
|
||||
|
||||
let fpc = FloatingPanelController(delegate: delegate)
|
||||
fpc.showForTest()
|
||||
|
||||
let fullPos = fpc.originYOfSurface(for: .full)
|
||||
let hiddenPos = fpc.originYOfSurface(for: .hidden)
|
||||
|
||||
XCTAssertEqual(fpc.floatingPanel.getBackdropAlpha(at: fullPos - 100.0, with: CGPoint(x: 0.0, y: -100.0)), 0.3)
|
||||
XCTAssertEqual(fpc.floatingPanel.getBackdropAlpha(at: fullPos, with: CGPoint(x: 0.0, y: 0.0)), 0.3)
|
||||
XCTAssertEqual(fpc.floatingPanel.getBackdropAlpha(at: hiddenPos, with: CGPoint(x: 0.0, y: 100.0)), 0.0)
|
||||
}
|
||||
|
||||
func test_getBackdropAlpha_3positions() {
|
||||
let fpc = FloatingPanelController()
|
||||
fpc.showForTest()
|
||||
|
||||
let fullPos = fpc.originYOfSurface(for: .full)
|
||||
let halfPos = fpc.originYOfSurface(for: .half)
|
||||
let tipPos = fpc.originYOfSurface(for: .tip)
|
||||
let distance1 = abs(halfPos - fullPos)
|
||||
let distance2 = abs(tipPos - halfPos)
|
||||
|
||||
fpc.move(to: .full, animated: false)
|
||||
XCTAssertEqual(fpc.floatingPanel.getBackdropAlpha(at: fullPos, with: CGPoint(x: 0.0, y: 0.0)), 0.3)
|
||||
XCTAssertEqual(fpc.floatingPanel.getBackdropAlpha(at: fullPos + distance1 * 0.5, with: CGPoint(x: 0.0, y: distance1 * 0.5)), 0.3 * 0.5)
|
||||
XCTAssertEqual(fpc.floatingPanel.getBackdropAlpha(at: halfPos, with: CGPoint(x: 0.0, y: distance1)), 0.0)
|
||||
|
||||
fpc.move(to: .half, animated: false)
|
||||
XCTAssertEqual(fpc.floatingPanel.getBackdropAlpha(at: halfPos, with: CGPoint(x: 0.0, y: 0.0)), 0.0)
|
||||
XCTAssertEqual(fpc.floatingPanel.getBackdropAlpha(at: fullPos + distance1 * 0.5, with: CGPoint(x: 0.0, y: -0.5 * distance1)), 0.3 * 0.5)
|
||||
XCTAssertEqual(fpc.floatingPanel.getBackdropAlpha(at: fullPos, with: CGPoint(x: 0.0, y: -1 * distance1)), 0.3)
|
||||
|
||||
fpc.move(to: .tip, animated: false)
|
||||
XCTAssertEqual(fpc.floatingPanel.getBackdropAlpha(at: tipPos, with: CGPoint(x: 0.0, y: 0.0)), 0.0)
|
||||
XCTAssertEqual(fpc.floatingPanel.getBackdropAlpha(at: halfPos + distance2 * 0.5, with: CGPoint(x: 0.0, y: -0.5 * distance2)), 0.0)
|
||||
XCTAssertEqual(fpc.floatingPanel.getBackdropAlpha(at: halfPos, with: CGPoint(x: 0.0, y: -1 * distance2)), 0.0)
|
||||
}
|
||||
|
||||
func test_targetPosition_1positions() {
|
||||
class FloatingPanelLayout1Positions: FloatingPanelTestLayout {
|
||||
let initialPosition: FloatingPanelPosition = .full
|
||||
let supportedPositions: Set<FloatingPanelPosition> = [.full]
|
||||
}
|
||||
let delegate = FloatingPanelTestDelegate()
|
||||
delegate.layout = FloatingPanelLayout1Positions()
|
||||
|
||||
let fpc = FloatingPanelController(delegate: delegate)
|
||||
fpc.showForTest()
|
||||
|
||||
let fullPos = fpc.originYOfSurface(for: .full)
|
||||
|
||||
fpc.move(to: .full, animated: false)
|
||||
assertTargetPosition(fpc.floatingPanel, with: [
|
||||
(#line, fullPos - 10.0, CGPoint(x: 0.0, y: 1000.0), .full), // redirect
|
||||
(#line, fullPos, CGPoint(x: 0.0, y: -1000.0), .full), // redirect
|
||||
(#line, fullPos, CGPoint(x: 0.0, y: -100.0), .full), // redirect
|
||||
(#line, fullPos, CGPoint(x: 0.0, y: 0.0), .full),
|
||||
(#line, fullPos, CGPoint(x: 0.0, y: 100.0), .full), // redirect
|
||||
(#line, fullPos, CGPoint(x: 0.0, y: 1000.0), .full), // redirect
|
||||
(#line, fullPos + 10.0, CGPoint(x: 0.0, y: 100.0), .full), // redirect
|
||||
])
|
||||
}
|
||||
|
||||
func test_targetPosition_2positions() {
|
||||
class FloatingPanelLayout2Positions: FloatingPanelTestLayout {
|
||||
let initialPosition: FloatingPanelPosition = .half
|
||||
let supportedPositions: Set<FloatingPanelPosition> = [.half, .full]
|
||||
}
|
||||
let delegate = FloatingPanelTestDelegate()
|
||||
delegate.layout = FloatingPanelLayout2Positions()
|
||||
|
||||
let fpc = FloatingPanelController(delegate: delegate)
|
||||
fpc.showForTest()
|
||||
|
||||
let fullPos = fpc.originYOfSurface(for: .full)
|
||||
let halfPos = fpc.originYOfSurface(for: .half)
|
||||
|
||||
fpc.move(to: .full, animated: false)
|
||||
assertTargetPosition(fpc.floatingPanel, with: [
|
||||
(#line, fullPos - 10.0, CGPoint(x: 0.0, y: 1000.0), .half), // project to half
|
||||
(#line, fullPos, CGPoint(x: 0.0, y: -1000.0), .full), // redirect
|
||||
(#line, fullPos, CGPoint(x: 0.0, y: -100.0), .full),
|
||||
(#line, fullPos, CGPoint(x: 0.0, y: 0.0), .full),
|
||||
(#line, fullPos, CGPoint(x: 0.0, y: 100.0), .full),
|
||||
(#line, fullPos, CGPoint(x: 0.0, y: 1000.0), .half), // project to half
|
||||
(#line, fullPos + 10.0, CGPoint(x: 0.0, y: 100.0), .full), // redirect
|
||||
(#line, halfPos - 10.0, CGPoint(x: 0.0, y: -100.0), .half), // redirect
|
||||
(#line, halfPos, CGPoint(x: 0.0, y: -1000.0), .full), // project to full
|
||||
(#line, halfPos, CGPoint(x: 0.0, y: -100.0), .half),
|
||||
(#line, halfPos, CGPoint(x: 0.0, y: 0.0), .half),
|
||||
(#line, halfPos, CGPoint(x: 0.0, y: 100.0), .half),
|
||||
(#line, halfPos, CGPoint(x: 0.0, y: 1000.0), .half), // redirect
|
||||
(#line, halfPos + 10.0, CGPoint(x: 0.0, y: -1000.0), .full), // project to full
|
||||
])
|
||||
fpc.move(to: .half, animated: false)
|
||||
assertTargetPosition(fpc.floatingPanel, with: [
|
||||
(#line, fullPos - 10.0, CGPoint(x: 0.0, y: 1000.0), .half), // project to half
|
||||
(#line, fullPos, CGPoint(x: 0.0, y: -1000.0), .full), // redirect
|
||||
(#line, fullPos, CGPoint(x: 0.0, y: -100.0), .full),
|
||||
(#line, fullPos, CGPoint(x: 0.0, y: 0.0), .full),
|
||||
(#line, fullPos, CGPoint(x: 0.0, y: 100.0), .full),
|
||||
(#line, fullPos, CGPoint(x: 0.0, y: 1000.0), .half), // project to half
|
||||
(#line, fullPos + 10.0, CGPoint(x: 0.0, y: 100.0), .full), // redirect
|
||||
(#line, halfPos - 10.0, CGPoint(x: 0.0, y: -100.0), .half), // redirect
|
||||
(#line, halfPos, CGPoint(x: 0.0, y: -1000.0), .full), // project to full
|
||||
(#line, halfPos, CGPoint(x: 0.0, y: -100.0), .half),
|
||||
(#line, halfPos, CGPoint(x: 0.0, y: 0.0), .half),
|
||||
(#line, halfPos, CGPoint(x: 0.0, y: 100.0), .half),
|
||||
(#line, halfPos, CGPoint(x: 0.0, y: 1000.0), .half), // redirect
|
||||
(#line, halfPos + 10.0, CGPoint(x: 0.0, y: -1000.0), .full), // project to full
|
||||
])
|
||||
}
|
||||
|
||||
func test_targetPosition_2positionsWithHidden() {
|
||||
class FloatingPanelLayout2Positions: FloatingPanelTestLayout {
|
||||
let initialPosition: FloatingPanelPosition = .hidden
|
||||
let supportedPositions: Set<FloatingPanelPosition> = [.hidden, .full]
|
||||
}
|
||||
let delegate = FloatingPanelTestDelegate()
|
||||
delegate.layout = FloatingPanelLayout2Positions()
|
||||
|
||||
let fpc = FloatingPanelController(delegate: delegate)
|
||||
fpc.showForTest()
|
||||
|
||||
let fullPos = fpc.originYOfSurface(for: .full)
|
||||
let hiddenPos = fpc.originYOfSurface(for: .hidden)
|
||||
|
||||
fpc.move(to: .full, animated: false)
|
||||
assertTargetPosition(fpc.floatingPanel, with: [
|
||||
(#line, fullPos - 10.0, CGPoint(x: 0.0, y: 1000.0), .hidden), // project to hidden
|
||||
(#line, fullPos, CGPoint(x: 0.0, y: -1000.0), .full), // redirect
|
||||
(#line, fullPos, CGPoint(x: 0.0, y: -100.0), .full),
|
||||
(#line, fullPos, CGPoint(x: 0.0, y: 0.0), .full),
|
||||
(#line, fullPos, CGPoint(x: 0.0, y: 100.0), .full),
|
||||
(#line, fullPos, CGPoint(x: 0.0, y: 1000.0), .hidden), // project to hidden
|
||||
(#line, fullPos + 10.0, CGPoint(x: 0.0, y: 100.0), .full), // redirect
|
||||
(#line, hiddenPos - 10.0, CGPoint(x: 0.0, y: -100.0), .hidden), // redirect
|
||||
(#line, hiddenPos, CGPoint(x: 0.0, y: -1000.0), .full), // project to full
|
||||
(#line, hiddenPos, CGPoint(x: 0.0, y: -100.0), .hidden),
|
||||
(#line, hiddenPos, CGPoint(x: 0.0, y: 0.0), .hidden),
|
||||
(#line, hiddenPos, CGPoint(x: 0.0, y: 100.0), .hidden),
|
||||
(#line, hiddenPos, CGPoint(x: 0.0, y: 1000.0), .hidden), // redirect
|
||||
(#line, hiddenPos + 10.0, CGPoint(x: 0.0, y: -1000.0), .full), // project to full
|
||||
])
|
||||
fpc.move(to: .hidden, animated: false)
|
||||
assertTargetPosition(fpc.floatingPanel, with: [
|
||||
(#line, fullPos - 10.0, CGPoint(x: 0.0, y: 1000.0), .hidden), // project to hidden
|
||||
(#line, fullPos, CGPoint(x: 0.0, y: -1000.0), .full), // redirect
|
||||
(#line, fullPos, CGPoint(x: 0.0, y: -100.0), .full),
|
||||
(#line, fullPos, CGPoint(x: 0.0, y: 0.0), .full),
|
||||
(#line, fullPos, CGPoint(x: 0.0, y: 100.0), .full),
|
||||
(#line, fullPos, CGPoint(x: 0.0, y: 1000.0), .hidden), // project to hidden
|
||||
(#line, fullPos + 10.0, CGPoint(x: 0.0, y: 100.0), .full), // redirect
|
||||
(#line, hiddenPos - 10.0, CGPoint(x: 0.0, y: -100.0), .hidden), // redirect
|
||||
(#line, hiddenPos, CGPoint(x: 0.0, y: -1000.0), .full), // project to full
|
||||
(#line, hiddenPos, CGPoint(x: 0.0, y: -100.0), .hidden),
|
||||
(#line, hiddenPos, CGPoint(x: 0.0, y: 0.0), .hidden),
|
||||
(#line, hiddenPos, CGPoint(x: 0.0, y: 100.0), .hidden),
|
||||
(#line, hiddenPos, CGPoint(x: 0.0, y: 1000.0), .hidden), // redirect
|
||||
(#line, hiddenPos + 10.0, CGPoint(x: 0.0, y: -1000.0), .full), // project to full
|
||||
])
|
||||
}
|
||||
|
||||
func test_targetPosition_2positionsFromFull() {
|
||||
let delegate = FloatingPanelTestDelegate()
|
||||
delegate.layout = FloatingPanelLayout3Positions()
|
||||
let fpc = FloatingPanelController(delegate: delegate)
|
||||
fpc.showForTest()
|
||||
|
||||
let fullPos = fpc.originYOfSurface(for: .full)
|
||||
let halfPos = fpc.originYOfSurface(for: .half)
|
||||
let tipPos = fpc.originYOfSurface(for: .tip)
|
||||
// From .full
|
||||
fpc.move(to: .full, animated: false)
|
||||
assertTargetPosition(fpc.floatingPanel, with: [
|
||||
(#line, fullPos - 500.0, CGPoint(x: 0.0, y: -100.0), .full), // far from topMostState
|
||||
(#line, fullPos - 500.0, CGPoint(x: 0.0, y: 0.0), .full), // far from topMostState
|
||||
(#line, fullPos - 500.0, CGPoint(x: 0.0, y: 100.0), .full), // far from topMostState
|
||||
(#line, fullPos - 10.0, CGPoint(x: 0.0, y: 3000.0), .half), // block projecting to tip at half
|
||||
(#line, fullPos, CGPoint(x: 0.0, y: -100.0), .full),
|
||||
(#line, fullPos, CGPoint(x: 0.0, y: 0.0), .full),
|
||||
(#line, fullPos, CGPoint(x: 0.0, y: 500.0), .half), // project to half
|
||||
(#line, fullPos, CGPoint(x: 0.0, y: 1000.0), .half), // block projecting to tip at half
|
||||
(#line, fullPos, CGPoint(x: 0.0, y: 3000.0), .half), // block projecting to tip at half
|
||||
(#line, fullPos + 10.0, CGPoint(x: 0.0, y: 100.0), .full), // redirect
|
||||
(#line, halfPos - 10.0, CGPoint(x: 0.0, y: -100.0), .half), // redirect
|
||||
(#line, halfPos, CGPoint(x: 0.0, y: -1000.0), .full), //project to full
|
||||
(#line, halfPos, CGPoint(x: 0.0, y: -100.0), .half),
|
||||
(#line, halfPos, CGPoint(x: 0.0, y: 0.0), .half),
|
||||
(#line, halfPos, CGPoint(x: 0.0, y: 100.0), .half),
|
||||
(#line, halfPos, CGPoint(x: 0.0, y: 1000.0), .tip), // project to tip
|
||||
(#line, halfPos + 10.0, CGPoint(x: 0.0, y: 100.0), .half), // redirect
|
||||
(#line, tipPos - 10.0, CGPoint(x: 0.0, y: -100.0), .tip), // redirect
|
||||
(#line, tipPos, CGPoint(x: 0.0, y: -3000.0), .half), // block projecting to full at half
|
||||
(#line, tipPos, CGPoint(x: 0.0, y: -1000.0), .half), // block projecting to full at half
|
||||
(#line, tipPos, CGPoint(x: 0.0, y: -500.0), .half), // project to half
|
||||
(#line, tipPos, CGPoint(x: 0.0, y: -100.0), .tip),
|
||||
(#line, tipPos, CGPoint(x: 0.0, y: 0.0), .tip),
|
||||
(#line, tipPos, CGPoint(x: 0.0, y: 100.0), .tip),
|
||||
(#line, tipPos + 10.0, CGPoint(x: 0.0, y: -3000.0), .half), // block projecting to full at half
|
||||
(#line, tipPos + 500.0, CGPoint(x: 0.0, y: -100.0), .tip), // far from bottomMostState
|
||||
(#line, tipPos + 500.0, CGPoint(x: 0.0, y: 0.0), .tip), // far from bottomMostState
|
||||
(#line, tipPos + 500.0, CGPoint(x: 0.0, y: 100.0), .tip), // far from bottomMostState
|
||||
])
|
||||
}
|
||||
|
||||
func test_targetPosition_3positionsFromHalf() {
|
||||
let delegate = FloatingPanelTestDelegate()
|
||||
delegate.layout = FloatingPanelLayout3Positions()
|
||||
let fpc = FloatingPanelController(delegate: delegate)
|
||||
fpc.showForTest()
|
||||
|
||||
let fullPos = fpc.originYOfSurface(for: .full)
|
||||
let halfPos = fpc.originYOfSurface(for: .half)
|
||||
let tipPos = fpc.originYOfSurface(for: .tip)
|
||||
// From .half
|
||||
fpc.move(to: .half, animated: false)
|
||||
assertTargetPosition(fpc.floatingPanel, with: [
|
||||
(#line, fullPos - 500.0, CGPoint(x: 0.0, y: -100.0), .full), // far from topMostState
|
||||
(#line, fullPos - 500.0, CGPoint(x: 0.0, y: 0.0), .full), // far from topMostState
|
||||
(#line, fullPos - 500.0, CGPoint(x: 0.0, y: 100.0), .full), // far from topMostState
|
||||
(#line, fullPos, CGPoint(x: 0.0, y: -100.0), .full),
|
||||
(#line, fullPos, CGPoint(x: 0.0, y: 0.0), .full),
|
||||
(#line, fullPos, CGPoint(x: 0.0, y: 500.0), .half), // project to half
|
||||
(#line, fullPos, CGPoint(x: 0.0, y: 1000.0), .half), // block projecting to tip at half
|
||||
(#line, fullPos, CGPoint(x: 0.0, y: 3000.0), .half), // block projecting to tip at half
|
||||
(#line, fullPos + 10.0, CGPoint(x: 0.0, y: 100.0), .full), // redirect
|
||||
(#line, halfPos - 10.0, CGPoint(x: 0.0, y: -100.0), .half), // redirect
|
||||
(#line, halfPos, CGPoint(x: 0.0, y: -1000.0), .full),// project to full
|
||||
(#line, halfPos, CGPoint(x: 0.0, y: -100.0), .half),
|
||||
(#line, halfPos, CGPoint(x: 0.0, y: 0.0), .half),
|
||||
(#line, halfPos, CGPoint(x: 0.0, y: 100.0), .half),
|
||||
(#line, halfPos, CGPoint(x: 0.0, y: 1000.0), .tip), // project to tip
|
||||
(#line, halfPos + 10.0, CGPoint(x: 0.0, y: 100.0), .half), // redirect
|
||||
(#line, tipPos - 10.0, CGPoint(x: 0.0, y: -100.0), .tip), // redirect
|
||||
(#line, tipPos, CGPoint(x: 0.0, y: -3000.0), .half), // block projecting to full at half
|
||||
(#line, tipPos, CGPoint(x: 0.0, y: -1000.0), .half), // block projecting to full at half
|
||||
(#line, tipPos, CGPoint(x: 0.0, y: -500.0), .half), // project to half
|
||||
(#line, tipPos, CGPoint(x: 0.0, y: -100.0), .tip),
|
||||
(#line, tipPos, CGPoint(x: 0.0, y: 0.0), .tip),
|
||||
(#line, tipPos, CGPoint(x: 0.0, y: 100.0), .tip),
|
||||
(#line, tipPos + 500.0, CGPoint(x: 0.0, y: -100.0), .tip), // far from bottomMostState
|
||||
(#line, tipPos + 500.0, CGPoint(x: 0.0, y: 0.0), .tip), // far from bottomMostState
|
||||
(#line, tipPos + 500.0, CGPoint(x: 0.0, y: 100.0), .tip), // far from bottomMostState
|
||||
])
|
||||
}
|
||||
|
||||
func test_targetPosition_3positionsFromTip() {
|
||||
let delegate = FloatingPanelTestDelegate()
|
||||
delegate.layout = FloatingPanelLayout3Positions()
|
||||
let fpc = FloatingPanelController(delegate: delegate)
|
||||
fpc.showForTest()
|
||||
|
||||
let fullPos = fpc.originYOfSurface(for: .full)
|
||||
let halfPos = fpc.originYOfSurface(for: .half)
|
||||
let tipPos = fpc.originYOfSurface(for: .tip)
|
||||
|
||||
// From .tip
|
||||
fpc.move(to: .tip, animated: false)
|
||||
assertTargetPosition(fpc.floatingPanel, with: [
|
||||
(#line, fullPos - 500.0, CGPoint(x: 0.0, y: -100.0), .full), // far from topMostState
|
||||
(#line, fullPos - 500.0, CGPoint(x: 0.0, y: 0.0), .full), // far from topMostState
|
||||
(#line, fullPos - 500.0, CGPoint(x: 0.0, y: 100.0), .full), // far from topMostState
|
||||
(#line, fullPos, CGPoint(x: 0.0, y: -100.0), .full),
|
||||
(#line, fullPos, CGPoint(x: 0.0, y: 0.0), .full),
|
||||
(#line, fullPos, CGPoint(x: 0.0, y: 500.0), .half), // project to half
|
||||
(#line, fullPos, CGPoint(x: 0.0, y: 1000.0), .half), // block projecting to tip at half
|
||||
(#line, fullPos, CGPoint(x: 0.0, y: 3000.0), .half), // block projecting to tip at half
|
||||
(#line, fullPos + 10.0, CGPoint(x: 0.0, y: 100.0), .full), // redirect
|
||||
(#line, halfPos - 10.0, CGPoint(x: 0.0, y: -100.0), .half), // redirect
|
||||
(#line, halfPos, CGPoint(x: 0.0, y: -3000.0), .full), // project to full
|
||||
(#line, halfPos, CGPoint(x: 0.0, y: -100.0), .half),
|
||||
(#line, halfPos, CGPoint(x: 0.0, y: 0.0), .half),
|
||||
(#line, halfPos, CGPoint(x: 0.0, y: 100.0), .half),
|
||||
(#line, halfPos, CGPoint(x: 0.0, y: 1000.0), .tip), // project to tip
|
||||
(#line, halfPos + 10.0, CGPoint(x: 0.0, y: 100.0), .half), // redirect
|
||||
(#line, tipPos - 10.0, CGPoint(x: 0.0, y: -100.0), .tip), // redirect
|
||||
(#line, tipPos, CGPoint(x: 0.0, y: -3000.0), .half), // block projecting to full at half
|
||||
(#line, tipPos, CGPoint(x: 0.0, y: -1000.0), .half), // block projecting to full at half
|
||||
(#line, tipPos, CGPoint(x: 0.0, y: -500.0), .half), // project to half
|
||||
(#line, tipPos, CGPoint(x: 0.0, y: -100.0), .tip),
|
||||
(#line, tipPos, CGPoint(x: 0.0, y: 0.0), .tip),
|
||||
(#line, tipPos, CGPoint(x: 0.0, y: 100.0), .tip),
|
||||
(#line, tipPos + 500.0, CGPoint(x: 0.0, y: -100.0), .tip), // far from bottomMostState
|
||||
(#line, tipPos + 500.0, CGPoint(x: 0.0, y: 0.0), .tip), // far from bottomMostState
|
||||
(#line, tipPos + 500.0, CGPoint(x: 0.0, y: 100.0), .tip), // far from bottomMostState
|
||||
])
|
||||
}
|
||||
|
||||
func test_targetPosition_3positionsAllProjection() {
|
||||
let delegate = FloatingPanelTestDelegate()
|
||||
delegate.layout = FloatingPanelLayout3Positions()
|
||||
delegate.behavior = FloatingPanelProjectionalBehavior()
|
||||
|
||||
let fpc = FloatingPanelController(delegate: delegate)
|
||||
fpc.showForTest()
|
||||
|
||||
let fullPos = fpc.originYOfSurface(for: .full)
|
||||
let halfPos = fpc.originYOfSurface(for: .half)
|
||||
let tipPos = fpc.originYOfSurface(for: .tip)
|
||||
|
||||
// From .full
|
||||
fpc.move(to: .full, animated: false)
|
||||
assertTargetPosition(fpc.floatingPanel, with: [
|
||||
(#line, fullPos - 10.0, CGPoint(x: 0.0, y: 3000.0), .tip),
|
||||
(#line, fullPos, CGPoint(x: 0.0, y: 1000.0), .tip),
|
||||
(#line, fullPos, CGPoint(x: 0.0, y: 3000.0), .tip),
|
||||
(#line, halfPos, CGPoint(x: 0.0, y: 1000.0), .tip),
|
||||
(#line, halfPos, CGPoint(x: 0.0, y: -1000.0), .full),
|
||||
(#line, tipPos, CGPoint(x: 0.0, y: -3000.0), .full),
|
||||
(#line, tipPos, CGPoint(x: 0.0, y: -1000.0), .full),
|
||||
(#line, tipPos + 10.0, CGPoint(x: 0.0, y: -3000.0), .full),
|
||||
])
|
||||
|
||||
// From .half
|
||||
fpc.move(to: .tip, animated: false)
|
||||
assertTargetPosition(fpc.floatingPanel, with: [
|
||||
(#line, fullPos, CGPoint(x: 0.0, y: 1000.0), .tip),
|
||||
(#line, fullPos, CGPoint(x: 0.0, y: 3000.0), .tip),
|
||||
(#line, tipPos, CGPoint(x: 0.0, y: -3000.0), .full),
|
||||
(#line, tipPos, CGPoint(x: 0.0, y: -1000.0), .full),
|
||||
])
|
||||
|
||||
// From .tip
|
||||
fpc.move(to: .tip, animated: false)
|
||||
assertTargetPosition(fpc.floatingPanel, with: [
|
||||
(#line, fullPos - 10.0, CGPoint(x: 0.0, y: 3000.0), .tip),
|
||||
(#line, fullPos, CGPoint(x: 0.0, y: 1000.0), .tip),
|
||||
(#line, fullPos, CGPoint(x: 0.0, y: 3000.0), .tip),
|
||||
(#line, halfPos, CGPoint(x: 0.0, y: 1000.0), .tip),
|
||||
(#line, halfPos, CGPoint(x: 0.0, y: -1000.0), .full),
|
||||
(#line, tipPos, CGPoint(x: 0.0, y: -3000.0), .full),
|
||||
(#line, tipPos, CGPoint(x: 0.0, y: -1000.0), .full),
|
||||
(#line, tipPos + 10.0, CGPoint(x: 0.0, y: -3000.0), .full),
|
||||
])
|
||||
}
|
||||
|
||||
func test_targetPosition_3positionsWithHidden() {
|
||||
class FloatingPanelLayout3PositionsWithHidden: FloatingPanelTestLayout {
|
||||
let initialPosition: FloatingPanelPosition = .hidden
|
||||
let supportedPositions: Set<FloatingPanelPosition> = [.hidden, .half, .full]
|
||||
}
|
||||
let delegate = FloatingPanelTestDelegate()
|
||||
delegate.layout = FloatingPanelLayout3PositionsWithHidden()
|
||||
|
||||
let fpc = FloatingPanelController(delegate: delegate)
|
||||
fpc.showForTest()
|
||||
XCTAssertEqual(fpc.position, .hidden)
|
||||
|
||||
fpc.move(to: .full, animated: false)
|
||||
assertTargetPosition(fpc.floatingPanel, with: [
|
||||
(#line, fpc.surfaceView.frame.minY, CGPoint(x: 0.0, y: 1000.0), .half),
|
||||
])
|
||||
fpc.move(to: .half, animated: false)
|
||||
assertTargetPosition(fpc.floatingPanel, with: [
|
||||
(#line, fpc.surfaceView.frame.minY, CGPoint(x: 0.0, y: -100.0), .half),
|
||||
(#line, fpc.surfaceView.frame.minY, CGPoint(x: 0.0, y: -1000.0), .full),
|
||||
(#line, fpc.surfaceView.frame.minY, CGPoint(x: 0.0, y: 0.0), .half),
|
||||
(#line, fpc.surfaceView.frame.minY, CGPoint(x: 0.0, y: 1000.0), .hidden),
|
||||
])
|
||||
}
|
||||
|
||||
func test_targetPosition_3positionsWithHiddenWithoutFull() {
|
||||
class FloatingPanelLayout3Positions: FloatingPanelTestLayout {
|
||||
let initialPosition: FloatingPanelPosition = .hidden
|
||||
let supportedPositions: Set<FloatingPanelPosition> = [.hidden, .tip, .half]
|
||||
}
|
||||
|
||||
let delegate = FloatingPanelTestDelegate()
|
||||
delegate.layout = FloatingPanelLayout3Positions()
|
||||
delegate.behavior = FloatingPanelProjectionalBehavior()
|
||||
|
||||
let fpc = FloatingPanelController(delegate: delegate)
|
||||
fpc.showForTest()
|
||||
XCTAssertEqual(fpc.position, .hidden)
|
||||
|
||||
let halfPos = fpc.originYOfSurface(for: .half)
|
||||
let tipPos = fpc.originYOfSurface(for: .tip)
|
||||
//let hiddenPos = fpc.originYOfSurface(for: .hidden)
|
||||
|
||||
fpc.move(to: .half, animated: false)
|
||||
assertTargetPosition(fpc.floatingPanel, with: [
|
||||
(#line, halfPos, CGPoint(x: 0.0, y: -100.0), .half),
|
||||
(#line, halfPos, CGPoint(x: 0.0, y: 0.0), .half),
|
||||
(#line, halfPos, CGPoint(x: 0.0, y: 385.0), .tip), // projection
|
||||
(#line, halfPos, CGPoint(x: 0.0, y: 1000.0), .hidden), // projection
|
||||
(#line, halfPos + 10.0, CGPoint(x: 0.0, y: 100.0), .half), // redirection
|
||||
(#line, tipPos - 10.0, CGPoint(x: 0.0, y: -100.0), .tip), // redirection
|
||||
(#line, tipPos, CGPoint(x: 0.0, y: -3000.0), .half), //projection
|
||||
(#line, tipPos, CGPoint(x: 0.0, y: -10.0), .tip),
|
||||
(#line, tipPos, CGPoint(x: 0.0, y: 0.0), .tip),
|
||||
(#line, tipPos, CGPoint(x: 0.0, y: 10.0), .tip),
|
||||
(#line, tipPos, CGPoint(x: 0.0, y: 1000.0), .hidden), //projection
|
||||
(#line, tipPos + 10.0, CGPoint(x: 0.0, y: 10.0), .tip), // redirection
|
||||
(#line, tipPos - 10.0, CGPoint(x: 0.0, y: 10.0), .tip), // redirection
|
||||
])
|
||||
fpc.move(to: .tip, animated: false)
|
||||
assertTargetPosition(fpc.floatingPanel, with: [
|
||||
(#line, tipPos, CGPoint(x: 0.0, y: -100.0), .tip),
|
||||
(#line, tipPos, CGPoint(x: 0.0, y: -1000.0), .half),
|
||||
(#line, tipPos, CGPoint(x: 0.0, y: 0.0), .tip),
|
||||
(#line, tipPos, CGPoint(x: 0.0, y: 1000.0), .hidden),
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
private class FloatingPanelLayout3Positions: FloatingPanelTestLayout {
|
||||
let initialPosition: FloatingPanelPosition = .tip
|
||||
let supportedPositions: Set<FloatingPanelPosition> = [.tip, .half, .full]
|
||||
}
|
||||
|
||||
private typealias TestParameter = (UInt, CGFloat,CGPoint, FloatingPanelPosition)
|
||||
private func assertTargetPosition(_ floatingPanel: FloatingPanel, with params: [TestParameter]) {
|
||||
params.forEach { (line, pos, velocity, result) in
|
||||
floatingPanel.surfaceView.frame.origin.y = pos
|
||||
XCTAssertEqual(floatingPanel.targetPosition(from: pos, with: velocity), result, line: line)
|
||||
}
|
||||
}
|
||||
|
||||
private class FloatingPanelProjectionalBehavior: FloatingPanelBehavior {
|
||||
func shouldProjectMomentum(_ fpc: FloatingPanelController, for proposedTargetPosition: FloatingPanelPosition) -> Bool {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
//
|
||||
// Created by Shin Yamamoto on 2019/06/27.
|
||||
// Copyright © 2019 scenee. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
@testable import FloatingPanel
|
||||
|
||||
func waitRunLoop(secs: TimeInterval = 0) {
|
||||
RunLoop.main.run(until: Date(timeIntervalSinceNow: secs))
|
||||
}
|
||||
|
||||
extension FloatingPanelController {
|
||||
func showForTest() {
|
||||
loadViewIfNeeded()
|
||||
view.frame = CGRect(x: 0, y: 0, width: 375, height: 667)
|
||||
show(animated: false, completion: nil)
|
||||
}
|
||||
}
|
||||
|
||||
class FloatingPanelTestDelegate: FloatingPanelControllerDelegate {
|
||||
var layout: FloatingPanelLayout?
|
||||
var behavior: FloatingPanelBehavior?
|
||||
func floatingPanel(_ vc: FloatingPanelController, layoutFor newCollection: UITraitCollection) -> FloatingPanelLayout? {
|
||||
return layout
|
||||
}
|
||||
func floatingPanel(_ vc: FloatingPanelController, behaviorFor newCollection: UITraitCollection) -> FloatingPanelBehavior? {
|
||||
return behavior
|
||||
}
|
||||
}
|
||||
|
||||
protocol FloatingPanelTestLayout: FloatingPanelFullScreenLayout {}
|
||||
extension FloatingPanelTestLayout {
|
||||
func insetFor(position: FloatingPanelPosition) -> CGFloat? {
|
||||
switch position {
|
||||
case .full: return 20.0
|
||||
case .half: return 250.0
|
||||
case .tip: return 60.0
|
||||
default: return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -93,6 +93,10 @@ For [Carthage](https://github.com/Carthage/Carthage), add the following to your
|
||||
github "scenee/FloatingPanel"
|
||||
```
|
||||
|
||||
### Swift Package Manager with Xcode 11
|
||||
|
||||
Follow [this doc](https://developer.apple.com/documentation/swift_packages/adding_package_dependencies_to_your_app).
|
||||
|
||||
## Getting Started
|
||||
|
||||
### Add a floating panel as a child view controller
|
||||
|
||||
Reference in New Issue
Block a user