Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4edaad2cf4 | |||
| 92fc0621e2 | |||
| e9f4392c48 | |||
| 4df40becaf | |||
| ba11e7c7d7 | |||
| ae671f22c6 | |||
| f22f58212b | |||
| 54ff1c360d | |||
| 772d6c3ef3 | |||
| a94c3b3c26 | |||
| d0ffc4ceb1 | |||
| 597ce487aa | |||
| 87eb8d94fd | |||
| 4944fc516a | |||
| 7537384339 |
@@ -1,7 +1,7 @@
|
||||
Pod::Spec.new do |s|
|
||||
|
||||
s.name = "FloatingPanel"
|
||||
s.version = "1.7.0"
|
||||
s.version = "1.7.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.
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
54CDC5D3215B6D5A007D205C /* FloatingPanelSurfaceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CDC5D2215B6D5A007D205C /* FloatingPanelSurfaceView.swift */; };
|
||||
54CDC5D5215B6D8D007D205C /* FloatingPanelBackdropView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CDC5D4215B6D8D007D205C /* FloatingPanelBackdropView.swift */; };
|
||||
54CFBFC3215CD045006B5735 /* FloatingPanelLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CFBFC2215CD045006B5735 /* FloatingPanelLayout.swift */; };
|
||||
54CFBFC5215CD09C006B5735 /* FloatingPanel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CFBFC4215CD09C006B5735 /* FloatingPanel.swift */; };
|
||||
54CFBFC5215CD09C006B5735 /* FloatingPanelCore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CFBFC4215CD09C006B5735 /* FloatingPanelCore.swift */; };
|
||||
54E740CD218AFD67005C1A34 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54E740CC218AFD67005C1A34 /* AppDelegate.swift */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
@@ -70,7 +70,7 @@
|
||||
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>"; };
|
||||
54CFBFC2215CD045006B5735 /* FloatingPanelLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FloatingPanelLayout.swift; sourceTree = "<group>"; };
|
||||
54CFBFC4215CD09C006B5735 /* FloatingPanel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FloatingPanel.swift; sourceTree = "<group>"; };
|
||||
54CFBFC4215CD09C006B5735 /* FloatingPanelCore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FloatingPanelCore.swift; sourceTree = "<group>"; };
|
||||
54E740CA218AFD67005C1A34 /* FloatingPanelTesting.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = FloatingPanelTesting.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
54E740CC218AFD67005C1A34 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||
54E740D8218AFD6A005C1A34 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
@@ -129,7 +129,7 @@
|
||||
545DB9C42151169500CA77B8 /* FloatingPanel.h */,
|
||||
545DB9DF21511AC100CA77B8 /* FloatingPanelController.swift */,
|
||||
54352E9521A51A2500CBCA08 /* FloatingPanelTransitioning.swift */,
|
||||
54CFBFC4215CD09C006B5735 /* FloatingPanel.swift */,
|
||||
54CFBFC4215CD09C006B5735 /* FloatingPanelCore.swift */,
|
||||
54CFBFC2215CD045006B5735 /* FloatingPanelLayout.swift */,
|
||||
5450EEE321646DF500135936 /* FloatingPanelBehavior.swift */,
|
||||
54352E9721A521CA00CBCA08 /* FloatingPanelView.swift */,
|
||||
@@ -311,7 +311,7 @@
|
||||
54CFBFC3215CD045006B5735 /* FloatingPanelLayout.swift in Sources */,
|
||||
54CDC5D5215B6D8D007D205C /* FloatingPanelBackdropView.swift in Sources */,
|
||||
54352E9821A521CA00CBCA08 /* FloatingPanelView.swift in Sources */,
|
||||
54CFBFC5215CD09C006B5735 /* FloatingPanel.swift in Sources */,
|
||||
54CFBFC5215CD09C006B5735 /* FloatingPanelCore.swift in Sources */,
|
||||
54ABD7AF216CCFF7002E6C13 /* Logger.swift in Sources */,
|
||||
545DB9E021511AC100CA77B8 /* FloatingPanelController.swift in Sources */,
|
||||
5450EEE421646DF500135936 /* FloatingPanelBehavior.swift in Sources */,
|
||||
@@ -484,6 +484,7 @@
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
APPLICATION_EXTENSION_API_ONLY = YES;
|
||||
BUILD_LIBRARY_FOR_DISTRIBUTION = YES;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_IDENTITY = "";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
@@ -514,6 +515,7 @@
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
APPLICATION_EXTENSION_API_ONLY = YES;
|
||||
BUILD_LIBRARY_FOR_DISTRIBUTION = YES;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_IDENTITY = "";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
|
||||
@@ -197,7 +197,7 @@ open class FloatingPanelController: UIViewController {
|
||||
|
||||
private var _contentViewController: UIViewController?
|
||||
|
||||
private(set) var floatingPanel: FloatingPanel!
|
||||
private(set) var floatingPanel: FloatingPanelCore!
|
||||
private var preSafeAreaInsets: UIEdgeInsets = .zero // Capture the latest one
|
||||
private var safeAreaInsetsObservation: NSKeyValueObservation?
|
||||
private let modalTransition = FloatingPanelModalTransition()
|
||||
@@ -220,7 +220,7 @@ open class FloatingPanelController: UIViewController {
|
||||
modalPresentationStyle = .custom
|
||||
transitioningDelegate = modalTransition
|
||||
|
||||
floatingPanel = FloatingPanel(self,
|
||||
floatingPanel = FloatingPanelCore(self,
|
||||
layout: fetchLayout(for: self.traitCollection),
|
||||
behavior: fetchBehavior(for: self.traitCollection))
|
||||
}
|
||||
@@ -229,7 +229,7 @@ open class FloatingPanelController: UIViewController {
|
||||
floatingPanel.layoutAdapter.layout = fetchLayout(for: traitCollection)
|
||||
floatingPanel.behavior = fetchBehavior(for: self.traitCollection)
|
||||
}
|
||||
|
||||
|
||||
// MARK:- Overrides
|
||||
|
||||
/// Creates the view that the controller manages.
|
||||
@@ -425,9 +425,9 @@ open class FloatingPanelController: UIViewController {
|
||||
show(animated: animated) { [weak self] in
|
||||
guard let `self` = self else { return }
|
||||
#if swift(>=4.2)
|
||||
self.didMove(toParent: self)
|
||||
self.didMove(toParent: parent)
|
||||
#else
|
||||
self.didMove(toParentViewController: self)
|
||||
self.didMove(toParentViewController: parent)
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import UIKit.UIGestureRecognizerSubclass // For Xcode 9.4.1
|
||||
///
|
||||
/// FloatingPanel presentation model
|
||||
///
|
||||
class FloatingPanel: NSObject, UIGestureRecognizerDelegate {
|
||||
class FloatingPanelCore: NSObject, UIGestureRecognizerDelegate {
|
||||
// MUST be a weak reference to prevent UI freeze on the presentation modally
|
||||
weak var viewcontroller: FloatingPanelController?
|
||||
|
||||
@@ -81,7 +81,6 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate {
|
||||
super.init()
|
||||
|
||||
panGestureRecognizer.floatingPanel = self
|
||||
|
||||
surfaceView.addGestureRecognizer(panGestureRecognizer)
|
||||
panGestureRecognizer.addTarget(self, action: #selector(handle(panGesture:)))
|
||||
panGestureRecognizer.delegate = self
|
||||
@@ -203,7 +202,10 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate {
|
||||
default:
|
||||
// Should recognize tap/long press gestures in parallel when the surface view is at an anchor position.
|
||||
let surfaceFrame = surfaceView.layer.presentation()?.frame ?? surfaceView.frame
|
||||
return surfaceFrame.minY == layoutAdapter.positionY(for: state)
|
||||
let surfaceY = surfaceFrame.minY
|
||||
let adapterY = layoutAdapter.positionY(for: state)
|
||||
|
||||
return abs(surfaceY - adapterY) < (1.0 / surfaceView.traitCollection.displayScale)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -230,7 +232,13 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate {
|
||||
// the panel's pan gesture if not returns false
|
||||
if let scrollGestureRecognizers = scrollView.gestureRecognizers,
|
||||
scrollGestureRecognizers.contains(otherGestureRecognizer) {
|
||||
return false
|
||||
switch otherGestureRecognizer {
|
||||
case scrollView.panGestureRecognizer:
|
||||
let offset = scrollView.contentOffset.y - scrollView.contentOffsetZero.y
|
||||
return allowScrollPanGesture(at: CGPoint(x: 0.0, y: offset))
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -279,7 +287,9 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate {
|
||||
|
||||
let location = panGesture.location(in: surfaceView)
|
||||
|
||||
let belowTop = surfaceView.presentationFrame.minY > layoutAdapter.topY
|
||||
let surfaceMinY = surfaceView.presentationFrame.minY
|
||||
let adapterTopY = layoutAdapter.topY
|
||||
let belowTop = surfaceMinY > (adapterTopY + (1.0 / surfaceView.traitCollection.displayScale))
|
||||
let offset = scrollView.contentOffset.y - scrollView.contentOffsetZero.y
|
||||
|
||||
log.debug("scroll gesture(\(state):\(panGesture.state)) --",
|
||||
@@ -330,11 +340,11 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate {
|
||||
} else {
|
||||
if state == layoutAdapter.topMostState {
|
||||
// Hide a scroll indicator just before starting an interaction by swiping a panel down.
|
||||
if offset < 0, velocity.y > 0 {
|
||||
if velocity.y > 0, !allowScrollPanGesture(at: CGPoint(x: 0.0, y: offset)) {
|
||||
lockScrollView()
|
||||
}
|
||||
// Show a scroll indicator when an animation is interrupted at the top and content is scrolled up
|
||||
if offset > 0, velocity.y < 0 {
|
||||
if velocity.y < 0, allowScrollPanGesture(at: CGPoint(x: 0.0, y: offset)) {
|
||||
unlockScrollView()
|
||||
}
|
||||
|
||||
@@ -565,7 +575,7 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate {
|
||||
return
|
||||
}
|
||||
|
||||
stopScrollDeceleration = (surfaceView.frame.minY > layoutAdapter.topY) // Projecting the dragging to the scroll dragging or not
|
||||
stopScrollDeceleration = surfaceView.frame.minY > (layoutAdapter.topY + (1.0 / surfaceView.traitCollection.displayScale)) // Projecting the dragging to the scroll dragging or not
|
||||
if stopScrollDeceleration {
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
guard let `self` = self else { return }
|
||||
@@ -870,10 +880,17 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate {
|
||||
// Must use setContentOffset(_:animated) to force-stop deceleration
|
||||
scrollView?.setContentOffset(contentOffset, animated: false)
|
||||
}
|
||||
|
||||
private func allowScrollPanGesture(at contentOffset: CGPoint) -> Bool {
|
||||
if state == layoutAdapter.topMostState {
|
||||
return contentOffset.y <= -30.0 || contentOffset.y > 0
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
class FloatingPanelPanGestureRecognizer: UIPanGestureRecognizer {
|
||||
fileprivate weak var floatingPanel: FloatingPanel?
|
||||
fileprivate weak var floatingPanel: FloatingPanelCore?
|
||||
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
|
||||
super.touchesBegan(touches, with: event)
|
||||
if floatingPanel?.animator != nil {
|
||||
@@ -885,7 +902,7 @@ class FloatingPanelPanGestureRecognizer: UIPanGestureRecognizer {
|
||||
return super.delegate
|
||||
}
|
||||
set {
|
||||
guard newValue is FloatingPanel else {
|
||||
guard newValue is FloatingPanelCore else {
|
||||
let exception = NSException(name: .invalidArgumentException,
|
||||
reason: "FloatingPanelController's built-in pan gesture recognizer must have its controller as its delegate.",
|
||||
userInfo: nil)
|
||||
@@ -94,6 +94,7 @@ public protocol FloatingPanelLayout: class {
|
||||
func backdropAlphaFor(position: FloatingPanelPosition) -> CGFloat
|
||||
|
||||
var positionReference: FloatingPanelLayoutReference { get }
|
||||
|
||||
}
|
||||
|
||||
public extension FloatingPanelLayout {
|
||||
|
||||
@@ -34,7 +34,7 @@ public class FloatingPanelSurfaceView: UIView {
|
||||
|
||||
/// A root view of a content view controller
|
||||
public weak var contentView: UIView!
|
||||
|
||||
|
||||
/// The content insets specifying the insets around the content view.
|
||||
public var contentInsets: UIEdgeInsets = .zero {
|
||||
didSet {
|
||||
@@ -101,7 +101,7 @@ public class FloatingPanelSurfaceView: UIView {
|
||||
private lazy var containerViewHeightConstraint = containerView.heightAnchor.constraint(equalTo: heightAnchor, multiplier: 1.0)
|
||||
private lazy var containerViewLeftConstraint = containerView.leftAnchor.constraint(equalTo: leftAnchor, constant: 0.0)
|
||||
private lazy var containerViewRightConstraint = containerView.rightAnchor.constraint(equalTo: rightAnchor, constant: 0.0)
|
||||
|
||||
|
||||
/// The content view top constraint
|
||||
private var contentViewTopConstraint: NSLayoutConstraint?
|
||||
/// The content view left constraint
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>FMWK</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.7.0</string>
|
||||
<string>1.7.1</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||
</dict>
|
||||
|
||||
@@ -238,7 +238,7 @@ class FloatingPanelLayoutTests: XCTestCase {
|
||||
}
|
||||
|
||||
private typealias LayoutSegmentTestParameter = (UInt, pos: CGFloat, forwardY: Bool, lower: FloatingPanelPosition?, upper: FloatingPanelPosition?)
|
||||
private func assertLayoutSegment(_ floatingPanel: FloatingPanel, with params: [LayoutSegmentTestParameter]) {
|
||||
private func assertLayoutSegment(_ floatingPanel: FloatingPanelCore, 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)
|
||||
|
||||
@@ -523,7 +523,7 @@ private class FloatingPanelLayout3Positions: FloatingPanelTestLayout {
|
||||
}
|
||||
|
||||
private typealias TestParameter = (UInt, CGFloat,CGPoint, FloatingPanelPosition)
|
||||
private func assertTargetPosition(_ floatingPanel: FloatingPanel, with params: [TestParameter]) {
|
||||
private func assertTargetPosition(_ floatingPanel: FloatingPanelCore, 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)
|
||||
|
||||
@@ -181,41 +181,60 @@ FloatingPanelController.view (FloatingPanelPassThroughView)
|
||||
|
||||
### Show/Hide a floating panel in a view with your view hierarchy
|
||||
|
||||
If you need more control over showing and hiding the floating panel, you can forgo the `addPanel` and `removePanelFromParent` methods. These methods are a convenience wrapper for **FloatingPanel**'s `show` and `hide` methods along with some required setup.
|
||||
|
||||
There are two ways to work with the `FloatingPanelController`:
|
||||
1. Add it to the hierarchy once and then call `show` and `hide` methods to make it appear/disappear.
|
||||
2. Add it to the hierarchy when needed and remove afterwards.
|
||||
|
||||
The following example shows how to add the controller to your `UIViewController` and how to remove it. Make sure that you never add the same `FloatingPanelController` to the hierarchy before removing it.
|
||||
|
||||
**NOTE**: `self.` prefix is not required, nor recommended. It's used here to make it clearer where do the functions used come from. `self` is an instance of a custom UIViewController in your code.
|
||||
|
||||
```swift
|
||||
// Add the controller and the managed views to a view controller.
|
||||
// From the second time, just call `show(animated:completion)`.
|
||||
view.addSubview(fpc.view)
|
||||
// Add the floating panel view to the controller's view on top of other views.
|
||||
self.view.addSubview(fpc.view)
|
||||
|
||||
// REQUIRED. It makes the floating panel view have the same size as the controller's view.
|
||||
fpc.view.frame = self.view.bounds
|
||||
|
||||
fpc.view.frame = view.bounds // MUST
|
||||
// In addition, Auto Layout constraints are highly recommended.
|
||||
// Because it makes the layout more robust on trait collection change.
|
||||
//
|
||||
// fpc.view.translatesAutoresizingMaskIntoConstraints = false
|
||||
// NSLayoutConstraint.activate([...])
|
||||
//
|
||||
// Constraint the fpc.view to all four edges of your controller's view.
|
||||
// It makes the layout more robust on trait collection change.
|
||||
fpc.view.translatesAutoresizingMaskIntoConstraints = false
|
||||
NSLayoutConstraint.activate([
|
||||
fpc.view.topAnchor.constraint(equalTo: self.view.topAnchor, constant: 0.0),
|
||||
fpc.view.leftAnchor.constraint(equalTo: self.view.leftAnchor, constant: 0.0),
|
||||
fpc.view.rightAnchor.constraint(equalTo: self.view.rightAnchor, constant: 0.0),
|
||||
fpc.view.bottomAnchor.constraint(equalTo: self.view.bottomAnchor, constant: 0.0),
|
||||
])
|
||||
|
||||
parent.addChild(fpc)
|
||||
// Add the floating panel controller to the controller hierarchy.
|
||||
self.addChild(fpc)
|
||||
|
||||
// Show a floating panel to the initial position defined in your `FloatingPanelLayout` object.
|
||||
// Show the floating panel at the initial position defined in your `FloatingPanelLayout` object.
|
||||
fpc.show(animated: true) {
|
||||
|
||||
// Only for the first time
|
||||
self.didMove(toParent: self)
|
||||
}
|
||||
|
||||
...
|
||||
|
||||
// Hide it
|
||||
fpc.hide(animated: true) {
|
||||
|
||||
// Remove it if needed
|
||||
self.willMove(toParent: nil)
|
||||
self.view.removeFromSuperview()
|
||||
self.removeFromParent()
|
||||
// Inform the floating panel controller that the transition to the controller hierarchy has completed.
|
||||
fpc.didMove(toParent: self)
|
||||
}
|
||||
```
|
||||
|
||||
NOTE: `FloatingPanelController` wraps `show`/`hide` with `addPanel`/`removePanelFromParent` for easy-to-use. But `show`/`hide` are more convenience for your app.
|
||||
After you add the `FloatingPanelController` as seen above, you can call `fpc.show(animated: true) { }` to show the panel and `fpc.hide(animated: true) { }` to hide it.
|
||||
|
||||
To remove the `FloatingPanelController` from the hierarchy, follow the example below.
|
||||
|
||||
```swift
|
||||
// Inform the panel controller that it will be removed from the hierarchy.
|
||||
fpc.willMove(toParent: nil)
|
||||
|
||||
// Hide the floating panel.
|
||||
fpc.hide(animated: true) {
|
||||
// Remove the floating panel view from your controller's view.
|
||||
fpc.view.removeFromSuperview()
|
||||
// Remove the floating panel controller from the controller hierarchy.
|
||||
fpc.removeFromParent()
|
||||
}
|
||||
```
|
||||
|
||||
### Scale the content view when the surface position changes
|
||||
|
||||
@@ -227,6 +246,8 @@ fpc.contentMode = .fitToBounds
|
||||
|
||||
Otherwise, `FloatingPanelController` fixes the content by the height of the top most position.
|
||||
|
||||
✏️ In `.fitToBounds` mode, the surface height changes as following a user interaction so that you have a responsibility to configure Auto Layout constrains not to break the layout of a content view by the elastic surface height.
|
||||
|
||||
### Customize the layout with `FloatingPanelLayout` protocol
|
||||
|
||||
#### Change the initial position and height
|
||||
@@ -378,7 +399,7 @@ This allows full projectional panel behavior. For example, a user can swipe up a
|
||||
```swift
|
||||
class FloatingPanelBehavior: FloatingPanelBehavior {
|
||||
...
|
||||
func shouldProjectMomentum(_ fpc: FloatingPanelController, for proposedTargetPosition: FloatingPanelPosition) -> Bool
|
||||
func shouldProjectMomentum(_ fpc: FloatingPanelController, for proposedTargetPosition: FloatingPanelPosition) -> Bool {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user