Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 837edc6abf | |||
| 20fda3888f | |||
| 4728c6848b | |||
| 70c945c9f5 |
@@ -4,8 +4,18 @@
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import FloatingPanel
|
||||
|
||||
@UIApplicationMain
|
||||
class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
var window: UIWindow?
|
||||
|
||||
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
|
||||
FloatingPanelSurfaceView.appearance().shadowHidden = false
|
||||
FloatingPanelSurfaceView.appearance().cornerRadius = 6.0
|
||||
// FloatingPanelSurfaceView.appearance().backgroundColor = .lightGray
|
||||
// FloatingPanelBackdropView.appearance().backgroundColor = .red
|
||||
// GrabberHandleView.appearance().barColor = .red
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -123,10 +123,6 @@ class SampleListViewController: UIViewController {
|
||||
mainPanelVC = FloatingPanelController()
|
||||
mainPanelVC.delegate = self
|
||||
|
||||
// Initialize FloatingPanelController and add the view
|
||||
mainPanelVC.surfaceView.cornerRadius = 6.0
|
||||
mainPanelVC.surfaceView.shadowHidden = false
|
||||
|
||||
// Set a content view controller
|
||||
mainPanelVC.set(contentViewController: contentVC)
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
Pod::Spec.new do |s|
|
||||
|
||||
s.name = "FloatingPanel"
|
||||
s.version = "1.6.2"
|
||||
s.version = "1.5.1"
|
||||
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,8 +7,6 @@
|
||||
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 */; };
|
||||
@@ -20,7 +18,7 @@
|
||||
545DBA2B2152383100CA77B8 /* GrabberHandleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 545DBA2A2152383100CA77B8 /* GrabberHandleView.swift */; };
|
||||
54A6B6B122968B530077F348 /* FloatingPanelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54A6B6B022968B530077F348 /* FloatingPanelTests.swift */; };
|
||||
54A6B6B622968F710077F348 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 54A6B6B522968F710077F348 /* LaunchScreen.storyboard */; };
|
||||
54A6B6B82296A8520077F348 /* FloatingPanelSurfaceViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54A6B6B72296A8520077F348 /* FloatingPanelSurfaceViewTests.swift */; };
|
||||
54A6B6B82296A8520077F348 /* FloatingPanelViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54A6B6B72296A8520077F348 /* FloatingPanelViewTests.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 */; };
|
||||
@@ -47,8 +45,6 @@
|
||||
/* 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>"; };
|
||||
@@ -63,7 +59,7 @@
|
||||
545DBA2A2152383100CA77B8 /* GrabberHandleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GrabberHandleView.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 /* FloatingPanelSurfaceViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FloatingPanelSurfaceViewTests.swift; sourceTree = "<group>"; };
|
||||
54A6B6B72296A8520077F348 /* FloatingPanelViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FloatingPanelViewTests.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>"; };
|
||||
@@ -144,10 +140,8 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
54A6B6B022968B530077F348 /* FloatingPanelTests.swift */,
|
||||
542753C522C49A6E00D17955 /* FloatingPanelLayoutTests.swift */,
|
||||
54A6B6B72296A8520077F348 /* FloatingPanelSurfaceViewTests.swift */,
|
||||
54A6B6B72296A8520077F348 /* FloatingPanelViewTests.swift */,
|
||||
545DB9CF2151169500CA77B8 /* FloatingPanelControllerTests.swift */,
|
||||
542753C722C49A8F00D17955 /* Utils.swift */,
|
||||
545DB9D12151169500CA77B8 /* Info.plist */,
|
||||
);
|
||||
path = Tests;
|
||||
@@ -315,7 +309,6 @@
|
||||
545DBA2B2152383100CA77B8 /* GrabberHandleView.swift in Sources */,
|
||||
54352E9621A51A2500CBCA08 /* FloatingPanelTransitioning.swift in Sources */,
|
||||
545DB9DE215118C800CA77B8 /* UIExtensions.swift in Sources */,
|
||||
542753C822C49A8F00D17955 /* Utils.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -325,8 +318,7 @@
|
||||
files = (
|
||||
54A6B6B122968B530077F348 /* FloatingPanelTests.swift in Sources */,
|
||||
545DB9D02151169500CA77B8 /* FloatingPanelControllerTests.swift in Sources */,
|
||||
542753C622C49A6E00D17955 /* FloatingPanelLayoutTests.swift in Sources */,
|
||||
54A6B6B82296A8520077F348 /* FloatingPanelSurfaceViewTests.swift in Sources */,
|
||||
54A6B6B82296A8520077F348 /* FloatingPanelViewTests.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
||||
@@ -29,9 +29,7 @@
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<Testables>
|
||||
<TestableReference
|
||||
skipped = "NO"
|
||||
parallelizable = "YES"
|
||||
testExecutionOrdering = "random">
|
||||
skipped = "NO">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "545DB9C92151169500CA77B8"
|
||||
|
||||
@@ -8,9 +8,9 @@ import UIKit.UIGestureRecognizerSubclass // For Xcode 9.4.1
|
||||
///
|
||||
/// FloatingPanel presentation model
|
||||
///
|
||||
class FloatingPanel: NSObject, UIGestureRecognizerDelegate {
|
||||
class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate {
|
||||
// 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,11 +25,7 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate {
|
||||
}
|
||||
|
||||
private(set) var state: FloatingPanelPosition = .hidden {
|
||||
didSet {
|
||||
if let vc = viewcontroller {
|
||||
vc.delegate?.floatingPanelDidChangePosition(vc)
|
||||
}
|
||||
}
|
||||
didSet { viewcontroller.delegate?.floatingPanelDidChangePosition(viewcontroller) }
|
||||
}
|
||||
|
||||
private var isBottomState: Bool {
|
||||
@@ -40,7 +36,15 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate {
|
||||
let panGestureRecognizer: FloatingPanelPanGestureRecognizer
|
||||
var isRemovalInteractionEnabled: Bool = false
|
||||
|
||||
fileprivate var animator: UIViewPropertyAnimator?
|
||||
fileprivate var animator: UIViewPropertyAnimator? {
|
||||
didSet {
|
||||
// This intends to avoid `tableView(_:didSelectRowAt:)` not being
|
||||
// called on first tap after the moving animation, but it doesn't
|
||||
// seem to be enough. The same issue happens on Apple Maps so it
|
||||
// might be an issue in `UITableView`.
|
||||
scrollView?.isUserInteractionEnabled = (animator == nil)
|
||||
}
|
||||
}
|
||||
|
||||
private var initialFrame: CGRect = .zero
|
||||
private var initialTranslationY: CGFloat = 0
|
||||
@@ -56,16 +60,16 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate {
|
||||
private var scrollBouncable = false
|
||||
private var scrollIndictorVisible = false
|
||||
|
||||
private var isScrollLocked: Bool = false
|
||||
|
||||
// MARK: - Interface
|
||||
|
||||
init(_ vc: FloatingPanelController, layout: FloatingPanelLayout, behavior: FloatingPanelBehavior) {
|
||||
viewcontroller = vc
|
||||
|
||||
surfaceView = FloatingPanelSurfaceView()
|
||||
surfaceView.backgroundColor = .white
|
||||
|
||||
backdropView = FloatingPanelBackdropView()
|
||||
backdropView.backgroundColor = .black
|
||||
backdropView.alpha = 0.0
|
||||
|
||||
self.layoutAdapter = FloatingPanelLayoutAdapter(surfaceView: surfaceView,
|
||||
@@ -93,10 +97,6 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate {
|
||||
}
|
||||
|
||||
private func move(from: FloatingPanelPosition, to: FloatingPanelPosition, animated: Bool, completion: (() -> Void)? = nil) {
|
||||
guard let vc = viewcontroller else {
|
||||
completion?()
|
||||
return
|
||||
}
|
||||
if state != layoutAdapter.topMostState {
|
||||
lockScrollView()
|
||||
}
|
||||
@@ -106,11 +106,11 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate {
|
||||
let animator: UIViewPropertyAnimator
|
||||
switch (from, to) {
|
||||
case (.hidden, let to):
|
||||
animator = behavior.addAnimator(vc, to: to)
|
||||
animator = behavior.addAnimator(self.viewcontroller, to: to)
|
||||
case (let from, .hidden):
|
||||
animator = behavior.removeAnimator(vc, from: from)
|
||||
animator = behavior.removeAnimator(self.viewcontroller, from: from)
|
||||
case (let from, let to):
|
||||
animator = behavior.moveAnimator(vc, from: from, to: to)
|
||||
animator = behavior.moveAnimator(self.viewcontroller, from: from, to: to)
|
||||
}
|
||||
|
||||
animator.addAnimations { [weak self] in
|
||||
@@ -122,11 +122,7 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate {
|
||||
animator.addCompletion { [weak self] _ in
|
||||
guard let `self` = self else { return }
|
||||
self.animator = nil
|
||||
if self.state == self.layoutAdapter.topMostState {
|
||||
self.unlockScrollView()
|
||||
} else {
|
||||
self.lockScrollView()
|
||||
}
|
||||
self.unlockScrollView()
|
||||
completion?()
|
||||
}
|
||||
self.animator = animator
|
||||
@@ -134,11 +130,7 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate {
|
||||
} else {
|
||||
self.state = to
|
||||
self.updateLayout(to: to)
|
||||
if self.state == self.layoutAdapter.topMostState {
|
||||
self.unlockScrollView()
|
||||
} else {
|
||||
self.lockScrollView()
|
||||
}
|
||||
self.unlockScrollView()
|
||||
completion?()
|
||||
}
|
||||
}
|
||||
@@ -175,8 +167,7 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate {
|
||||
|
||||
/* log.debug("shouldRecognizeSimultaneouslyWith", otherGestureRecognizer) */
|
||||
|
||||
if let vc = viewcontroller,
|
||||
vc.delegate?.floatingPanel(vc, shouldRecognizeSimultaneouslyWith: otherGestureRecognizer) ?? false {
|
||||
if viewcontroller.delegate?.floatingPanel(viewcontroller, shouldRecognizeSimultaneouslyWith: otherGestureRecognizer) ?? false {
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -223,11 +214,11 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
if let vc = viewcontroller,
|
||||
vc.delegate?.floatingPanel(vc, shouldRecognizeSimultaneouslyWith: otherGestureRecognizer) ?? false {
|
||||
if viewcontroller.delegate?.floatingPanel(viewcontroller, shouldRecognizeSimultaneouslyWith: otherGestureRecognizer) ?? false {
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
switch otherGestureRecognizer {
|
||||
case is UIPanGestureRecognizer,
|
||||
is UISwipeGestureRecognizer,
|
||||
@@ -278,6 +269,13 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate {
|
||||
if grabberAreaFrame.contains(location) {
|
||||
// Preserve the current content offset in moving from full.
|
||||
scrollView.setContentOffset(initialScrollOffset, animated: false)
|
||||
} else {
|
||||
let offset = scrollView.contentOffset.y - scrollView.contentOffsetZero.y
|
||||
if offset < 0 {
|
||||
fitToBounds(scrollView: scrollView)
|
||||
let translation = panGesture.translation(in: panGestureRecognizer.view!.superview)
|
||||
startInteraction(with: translation, at: location)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -289,17 +287,15 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate {
|
||||
lockScrollView()
|
||||
}
|
||||
} else {
|
||||
let offset = scrollView.contentOffset.y - scrollView.contentOffsetZero.y
|
||||
// Always show a scroll indicator at the top.
|
||||
if interactionInProgress {
|
||||
if offset > 0 {
|
||||
unlockScrollView()
|
||||
}
|
||||
unlockScrollView()
|
||||
} 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()
|
||||
let offset = scrollView.contentOffset.y - scrollView.contentOffsetZero.y
|
||||
if state == layoutAdapter.topMostState, offset < 0, velocity.y > 0 {
|
||||
fitToBounds(scrollView: scrollView)
|
||||
let translation = panGesture.translation(in: panGestureRecognizer.view!.superview)
|
||||
startInteraction(with: translation, at: location)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -325,8 +321,7 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate {
|
||||
}
|
||||
|
||||
if interactionInProgress == false,
|
||||
let vc = viewcontroller,
|
||||
vc.delegate?.floatingPanelShouldBeginDragging(vc) == false {
|
||||
viewcontroller.delegate?.floatingPanelShouldBeginDragging(viewcontroller) == false {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -439,9 +434,7 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate {
|
||||
let didMove = (pre != surfaceView.frame.minY)
|
||||
guard didMove else { return }
|
||||
|
||||
if let vc = viewcontroller {
|
||||
vc.delegate?.floatingPanelDidMove(vc)
|
||||
}
|
||||
viewcontroller.delegate?.floatingPanelDidMove(viewcontroller)
|
||||
}
|
||||
|
||||
private func allowsTopBuffer(for translationY: CGFloat) -> Bool {
|
||||
@@ -459,12 +452,11 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate {
|
||||
// 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 {
|
||||
vc.contentViewController?.view?.constraints.forEach({ (const) in
|
||||
switch vc.contentViewController?.layoutGuide.bottomAnchor {
|
||||
viewcontroller.contentViewController?.view?.constraints.forEach({ (const) in
|
||||
switch viewcontroller.contentViewController?.layoutGuide.bottomAnchor {
|
||||
case const.firstAnchor:
|
||||
(const.secondItem as? UIView)?.disableAutoLayout()
|
||||
const.isActive = false
|
||||
@@ -479,8 +471,8 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate {
|
||||
disabledBottomAutoLayout = true
|
||||
} else {
|
||||
if disabledBottomAutoLayout {
|
||||
vc.contentViewController?.view?.constraints.forEach({ (const) in
|
||||
switch vc.contentViewController?.layoutGuide.bottomAnchor {
|
||||
viewcontroller.contentViewController?.view?.constraints.forEach({ (const) in
|
||||
switch viewcontroller.contentViewController?.layoutGuide.bottomAnchor {
|
||||
case const.firstAnchor:
|
||||
(const.secondItem as? UIView)?.enableAutoLayout()
|
||||
const.isActive = true
|
||||
@@ -521,18 +513,21 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate {
|
||||
let velocityVector = (distance != 0) ? CGVector(dx: 0,
|
||||
dy: min(abs(velocity.y)/distance, behavior.removalVelocity)) : .zero
|
||||
|
||||
if shouldStartRemovalAnimation(with: velocityVector), let vc = viewcontroller {
|
||||
vc.delegate?.floatingPanelDidEndDraggingToRemove(vc, withVelocity: velocity)
|
||||
startRemovalAnimation(vc, with: velocityVector) { [weak self] in
|
||||
self?.finishRemovalAnimation()
|
||||
if shouldStartRemovalAnimation(with: velocityVector) {
|
||||
|
||||
viewcontroller.delegate?.floatingPanelDidEndDraggingToRemove(viewcontroller, withVelocity: velocity)
|
||||
self.startRemovalAnimation(with: velocityVector) { [weak self] in
|
||||
guard let `self` = self else { return }
|
||||
self.viewcontroller.dismiss(animated: false, completion: { [weak self] in
|
||||
guard let `self` = self else { return }
|
||||
self.viewcontroller.delegate?.floatingPanelDidEndRemove(self.viewcontroller)
|
||||
})
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if let vc = viewcontroller {
|
||||
vc.delegate?.floatingPanelDidEndDragging(vc, withVelocity: velocity, targetPosition: targetPosition)
|
||||
}
|
||||
viewcontroller.delegate?.floatingPanelDidEndDragging(viewcontroller, withVelocity: velocity, targetPosition: targetPosition)
|
||||
|
||||
// Workaround: Disable a tracking scroll to prevent bouncing a scroll content in a panel animating
|
||||
let isScrollEnabled = scrollView?.isScrollEnabled
|
||||
@@ -565,8 +560,8 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate {
|
||||
return true
|
||||
}
|
||||
|
||||
private func startRemovalAnimation(_ vc: FloatingPanelController, with velocityVector: CGVector, completion: (() -> Void)?) {
|
||||
let animator = behavior.removalInteractionAnimator(vc, with: velocityVector)
|
||||
private func startRemovalAnimation(with velocityVector: CGVector, completion: (() -> Void)?) {
|
||||
let animator = self.behavior.removalInteractionAnimator(self.viewcontroller, with: velocityVector)
|
||||
|
||||
animator.addAnimations { [weak self] in
|
||||
self?.updateLayout(to: .hidden)
|
||||
@@ -579,13 +574,6 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate {
|
||||
animator.startAnimation()
|
||||
}
|
||||
|
||||
private func finishRemovalAnimation() {
|
||||
viewcontroller?.dismiss(animated: false) { [weak self] in
|
||||
guard let vc = self?.viewcontroller else { return }
|
||||
vc.delegate?.floatingPanelDidEndRemove(vc)
|
||||
}
|
||||
}
|
||||
|
||||
private func startInteraction(with translation: CGPoint, at location: CGPoint) {
|
||||
/* Don't lock a scroll view to show a scroll indicator after hitting the top */
|
||||
log.debug("startInteraction -- translation = \(translation.y), location = \(location.y)")
|
||||
@@ -596,7 +584,6 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate {
|
||||
if grabberAreaFrame.contains(location) {
|
||||
initialScrollOffset = scrollView.contentOffset
|
||||
} else {
|
||||
fitToBounds(scrollView: scrollView)
|
||||
settle(scrollView: scrollView)
|
||||
initialScrollOffset = scrollView.contentOffsetZero
|
||||
}
|
||||
@@ -605,9 +592,7 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate {
|
||||
|
||||
initialTranslationY = translation.y
|
||||
|
||||
if let vc = viewcontroller {
|
||||
vc.delegate?.floatingPanelWillBeginDragging(vc)
|
||||
}
|
||||
viewcontroller.delegate?.floatingPanelWillBeginDragging(viewcontroller)
|
||||
|
||||
layoutAdapter.startInteraction(at: state)
|
||||
|
||||
@@ -639,14 +624,12 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate {
|
||||
|
||||
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
|
||||
|
||||
vc.delegate?.floatingPanelWillBeginDecelerating(vc)
|
||||
viewcontroller.delegate?.floatingPanelWillBeginDecelerating(viewcontroller)
|
||||
|
||||
let velocityVector = (distance != 0) ? CGVector(dx: 0, dy: min(abs(velocity.y)/distance, 30.0)) : .zero
|
||||
let animator = behavior.interactionAnimator(vc, to: targetPosition, with: velocityVector)
|
||||
let animator = behavior.interactionAnimator(self.viewcontroller, to: targetPosition, with: velocityVector)
|
||||
animator.addAnimations { [weak self] in
|
||||
guard let `self` = self else { return }
|
||||
self.state = targetPosition
|
||||
@@ -666,9 +649,7 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate {
|
||||
self.isDecelerating = false
|
||||
self.animator = nil
|
||||
|
||||
if let vc = viewcontroller {
|
||||
vc.delegate?.floatingPanelDidEndDecelerating(vc)
|
||||
}
|
||||
self.viewcontroller.delegate?.floatingPanelDidEndDecelerating(self.viewcontroller)
|
||||
|
||||
if let scrollView = scrollView {
|
||||
log.debug("finishAnimation -- scroll offset = \(scrollView.contentOffset)")
|
||||
@@ -682,9 +663,21 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate {
|
||||
}
|
||||
|
||||
private func distance(to targetPosition: FloatingPanelPosition) -> CGFloat {
|
||||
let topY = layoutAdapter.topY
|
||||
let middleY = layoutAdapter.middleY
|
||||
let bottomY = layoutAdapter.bottomY
|
||||
let currentY = surfaceView.frame.minY
|
||||
let targetY = layoutAdapter.positionY(for: targetPosition)
|
||||
return CGFloat(abs(currentY - targetY))
|
||||
|
||||
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 {
|
||||
@@ -725,7 +718,6 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate {
|
||||
}
|
||||
|
||||
private func targetPosition(with velocity: CGPoint) -> (FloatingPanelPosition) {
|
||||
guard let vc = viewcontroller else { return state }
|
||||
let currentY = surfaceView.frame.minY
|
||||
let supportedPositions = layoutAdapter.supportedPositions
|
||||
|
||||
@@ -768,7 +760,7 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate {
|
||||
fatalError("Now .hidden must not be used for a user interaction")
|
||||
}
|
||||
|
||||
let redirectionalProgress = max(min(behavior.redirectionalProgress(vc, from: state, to: nextState), 1.0), 0.0)
|
||||
let redirectionalProgress = max(min(behavior.redirectionalProgress(viewcontroller, from: state, to: nextState), 1.0), 0.0)
|
||||
|
||||
let th1: CGFloat
|
||||
let th2: CGFloat
|
||||
@@ -781,7 +773,7 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate {
|
||||
th2 = bottomY - (bottomY - middleY) * redirectionalProgress
|
||||
}
|
||||
|
||||
let decelerationRate = behavior.momentumProjectionRate(vc)
|
||||
let decelerationRate = behavior.momentumProjectionRate(viewcontroller)
|
||||
|
||||
let baseY = abs(bottomY - topY)
|
||||
let vecY = velocity.y / baseY
|
||||
@@ -791,7 +783,7 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate {
|
||||
case ..<th1:
|
||||
switch pY {
|
||||
case bottomY...:
|
||||
return behavior.shouldProjectMomentum(vc, for: .tip) ? .tip : .half
|
||||
return behavior.shouldProjectMomentum(viewcontroller, for: .tip) ? .tip : .half
|
||||
case middleY...:
|
||||
return .half
|
||||
case topY...:
|
||||
@@ -802,7 +794,7 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate {
|
||||
case ...middleY:
|
||||
switch pY {
|
||||
case bottomY...:
|
||||
return behavior.shouldProjectMomentum(vc, for: .tip) ? .tip : .half
|
||||
return behavior.shouldProjectMomentum(viewcontroller, for: .tip) ? .tip : .half
|
||||
case middleY...:
|
||||
return .half
|
||||
case topY...:
|
||||
@@ -819,7 +811,7 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate {
|
||||
case topY...:
|
||||
return .half
|
||||
default:
|
||||
return behavior.shouldProjectMomentum(vc, for: .full) ? .full : .half
|
||||
return behavior.shouldProjectMomentum(viewcontroller, for: .full) ? .full : .half
|
||||
}
|
||||
default:
|
||||
switch pY {
|
||||
@@ -830,14 +822,13 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate {
|
||||
case topY...:
|
||||
return .half
|
||||
default:
|
||||
return behavior.shouldProjectMomentum(vc, for: .full) ? .full : .half
|
||||
return behavior.shouldProjectMomentum(viewcontroller, for: .full) ? .full : .half
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func targetPosition(from positions: [FloatingPanelPosition], at currentY: CGFloat, velocity: CGPoint) -> FloatingPanelPosition {
|
||||
guard let vc = viewcontroller else { return state }
|
||||
assert(positions.count == 2)
|
||||
|
||||
let top = positions[0]
|
||||
@@ -847,11 +838,11 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate {
|
||||
let bottomY = layoutAdapter.positionY(for: bottom)
|
||||
|
||||
let target = top == state ? bottom : top
|
||||
let redirectionalProgress = max(min(behavior.redirectionalProgress(vc, from: state, to: target), 1.0), 0.0)
|
||||
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(vc)
|
||||
let decelerationRate = behavior.momentumProjectionRate(viewcontroller)
|
||||
let pY = project(initialVelocity: velocity.y, decelerationRate: decelerationRate) + currentY
|
||||
|
||||
switch currentY {
|
||||
@@ -875,10 +866,11 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate {
|
||||
private func lockScrollView() {
|
||||
guard let scrollView = scrollView else { return }
|
||||
|
||||
if scrollView.isLocked {
|
||||
if isScrollLocked {
|
||||
log.debug("Already scroll locked.")
|
||||
return
|
||||
}
|
||||
isScrollLocked = true
|
||||
|
||||
scrollBouncable = scrollView.bounces
|
||||
scrollIndictorVisible = scrollView.showsVerticalScrollIndicator
|
||||
@@ -889,7 +881,9 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate {
|
||||
}
|
||||
|
||||
private func unlockScrollView() {
|
||||
guard let scrollView = scrollView, scrollView.isLocked else { return }
|
||||
guard let scrollView = scrollView, isScrollLocked else { return }
|
||||
|
||||
isScrollLocked = false
|
||||
|
||||
scrollView.isDirectionalLockEnabled = false
|
||||
scrollView.bounces = scrollBouncable
|
||||
|
||||
@@ -6,4 +6,26 @@
|
||||
import UIKit
|
||||
|
||||
/// A view that presents a backdrop interface behind a floating panel.
|
||||
public class FloatingPanelBackdropView: UIView { }
|
||||
public class FloatingPanelBackdropView: UIView {
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
setUp()
|
||||
}
|
||||
|
||||
required public init?(coder aDecoder: NSCoder) {
|
||||
super.init(coder: aDecoder)
|
||||
setUp()
|
||||
}
|
||||
|
||||
private func setUp() {
|
||||
layer.backgroundColor = UIColor.black.cgColor
|
||||
}
|
||||
|
||||
@objc dynamic public override var backgroundColor: UIColor? {
|
||||
get {
|
||||
guard let color = layer.backgroundColor else { return nil }
|
||||
return UIColor(cgColor: color)
|
||||
}
|
||||
set { layer.backgroundColor = newValue?.cgColor }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,10 +68,6 @@ public enum FloatingPanelPosition: Int {
|
||||
case half
|
||||
case tip
|
||||
case hidden
|
||||
|
||||
static var allCases: [FloatingPanelPosition] {
|
||||
return [.full, .half, .tip, .hidden]
|
||||
}
|
||||
}
|
||||
|
||||
///
|
||||
@@ -149,7 +145,7 @@ open class FloatingPanelController: UIViewController, UIScrollViewDelegate, UIGe
|
||||
}
|
||||
private var _contentViewController: UIViewController?
|
||||
|
||||
private(set) var floatingPanel: FloatingPanel!
|
||||
private var floatingPanel: FloatingPanel!
|
||||
private var preSafeAreaInsets: UIEdgeInsets = .zero // Capture the latest one
|
||||
private var safeAreaInsetsObservation: NSKeyValueObservation?
|
||||
private let modalTransition = FloatingPanelModalTransition()
|
||||
@@ -200,7 +196,7 @@ open class FloatingPanelController: UIViewController, UIScrollViewDelegate, UIGe
|
||||
self.view = view as UIView
|
||||
}
|
||||
|
||||
open override func viewDidLayoutSubviews() {
|
||||
open override func viewDidLayoutSubviews() {
|
||||
super.viewDidLayoutSubviews()
|
||||
if #available(iOS 11.0, *) {}
|
||||
else {
|
||||
@@ -211,7 +207,7 @@ open class FloatingPanelController: UIViewController, UIScrollViewDelegate, UIGe
|
||||
}
|
||||
}
|
||||
|
||||
open override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
|
||||
open override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
|
||||
super.viewWillTransition(to: size, with: coordinator)
|
||||
|
||||
if view.translatesAutoresizingMaskIntoConstraints {
|
||||
@@ -220,9 +216,14 @@ open class FloatingPanelController: UIViewController, UIScrollViewDelegate, UIGe
|
||||
}
|
||||
}
|
||||
|
||||
open override func willTransition(to newCollection: UITraitCollection, with coordinator: UIViewControllerTransitionCoordinator) {
|
||||
open override func willTransition(to newCollection: UITraitCollection, with coordinator: UIViewControllerTransitionCoordinator) {
|
||||
super.willTransition(to: newCollection, with: coordinator)
|
||||
self.prepare(for: newCollection)
|
||||
|
||||
// Change layout for a new trait collection
|
||||
reloadLayout(for: newCollection)
|
||||
setUpLayout()
|
||||
|
||||
floatingPanel.behavior = fetchBehavior(for: newCollection)
|
||||
}
|
||||
|
||||
open override func viewWillDisappear(_ animated: Bool) {
|
||||
@@ -230,15 +231,6 @@ open class FloatingPanelController: UIViewController, UIScrollViewDelegate, UIGe
|
||||
safeAreaInsetsObservation = nil
|
||||
}
|
||||
|
||||
// MARK:- Internals
|
||||
func prepare(for newCollection: UITraitCollection) {
|
||||
guard newCollection.shouldUpdateLayout(from: traitCollection) else { return }
|
||||
// Change a layout & behavior for a new trait collection
|
||||
reloadLayout(for: newCollection)
|
||||
activateLayout()
|
||||
floatingPanel.behavior = fetchBehavior(for: newCollection)
|
||||
}
|
||||
|
||||
// MARK:- Privates
|
||||
|
||||
private func fetchLayout(for traitCollection: UITraitCollection) -> FloatingPanelLayout {
|
||||
@@ -265,7 +257,7 @@ open class FloatingPanelController: UIViewController, UIScrollViewDelegate, UIGe
|
||||
// Prevent an infinite loop on iOS 10: setUpLayout() -> viewDidLayoutSubviews() -> setUpLayout()
|
||||
preSafeAreaInsets = safeAreaInsets
|
||||
|
||||
activateLayout()
|
||||
setUpLayout()
|
||||
|
||||
switch contentInsetAdjustmentBehavior {
|
||||
case .always:
|
||||
@@ -290,7 +282,7 @@ open class FloatingPanelController: UIViewController, UIScrollViewDelegate, UIGe
|
||||
}
|
||||
}
|
||||
|
||||
private func activateLayout() {
|
||||
private func setUpLayout() {
|
||||
// preserve the current content offset
|
||||
let contentOffset = scrollView?.contentOffset
|
||||
|
||||
@@ -306,7 +298,7 @@ open class FloatingPanelController: UIViewController, UIScrollViewDelegate, UIGe
|
||||
public func show(animated: Bool = false, completion: (() -> Void)? = nil) {
|
||||
// Must apply the current layout here
|
||||
reloadLayout(for: traitCollection)
|
||||
activateLayout()
|
||||
setUpLayout()
|
||||
|
||||
if #available(iOS 11.0, *) {
|
||||
// Must track the safeAreaInsets of `self.view` to update the layout.
|
||||
@@ -521,7 +513,7 @@ open class FloatingPanelController: UIViewController, UIScrollViewDelegate, UIGe
|
||||
/// animation block.
|
||||
public func updateLayout() {
|
||||
reloadLayout(for: traitCollection)
|
||||
activateLayout()
|
||||
setUpLayout()
|
||||
}
|
||||
|
||||
/// Returns the y-coordinate of the point at the origin of the surface view.
|
||||
|
||||
@@ -181,11 +181,13 @@ class FloatingPanelLayoutAdapter {
|
||||
}
|
||||
|
||||
var topMostState: FloatingPanelPosition {
|
||||
return supportedPositions.sorted(by: { $0.rawValue < $1.rawValue }).first ?? .hidden
|
||||
}
|
||||
|
||||
var bottomMostState: FloatingPanelPosition {
|
||||
return supportedPositions.sorted(by: { $0.rawValue < $1.rawValue }).last ?? .hidden
|
||||
if supportedPositions.contains(.full) {
|
||||
return .full
|
||||
}
|
||||
if supportedPositions.contains(.half) {
|
||||
return .half
|
||||
}
|
||||
return .tip
|
||||
}
|
||||
|
||||
var topY: CGFloat {
|
||||
|
||||
@@ -15,7 +15,7 @@ public class FloatingPanelSurfaceView: UIView {
|
||||
public let grabberHandle: GrabberHandleView = GrabberHandleView()
|
||||
|
||||
/// Offset of the grabber handle from the top
|
||||
public var grabberTopPadding: CGFloat = 6.0 { didSet {
|
||||
@objc dynamic public var grabberTopPadding: CGFloat = 6.0 { didSet {
|
||||
setNeedsUpdateConstraints()
|
||||
} }
|
||||
|
||||
@@ -25,10 +25,10 @@ public class FloatingPanelSurfaceView: UIView {
|
||||
}
|
||||
|
||||
/// Grabber view width and height
|
||||
public var grabberHandleWidth: CGFloat = 36.0 { didSet {
|
||||
@objc dynamic public var grabberHandleWidth: CGFloat = 36.0 { didSet {
|
||||
setNeedsUpdateConstraints()
|
||||
} }
|
||||
public var grabberHandleHeight: CGFloat = 5.0 { didSet {
|
||||
@objc dynamic public var grabberHandleHeight: CGFloat = 5.0 { didSet {
|
||||
setNeedsUpdateConstraints()
|
||||
} }
|
||||
|
||||
@@ -48,7 +48,7 @@ public class FloatingPanelSurfaceView: UIView {
|
||||
private var color: UIColor? = .white { didSet { setNeedsLayout() } }
|
||||
var bottomOverflow: CGFloat = 0.0 // Must not call setNeedsLayout()
|
||||
|
||||
public override var backgroundColor: UIColor? {
|
||||
@objc dynamic public override var backgroundColor: UIColor? {
|
||||
get { return color }
|
||||
set { color = newValue }
|
||||
}
|
||||
@@ -57,34 +57,34 @@ public class FloatingPanelSurfaceView: UIView {
|
||||
///
|
||||
/// `self.contentView` is masked with the top rounded corners automatically on iOS 11 and later.
|
||||
/// On iOS 10, they are not automatically masked because of a UIVisualEffectView issue. See https://forums.developer.apple.com/thread/50854
|
||||
public var cornerRadius: CGFloat {
|
||||
@objc dynamic public var cornerRadius: CGFloat {
|
||||
set { containerView.layer.cornerRadius = newValue; setNeedsLayout() }
|
||||
get { return containerView.layer.cornerRadius }
|
||||
}
|
||||
|
||||
/// A Boolean indicating whether the surface shadow is displayed.
|
||||
public var shadowHidden: Bool = false { didSet { setNeedsLayout() } }
|
||||
@objc dynamic public var shadowHidden: Bool = false { didSet { setNeedsLayout() } }
|
||||
|
||||
/// The color of the surface shadow.
|
||||
public var shadowColor: UIColor = .black { didSet { setNeedsLayout() } }
|
||||
@objc dynamic public var shadowColor: UIColor = .black { didSet { setNeedsLayout() } }
|
||||
|
||||
/// The offset (in points) of the surface shadow.
|
||||
public var shadowOffset: CGSize = CGSize(width: 0.0, height: 1.0) { didSet { setNeedsLayout() } }
|
||||
@objc dynamic public var shadowOffset: CGSize = CGSize(width: 0.0, height: 1.0) { didSet { setNeedsLayout() } }
|
||||
|
||||
/// The opacity of the surface shadow.
|
||||
public var shadowOpacity: Float = 0.2 { didSet { setNeedsLayout() } }
|
||||
@objc dynamic public var shadowOpacity: Float = 0.2 { didSet { setNeedsLayout() } }
|
||||
|
||||
/// The blur radius (in points) used to render the surface shadow.
|
||||
public var shadowRadius: CGFloat = 3 { didSet { setNeedsLayout() } }
|
||||
@objc dynamic public var shadowRadius: CGFloat = 3 { didSet { setNeedsLayout() } }
|
||||
|
||||
/// The width of the surface border.
|
||||
public var borderColor: UIColor? { didSet { setNeedsLayout() } }
|
||||
@objc dynamic public var borderColor: UIColor? { didSet { setNeedsLayout() } }
|
||||
|
||||
/// The color of the surface border.
|
||||
public var borderWidth: CGFloat = 0.0 { didSet { setNeedsLayout() } }
|
||||
@objc dynamic public var borderWidth: CGFloat = 0.0 { didSet { setNeedsLayout() } }
|
||||
|
||||
/// Offset of the container view from the top
|
||||
public var containerTopInset: CGFloat = 0.0 { didSet {
|
||||
@objc dynamic public var containerTopInset: CGFloat = 0.0 { didSet {
|
||||
setNeedsUpdateConstraints()
|
||||
} }
|
||||
|
||||
@@ -149,6 +149,7 @@ public class FloatingPanelSurfaceView: UIView {
|
||||
}
|
||||
|
||||
public override func updateConstraints() {
|
||||
super.updateConstraints()
|
||||
containerViewTopInsetConstraint.constant = containerTopInset
|
||||
containerViewHeightConstraint.constant = bottomOverflow
|
||||
|
||||
@@ -160,8 +161,6 @@ public class FloatingPanelSurfaceView: UIView {
|
||||
grabberHandleTopConstraint.constant = grabberTopPadding
|
||||
grabberHandleWidthConstraint.constant = grabberHandleWidth
|
||||
grabberHandleHeightConstraint.constant = grabberHandleHeight
|
||||
|
||||
super.updateConstraints()
|
||||
}
|
||||
|
||||
public override func layoutSubviews() {
|
||||
|
||||
@@ -7,7 +7,9 @@ import UIKit
|
||||
|
||||
public class GrabberHandleView: UIView {
|
||||
|
||||
public var barColor = UIColor(displayP3Red: 0.76, green: 0.77, blue: 0.76, alpha: 1.0) { didSet { backgroundColor = barColor } }
|
||||
@objc dynamic public var barColor = UIColor(displayP3Red: 0.76, green: 0.77, blue: 0.76, alpha: 1.0) {
|
||||
didSet { backgroundColor = barColor }
|
||||
}
|
||||
|
||||
required public init?(coder aDecoder: NSCoder) {
|
||||
super.init(coder: aDecoder)
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>FMWK</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.6.2</string>
|
||||
<string>1.5.1</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||
</dict>
|
||||
|
||||
@@ -107,9 +107,6 @@ extension UIScrollView {
|
||||
var contentOffsetZero: CGPoint {
|
||||
return CGPoint(x: 0.0, y: 0.0 - contentInset.top)
|
||||
}
|
||||
var isLocked: Bool {
|
||||
return !showsVerticalScrollIndicator && !bounces && isDirectionalLockEnabled
|
||||
}
|
||||
}
|
||||
|
||||
extension UISpringTimingParameters {
|
||||
@@ -127,12 +124,3 @@ extension CGPoint {
|
||||
y: CGFloat.nan)
|
||||
}
|
||||
}
|
||||
|
||||
extension UITraitCollection {
|
||||
func shouldUpdateLayout(from previous: UITraitCollection) -> Bool {
|
||||
return previous.horizontalSizeClass != horizontalSizeClass
|
||||
|| previous.verticalSizeClass != verticalSizeClass
|
||||
|| previous.preferredContentSizeCategory != preferredContentSizeCategory
|
||||
|| previous.layoutDirection != layoutDirection
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,33 +28,24 @@ class FloatingPanelControllerTests: XCTestCase {
|
||||
|
||||
func test_addPanel() {
|
||||
guard let rootVC = UIApplication.shared.keyWindow?.rootViewController else { fatalError() }
|
||||
|
||||
let fpc = FloatingPanelController()
|
||||
fpc.addPanel(toParent: rootVC)
|
||||
XCTAssert(fpc.surfaceView.frame.minY == (fpc.view.bounds.height - fpc.layoutInsets.bottom) - fpc.layout.insetFor(position: .half)!)
|
||||
fpc.move(to: .tip, animated: false)
|
||||
XCTAssert(fpc.surfaceView.frame.minY == (fpc.view.bounds.height - fpc.layoutInsets.bottom) - fpc.layout.insetFor(position: .tip)!)
|
||||
}
|
||||
|
||||
@available(iOS 12.0, *)
|
||||
func test_updateLayout_willTransition() {
|
||||
class MyDelegate: FloatingPanelControllerDelegate {
|
||||
func floatingPanel(_ vc: FloatingPanelController, layoutFor newCollection: UITraitCollection) -> FloatingPanelLayout? {
|
||||
if newCollection.userInterfaceStyle == .dark {
|
||||
XCTFail()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
let myDelegate = MyDelegate()
|
||||
let fpc = FloatingPanelController(delegate: myDelegate)
|
||||
let traitCollection = UITraitCollection(traitsFrom: [fpc.traitCollection,
|
||||
UITraitCollection(userInterfaceStyle: .dark)])
|
||||
XCTAssertEqual(traitCollection.userInterfaceStyle, .dark)
|
||||
fpc.prepare(for: traitCollection)
|
||||
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)
|
||||
XCTAssert(fpc.surfaceView.frame.minY == (fpc.view.bounds.height - fpc.layoutInsets.bottom) - fpc.layout.insetFor(position: .tip)!)
|
||||
}
|
||||
}
|
||||
|
||||
private class MyZombieViewController: UIViewController, FloatingPanelLayout, FloatingPanelBehavior, FloatingPanelControllerDelegate {
|
||||
func waitRunLoop(secs: TimeInterval = 0) {
|
||||
RunLoop.main.run(until: Date(timeIntervalSinceNow: secs))
|
||||
}
|
||||
|
||||
class MyZombieViewController: UIViewController, FloatingPanelLayout, FloatingPanelBehavior, FloatingPanelControllerDelegate {
|
||||
var fpc: FloatingPanelController?
|
||||
override func viewDidLoad() {
|
||||
fpc = FloatingPanelController(delegate: self)
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
//
|
||||
// Created by Shin Yamamoto on 2019/06/27.
|
||||
// Copyright © 2019 scenee. All rights reserved.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
@testable import FloatingPanel
|
||||
|
||||
class FloatingPanelLayoutTests: XCTestCase {
|
||||
override func setUp() {}
|
||||
override func tearDown() {}
|
||||
|
||||
func test_layoutAdapter_topAndBottomMostState() {
|
||||
let fpc = FloatingPanelController(delegate: nil)
|
||||
fpc.loadViewIfNeeded()
|
||||
fpc.view.frame = CGRect(x: 0, y: 0, width: 375, height: 667)
|
||||
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, .half) // Will fixed on fix-hidden-position branch
|
||||
|
||||
delegate.layout = FloatingPanelLayout2Positions()
|
||||
fpc.delegate = delegate
|
||||
XCTAssertEqual(fpc.floatingPanel.layoutAdapter.topMostState, .half)
|
||||
XCTAssertEqual(fpc.floatingPanel.layoutAdapter.bottomMostState, .tip)
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,6 @@
|
||||
//
|
||||
|
||||
import XCTest
|
||||
@testable import FloatingPanel
|
||||
|
||||
class FloatingPanelTests: XCTestCase {
|
||||
|
||||
@@ -12,67 +11,4 @@ class FloatingPanelTests: XCTestCase {
|
||||
|
||||
override func tearDown() {}
|
||||
|
||||
func test_scrolllock() {
|
||||
let fpc = FloatingPanelController()
|
||||
fpc.loadViewIfNeeded()
|
||||
fpc.view.frame = CGRect(x: 0, y: 0, width: 375, height: 667)
|
||||
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.show(animated: false, completion: nil) // half
|
||||
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)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private protocol FloatingPanelTestLayout: FloatingPanelLayout {}
|
||||
private extension FloatingPanelTestLayout {
|
||||
func insetFor(position: FloatingPanelPosition) -> CGFloat? {
|
||||
switch position {
|
||||
case .full: return 18.0
|
||||
case .half: return 262.0
|
||||
case .tip: return 69.0
|
||||
default: return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+2
-21
@@ -6,7 +6,8 @@
|
||||
import XCTest
|
||||
@testable import FloatingPanel
|
||||
|
||||
class FloatingPanelSurfaceViewTests: XCTestCase {
|
||||
class FloatingPanelViewTests: XCTestCase {
|
||||
|
||||
override func setUp() {}
|
||||
|
||||
override func tearDown() {}
|
||||
@@ -23,26 +24,6 @@ class FloatingPanelSurfaceViewTests: 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)
|
||||
@@ -1,18 +0,0 @@
|
||||
//
|
||||
// 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))
|
||||
}
|
||||
|
||||
class FloatingPanelTestDelegate: FloatingPanelControllerDelegate {
|
||||
var layout: FloatingPanelLayout?
|
||||
func floatingPanel(_ vc: FloatingPanelController, layoutFor newCollection: UITraitCollection) -> FloatingPanelLayout? {
|
||||
return layout
|
||||
}
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
// swift-tools-version:5.0
|
||||
// The swift-tools-version declares the minimum version of Swift required to build this package.
|
||||
|
||||
import PackageDescription
|
||||
|
||||
let package = Package(
|
||||
name: "FloatingPanel",
|
||||
platforms: [
|
||||
.iOS(.v10)
|
||||
],
|
||||
products: [
|
||||
// Products define the executables and libraries produced by a package, and make them visible to other packages.
|
||||
.library(
|
||||
name: "FloatingPanel",
|
||||
targets: ["FloatingPanel"]),
|
||||
],
|
||||
dependencies: [
|
||||
// Dependencies declare other packages that this package depends on.
|
||||
// .package(url: /* package url */, from: "1.0.0"),
|
||||
],
|
||||
targets: [
|
||||
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
|
||||
// Targets can depend on other targets in this package, and on products in packages which this package depends on.
|
||||
.target(name: "FloatingPanel", path: "Framework/Sources"),
|
||||
],
|
||||
swiftLanguageVersions: [.version("5")]
|
||||
)
|
||||
Reference in New Issue
Block a user