Compare commits
89 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6b75523428 | |||
| 773434d4f6 | |||
| ad6dcd0314 | |||
| b9e29ad87d | |||
| 32b965ba87 | |||
| f1b315c9ea | |||
| 459fc75af3 | |||
| 9b0cd3511f | |||
| af9b988507 | |||
| 36f297c35b | |||
| ff959f71a7 | |||
| 0a4312ada6 | |||
| 5411cdc07a | |||
| a8c6fba3c1 | |||
| 11b115b47b | |||
| 22edf5ce46 | |||
| f43f7df7f3 | |||
| 3a2633d818 | |||
| 04a62bcf74 | |||
| 6c1320168c | |||
| 8657c91002 | |||
| bafe492009 | |||
| c6197ef6a3 | |||
| 1b3f16bcd5 | |||
| 28712fdeca | |||
| 0c30b68a9e | |||
| 30c4bee432 | |||
| ece9ced085 | |||
| f231105752 | |||
| 91dfc1e086 | |||
| b2c59c17aa | |||
| 10d1a920f0 | |||
| 4cb79a14fc | |||
| 402b9bd8dc | |||
| c39cc9d93b | |||
| aad56ab0a7 | |||
| fe18e493a9 | |||
| 5d14166508 | |||
| e1a745e3b5 | |||
| a0cac28ed0 | |||
| c205dc8672 | |||
| 5c0ed4cf7d | |||
| 780472a17f | |||
| 0264db3d54 | |||
| c117594669 | |||
| 5214bd8936 | |||
| a1195be08e | |||
| b69d366538 | |||
| c9de6f0dc3 | |||
| 21be693a9a | |||
| 129362fcd0 | |||
| 75e2fcc3ce | |||
| 4f56b57b0e | |||
| f9bbdf3427 | |||
| fcf200e169 | |||
| 7d668c8525 | |||
| ebd4a32bfc | |||
| 6aa739231d | |||
| 8877d32ced | |||
| 7f025ae845 | |||
| 1b2dae2135 | |||
| 6e4e9df616 | |||
| 49e868a505 | |||
| f1b70e0367 | |||
| 1d0e747578 | |||
| 2c72d07cab | |||
| 31c057f9f8 | |||
| d3033df9da | |||
| 459d82b1c6 | |||
| 85d7ca640e | |||
| c1b7f2f092 | |||
| b7a7e0d4ad | |||
| dc7f6d58f9 | |||
| a2a10bd0d3 | |||
| b54c8ee6ee | |||
| 08d275690a | |||
| 1c307f751e | |||
| 6f06a0f7fc | |||
| a095ace30e | |||
| a486f61f5f | |||
| 14e0abc240 | |||
| 32203c48bd | |||
| 9d6024f603 | |||
| a4dd4e48e7 | |||
| e6f7456a0f | |||
| fca79c9b0c | |||
| 4ad7f11e93 | |||
| 0412bdc996 | |||
| 31faeaada3 |
@@ -0,0 +1,27 @@
|
||||
> Please fill out this template appropriately when filing a bug report.
|
||||
>
|
||||
> Please remove this line and everything above it before submitting.
|
||||
|
||||
### Short description
|
||||
|
||||
### Expected behavior
|
||||
|
||||
### Actual behavior
|
||||
|
||||
### Steps to reproduce
|
||||
|
||||
**Code example that reproduces the issue**
|
||||
|
||||
### Environment
|
||||
|
||||
**Library version**
|
||||
|
||||
**Installation method**
|
||||
|
||||
- [ ] CocoaPods
|
||||
- [ ] Carthage
|
||||
- [ ] Git submodules
|
||||
|
||||
**iOS version(s)**
|
||||
|
||||
**Xcode version**
|
||||
+17
-7
@@ -5,10 +5,10 @@ branches:
|
||||
- next
|
||||
cache:
|
||||
directories:
|
||||
- build
|
||||
- vendor
|
||||
- /usr/local/Homebrew
|
||||
- $HOME/Library/Caches/Homebrew
|
||||
before_cache:
|
||||
- brew cleanup
|
||||
env:
|
||||
global:
|
||||
- LANG=en_US.UTF-8
|
||||
@@ -16,7 +16,17 @@ env:
|
||||
skip_cleanup: true
|
||||
jobs:
|
||||
include:
|
||||
- stage: carthage
|
||||
- stage: Build framework(swift 4.1)
|
||||
osx_image: xcode9.4
|
||||
script:
|
||||
- xcodebuild -scheme FloatingPanel clean build
|
||||
|
||||
- stage: Build framework(swift 4.2)
|
||||
osx_image: xcode10
|
||||
script:
|
||||
- xcodebuild -scheme FloatingPanel clean build
|
||||
|
||||
- stage: Carthage
|
||||
osx_image: xcode10
|
||||
before_install:
|
||||
- brew update
|
||||
@@ -24,22 +34,22 @@ jobs:
|
||||
script:
|
||||
- carthage build --no-skip-current
|
||||
|
||||
- stage: podspec
|
||||
- stage: Podspec
|
||||
osx_image: xcode10
|
||||
script:
|
||||
- pod spec lint
|
||||
|
||||
- stage: check Maps example
|
||||
- stage: Build maps example
|
||||
osx_image: xcode10
|
||||
script:
|
||||
- xcodebuild -scheme Maps -sdk iphonesimulator clean build
|
||||
|
||||
- stage: check Stocks example
|
||||
- stage: Build stocks example
|
||||
osx_image: xcode10
|
||||
script:
|
||||
- xcodebuild -scheme Stocks -sdk iphonesimulator clean build
|
||||
|
||||
- stage: check Samples example
|
||||
- stage: Build samples example
|
||||
osx_image: xcode10
|
||||
script:
|
||||
- xcodebuild -scheme Samples -sdk iphonesimulator clean build
|
||||
|
||||
@@ -162,7 +162,7 @@
|
||||
</objects>
|
||||
<point key="canvasLocation" x="708" y="-200"/>
|
||||
</scene>
|
||||
<!--Item 2-->
|
||||
<!--Layout 2-->
|
||||
<scene sceneID="lRc-OZ-sL4">
|
||||
<objects>
|
||||
<viewController id="RpE-lI-27a" customClass="TabBarContentViewController" customModule="Samples" customModuleProvider="target" sceneMemberID="viewController">
|
||||
@@ -170,12 +170,6 @@
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="812"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Item 2" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="AiP-dx-mFn">
|
||||
<rect key="frame" x="163.66666666666666" y="395.66666666666669" width="48" height="21"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="IvG-yp-yzI">
|
||||
<rect key="frame" x="20" y="44" width="39" height="30"/>
|
||||
<state key="normal" title="Close"/>
|
||||
@@ -188,19 +182,49 @@
|
||||
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<constraints>
|
||||
<constraint firstItem="IvG-yp-yzI" firstAttribute="top" secondItem="954-Dk-zvc" secondAttribute="top" id="18k-sV-PgT"/>
|
||||
<constraint firstItem="AiP-dx-mFn" firstAttribute="centerY" secondItem="JER-jz-KSq" secondAttribute="centerY" id="NUc-tM-0dN"/>
|
||||
<constraint firstItem="AiP-dx-mFn" firstAttribute="centerX" secondItem="954-Dk-zvc" secondAttribute="centerX" id="hwP-mu-Vmz"/>
|
||||
<constraint firstItem="954-Dk-zvc" firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="IvG-yp-yzI" secondAttribute="trailing" id="mpr-u5-MZu"/>
|
||||
<constraint firstItem="IvG-yp-yzI" firstAttribute="leading" secondItem="954-Dk-zvc" secondAttribute="leading" constant="20" id="pYt-jE-CTF"/>
|
||||
</constraints>
|
||||
<viewLayoutGuide key="safeArea" id="954-Dk-zvc"/>
|
||||
</view>
|
||||
<tabBarItem key="tabBarItem" tag="1" title="Item 2" id="qb3-RB-B28"/>
|
||||
<tabBarItem key="tabBarItem" tag="1" title="Layout 2" id="qb3-RB-B28"/>
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="NhZ-u5-Beh" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="-308" y="1546"/>
|
||||
</scene>
|
||||
<!--Item 1-->
|
||||
<!--Layout 3-->
|
||||
<scene sceneID="r9h-Ql-gIv">
|
||||
<objects>
|
||||
<viewController id="pOk-Zm-vD9" customClass="TabBarContentViewController" customModule="Samples" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<view key="view" contentMode="scaleToFill" id="85d-ub-G8k">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="812"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="NbG-e8-HdI">
|
||||
<rect key="frame" x="20" y="44" width="39" height="30"/>
|
||||
<state key="normal" title="Close"/>
|
||||
<connections>
|
||||
<action selector="closeWithSender:" destination="pOk-Zm-vD9" eventType="touchUpInside" id="111-PD-Pop"/>
|
||||
<action selector="closeWithSender:" destination="bYI-y3-Rzb" eventType="touchUpInside" id="1Rg-YG-TtU"/>
|
||||
</connections>
|
||||
</button>
|
||||
</subviews>
|
||||
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<constraints>
|
||||
<constraint firstItem="0ao-SI-QZW" firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="NbG-e8-HdI" secondAttribute="trailing" id="K9F-6x-KWn"/>
|
||||
<constraint firstItem="NbG-e8-HdI" firstAttribute="top" secondItem="0ao-SI-QZW" secondAttribute="top" id="nsE-so-rTl"/>
|
||||
<constraint firstItem="NbG-e8-HdI" firstAttribute="leading" secondItem="0ao-SI-QZW" secondAttribute="leading" constant="20" id="sF4-Dm-aoY"/>
|
||||
</constraints>
|
||||
<viewLayoutGuide key="safeArea" id="0ao-SI-QZW"/>
|
||||
</view>
|
||||
<tabBarItem key="tabBarItem" tag="2" title="Layout 3" id="RJD-TF-Sdh"/>
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="Oe3-FT-q1C" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="332" y="1546"/>
|
||||
</scene>
|
||||
<!--Layout 1-->
|
||||
<scene sceneID="m6X-j6-yBM">
|
||||
<objects>
|
||||
<viewController id="lto-Zc-Vtp" customClass="TabBarContentViewController" customModule="Samples" customModuleProvider="target" sceneMemberID="viewController">
|
||||
@@ -208,12 +232,6 @@
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="812"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Item 1" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="uoW-c8-9wx">
|
||||
<rect key="frame" x="164.66666666666666" y="395.66666666666669" width="46" height="21"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="eFN-tN-4Ct">
|
||||
<rect key="frame" x="20" y="44" width="39" height="30"/>
|
||||
<state key="normal" title="Close"/>
|
||||
@@ -226,13 +244,12 @@
|
||||
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<constraints>
|
||||
<constraint firstItem="eFN-tN-4Ct" firstAttribute="leading" secondItem="5Ns-4l-Ufg" secondAttribute="leading" constant="20" id="5BT-yZ-EKe"/>
|
||||
<constraint firstItem="uoW-c8-9wx" firstAttribute="centerY" secondItem="ji9-Ez-N7i" secondAttribute="centerY" id="Nyw-Wt-78z"/>
|
||||
<constraint firstItem="5Ns-4l-Ufg" firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="eFN-tN-4Ct" secondAttribute="trailing" id="OzZ-Dz-RNF"/>
|
||||
<constraint firstItem="eFN-tN-4Ct" firstAttribute="top" secondItem="5Ns-4l-Ufg" secondAttribute="top" id="hUV-3a-XkY"/>
|
||||
<constraint firstItem="uoW-c8-9wx" firstAttribute="centerX" secondItem="5Ns-4l-Ufg" secondAttribute="centerX" id="wDv-OH-7PX"/>
|
||||
</constraints>
|
||||
<viewLayoutGuide key="safeArea" id="5Ns-4l-Ufg"/>
|
||||
</view>
|
||||
<tabBarItem key="tabBarItem" title="Item 1" id="HEV-kf-jxH"/>
|
||||
<tabBarItem key="tabBarItem" title="Layout 1" id="HEV-kf-jxH"/>
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="bkL-bc-hZC" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
@@ -280,6 +297,7 @@
|
||||
<connections>
|
||||
<segue destination="lto-Zc-Vtp" kind="relationship" relationship="viewControllers" id="6hP-AH-YiH"/>
|
||||
<segue destination="RpE-lI-27a" kind="relationship" relationship="viewControllers" id="g6X-Sq-uSW"/>
|
||||
<segue destination="pOk-Zm-vD9" kind="relationship" relationship="viewControllers" id="OPp-iO-iDK"/>
|
||||
</connections>
|
||||
</tabBarController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="Z9x-EI-p2b" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
@@ -654,6 +672,8 @@ Section 1.10.33 of "de Finibus Bonorum et Malorum", written by Cicero in 45 BC
|
||||
<size key="freeformSize" width="375" height="778"/>
|
||||
<connections>
|
||||
<outlet property="textView" destination="rN1-HL-YHv" id="gmr-Uf-jd8"/>
|
||||
<outlet property="textViewTopConstraint" destination="fiO-LL-nSC" id="Rum-TN-c2e"/>
|
||||
<outlet property="view" destination="9YG-0j-Zzg" id="jhb-eT-nEn"/>
|
||||
</connections>
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="x1h-y1-h8q" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
|
||||
@@ -384,6 +384,7 @@ class NestedScrollViewController: UIViewController {
|
||||
|
||||
class DebugTextViewController: UIViewController, UITextViewDelegate {
|
||||
@IBOutlet weak var textView: UITextView!
|
||||
@IBOutlet weak var textViewTopConstraint: NSLayoutConstraint!
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
@@ -725,10 +726,24 @@ class ModalSecondLayout: FloatingPanelLayout {
|
||||
|
||||
class TabBarViewController: UITabBarController {}
|
||||
|
||||
class TabBarContentViewController: UIViewController, FloatingPanelControllerDelegate {
|
||||
class TabBarContentViewController: UIViewController {
|
||||
enum Tab3Mode {
|
||||
case changeOffset
|
||||
case changeAutoLayout
|
||||
var label: String {
|
||||
switch self {
|
||||
case .changeAutoLayout: return "Use AutoLayout(OK)"
|
||||
case .changeOffset: return "Use ContentOffset(NG)"
|
||||
}
|
||||
}
|
||||
}
|
||||
var fpc: FloatingPanelController!
|
||||
var consoleVC: DebugTextViewController!
|
||||
|
||||
var threeLayout: ThreeTabBarPanelLayout!
|
||||
var tab3Mode: Tab3Mode = .changeAutoLayout
|
||||
var switcherLabel: UILabel!
|
||||
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
// Initialize FloatingPanelController
|
||||
@@ -742,11 +757,47 @@ class TabBarContentViewController: UIViewController, FloatingPanelControllerDele
|
||||
// Set a content view controller and track the scroll view
|
||||
let consoleVC = storyboard?.instantiateViewController(withIdentifier: "ConsoleViewController") as! DebugTextViewController
|
||||
fpc.set(contentViewController: consoleVC)
|
||||
consoleVC.textView.delegate = self // MUST call it before fpc.track(scrollView:)
|
||||
fpc.track(scrollView: consoleVC.textView)
|
||||
self.consoleVC = consoleVC
|
||||
|
||||
// Add FloatingPanel to self.view
|
||||
fpc.addPanel(toParent: self)
|
||||
|
||||
|
||||
if tabBarItem.tag == 2 {
|
||||
let switcher = UISwitch()
|
||||
fpc.view.addSubview(switcher)
|
||||
switcher.translatesAutoresizingMaskIntoConstraints = false
|
||||
NSLayoutConstraint.activate([
|
||||
switcher.bottomAnchor.constraint(equalTo: fpc.surfaceView.topAnchor, constant: -16.0),
|
||||
switcher.rightAnchor.constraint(equalTo: fpc.surfaceView.rightAnchor, constant: -16.0),
|
||||
])
|
||||
switcher.isOn = true
|
||||
switcher.tintColor = .white
|
||||
switcher.backgroundColor = .white
|
||||
switcher.layer.cornerRadius = 16.0
|
||||
switcher.addTarget(self,
|
||||
action: #selector(changeTab3Mode(_:)),
|
||||
for: .valueChanged)
|
||||
let label = UILabel()
|
||||
label.text = tab3Mode.label
|
||||
fpc.view.addSubview(label)
|
||||
switcherLabel = label
|
||||
label.translatesAutoresizingMaskIntoConstraints = false
|
||||
NSLayoutConstraint.activate([
|
||||
label.centerYAnchor.constraint(equalTo: switcher.centerYAnchor, constant: 0.0),
|
||||
label.rightAnchor.constraint(equalTo: switcher.leftAnchor, constant: -16.0),
|
||||
])
|
||||
|
||||
// Turn off the mask instead of content inset change
|
||||
consoleVC.textView.clipsToBounds = false
|
||||
}
|
||||
}
|
||||
|
||||
override func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
fpc.updateLayout()
|
||||
}
|
||||
|
||||
override func viewWillDisappear(_ animated: Bool) {
|
||||
@@ -755,19 +806,125 @@ class TabBarContentViewController: UIViewController, FloatingPanelControllerDele
|
||||
fpc.removePanelFromParent(animated: false)
|
||||
}
|
||||
|
||||
// MARK: - Action
|
||||
|
||||
@IBAction func close(sender: UIButton) {
|
||||
dismiss(animated: true, completion: nil)
|
||||
}
|
||||
|
||||
// MAKR: - Private
|
||||
|
||||
@objc
|
||||
private func changeTab3Mode(_ sender: UISwitch) {
|
||||
if sender.isOn {
|
||||
tab3Mode = .changeAutoLayout
|
||||
} else {
|
||||
tab3Mode = .changeOffset
|
||||
}
|
||||
switcherLabel.text = tab3Mode.label
|
||||
}
|
||||
}
|
||||
|
||||
extension TabBarContentViewController: UITextViewDelegate {
|
||||
func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
||||
guard self.tabBarItem.tag == 2 else { return }
|
||||
}
|
||||
}
|
||||
|
||||
extension TabBarContentViewController: FloatingPanelControllerDelegate {
|
||||
// MARK: - FloatingPanel
|
||||
|
||||
func floatingPanel(_ vc: FloatingPanelController, layoutFor newCollection: UITraitCollection) -> FloatingPanelLayout? {
|
||||
switch self.tabBarItem.tag {
|
||||
case 0:
|
||||
return OneTabBarPanelLayout()
|
||||
case 1:
|
||||
return TwoTabBarPanel2Layout()
|
||||
return TwoTabBarPanelLayout()
|
||||
case 2:
|
||||
threeLayout = ThreeTabBarPanelLayout(parent: self)
|
||||
return threeLayout
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
@IBAction func close(sender: UIButton) {
|
||||
dismiss(animated: true, completion: nil)
|
||||
func floatingPanelDidMove(_ vc: FloatingPanelController) {
|
||||
guard self.tabBarItem.tag == 2 else { return }
|
||||
|
||||
switch tab3Mode {
|
||||
case .changeAutoLayout:
|
||||
/* Good solution: Manipulate top constraint */
|
||||
assert(consoleVC.textViewTopConstraint != nil)
|
||||
if vc.surfaceView.frame.minY + threeLayout.topPadding < vc.layoutInsets.top {
|
||||
consoleVC.textViewTopConstraint?.constant = vc.layoutInsets.top - vc.surfaceView.frame.minY
|
||||
} else {
|
||||
consoleVC.textViewTopConstraint?.constant = threeLayout.topPadding
|
||||
}
|
||||
case .changeOffset:
|
||||
/*
|
||||
Bad solution: Manipulate scoll content inset
|
||||
|
||||
FloatingPanelController keeps a content offset in moving a panel
|
||||
so that changing content inset or offset causes a buggy behavior.
|
||||
*/
|
||||
guard let scrollView = consoleVC.textView else { return }
|
||||
var insets = vc.adjustedContentInsets
|
||||
if vc.surfaceView.frame.minY < vc.layoutInsets.top {
|
||||
insets.top = vc.layoutInsets.top - vc.surfaceView.frame.minY
|
||||
} else {
|
||||
insets.top = 0.0
|
||||
}
|
||||
scrollView.contentInset = insets
|
||||
|
||||
if vc.surfaceView.frame.minY > 0 {
|
||||
scrollView.contentOffset = CGPoint(x: 0.0,
|
||||
y: 0.0 - scrollView.contentInset.top)
|
||||
}
|
||||
}
|
||||
|
||||
if vc.surfaceView.frame.minY > vc.originYOfSurface(for: .half) {
|
||||
let progress = (vc.surfaceView.frame.minY - vc.originYOfSurface(for: .half)) / (vc.originYOfSurface(for: .tip) - vc.originYOfSurface(for: .half))
|
||||
threeLayout.leftConstraint.constant = max(min(progress, 1.0), 0.0) * threeLayout.sideMargin
|
||||
threeLayout.rightConstraint.constant = -max(min(progress, 1.0), 0.0) * threeLayout.sideMargin
|
||||
} else {
|
||||
threeLayout.leftConstraint.constant = 0.0
|
||||
threeLayout.rightConstraint.constant = 0.0
|
||||
}
|
||||
|
||||
vc.view.layoutIfNeeded() // MUST
|
||||
}
|
||||
|
||||
func floatingPanelDidChangePosition(_ vc: FloatingPanelController) {
|
||||
guard self.tabBarItem.tag == 2 else { return }
|
||||
|
||||
switch tab3Mode {
|
||||
case .changeAutoLayout:
|
||||
/* Good Solution: Manipulate top constraint */
|
||||
assert(consoleVC.textViewTopConstraint != nil)
|
||||
consoleVC.textViewTopConstraint?.constant = (vc.position == .full) ? vc.layoutInsets.top : 17.0
|
||||
|
||||
case .changeOffset:
|
||||
/* Bad Solution: Manipulate scoll content inset */
|
||||
guard let scrollView = consoleVC.textView else { return }
|
||||
var insets = vc.adjustedContentInsets
|
||||
insets.top = (vc.position == .full) ? vc.layoutInsets.top : 0.0
|
||||
scrollView.contentInset = insets
|
||||
if scrollView.contentOffset.y - scrollView.contentInset.top < 0.0 {
|
||||
scrollView.contentOffset = CGPoint(x: 0.0,
|
||||
y: 0.0 - scrollView.contentInset.top)
|
||||
}
|
||||
}
|
||||
|
||||
if vc.position == .tip {
|
||||
threeLayout.leftConstraint.constant = threeLayout.sideMargin
|
||||
threeLayout.rightConstraint.constant = -threeLayout.sideMargin
|
||||
} else {
|
||||
threeLayout.leftConstraint.constant = 0.0
|
||||
threeLayout.rightConstraint.constant = 0.0
|
||||
}
|
||||
// Can call it, but it's not necessary because it will be also called
|
||||
// by FloatingPanelController after the delegate method
|
||||
vc.view.layoutIfNeeded()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -804,7 +961,7 @@ class OneTabBarPanelLayout: FloatingPanelLayout {
|
||||
}
|
||||
}
|
||||
|
||||
class TwoTabBarPanel2Layout: FloatingPanelLayout {
|
||||
class TwoTabBarPanelLayout: FloatingPanelLayout {
|
||||
var initialPosition: FloatingPanelPosition {
|
||||
return .half
|
||||
}
|
||||
@@ -824,6 +981,50 @@ class TwoTabBarPanel2Layout: FloatingPanelLayout {
|
||||
}
|
||||
}
|
||||
|
||||
class ThreeTabBarPanelLayout: FloatingPanelFullScreenLayout {
|
||||
weak var parentVC: UIViewController!
|
||||
|
||||
var leftConstraint: NSLayoutConstraint!
|
||||
var rightConstraint: NSLayoutConstraint!
|
||||
|
||||
let topPadding: CGFloat = 17.0
|
||||
let sideMargin: CGFloat = 16.0
|
||||
|
||||
init(parent: UIViewController) {
|
||||
parentVC = parent
|
||||
}
|
||||
|
||||
var bottomInteractionBuffer: CGFloat = 44.0
|
||||
|
||||
var initialPosition: FloatingPanelPosition {
|
||||
return .half
|
||||
}
|
||||
var supportedPositions: Set<FloatingPanelPosition> {
|
||||
return [.full, .half, .tip]
|
||||
}
|
||||
func insetFor(position: FloatingPanelPosition) -> CGFloat? {
|
||||
switch position {
|
||||
case .full: return 0.0
|
||||
case .half: return 261.0 + parentVC.layoutInsets.bottom
|
||||
case .tip: return 88.0 + parentVC.layoutInsets.bottom
|
||||
default: return nil
|
||||
}
|
||||
}
|
||||
func backdropAlphaFor(position: FloatingPanelPosition) -> CGFloat {
|
||||
return 0.3
|
||||
}
|
||||
func prepareLayout(surfaceView: UIView, in view: UIView) -> [NSLayoutConstraint] {
|
||||
if #available(iOS 11.0, *) {
|
||||
leftConstraint = surfaceView.leftAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leftAnchor, constant: 0.0)
|
||||
rightConstraint = surfaceView.rightAnchor.constraint(equalTo: view.safeAreaLayoutGuide.rightAnchor, constant: 0.0)
|
||||
} else {
|
||||
leftConstraint = surfaceView.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 0.0)
|
||||
rightConstraint = surfaceView.rightAnchor.constraint(equalTo: view.rightAnchor, constant: 0.0)
|
||||
}
|
||||
return [ leftConstraint, rightConstraint ]
|
||||
}
|
||||
}
|
||||
|
||||
class SettingsViewController: InspectableViewController {
|
||||
@IBOutlet weak var largeTitlesSwicth: UISwitch!
|
||||
@IBOutlet weak var translucentSwicth: UISwitch!
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
Pod::Spec.new do |s|
|
||||
|
||||
s.name = "FloatingPanel"
|
||||
s.version = "1.3.1"
|
||||
s.version = "1.4.0"
|
||||
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.
|
||||
@@ -13,7 +13,7 @@ The new interface displays the related contents and utilities in parallel as a u
|
||||
s.platform = :ios, "10.0"
|
||||
s.source = { :git => "https://github.com/SCENEE/FloatingPanel.git", :tag => "v#{s.version}" }
|
||||
s.source_files = "Framework/Sources/*.swift"
|
||||
s.swift_version = "4.2"
|
||||
s.swift_version = "4.0"
|
||||
s.pod_target_xcconfig = { 'SWIFT_WHOLE_MODULE_OPTIMIZATION' => 'YES', 'APPLICATION_EXTENSION_API_ONLY' => 'YES' }
|
||||
|
||||
s.framework = "UIKit"
|
||||
|
||||
@@ -7,8 +7,8 @@
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
54352E9821A521CA00CBCA08 /* FloatingPanelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54352E9721A521CA00CBCA08 /* FloatingPanelView.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 */; };
|
||||
545DB9CB2151169500CA77B8 /* FloatingPanel.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 545DB9C12151169500CA77B8 /* FloatingPanel.framework */; };
|
||||
545DB9D02151169500CA77B8 /* ViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 545DB9CF2151169500CA77B8 /* ViewTests.swift */; };
|
||||
@@ -34,8 +34,8 @@
|
||||
/* End PBXContainerItemProxy section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
54352E9721A521CA00CBCA08 /* FloatingPanelView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FloatingPanelView.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>"; };
|
||||
545DB9C12151169500CA77B8 /* FloatingPanel.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = FloatingPanel.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
545DB9C42151169500CA77B8 /* FloatingPanelController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FloatingPanelController.h; sourceTree = "<group>"; };
|
||||
@@ -406,7 +406,7 @@
|
||||
SKIP_INSTALL = YES;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
SWIFT_VERSION = 4.2;
|
||||
SWIFT_VERSION = 4.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Debug;
|
||||
@@ -432,7 +432,7 @@
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.scenee.FloatingPanel;
|
||||
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
|
||||
SKIP_INSTALL = YES;
|
||||
SWIFT_VERSION = 4.2;
|
||||
SWIFT_VERSION = 4.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Release;
|
||||
|
||||
@@ -3,12 +3,13 @@
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import UIKit.UIGestureRecognizerSubclass // For Xcode 9.4.1
|
||||
|
||||
///
|
||||
/// FloatingPanel presentation model
|
||||
///
|
||||
class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate {
|
||||
// MUST be a weak reference to prevent UI freeze on the presentaion modally
|
||||
// MUST be a weak reference to prevent UI freeze on the presentation modally
|
||||
weak var viewcontroller: FloatingPanelController!
|
||||
|
||||
let surfaceView: FloatingPanelSurfaceView
|
||||
@@ -35,17 +36,20 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
|
||||
return remains.count == 0
|
||||
}
|
||||
|
||||
let panGesture: FloatingPanelPanGestureRecognizer
|
||||
let panGestureRecognizer: FloatingPanelPanGestureRecognizer
|
||||
var isRemovalInteractionEnabled: Bool = false
|
||||
|
||||
private var animator: UIViewPropertyAnimator?
|
||||
fileprivate var animator: UIViewPropertyAnimator?
|
||||
private var initialFrame: CGRect = .zero
|
||||
private var initialScrollOffset: CGPoint = .zero
|
||||
private var transOffsetY: CGFloat = 0
|
||||
private var initialTranslationY: CGFloat = 0
|
||||
private var initialLocation: CGPoint = .nan
|
||||
|
||||
var interactionInProgress: Bool = false
|
||||
var isDecelerating: Bool = false
|
||||
|
||||
// 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
|
||||
@@ -67,17 +71,19 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
|
||||
layout: layout)
|
||||
self.behavior = behavior
|
||||
|
||||
panGesture = FloatingPanelPanGestureRecognizer()
|
||||
panGestureRecognizer = FloatingPanelPanGestureRecognizer()
|
||||
|
||||
if #available(iOS 11.0, *) {
|
||||
panGesture.name = "FloatingPanelSurface"
|
||||
panGestureRecognizer.name = "FloatingPanelSurface"
|
||||
}
|
||||
|
||||
super.init()
|
||||
|
||||
surfaceView.addGestureRecognizer(panGesture)
|
||||
panGesture.addTarget(self, action: #selector(handle(panGesture:)))
|
||||
panGesture.delegate = self
|
||||
panGestureRecognizer.floatingPanel = self
|
||||
|
||||
surfaceView.addGestureRecognizer(panGestureRecognizer)
|
||||
panGestureRecognizer.addTarget(self, action: #selector(handle(panGesture:)))
|
||||
panGestureRecognizer.delegate = self
|
||||
}
|
||||
|
||||
func move(to: FloatingPanelPosition, animated: Bool, completion: (() -> Void)? = nil) {
|
||||
@@ -88,6 +94,7 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
|
||||
if to != .full {
|
||||
lockScrollView()
|
||||
}
|
||||
tearDownActiveInteraction()
|
||||
|
||||
if animated {
|
||||
let animator: UIViewPropertyAnimator
|
||||
@@ -101,18 +108,21 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
|
||||
}
|
||||
|
||||
animator.addAnimations { [weak self] in
|
||||
guard let self = self else { return }
|
||||
guard let `self` = self else { return }
|
||||
|
||||
self.updateLayout(to: to)
|
||||
self.state = to
|
||||
self.updateLayout(to: to)
|
||||
}
|
||||
animator.addCompletion { _ in
|
||||
animator.addCompletion { [weak self] _ in
|
||||
guard let `self` = self else { return }
|
||||
self.animator = nil
|
||||
completion?()
|
||||
}
|
||||
self.animator = animator
|
||||
animator.startAnimation()
|
||||
} else {
|
||||
self.updateLayout(to: to)
|
||||
self.state = to
|
||||
self.updateLayout(to: to)
|
||||
completion?()
|
||||
}
|
||||
}
|
||||
@@ -124,10 +134,10 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
|
||||
}
|
||||
|
||||
private func getBackdropAlpha(with translation: CGPoint) -> CGFloat {
|
||||
let currentY = getCurrentY(from: initialFrame, with: translation)
|
||||
let currentY = surfaceView.frame.minY
|
||||
|
||||
let next = directionalPosition(with: translation)
|
||||
let pre = redirectionalPosition(with: translation)
|
||||
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)
|
||||
|
||||
@@ -145,7 +155,7 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
|
||||
|
||||
public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer,
|
||||
shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
|
||||
guard gestureRecognizer == panGesture else { return false }
|
||||
guard gestureRecognizer == panGestureRecognizer else { return false }
|
||||
|
||||
/* log.debug("shouldRecognizeSimultaneouslyWith", otherGestureRecognizer) */
|
||||
|
||||
@@ -153,19 +163,29 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
|
||||
return true
|
||||
}
|
||||
|
||||
// all gestures of the tracking scroll view should be recognized in parallel
|
||||
// and handle them in self.handle(panGesture:)
|
||||
return scrollView?.gestureRecognizers?.contains(otherGestureRecognizer) ?? false
|
||||
switch otherGestureRecognizer {
|
||||
case is UIPanGestureRecognizer,
|
||||
is UISwipeGestureRecognizer,
|
||||
is UIRotationGestureRecognizer,
|
||||
is UIScreenEdgePanGestureRecognizer,
|
||||
is UIPinchGestureRecognizer:
|
||||
// all gestures of the tracking scroll view should be recognized in parallel
|
||||
// and handle them in self.handle(panGesture:)
|
||||
return scrollView?.gestureRecognizers?.contains(otherGestureRecognizer) ?? false
|
||||
default:
|
||||
// Should always recognize tap/long press gestures in parallel
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldBeRequiredToFailBy otherGestureRecognizer: UIGestureRecognizer) -> Bool {
|
||||
guard gestureRecognizer == panGesture else { return false }
|
||||
guard gestureRecognizer == panGestureRecognizer else { return false }
|
||||
/* log.debug("shouldBeRequiredToFailBy", otherGestureRecognizer) */
|
||||
return false
|
||||
}
|
||||
|
||||
public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRequireFailureOf otherGestureRecognizer: UIGestureRecognizer) -> Bool {
|
||||
guard gestureRecognizer == panGesture else { return false }
|
||||
guard gestureRecognizer == panGestureRecognizer else { return false }
|
||||
|
||||
/* log.debug("shouldRequireFailureOf", otherGestureRecognizer) */
|
||||
|
||||
@@ -199,7 +219,7 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
|
||||
// Do not begin the pan gesture until these gestures fail
|
||||
return true
|
||||
default:
|
||||
// Should begin the pan gesture witout waiting tap/long press gestures fail
|
||||
// Should begin the pan gesture without waiting tap/long press gestures fail
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -213,33 +233,41 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
|
||||
}
|
||||
|
||||
// MARK: - Gesture handling
|
||||
private let offsetThreshold: CGFloat = 5.0 // Optimal value from testing
|
||||
@objc func handle(panGesture: UIPanGestureRecognizer) {
|
||||
log.debug("Gesture >>>>", panGesture)
|
||||
let velocity = panGesture.velocity(in: panGesture.view)
|
||||
|
||||
switch panGesture {
|
||||
case scrollView?.panGestureRecognizer:
|
||||
guard let scrollView = scrollView else { return }
|
||||
|
||||
log.debug("SrollPanGesture ScrollView.contentOffset >>>", scrollView.contentOffset.y, scrollView.contentSize, scrollView.bounds.size)
|
||||
let location = panGesture.location(in: surfaceView)
|
||||
|
||||
// Prevent scoll slip by the top bounce when the scroll view's height is
|
||||
// less than the content's height
|
||||
if scrollView.isDecelerating == false, scrollView.contentSize.height > scrollView.bounds.height {
|
||||
scrollView.bounces = (scrollView.contentOffset.y > offsetThreshold)
|
||||
}
|
||||
let belowTop = surfaceView.frame.minY > layoutAdapter.topY
|
||||
|
||||
if surfaceView.frame.minY > layoutAdapter.topY {
|
||||
log.debug("scroll gesture(\(state):\(panGesture.state)) --",
|
||||
"belowTop = \(belowTop),",
|
||||
"interactionInProgress = \(interactionInProgress),",
|
||||
"scroll offset = \(scrollView.contentOffset.y),",
|
||||
"location = \(location.y), velocity = \(velocity.y)")
|
||||
|
||||
if belowTop {
|
||||
// Scroll offset pinning
|
||||
switch state {
|
||||
case .full:
|
||||
let point = panGesture.location(in: surfaceView)
|
||||
if grabberAreaFrame.contains(point) {
|
||||
// Preserve the current content offset in moving from full.
|
||||
scrollView.contentOffset.y = initialScrollOffset.y
|
||||
if interactionInProgress {
|
||||
log.debug("settle offset --", initialScrollOffset.y)
|
||||
scrollView.setContentOffset(initialScrollOffset, animated: false)
|
||||
} else {
|
||||
// Prevent over scrolling in moving from full.
|
||||
scrollView.contentOffset.y = scrollView.contentOffsetZero.y
|
||||
if grabberAreaFrame.contains(location) {
|
||||
// Preserve the current content offset in moving from full.
|
||||
scrollView.contentOffset.y = initialScrollOffset.y
|
||||
} else {
|
||||
if scrollView.contentOffset.y < 0 {
|
||||
fitToBounds(scrollView: scrollView)
|
||||
let translation = panGesture.translation(in: panGestureRecognizer.view!.superview)
|
||||
startInteraction(with: translation, at: location)
|
||||
}
|
||||
}
|
||||
}
|
||||
case .half, .tip:
|
||||
guard scrollView.isDecelerating == false else {
|
||||
@@ -251,7 +279,7 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
|
||||
// Fix the scroll offset in moving the panel from half and tip.
|
||||
scrollView.contentOffset.y = initialScrollOffset.y
|
||||
case .hidden:
|
||||
fatalError("A floating panel hidden must not be used by a user")
|
||||
break
|
||||
}
|
||||
|
||||
// Always hide a scroll indicator at the non-top.
|
||||
@@ -262,34 +290,52 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
|
||||
// Always show a scroll indicator at the top.
|
||||
if interactionInProgress {
|
||||
unlockScrollView()
|
||||
} else {
|
||||
if state == .full, scrollView.contentOffset.y < 0, velocity.y > 0 {
|
||||
fitToBounds(scrollView: scrollView)
|
||||
let translation = panGesture.translation(in: panGestureRecognizer.view!.superview)
|
||||
startInteraction(with: translation, at: location)
|
||||
}
|
||||
}
|
||||
}
|
||||
case panGesture:
|
||||
let translation = panGesture.translation(in: panGesture.view!.superview)
|
||||
case panGestureRecognizer:
|
||||
let translation = panGesture.translation(in: panGestureRecognizer.view!.superview)
|
||||
let location = panGesture.location(in: panGesture.view)
|
||||
|
||||
log.debug(panGesture.state, ">>>", "translation: \(translation.y), velocity: \(velocity.y)")
|
||||
log.debug("panel gesture(\(state):\(panGesture.state)) --",
|
||||
"translation = \(translation.y), location = \(location.y), velocity = \(velocity.y)")
|
||||
|
||||
if let animator = self.animator {
|
||||
if animator.isInterruptible {
|
||||
animator.stopAnimation(false)
|
||||
animator.finishAnimation(at: .current)
|
||||
}
|
||||
self.animator = nil
|
||||
}
|
||||
|
||||
if interactionInProgress == false,
|
||||
viewcontroller.delegate?.floatingPanelShouldBeginDragging(viewcontroller) == false {
|
||||
return
|
||||
}
|
||||
|
||||
if panGesture.state == .began {
|
||||
panningBegan(at: location)
|
||||
return
|
||||
}
|
||||
|
||||
if shouldScrollViewHandleTouch(scrollView, point: location, velocity: velocity) {
|
||||
return
|
||||
}
|
||||
|
||||
if let animator = self.animator, animator.isInterruptible {
|
||||
animator.stopAnimation(true)
|
||||
self.animator = nil
|
||||
}
|
||||
|
||||
switch panGesture.state {
|
||||
case .began:
|
||||
panningBegan()
|
||||
case .changed:
|
||||
if interactionInProgress == false {
|
||||
startInteraction(with: translation)
|
||||
startInteraction(with: translation, at: location)
|
||||
}
|
||||
panningChange(with: translation)
|
||||
case .ended, .cancelled, .failed:
|
||||
panningEnd(with: translation, velocity: velocity)
|
||||
case .possible:
|
||||
default:
|
||||
break
|
||||
}
|
||||
default:
|
||||
@@ -315,17 +361,30 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
|
||||
|
||||
guard
|
||||
state == .full, // When not .full, don't scroll.
|
||||
interactionInProgress == false, // When interaction already in progress, don't scroll.
|
||||
scrollView.frame.contains(point), // When point not in scrollView, don't scroll.
|
||||
!grabberAreaFrame.contains(point) // When point within grabber area, don't scroll.
|
||||
interactionInProgress == false // When interaction already in progress, don't scroll.
|
||||
else {
|
||||
return false
|
||||
}
|
||||
|
||||
log.debug("ScrollView.contentOffset >>>", scrollView.contentOffset.y)
|
||||
// When the current and initial point within grabber area, do scroll.
|
||||
if grabberAreaFrame.contains(point), !grabberAreaFrame.contains(initialLocation) {
|
||||
return true
|
||||
}
|
||||
|
||||
guard
|
||||
scrollView.frame.contains(initialLocation), // When initialLocation not in scrollView, don't scroll.
|
||||
!grabberAreaFrame.contains(point) // When point within grabber area, don't scroll.
|
||||
else {
|
||||
return false
|
||||
}
|
||||
|
||||
let offset = scrollView.contentOffset.y - scrollView.contentOffsetZero.y
|
||||
if abs(offset) > offsetThreshold {
|
||||
// 10 pt is introduced from my testing(there might be better one)
|
||||
// It should be low as possible because a user scroll view frame will
|
||||
// change as far as the specified value temporarily.
|
||||
// The zero offset is an exception because the offset is usually zero
|
||||
// when a panel moves from half or tip position to full.
|
||||
if offset > -10.0, offset != 0.0 {
|
||||
return true
|
||||
}
|
||||
if scrollView.isDecelerating {
|
||||
@@ -338,52 +397,118 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
|
||||
return false
|
||||
}
|
||||
|
||||
private func panningBegan() {
|
||||
private func panningBegan(at location: CGPoint) {
|
||||
// A user interaction does not always start from Began state of the pan gesture
|
||||
// because it can be recognized in scrolling a content in a content view controller.
|
||||
// So do nothing here.
|
||||
log.debug("panningBegan")
|
||||
// So here just preserve the current state if needed.
|
||||
log.debug("panningBegan -- location = \(location.y)")
|
||||
initialLocation = location
|
||||
switch state {
|
||||
case .full:
|
||||
if let scrollView = scrollView {
|
||||
initialScrollFrame = scrollView.frame
|
||||
}
|
||||
default:
|
||||
if let scrollView = scrollView {
|
||||
initialScrollOffset = scrollView.contentOffset
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func panningChange(with translation: CGPoint) {
|
||||
log.debug("panningChange")
|
||||
log.debug("panningChange -- translation = \(translation.y)")
|
||||
let pre = surfaceView.frame.minY
|
||||
let dy = translation.y - initialTranslationY
|
||||
|
||||
let currentY = getCurrentY(from: initialFrame, with: translation)
|
||||
layoutAdapter.updateInteractiveTopConstraint(diff: dy,
|
||||
allowsTopBuffer: allowsTopBuffer(for: dy))
|
||||
|
||||
var frame = initialFrame
|
||||
frame.origin.y = currentY
|
||||
surfaceView.frame = frame
|
||||
backdropView.alpha = getBackdropAlpha(with: translation)
|
||||
preserveContentVCLayoutIfNeeded()
|
||||
|
||||
let didMove = (pre != surfaceView.frame.minY)
|
||||
guard didMove else { return }
|
||||
|
||||
viewcontroller.delegate?.floatingPanelDidMove(viewcontroller)
|
||||
}
|
||||
|
||||
private func allowsTopBuffer(for translationY: CGFloat) -> Bool {
|
||||
let preY = surfaceView.frame.minY
|
||||
let nextY = initialFrame.offsetBy(dx: 0.0, dy: translationY).minY
|
||||
if let scrollView = scrollView, scrollView.panGestureRecognizer.state == .changed,
|
||||
preY > 0 && preY > nextY {
|
||||
return false
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
private var disabledBottomAutoLayout = false
|
||||
// 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() {
|
||||
// Must include topY
|
||||
if (surfaceView.frame.minY <= layoutAdapter.topY) {
|
||||
if !disabledBottomAutoLayout {
|
||||
viewcontroller.contentViewController?.view?.constraints.forEach({ (const) in
|
||||
switch viewcontroller.contentViewController?.layoutGuide.bottomAnchor {
|
||||
case const.firstAnchor:
|
||||
(const.secondItem as? UIView)?.disableAutoLayout()
|
||||
const.isActive = false
|
||||
case const.secondAnchor:
|
||||
(const.firstItem as? UIView)?.disableAutoLayout()
|
||||
const.isActive = false
|
||||
default:
|
||||
break
|
||||
}
|
||||
})
|
||||
}
|
||||
disabledBottomAutoLayout = true
|
||||
} else {
|
||||
if disabledBottomAutoLayout {
|
||||
viewcontroller.contentViewController?.view?.constraints.forEach({ (const) in
|
||||
switch viewcontroller.contentViewController?.layoutGuide.bottomAnchor {
|
||||
case const.firstAnchor:
|
||||
(const.secondItem as? UIView)?.enableAutoLayout()
|
||||
const.isActive = true
|
||||
case const.secondAnchor:
|
||||
(const.firstItem as? UIView)?.enableAutoLayout()
|
||||
const.isActive = true
|
||||
default:
|
||||
break
|
||||
}
|
||||
})
|
||||
}
|
||||
disabledBottomAutoLayout = false
|
||||
}
|
||||
}
|
||||
|
||||
private func panningEnd(with translation: CGPoint, velocity: CGPoint) {
|
||||
log.debug("panningEnd")
|
||||
if interactionInProgress == false {
|
||||
initialFrame = surfaceView.frame
|
||||
log.debug("panningEnd -- translation = \(translation.y), velocity = \(velocity.y)")
|
||||
|
||||
if state == .hidden {
|
||||
log.debug("Already hidden")
|
||||
return
|
||||
}
|
||||
|
||||
stopScrollDeceleration = (surfaceView.frame.minY > layoutAdapter.topY) // Projecting the dragging to the scroll dragging or not
|
||||
|
||||
let targetPosition = self.targetPosition(with: translation, velocity: velocity)
|
||||
let distance = self.distance(to: targetPosition, with: translation)
|
||||
let targetPosition = self.targetPosition(with: velocity)
|
||||
let distance = self.distance(to: targetPosition)
|
||||
|
||||
endInteraction(for: targetPosition)
|
||||
|
||||
if isRemovalInteractionEnabled, isBottomState {
|
||||
let velocityVector = (distance != 0) ? CGVector(dx: 0,
|
||||
dy: max(min(velocity.y/distance, behavior.removalVelocity), 0.0)) : .zero
|
||||
dy: min(fabs(velocity.y)/distance, behavior.removalVelocity)) : .zero
|
||||
|
||||
|
||||
|
||||
if shouldStartRemovalAnimation(with: translation, velocityVector: velocityVector) {
|
||||
if shouldStartRemovalAnimation(with: velocityVector) {
|
||||
|
||||
viewcontroller.delegate?.floatingPanelDidEndDraggingToRemove(viewcontroller, withVelocity: velocity)
|
||||
self.startRemovalAnimation(with: velocityVector) { [weak self] in
|
||||
guard let self = self else { return }
|
||||
guard let `self` = self else { return }
|
||||
self.viewcontroller.dismiss(animated: false, completion: { [weak self] in
|
||||
guard let self = self else { return }
|
||||
guard let `self` = self else { return }
|
||||
self.viewcontroller.delegate?.floatingPanelDidEndRemove(self.viewcontroller)
|
||||
})
|
||||
}
|
||||
@@ -392,20 +517,19 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
|
||||
}
|
||||
|
||||
viewcontroller.delegate?.floatingPanelDidEndDragging(viewcontroller, withVelocity: velocity, targetPosition: targetPosition)
|
||||
viewcontroller.delegate?.floatingPanelWillBeginDecelerating(viewcontroller)
|
||||
|
||||
startAnimation(to: targetPosition, at: distance, with: velocity)
|
||||
}
|
||||
|
||||
private func shouldStartRemovalAnimation(with translation: CGPoint, velocityVector: CGVector) -> Bool {
|
||||
private func shouldStartRemovalAnimation(with velocityVector: CGVector) -> Bool {
|
||||
let posY = layoutAdapter.positionY(for: state)
|
||||
let currentY = getCurrentY(from: initialFrame, with: translation)
|
||||
let safeAreaBottomY = layoutAdapter.safeAreaBottomY
|
||||
let currentY = surfaceView.frame.minY
|
||||
let bottomMaxY = layoutAdapter.bottomMaxY
|
||||
let vth = behavior.removalVelocity
|
||||
let pth = max(min(behavior.removalProgress, 1.0), 0.0)
|
||||
|
||||
let num = (currentY - posY)
|
||||
let den = (safeAreaBottomY - posY)
|
||||
let den = (bottomMaxY - posY)
|
||||
|
||||
guard num >= 0, den != 0, (num / den >= pth || velocityVector.dy == vth)
|
||||
else { return false }
|
||||
@@ -420,110 +544,93 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
|
||||
self?.updateLayout(to: .hidden)
|
||||
}
|
||||
animator.addCompletion({ _ in
|
||||
self.animator = nil
|
||||
completion?()
|
||||
})
|
||||
self.animator = animator
|
||||
animator.startAnimation()
|
||||
}
|
||||
|
||||
private func startInteraction(with translation: CGPoint) {
|
||||
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")
|
||||
log.debug("startInteraction -- translation = \(translation.y), location = \(location.y)")
|
||||
guard interactionInProgress == false else { return }
|
||||
|
||||
initialFrame = surfaceView.frame
|
||||
if let scrollView = scrollView {
|
||||
initialScrollOffset = scrollView.contentOffset
|
||||
if state == .full, let scrollView = scrollView {
|
||||
if grabberAreaFrame.contains(location) {
|
||||
initialScrollOffset = scrollView.contentOffset
|
||||
} else {
|
||||
settle(scrollView: scrollView)
|
||||
initialScrollOffset = scrollView.contentOffsetZero
|
||||
}
|
||||
log.debug("initial scroll offset --", initialScrollOffset)
|
||||
}
|
||||
transOffsetY = translation.y
|
||||
|
||||
initialTranslationY = translation.y
|
||||
|
||||
viewcontroller.delegate?.floatingPanelWillBeginDragging(viewcontroller)
|
||||
|
||||
viewcontroller.contentViewController?.view?.constraints.forEach({ (const) in
|
||||
switch viewcontroller.contentViewController?.layoutGuide.bottomAnchor {
|
||||
case const.firstAnchor:
|
||||
(const.secondItem as? UIView)?.disableAutoLayout()
|
||||
case const.secondAnchor:
|
||||
(const.firstItem as? UIView)?.disableAutoLayout()
|
||||
default:
|
||||
break
|
||||
}
|
||||
})
|
||||
layoutAdapter.startInteraction(at: state)
|
||||
|
||||
interactionInProgress = true
|
||||
}
|
||||
|
||||
private func endInteraction(for targetPosition: FloatingPanelPosition) {
|
||||
log.debug("endInteraction for \(targetPosition)")
|
||||
log.debug("endInteraction to \(targetPosition)")
|
||||
|
||||
if let scrollView = scrollView {
|
||||
log.debug("endInteraction -- scroll offset = \(scrollView.contentOffset)")
|
||||
}
|
||||
|
||||
interactionInProgress = false
|
||||
|
||||
// Prevent to keep a scoll view indicator visible at the half/tip position
|
||||
// Prevent to keep a scroll view indicator visible at the half/tip position
|
||||
if targetPosition != .full {
|
||||
lockScrollView()
|
||||
}
|
||||
|
||||
viewcontroller.contentViewController?.view?.constraints.forEach({ (const) in
|
||||
switch viewcontroller.contentViewController?.layoutGuide.bottomAnchor {
|
||||
case const.firstAnchor:
|
||||
(const.secondItem as? UIView)?.enableAutoLayout()
|
||||
case const.secondAnchor:
|
||||
(const.firstItem as? UIView)?.enableAutoLayout()
|
||||
default:
|
||||
break
|
||||
}
|
||||
})
|
||||
layoutAdapter.endInteraction(at: targetPosition)
|
||||
}
|
||||
|
||||
private func getCurrentY(from rect: CGRect, with translation: CGPoint) -> CGFloat {
|
||||
let dy = translation.y - transOffsetY
|
||||
let y = rect.offsetBy(dx: 0.0, dy: dy).origin.y
|
||||
|
||||
let topY = layoutAdapter.topY
|
||||
let topBuffer = layoutAdapter.layout.topInteractionBuffer
|
||||
let bottomY = layoutAdapter.bottomY
|
||||
let bottomBuffer = layoutAdapter.layout.bottomInteractionBuffer
|
||||
|
||||
if let scrollView = scrollView, scrollView.panGestureRecognizer.state == .changed {
|
||||
let preY = surfaceView.frame.origin.y
|
||||
if preY > 0 && preY > y {
|
||||
return max(topY, min(bottomY, y))
|
||||
}
|
||||
}
|
||||
let topMax = layoutAdapter.topMaxY
|
||||
let bottomMax = layoutAdapter.bottomMaxY
|
||||
return max(max(topY - topBuffer, topMax), min(min(bottomY + bottomBuffer, bottomMax), y))
|
||||
private func tearDownActiveInteraction() {
|
||||
// Cancel the pan gesture so that panningEnd(with:velocity:) is called
|
||||
panGestureRecognizer.isEnabled = false
|
||||
panGestureRecognizer.isEnabled = true
|
||||
}
|
||||
|
||||
private func startAnimation(to targetPosition: FloatingPanelPosition, at distance: CGFloat, with velocity: CGPoint) {
|
||||
log.debug("startAnimation", targetPosition, distance, velocity)
|
||||
let targetY = layoutAdapter.positionY(for: targetPosition)
|
||||
let velocityVector = (distance != 0) ? CGVector(dx: 0, dy: max(min(velocity.y/distance, 30.0), -30.0)) : .zero
|
||||
log.debug("startAnimation to \(targetPosition) -- distance = \(distance), velocity = \(velocity.y)")
|
||||
|
||||
isDecelerating = true
|
||||
viewcontroller.delegate?.floatingPanelWillBeginDecelerating(viewcontroller)
|
||||
|
||||
let velocityVector = (distance != 0) ? CGVector(dx: 0, dy: min(fabs(velocity.y)/distance, 30.0)) : .zero
|
||||
let animator = behavior.interactionAnimator(self.viewcontroller, to: targetPosition, with: velocityVector)
|
||||
animator.addAnimations { [weak self] in
|
||||
guard let self = self else { return }
|
||||
if self.state == targetPosition {
|
||||
self.surfaceView.frame.origin.y = targetY
|
||||
self.layoutAdapter.setBackdropAlpha(of: targetPosition)
|
||||
} else {
|
||||
self.updateLayout(to: targetPosition)
|
||||
}
|
||||
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 }
|
||||
guard
|
||||
self.interactionInProgress == false,
|
||||
animator == self.animator,
|
||||
pos == .end
|
||||
else { return }
|
||||
guard let `self` = self else { return }
|
||||
self.finishAnimation(at: targetPosition)
|
||||
}
|
||||
animator.startAnimation()
|
||||
self.animator = animator
|
||||
animator.startAnimation()
|
||||
}
|
||||
|
||||
private func finishAnimation(at targetPosition: FloatingPanelPosition) {
|
||||
log.debug("finishAnimation \(targetPosition)")
|
||||
log.debug("finishAnimation to \(targetPosition)")
|
||||
self.isDecelerating = false
|
||||
self.animator = nil
|
||||
|
||||
self.viewcontroller.delegate?.floatingPanelDidEndDecelerating(self.viewcontroller)
|
||||
|
||||
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 targetPosition == .full {
|
||||
@@ -531,97 +638,63 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
|
||||
}
|
||||
}
|
||||
|
||||
private func distance(to targetPosition: FloatingPanelPosition, with translation: CGPoint) -> CGFloat {
|
||||
private func distance(to targetPosition: FloatingPanelPosition) -> CGFloat {
|
||||
let topY = layoutAdapter.topY
|
||||
let middleY = layoutAdapter.middleY
|
||||
let bottomY = layoutAdapter.bottomY
|
||||
let currentY = getCurrentY(from: initialFrame, with: translation)
|
||||
let currentY = surfaceView.frame.minY
|
||||
|
||||
switch targetPosition {
|
||||
case .full:
|
||||
return CGFloat(fabs(Double(currentY - topY)))
|
||||
return CGFloat(fabs(currentY - topY))
|
||||
case .half:
|
||||
return CGFloat(fabs(Double(currentY - middleY)))
|
||||
return CGFloat(fabs(currentY - middleY))
|
||||
case .tip:
|
||||
return CGFloat(fabs(Double(currentY - bottomY)))
|
||||
return CGFloat(fabs(currentY - bottomY))
|
||||
case .hidden:
|
||||
fatalError("A floating panel hidden must not be used by a user")
|
||||
fatalError("Now .hidden must not be used for a user interaction")
|
||||
}
|
||||
}
|
||||
|
||||
private func directionalPosition(with translation: CGPoint) -> FloatingPanelPosition {
|
||||
let currentY = getCurrentY(from: initialFrame, with: translation)
|
||||
|
||||
let supportedPositions = layoutAdapter.supportedPositions
|
||||
|
||||
if supportedPositions.count == 1 {
|
||||
return state
|
||||
}
|
||||
|
||||
switch supportedPositions {
|
||||
case [.full, .half]: return translation.y >= 0 ? .half : .full
|
||||
case [.half, .tip]: return translation.y >= 0 ? .tip : .half
|
||||
case [.full, .tip]: return translation.y >= 0 ? .tip : .full
|
||||
default:
|
||||
let middleY = layoutAdapter.middleY
|
||||
|
||||
switch state {
|
||||
case .full:
|
||||
if translation.y <= 0 {
|
||||
return .full
|
||||
}
|
||||
return currentY > middleY ? .tip : .half
|
||||
case .half:
|
||||
return currentY > middleY ? .tip : .full
|
||||
case .tip:
|
||||
if translation.y >= 0 {
|
||||
return .tip
|
||||
}
|
||||
return currentY > middleY ? .half : .full
|
||||
case .hidden:
|
||||
fatalError("A floating panel hidden must not be used by a user")
|
||||
}
|
||||
}
|
||||
private func directionalPosition(at currentY: CGFloat, with translation: CGPoint) -> FloatingPanelPosition {
|
||||
return getPosition(at: currentY, with: translation, directional: true)
|
||||
}
|
||||
|
||||
private func redirectionalPosition(with translation: CGPoint) -> FloatingPanelPosition {
|
||||
let currentY = getCurrentY(from: initialFrame, with: translation)
|
||||
|
||||
let supportedPositions = layoutAdapter.supportedPositions
|
||||
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 translation.y >= 0 ? .full : .half
|
||||
case [.half, .tip]: return translation.y >= 0 ? .half : .tip
|
||||
case [.full, .tip]: return translation.y >= 0 ? .full : .tip
|
||||
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
|
||||
|
||||
switch state {
|
||||
case .full:
|
||||
return currentY > middleY ? .half : .full
|
||||
case .half:
|
||||
return .half
|
||||
case .tip:
|
||||
return currentY > middleY ? .tip : .half
|
||||
case .hidden:
|
||||
fatalError("A floating panel hidden must not be used by a user")
|
||||
if currentY > middleY {
|
||||
return (isForwardYAxis == directional) ? .tip : .half
|
||||
} else {
|
||||
return (isForwardYAxis == directional) ? .half : .full
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Distance travelled after decelerating to zero velocity at a constant rate.
|
||||
// Refer to the slides p176 of [Designing Fluid Interfaces](https://developer.apple.com/videos/play/wwdc2018/803/)
|
||||
private func project(initialVelocity: CGFloat) -> CGFloat {
|
||||
let decelerationRate = UIScrollView.DecelerationRate.normal.rawValue
|
||||
private func project(initialVelocity: CGFloat, decelerationRate: CGFloat = UIScrollViewDecelerationRateNormal) -> CGFloat {
|
||||
return (initialVelocity / 1000.0) * decelerationRate / (1.0 - decelerationRate)
|
||||
}
|
||||
|
||||
private func targetPosition(with translation: CGPoint, velocity: CGPoint) -> (FloatingPanelPosition) {
|
||||
let currentY = getCurrentY(from: initialFrame, with: translation)
|
||||
private func targetPosition(with velocity: CGPoint) -> (FloatingPanelPosition) {
|
||||
let currentY = surfaceView.frame.minY
|
||||
let supportedPositions = layoutAdapter.supportedPositions
|
||||
|
||||
if supportedPositions.count == 1 {
|
||||
@@ -643,29 +716,27 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
|
||||
let middleY = layoutAdapter.middleY
|
||||
let bottomY = layoutAdapter.bottomY
|
||||
|
||||
let target: FloatingPanelPosition
|
||||
let nextState: FloatingPanelPosition
|
||||
let forwardYDirection: Bool
|
||||
|
||||
/*
|
||||
full <-> half <-> tip
|
||||
*/
|
||||
switch state {
|
||||
case .full:
|
||||
target = .half
|
||||
nextState = .half
|
||||
forwardYDirection = true
|
||||
case .half:
|
||||
if (currentY < middleY) {
|
||||
target = .full
|
||||
forwardYDirection = false
|
||||
} else {
|
||||
target = .tip
|
||||
forwardYDirection = true
|
||||
}
|
||||
nextState = (currentY > middleY) ? .tip : .full
|
||||
forwardYDirection = (currentY > middleY)
|
||||
case .tip:
|
||||
target = .half
|
||||
nextState = .half
|
||||
forwardYDirection = false
|
||||
case .hidden:
|
||||
fatalError("A floating panel hidden must not be used by a user")
|
||||
fatalError("Now .hidden must not be used for a user interaction")
|
||||
}
|
||||
|
||||
let redirectionalProgress = max(min(behavior.redirectionalProgress(viewcontroller, from: state, to: target), 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
|
||||
@@ -678,30 +749,56 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
|
||||
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:
|
||||
if project(initialVelocity: velocity.y) >= (middleY - currentY) {
|
||||
switch pY {
|
||||
case bottomY...:
|
||||
return behavior.shouldProjectMomentum(viewcontroller, for: .tip) ? .tip : .half
|
||||
case middleY...:
|
||||
return .half
|
||||
} else {
|
||||
case topY...:
|
||||
return .full
|
||||
default:
|
||||
return .full
|
||||
}
|
||||
case ...middleY:
|
||||
if project(initialVelocity: velocity.y) <= (topY - currentY) {
|
||||
return .full
|
||||
} else {
|
||||
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:
|
||||
if project(initialVelocity: velocity.y) >= (bottomY - currentY) {
|
||||
switch pY {
|
||||
case bottomY...:
|
||||
return .tip
|
||||
} else {
|
||||
case middleY...:
|
||||
return .half
|
||||
case topY...:
|
||||
return .half
|
||||
default:
|
||||
return behavior.shouldProjectMomentum(viewcontroller, for: .full) ? .full : .half
|
||||
}
|
||||
default:
|
||||
if project(initialVelocity: velocity.y) <= (middleY - currentY) {
|
||||
return .half
|
||||
} else {
|
||||
switch pY {
|
||||
case bottomY...:
|
||||
return .tip
|
||||
case middleY...:
|
||||
return .tip
|
||||
case topY...:
|
||||
return .half
|
||||
default:
|
||||
return behavior.shouldProjectMomentum(viewcontroller, for: .full) ? .full : .half
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -721,15 +818,18 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
|
||||
|
||||
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 project(initialVelocity: velocity.y) >= (bottomY - currentY) {
|
||||
if pY >= bottomY {
|
||||
return bottom
|
||||
} else {
|
||||
return top
|
||||
}
|
||||
default:
|
||||
if project(initialVelocity: velocity.y) <= (topY - currentY) {
|
||||
if pY <= topY {
|
||||
return top
|
||||
} else {
|
||||
return bottom
|
||||
@@ -755,6 +855,28 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
|
||||
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")
|
||||
|
||||
surfaceView.transform = .identity
|
||||
scrollView.transform = .identity
|
||||
scrollView.frame = initialScrollFrame
|
||||
scrollView.contentOffset = scrollView.contentOffsetZero
|
||||
scrollView.scrollIndicatorInsets = .zero
|
||||
}
|
||||
|
||||
|
||||
// MARK: - UIScrollViewDelegate Intermediation
|
||||
override func responds(to aSelector: Selector!) -> Bool {
|
||||
@@ -769,24 +891,29 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
|
||||
}
|
||||
}
|
||||
|
||||
func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) {
|
||||
if state != .full {
|
||||
initialScrollOffset = scrollView.contentOffset
|
||||
}
|
||||
userScrollViewDelegate?.scrollViewDidEndScrollingAnimation?(scrollView)
|
||||
}
|
||||
|
||||
func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
|
||||
if stopScrollDeceleration {
|
||||
targetContentOffset.pointee = scrollView.contentOffset
|
||||
stopScrollDeceleration = false
|
||||
} else {
|
||||
let targetOffset = targetContentOffset.pointee
|
||||
userScrollViewDelegate?.scrollViewWillEndDragging?(scrollView, withVelocity: velocity, targetContentOffset: targetContentOffset)
|
||||
// Stop scrolling on tip and half
|
||||
if state != .full, targetOffset == targetContentOffset.pointee {
|
||||
targetContentOffset.pointee.y = scrollView.contentOffset.y
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class FloatingPanelPanGestureRecognizer: UIPanGestureRecognizer {
|
||||
fileprivate var floatingPanel: FloatingPanel?
|
||||
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
|
||||
super.touchesBegan(touches, with: event)
|
||||
if floatingPanel?.animator != nil {
|
||||
self.state = .began
|
||||
}
|
||||
}
|
||||
override weak var delegate: UIGestureRecognizerDelegate? {
|
||||
get {
|
||||
return super.delegate
|
||||
|
||||
@@ -6,7 +6,18 @@
|
||||
import UIKit
|
||||
|
||||
public protocol FloatingPanelBehavior {
|
||||
/// Returns the progress to redirect to the previous position
|
||||
/// Asks the behavior object if the floating panel should project a momentum of a user interaction to move the proposed position.
|
||||
///
|
||||
/// The default implementation of this method returns true. This method is called for a layout to support all positions(tip, half and full).
|
||||
/// Therfore, `proposedTargetPosition` can only be `FloatingPanelPosition.tip` or `FloatingPanelPosition.full`.
|
||||
func shouldProjectMomentum(_ fpc: FloatingPanelController, for proposedTargetPosition: FloatingPanelPosition) -> Bool
|
||||
|
||||
/// Returns a deceleration rate to calculate a target position projected a dragging momentum.
|
||||
///
|
||||
/// The default implementation of this method returns the normal deceleration rate of UIScrollView.
|
||||
func momentumProjectionRate(_ fpc: FloatingPanelController) -> CGFloat
|
||||
|
||||
/// Returns the progress to redirect to the previous position.
|
||||
///
|
||||
/// The progress is represented by a floating-point value between 0.0 and 1.0, inclusive, where 1.0 indicates the floating panel is impossible to move to the next posiiton. The default value is 0.5. Values less than 0.0 and greater than 1.0 are pinned to those limits.
|
||||
func redirectionalProgress(_ fpc: FloatingPanelController, from: FloatingPanelPosition, to: FloatingPanelPosition) -> CGFloat
|
||||
@@ -49,10 +60,29 @@ 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
|
||||
}
|
||||
}
|
||||
|
||||
func momentumProjectionRate(_ fpc: FloatingPanelController) -> CGFloat {
|
||||
return UIScrollViewDecelerationRateNormal
|
||||
}
|
||||
|
||||
func redirectionalProgress(_ fpc: FloatingPanelController, from: FloatingPanelPosition, to: FloatingPanelPosition) -> CGFloat {
|
||||
return 0.5
|
||||
}
|
||||
|
||||
public func interactionAnimator(_ fpc: FloatingPanelController, to targetPosition: FloatingPanelPosition, with velocity: CGVector) -> UIViewPropertyAnimator {
|
||||
return defaultBehavior.interactionAnimator(fpc, to: targetPosition, with: velocity)
|
||||
}
|
||||
|
||||
func addAnimator(_ fpc: FloatingPanelController, to: FloatingPanelPosition) -> UIViewPropertyAnimator {
|
||||
return UIViewPropertyAnimator(duration: 0.25, curve: .easeInOut)
|
||||
}
|
||||
@@ -82,8 +112,12 @@ public extension FloatingPanelBehavior {
|
||||
}
|
||||
}
|
||||
|
||||
class FloatingPanelDefaultBehavior: FloatingPanelBehavior {
|
||||
func interactionAnimator(_ fpc: FloatingPanelController, to targetPosition: FloatingPanelPosition, with velocity: CGVector) -> UIViewPropertyAnimator {
|
||||
private let defaultBehavior = FloatingPanelDefaultBehavior()
|
||||
|
||||
public class FloatingPanelDefaultBehavior: FloatingPanelBehavior {
|
||||
public init() { }
|
||||
|
||||
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
|
||||
|
||||
@@ -14,7 +14,10 @@ public protocol FloatingPanelControllerDelegate: class {
|
||||
|
||||
func floatingPanelDidChangePosition(_ vc: FloatingPanelController) // changed the settled position in the model layer
|
||||
|
||||
func floatingPanelDidMove(_ vc: FloatingPanelController) // any offset changes
|
||||
/// Asks the delegate if dragging should begin by the pan gesture recognizer.
|
||||
func floatingPanelShouldBeginDragging(_ vc: FloatingPanelController) -> Bool
|
||||
|
||||
func floatingPanelDidMove(_ vc: FloatingPanelController) // any surface frame changes in dragging
|
||||
|
||||
// called on start of dragging (may require some time and or distance to move)
|
||||
func floatingPanelWillBeginDragging(_ vc: FloatingPanelController)
|
||||
@@ -28,7 +31,10 @@ public protocol FloatingPanelControllerDelegate: class {
|
||||
// called when its views are removed from a parent view controller
|
||||
func floatingPanelDidEndRemove(_ vc: FloatingPanelController)
|
||||
|
||||
func floatingPanel(_ vc: FloatingPanelController, shouldRecognizeSimultaneouslyWith gestureRecognizer: UIGestureRecognizer) -> Bool
|
||||
/// Asks the delegate if the other gesture recognizer should be allowed to recognize the gesture in parallel.
|
||||
///
|
||||
/// By default, any tap and long gesture recognizers are allowed to recognize gestures simultaneously.
|
||||
func floatingPanel(_ vc: FloatingPanelController, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool
|
||||
}
|
||||
|
||||
public extension FloatingPanelControllerDelegate {
|
||||
@@ -39,6 +45,9 @@ public extension FloatingPanelControllerDelegate {
|
||||
return nil
|
||||
}
|
||||
func floatingPanelDidChangePosition(_ vc: FloatingPanelController) {}
|
||||
func floatingPanelShouldBeginDragging(_ vc: FloatingPanelController) -> Bool {
|
||||
return true
|
||||
}
|
||||
func floatingPanelDidMove(_ vc: FloatingPanelController) {}
|
||||
func floatingPanelWillBeginDragging(_ vc: FloatingPanelController) {}
|
||||
func floatingPanelDidEndDragging(_ vc: FloatingPanelController, withVelocity velocity: CGPoint, targetPosition: FloatingPanelPosition) {}
|
||||
@@ -48,10 +57,13 @@ public extension FloatingPanelControllerDelegate {
|
||||
func floatingPanelDidEndDraggingToRemove(_ vc: FloatingPanelController, withVelocity velocity: CGPoint) {}
|
||||
func floatingPanelDidEndRemove(_ vc: FloatingPanelController) {}
|
||||
|
||||
func floatingPanel(_ vc: FloatingPanelController, shouldRecognizeSimultaneouslyWith gestureRecognizer: UIGestureRecognizer) -> Bool { return false }
|
||||
func floatingPanel(_ vc: FloatingPanelController, shouldRecognizeSimultaneouslyWith gestureRecognizer: UIGestureRecognizer) -> Bool {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
public enum FloatingPanelPosition: Int, CaseIterable {
|
||||
|
||||
public enum FloatingPanelPosition: Int {
|
||||
case full
|
||||
case half
|
||||
case tip
|
||||
@@ -69,7 +81,11 @@ public class FloatingPanelController: UIViewController, UIScrollViewDelegate, UI
|
||||
}
|
||||
|
||||
/// The delegate of the floating panel controller object.
|
||||
public weak var delegate: FloatingPanelControllerDelegate?
|
||||
public weak var delegate: FloatingPanelControllerDelegate?{
|
||||
didSet{
|
||||
didUpdateDelegate()
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the surface view managed by the controller object. It's the same as `self.view`.
|
||||
public var surfaceView: FloatingPanelSurfaceView! {
|
||||
@@ -88,7 +104,7 @@ public class FloatingPanelController: UIViewController, UIScrollViewDelegate, UI
|
||||
|
||||
// The underlying gesture recognizer for pan gestures
|
||||
public var panGestureRecognizer: UIPanGestureRecognizer {
|
||||
return floatingPanel.panGesture
|
||||
return floatingPanel.panGestureRecognizer
|
||||
}
|
||||
|
||||
/// The current position of the floating panel controller's contents.
|
||||
@@ -133,14 +149,15 @@ public class FloatingPanelController: UIViewController, UIScrollViewDelegate, UI
|
||||
private var safeAreaInsetsObservation: NSKeyValueObservation?
|
||||
private let modalTransition = FloatingPanelModalTransition()
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
required public init?(coder aDecoder: NSCoder) {
|
||||
super.init(coder: aDecoder)
|
||||
setUp()
|
||||
}
|
||||
|
||||
/// Initialize a newly created floating panel controller.
|
||||
public init() {
|
||||
public init(delegate: FloatingPanelControllerDelegate? = nil) {
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
self.delegate = delegate
|
||||
setUp()
|
||||
}
|
||||
|
||||
@@ -155,6 +172,11 @@ public class FloatingPanelController: UIViewController, UIScrollViewDelegate, UI
|
||||
behavior: fetchBehavior(for: self.traitCollection))
|
||||
}
|
||||
|
||||
private func didUpdateDelegate(){
|
||||
floatingPanel.layoutAdapter.layout = fetchLayout(for: traitCollection)
|
||||
floatingPanel.behavior = fetchBehavior(for: self.traitCollection)
|
||||
}
|
||||
|
||||
// MARK:- Overrides
|
||||
|
||||
/// Creates the view that the controller manages.
|
||||
@@ -201,6 +223,11 @@ public class FloatingPanelController: UIViewController, UIScrollViewDelegate, UI
|
||||
floatingPanel.behavior = fetchBehavior(for: newCollection)
|
||||
}
|
||||
|
||||
public override func viewWillDisappear(_ animated: Bool) {
|
||||
super.viewWillDisappear(animated)
|
||||
safeAreaInsetsObservation = nil
|
||||
}
|
||||
|
||||
// MARK:- Privates
|
||||
|
||||
private func fetchLayout(for traitCollection: UITraitCollection) -> FloatingPanelLayout {
|
||||
@@ -217,14 +244,13 @@ public class FloatingPanelController: UIViewController, UIScrollViewDelegate, UI
|
||||
}
|
||||
|
||||
private func update(safeAreaInsets: UIEdgeInsets) {
|
||||
// Don't re-layout the surface on SafeArea.Bottom enabled/disabled in interaction progress
|
||||
guard
|
||||
floatingPanel.layoutAdapter.safeAreaInsets != safeAreaInsets,
|
||||
self.floatingPanel.interactionInProgress == false
|
||||
else { return }
|
||||
self.floatingPanel.isDecelerating == false
|
||||
else { return }
|
||||
|
||||
log.debug("Update safeAreaInsets", safeAreaInsets)
|
||||
|
||||
|
||||
floatingPanel.layoutAdapter.safeAreaInsets = safeAreaInsets
|
||||
|
||||
setUpLayout()
|
||||
@@ -269,7 +295,7 @@ public class FloatingPanelController: UIViewController, UIScrollViewDelegate, UI
|
||||
// 2. The safe area top inset can be variable on the large title navigation bar(iOS11+).
|
||||
// That's why it needs the observation to keep `adjustedContentInsets` correct.
|
||||
safeAreaInsetsObservation = self.observe(\.view.safeAreaInsets) { [weak self] (vc, chaneg) in
|
||||
guard let self = self else { return }
|
||||
guard let `self` = self else { return }
|
||||
self.update(safeAreaInsets: vc.layoutInsets)
|
||||
}
|
||||
} else {
|
||||
@@ -312,7 +338,7 @@ public class FloatingPanelController: UIViewController, UIScrollViewDelegate, UI
|
||||
parent.view.addSubview(self.view)
|
||||
}
|
||||
|
||||
parent.addChild(self)
|
||||
parent.addChildViewController(self)
|
||||
|
||||
view.frame = parent.view.bounds // Needed for a correct safe area configuration
|
||||
view.translatesAutoresizingMaskIntoConstraints = false
|
||||
@@ -324,8 +350,8 @@ public class FloatingPanelController: UIViewController, UIScrollViewDelegate, UI
|
||||
])
|
||||
|
||||
show(animated: animated) { [weak self] in
|
||||
guard let self = self else { return }
|
||||
self.didMove(toParent: parent)
|
||||
guard let `self` = self else { return }
|
||||
self.didMove(toParentViewController: self)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -340,10 +366,10 @@ public class FloatingPanelController: UIViewController, UIScrollViewDelegate, UI
|
||||
}
|
||||
|
||||
hide(animated: animated) { [weak self] in
|
||||
guard let self = self else { return }
|
||||
self.willMove(toParent: nil)
|
||||
guard let `self` = self else { return }
|
||||
self.willMove(toParentViewController: nil)
|
||||
self.view.removeFromSuperview()
|
||||
self.removeFromParent()
|
||||
self.removeFromParentViewController()
|
||||
completion?()
|
||||
}
|
||||
}
|
||||
@@ -361,21 +387,27 @@ public class FloatingPanelController: UIViewController, UIScrollViewDelegate, UI
|
||||
/// Sets the view controller responsible for the content portion of the floating panel..
|
||||
public func set(contentViewController: UIViewController?) {
|
||||
if let vc = _contentViewController {
|
||||
vc.willMove(toParent: nil)
|
||||
vc.willMove(toParentViewController: nil)
|
||||
vc.view.removeFromSuperview()
|
||||
vc.removeFromParent()
|
||||
vc.removeFromParentViewController()
|
||||
|
||||
if let scrollView = floatingPanel.scrollView,
|
||||
let delegate = floatingPanel.userScrollViewDelegate,
|
||||
vc.view.subviews.contains(scrollView) {
|
||||
scrollView.delegate = delegate
|
||||
}
|
||||
}
|
||||
|
||||
if let vc = contentViewController {
|
||||
addChild(vc)
|
||||
addChildViewController(vc)
|
||||
let surfaceView = floatingPanel.surfaceView
|
||||
surfaceView.add(contentView: vc.view)
|
||||
vc.didMove(toParent: self)
|
||||
vc.didMove(toParentViewController: self)
|
||||
}
|
||||
|
||||
_contentViewController = contentViewController
|
||||
}
|
||||
|
||||
|
||||
@available(*, unavailable, renamed: "set(contentViewController:)")
|
||||
public override func show(_ vc: UIViewController, sender: Any?) {
|
||||
if let target = self.parent?.targetViewController(forAction: #selector(UIViewController.show(_:sender:)), sender: sender) {
|
||||
@@ -394,10 +426,22 @@ public class FloatingPanelController: UIViewController, UIScrollViewDelegate, UI
|
||||
|
||||
/// Tracks the specified scroll view to correspond with the scroll.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - scrollView: Specify a scroll view to continuously and seamlessly work in concert with interactions of the surface view or nil to cancel it.
|
||||
/// - Attention:
|
||||
/// The specified scroll view must be already assigned to the delegate property because the controller intermediates between the various delegate methods.
|
||||
///
|
||||
public func track(scrollView: UIScrollView) {
|
||||
public func track(scrollView: UIScrollView?) {
|
||||
if let trackingScrollView = floatingPanel.scrollView,
|
||||
let delegate = floatingPanel.userScrollViewDelegate {
|
||||
trackingScrollView.delegate = delegate // restore delegate
|
||||
floatingPanel.userScrollViewDelegate = nil
|
||||
}
|
||||
|
||||
guard let scrollView = scrollView else {
|
||||
floatingPanel.scrollView = nil
|
||||
return
|
||||
}
|
||||
|
||||
floatingPanel.scrollView = scrollView
|
||||
if scrollView.delegate !== floatingPanel {
|
||||
floatingPanel.userScrollViewDelegate = scrollView.delegate
|
||||
@@ -408,7 +452,7 @@ public class FloatingPanelController: UIViewController, UIScrollViewDelegate, UI
|
||||
if #available(iOS 11.0, *) {
|
||||
scrollView.contentInsetAdjustmentBehavior = .never
|
||||
} else {
|
||||
children.forEach { (vc) in
|
||||
childViewControllers.forEach { (vc) in
|
||||
vc.automaticallyAdjustsScrollViewInsets = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,12 +5,21 @@
|
||||
|
||||
import UIKit
|
||||
|
||||
/// FloatingPanelFullScreenLayout
|
||||
///
|
||||
/// Use the layout protocol if you configure full, half and tip insets from the superview, not the safe area.
|
||||
/// It can't be used with FloatingPanelIntrinsicLayout.
|
||||
public protocol FloatingPanelFullScreenLayout: FloatingPanelLayout { }
|
||||
|
||||
/// FloatingPanelIntrinsicLayout
|
||||
///
|
||||
/// Use the layout protocol if you want to layout a panel using the intrinsic height.
|
||||
/// It can't be used with FloatingPanelFullScreenLayout.
|
||||
///
|
||||
/// - Attention:
|
||||
/// `insetFor(position:)` must return `nil` for full position because the inset is determined automatically.
|
||||
/// You can customize insets only for half, tip and hidden positions
|
||||
/// on FloatingPanelIntrinsicLayout.
|
||||
/// `insetFor(position:)` must return `nil` for the full position. Because
|
||||
/// the inset is determined automatically by the intrinsic height.
|
||||
/// You can customize insets only for the half, tip and hidden positions.
|
||||
public protocol FloatingPanelIntrinsicLayout: FloatingPanelLayout { }
|
||||
|
||||
public extension FloatingPanelIntrinsicLayout {
|
||||
@@ -34,7 +43,7 @@ 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 exepct for `hidden` position. Because
|
||||
/// 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.
|
||||
var supportedPositions: Set<FloatingPanelPosition> { get }
|
||||
|
||||
@@ -46,7 +55,7 @@ public protocol FloatingPanelLayout: class {
|
||||
|
||||
/// Returns a CGFloat value to determine a Y coordinate of a floating panel for each position(full, half, tip and hidden).
|
||||
///
|
||||
/// Its returning value indicates a different inset for each positiion.
|
||||
/// Its returning value indicates a different inset for each position.
|
||||
/// For full position, a top inset from a safe area in `FloatingPanelController.view`.
|
||||
/// For half or tip position, a bottom inset from the safe area.
|
||||
/// For hidden position, a bottom inset from `FloatingPanelController.view`.
|
||||
@@ -72,7 +81,7 @@ public extension FloatingPanelLayout {
|
||||
var supportedPositions: Set<FloatingPanelPosition> {
|
||||
return Set([.full, .half, .tip])
|
||||
}
|
||||
|
||||
|
||||
func prepareLayout(surfaceView: UIView, in view: UIView) -> [NSLayoutConstraint] {
|
||||
return [
|
||||
surfaceView.leftAnchor.constraint(equalTo: view.sideLayoutGuide.leftAnchor, constant: 0.0),
|
||||
@@ -86,6 +95,8 @@ public extension FloatingPanelLayout {
|
||||
}
|
||||
|
||||
public class FloatingPanelDefaultLayout: FloatingPanelLayout {
|
||||
public init() { }
|
||||
|
||||
public var initialPosition: FloatingPanelPosition {
|
||||
return .half
|
||||
}
|
||||
@@ -101,6 +112,8 @@ public class FloatingPanelDefaultLayout: FloatingPanelLayout {
|
||||
}
|
||||
|
||||
public class FloatingPanelDefaultLandscapeLayout: FloatingPanelLayout {
|
||||
public init() { }
|
||||
|
||||
public var initialPosition: FloatingPanelPosition {
|
||||
return .tip
|
||||
}
|
||||
@@ -131,13 +144,16 @@ class FloatingPanelLayoutAdapter {
|
||||
|
||||
var safeAreaInsets: UIEdgeInsets = .zero
|
||||
|
||||
private var heightBuffer: CGFloat = 88.0 // For bounce
|
||||
private var initialConst: CGFloat = 0.0
|
||||
|
||||
private var fixedConstraints: [NSLayoutConstraint] = []
|
||||
private var fullConstraints: [NSLayoutConstraint] = []
|
||||
private var halfConstraints: [NSLayoutConstraint] = []
|
||||
private var tipConstraints: [NSLayoutConstraint] = []
|
||||
private var offConstraints: [NSLayoutConstraint] = []
|
||||
private var heightConstraints: [NSLayoutConstraint] = []
|
||||
private var interactiveTopConstraint: NSLayoutConstraint?
|
||||
|
||||
private var heightConstraint: NSLayoutConstraint?
|
||||
|
||||
private var fullInset: CGFloat {
|
||||
if layout is FloatingPanelIntrinsicLayout {
|
||||
@@ -164,9 +180,12 @@ class FloatingPanelLayoutAdapter {
|
||||
|
||||
var topY: CGFloat {
|
||||
if supportedPositions.contains(.full) {
|
||||
if layout is FloatingPanelIntrinsicLayout {
|
||||
switch layout {
|
||||
case is FloatingPanelIntrinsicLayout:
|
||||
return surfaceView.superview!.bounds.height - surfaceView.bounds.height
|
||||
} else {
|
||||
case is FloatingPanelFullScreenLayout:
|
||||
return fullInset
|
||||
default:
|
||||
return (safeAreaInsets.top + fullInset)
|
||||
}
|
||||
} else {
|
||||
@@ -175,12 +194,20 @@ class FloatingPanelLayoutAdapter {
|
||||
}
|
||||
|
||||
var middleY: CGFloat {
|
||||
return surfaceView.superview!.bounds.height - (safeAreaInsets.bottom + halfInset)
|
||||
if layout is FloatingPanelFullScreenLayout {
|
||||
return surfaceView.superview!.bounds.height - halfInset
|
||||
} else{
|
||||
return surfaceView.superview!.bounds.height - (safeAreaInsets.bottom + halfInset)
|
||||
}
|
||||
}
|
||||
|
||||
var bottomY: CGFloat {
|
||||
if supportedPositions.contains(.tip) {
|
||||
return surfaceView.superview!.bounds.height - (safeAreaInsets.bottom + tipInset)
|
||||
if layout is FloatingPanelFullScreenLayout {
|
||||
return surfaceView.superview!.bounds.height - tipInset
|
||||
} else{
|
||||
return surfaceView.superview!.bounds.height - (safeAreaInsets.bottom + tipInset)
|
||||
}
|
||||
} else {
|
||||
return middleY
|
||||
}
|
||||
@@ -190,12 +217,17 @@ class FloatingPanelLayoutAdapter {
|
||||
return surfaceView.superview!.bounds.height
|
||||
}
|
||||
|
||||
var safeAreaBottomY: CGFloat {
|
||||
return surfaceView.superview!.bounds.height - (safeAreaInsets.bottom + hiddenInset)
|
||||
var topMaxY: CGFloat {
|
||||
return layout is FloatingPanelFullScreenLayout ? 0.0 : safeAreaInsets.top
|
||||
}
|
||||
|
||||
var topMaxY: CGFloat { return -safeAreaInsets.top }
|
||||
var bottomMaxY: CGFloat { return safeAreaBottomY }
|
||||
var bottomMaxY: CGFloat {
|
||||
if layout is FloatingPanelFullScreenLayout{
|
||||
return surfaceView.superview!.bounds.height - hiddenInset
|
||||
} else {
|
||||
return surfaceView.superview!.bounds.height - (safeAreaInsets.bottom + hiddenInset)
|
||||
}
|
||||
}
|
||||
|
||||
var adjustedContentInsets: UIEdgeInsets {
|
||||
return UIEdgeInsets(top: 0.0,
|
||||
@@ -226,7 +258,7 @@ class FloatingPanelLayoutAdapter {
|
||||
}
|
||||
|
||||
func updateIntrinsicHeight() {
|
||||
let fittingSize = UIView.layoutFittingCompressedSize
|
||||
let fittingSize = UILayoutFittingCompressedSize
|
||||
var intrinsicHeight = surfaceView.contentView?.systemLayoutSizeFitting(fittingSize).height ?? 0.0
|
||||
var safeAreaBottom: CGFloat = 0.0
|
||||
if #available(iOS 11.0, *) {
|
||||
@@ -262,50 +294,100 @@ class FloatingPanelLayoutAdapter {
|
||||
|
||||
fixedConstraints = surfaceConstraints + backdropConstraints
|
||||
|
||||
// Flexible surface constarints for full, half, tip and off
|
||||
if layout is FloatingPanelIntrinsicLayout {
|
||||
// Flexible surface constraints for full, half, tip and off
|
||||
let topAnchor: NSLayoutYAxisAnchor = {
|
||||
if layout is FloatingPanelFullScreenLayout {
|
||||
return vc.view.topAnchor
|
||||
} else {
|
||||
return vc.layoutGuide.topAnchor
|
||||
}
|
||||
}()
|
||||
|
||||
switch layout {
|
||||
case is FloatingPanelIntrinsicLayout:
|
||||
// Set up on updateHeight()
|
||||
} else {
|
||||
break
|
||||
default:
|
||||
fullConstraints = [
|
||||
surfaceView.topAnchor.constraint(equalTo: vc.layoutGuide.topAnchor,
|
||||
surfaceView.topAnchor.constraint(equalTo: topAnchor,
|
||||
constant: fullInset),
|
||||
]
|
||||
}
|
||||
|
||||
let bottomAnchor: NSLayoutYAxisAnchor = {
|
||||
if layout is FloatingPanelFullScreenLayout {
|
||||
return vc.view.bottomAnchor
|
||||
} else {
|
||||
return vc.layoutGuide.bottomAnchor
|
||||
}
|
||||
}()
|
||||
|
||||
halfConstraints = [
|
||||
surfaceView.topAnchor.constraint(equalTo: vc.layoutGuide.bottomAnchor,
|
||||
surfaceView.topAnchor.constraint(equalTo: bottomAnchor,
|
||||
constant: -halfInset),
|
||||
]
|
||||
tipConstraints = [
|
||||
surfaceView.topAnchor.constraint(equalTo: vc.layoutGuide.bottomAnchor,
|
||||
surfaceView.topAnchor.constraint(equalTo: bottomAnchor,
|
||||
constant: -tipInset),
|
||||
]
|
||||
|
||||
offConstraints = [
|
||||
surfaceView.topAnchor.constraint(equalTo: vc.view.bottomAnchor,
|
||||
surfaceView.topAnchor.constraint(equalTo:vc.view.bottomAnchor,
|
||||
constant: -hiddenInset),
|
||||
]
|
||||
}
|
||||
|
||||
func startInteraction(at state: FloatingPanelPosition) {
|
||||
NSLayoutConstraint.deactivate(fullConstraints + halfConstraints + tipConstraints + offConstraints)
|
||||
|
||||
let interactiveTopConstraint: NSLayoutConstraint
|
||||
switch layout {
|
||||
case is FloatingPanelIntrinsicLayout,
|
||||
is FloatingPanelFullScreenLayout:
|
||||
initialConst = surfaceView.frame.minY
|
||||
interactiveTopConstraint = surfaceView.topAnchor.constraint(equalTo: vc.view.topAnchor,
|
||||
constant: initialConst)
|
||||
default:
|
||||
initialConst = surfaceView.frame.minY - safeAreaInsets.top
|
||||
interactiveTopConstraint = surfaceView.topAnchor.constraint(equalTo: vc.layoutGuide.topAnchor,
|
||||
constant: initialConst)
|
||||
}
|
||||
NSLayoutConstraint.activate([interactiveTopConstraint])
|
||||
self.interactiveTopConstraint = interactiveTopConstraint
|
||||
}
|
||||
|
||||
func endInteraction(at state: FloatingPanelPosition) {
|
||||
// Don't deactivate `interactiveTopConstraint` here because it leads to
|
||||
// unsatisfiable constraints
|
||||
}
|
||||
|
||||
// The method is separated from prepareLayout(to:) for the rotation support
|
||||
// It must be called in FloatingPanelController.traitCollectionDidChange(_:)
|
||||
func updateHeight() {
|
||||
guard let vc = vc else { return }
|
||||
|
||||
NSLayoutConstraint.deactivate(heightConstraints)
|
||||
|
||||
if layout is FloatingPanelIntrinsicLayout {
|
||||
updateIntrinsicHeight()
|
||||
heightConstraints = [
|
||||
surfaceView.heightAnchor.constraint(equalToConstant: intrinsicHeight + safeAreaInsets.bottom),
|
||||
]
|
||||
} else {
|
||||
heightConstraints = [
|
||||
surfaceView.heightAnchor.constraint(equalTo: vc.view.heightAnchor,
|
||||
constant: -(safeAreaInsets.top + fullInset)),
|
||||
]
|
||||
if let const = self.heightConstraint {
|
||||
NSLayoutConstraint.deactivate([const])
|
||||
}
|
||||
NSLayoutConstraint.activate(heightConstraints)
|
||||
|
||||
surfaceView.bottomOverflow = heightBuffer + layout.topInteractionBuffer
|
||||
let heightConstraint: NSLayoutConstraint
|
||||
|
||||
switch layout {
|
||||
case is FloatingPanelIntrinsicLayout:
|
||||
updateIntrinsicHeight()
|
||||
heightConstraint = surfaceView.heightAnchor.constraint(equalToConstant: intrinsicHeight + safeAreaInsets.bottom)
|
||||
case is FloatingPanelFullScreenLayout:
|
||||
heightConstraint = surfaceView.heightAnchor.constraint(equalTo: vc.view.heightAnchor,
|
||||
constant: -fullInset)
|
||||
default:
|
||||
heightConstraint = surfaceView.heightAnchor.constraint(equalTo: vc.view.heightAnchor,
|
||||
constant: -(safeAreaInsets.top + fullInset))
|
||||
}
|
||||
|
||||
NSLayoutConstraint.activate([heightConstraint])
|
||||
self.heightConstraint = heightConstraint
|
||||
|
||||
surfaceView.bottomOverflow = vc.view.bounds.height + layout.topInteractionBuffer
|
||||
|
||||
if layout is FloatingPanelIntrinsicLayout {
|
||||
NSLayoutConstraint.deactivate(fullConstraints)
|
||||
@@ -316,6 +398,40 @@ class FloatingPanelLayoutAdapter {
|
||||
}
|
||||
}
|
||||
|
||||
func updateInteractiveTopConstraint(diff: CGFloat, allowsTopBuffer: Bool) {
|
||||
defer {
|
||||
surfaceView.superview!.layoutIfNeeded() // MUST call here to update `surfaceView.frame`
|
||||
}
|
||||
|
||||
let minY: CGFloat = {
|
||||
var ret: CGFloat = 0.0
|
||||
switch layout {
|
||||
case is FloatingPanelIntrinsicLayout:
|
||||
ret = topY
|
||||
default:
|
||||
ret = fullInset
|
||||
}
|
||||
if allowsTopBuffer {
|
||||
ret -= layout.topInteractionBuffer
|
||||
}
|
||||
return max(ret, 0.0) // The top boundary is equal to the related topAnchor.
|
||||
}()
|
||||
let maxY: CGFloat = {
|
||||
var ret: CGFloat = 0.0
|
||||
switch layout {
|
||||
case is FloatingPanelIntrinsicLayout, is FloatingPanelFullScreenLayout:
|
||||
ret = bottomY
|
||||
default:
|
||||
ret = bottomY - safeAreaInsets.top
|
||||
}
|
||||
ret += layout.bottomInteractionBuffer
|
||||
return min(ret, bottomMaxY)
|
||||
}()
|
||||
let const = initialConst + diff
|
||||
|
||||
interactiveTopConstraint?.constant = max(minY, min(maxY, const))
|
||||
}
|
||||
|
||||
func activateLayout(of state: FloatingPanelPosition) {
|
||||
defer {
|
||||
surfaceView.superview!.layoutIfNeeded()
|
||||
@@ -325,6 +441,11 @@ class FloatingPanelLayoutAdapter {
|
||||
|
||||
setBackdropAlpha(of: state)
|
||||
|
||||
// Must deactivate `interactiveTopConstraint` here
|
||||
if let interactiveTopConstraint = interactiveTopConstraint {
|
||||
NSLayoutConstraint.deactivate([interactiveTopConstraint])
|
||||
self.interactiveTopConstraint = nil
|
||||
}
|
||||
NSLayoutConstraint.activate(fixedConstraints)
|
||||
|
||||
if supportedPositions.union([.hidden]).contains(state) == false {
|
||||
@@ -344,7 +465,7 @@ class FloatingPanelLayoutAdapter {
|
||||
}
|
||||
}
|
||||
|
||||
func setBackdropAlpha(of target: FloatingPanelPosition) {
|
||||
private func setBackdropAlpha(of target: FloatingPanelPosition) {
|
||||
if target == .hidden {
|
||||
self.backdropView.alpha = 0.0
|
||||
} else {
|
||||
|
||||
@@ -13,7 +13,7 @@ public class FloatingPanelSurfaceView: UIView {
|
||||
/// A GrabberHandleView object displayed at the top of the surface view.
|
||||
///
|
||||
/// To use a custom grabber handle, hide this and then add the custom one
|
||||
/// to the surface view at appropirate coordinates.
|
||||
/// to the surface view at appropriate coordinates.
|
||||
public var grabberHandle: GrabberHandleView!
|
||||
|
||||
/// The height of the grabber bar area
|
||||
@@ -59,7 +59,8 @@ public class FloatingPanelSurfaceView: UIView {
|
||||
/// The color of the surface border.
|
||||
public var borderWidth: CGFloat = 0.0 { didSet { setNeedsLayout() } }
|
||||
|
||||
private var backgroundLayer: CAShapeLayer! { didSet { setNeedsLayout() } }
|
||||
private var backgroundView: UIView!
|
||||
private var backgroundHeightConstraint: NSLayoutConstraint!
|
||||
|
||||
private struct Default {
|
||||
public static let grabberTopPadding: CGFloat = 6.0
|
||||
@@ -70,7 +71,7 @@ public class FloatingPanelSurfaceView: UIView {
|
||||
render()
|
||||
}
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
required public init?(coder aDecoder: NSCoder) {
|
||||
super.init(coder: aDecoder)
|
||||
render()
|
||||
}
|
||||
@@ -79,9 +80,19 @@ public class FloatingPanelSurfaceView: UIView {
|
||||
super.backgroundColor = .clear
|
||||
self.clipsToBounds = false
|
||||
|
||||
let backgroundLayer = CAShapeLayer()
|
||||
layer.insertSublayer(backgroundLayer, at: 0)
|
||||
self.backgroundLayer = backgroundLayer
|
||||
let backgroundView = UIView()
|
||||
addSubview(backgroundView)
|
||||
self.backgroundView = backgroundView
|
||||
|
||||
backgroundView.translatesAutoresizingMaskIntoConstraints = false
|
||||
backgroundHeightConstraint = backgroundView.heightAnchor.constraint(equalTo: heightAnchor, multiplier: 1.0)
|
||||
NSLayoutConstraint.activate([
|
||||
backgroundView.topAnchor.constraint(equalTo: topAnchor, constant: 0.0),
|
||||
backgroundView.leftAnchor.constraint(equalTo: leftAnchor, constant: 0.0),
|
||||
backgroundView.rightAnchor.constraint(equalTo: rightAnchor, constant: 0.0),
|
||||
backgroundHeightConstraint,
|
||||
])
|
||||
|
||||
|
||||
let grabberHandle = GrabberHandleView()
|
||||
addSubview(grabberHandle)
|
||||
@@ -96,9 +107,14 @@ public class FloatingPanelSurfaceView: UIView {
|
||||
])
|
||||
}
|
||||
|
||||
public override func updateConstraints() {
|
||||
super.updateConstraints()
|
||||
backgroundHeightConstraint.constant = bottomOverflow
|
||||
}
|
||||
|
||||
public override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
log.debug("SurfaceView frame", frame)
|
||||
log.debug("surface view frame = \(frame)")
|
||||
|
||||
updateLayers()
|
||||
updateContentViewMask()
|
||||
@@ -109,16 +125,10 @@ public class FloatingPanelSurfaceView: UIView {
|
||||
}
|
||||
|
||||
private func updateLayers() {
|
||||
log.debug("SurfaceView bounds", bounds)
|
||||
|
||||
var rect = bounds
|
||||
rect.size.height += bottomOverflow // Expand the height for overflow buffer
|
||||
let path = UIBezierPath(roundedRect: rect,
|
||||
byRoundingCorners: [.topLeft, .topRight],
|
||||
cornerRadii: CGSize(width: cornerRadius, height: cornerRadius))
|
||||
backgroundLayer.path = path.cgPath
|
||||
backgroundLayer.fillColor = color?.cgColor
|
||||
|
||||
backgroundView.backgroundColor = color
|
||||
backgroundView.layer.masksToBounds = true
|
||||
backgroundView.layer.cornerRadius = cornerRadius
|
||||
|
||||
if shadowHidden == false {
|
||||
layer.shadowColor = shadowColor.cgColor
|
||||
layer.shadowOffset = shadowOffset
|
||||
@@ -130,16 +140,11 @@ public class FloatingPanelSurfaceView: UIView {
|
||||
private func updateContentViewMask() {
|
||||
if #available(iOS 11, *) {
|
||||
// Don't use `contentView.clipToBounds` because it prevents content view from expanding the height of a subview of it
|
||||
// for the bottom overflow like Auto Layout settings of UIVisualEffectView in Main.storyborad of Example/Maps.
|
||||
// for the bottom overflow like Auto Layout settings of UIVisualEffectView in Main.storyboard of Example/Maps.
|
||||
// Because the bottom of contentView must be fit to the bottom of a screen to work the `safeLayoutGuide` of a content VC.
|
||||
let maskLayer = CAShapeLayer()
|
||||
var rect = bounds
|
||||
rect.size.height += bottomOverflow
|
||||
let path = UIBezierPath(roundedRect: rect,
|
||||
byRoundingCorners: [.topLeft, .topRight],
|
||||
cornerRadii: CGSize(width: cornerRadius, height: cornerRadius))
|
||||
maskLayer.path = path.cgPath
|
||||
contentView?.layer.mask = maskLayer
|
||||
contentView?.layer.masksToBounds = true
|
||||
contentView?.layer.cornerRadius = cornerRadius
|
||||
contentView?.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
|
||||
} else {
|
||||
// Don't use `contentView.layer.mask` because of a UIVisualEffectView issue in iOS 10, https://forums.developer.apple.com/thread/50854
|
||||
// Instead, a user can mask the content view manually in an application.
|
||||
|
||||
@@ -22,7 +22,11 @@ class FloatingPanelModalTransition: NSObject, UIViewControllerTransitioningDeleg
|
||||
}
|
||||
|
||||
class FloatingPanelPresentationController: UIPresentationController {
|
||||
override func presentationTransitionWillBegin() { }
|
||||
override func presentationTransitionWillBegin() {
|
||||
// Must call here even if duplicating on in containerViewWillLayoutSubviews()
|
||||
// Because it let the floating panel present correctly with the presentation animation
|
||||
addFloatingPanel()
|
||||
}
|
||||
|
||||
override func presentationTransitionDidEnd(_ completed: Bool) {
|
||||
// For non-animated presentation
|
||||
@@ -39,26 +43,23 @@ class FloatingPanelPresentationController: UIPresentationController {
|
||||
}
|
||||
fpc.view.removeFromSuperview()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
override func containerViewWillLayoutSubviews() {
|
||||
guard
|
||||
let containerView = self.containerView,
|
||||
let fpc = presentedViewController as? FloatingPanelController,
|
||||
let fpView = fpc.view
|
||||
let fpc = presentedViewController as? FloatingPanelController
|
||||
else { fatalError() }
|
||||
|
||||
containerView.addSubview(fpView)
|
||||
fpView.frame = containerView.bounds
|
||||
fpView.translatesAutoresizingMaskIntoConstraints = false
|
||||
NSLayoutConstraint.activate([
|
||||
fpView.topAnchor.constraint(equalTo: containerView.topAnchor, constant: 0.0),
|
||||
fpView.leftAnchor.constraint(equalTo: containerView.leftAnchor, constant: 0.0),
|
||||
fpView.rightAnchor.constraint(equalTo: containerView.rightAnchor, constant: 0.0),
|
||||
fpView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor, constant: 0.0),
|
||||
])
|
||||
/*
|
||||
* Layout the views managed by `FloatingPanelController` here for the
|
||||
* sake of the presentation and dismissal modally from the controller.
|
||||
*/
|
||||
addFloatingPanel()
|
||||
|
||||
// Forward touch events to the presenting view controller
|
||||
(fpc.view as? FloatingPanelPassThroughView)?.eventForwardingView = presentingViewController.view
|
||||
|
||||
// Set tap-to-dismiss in the backdrop view
|
||||
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleBackdrop(tapGesture:)))
|
||||
fpc.backdropView.addGestureRecognizer(tapGesture)
|
||||
}
|
||||
@@ -66,6 +67,17 @@ class FloatingPanelPresentationController: UIPresentationController {
|
||||
@objc func handleBackdrop(tapGesture: UITapGestureRecognizer) {
|
||||
presentedViewController.dismiss(animated: true, completion: nil)
|
||||
}
|
||||
|
||||
private func addFloatingPanel() {
|
||||
guard
|
||||
let containerView = self.containerView,
|
||||
let fpc = presentedViewController as? FloatingPanelController
|
||||
else { fatalError() }
|
||||
|
||||
containerView.addSubview(fpc.view)
|
||||
fpc.view.frame = containerView.bounds
|
||||
fpc.view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
||||
}
|
||||
}
|
||||
|
||||
class FloatingPanelModalPresentTransition: NSObject, UIViewControllerAnimatedTransitioning {
|
||||
|
||||
@@ -6,13 +6,14 @@
|
||||
import UIKit
|
||||
|
||||
class FloatingPanelPassThroughView: UIView {
|
||||
public weak var eventForwardingView: UIView?
|
||||
public override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||
let view = super.hitTest(point, with: event)
|
||||
switch view {
|
||||
case is FloatingPanelPassThroughView:
|
||||
return nil
|
||||
let hitView = super.hitTest(point, with: event)
|
||||
switch hitView {
|
||||
case self:
|
||||
return eventForwardingView?.hitTest(self.convert(point, to: eventForwardingView), with: event)
|
||||
default:
|
||||
return view
|
||||
return hitView
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ public class GrabberHandleView: UIView {
|
||||
public static let barColor = UIColor(displayP3Red: 0.76, green: 0.77, blue: 0.76, alpha: 1.0)
|
||||
}
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
required public init?(coder aDecoder: NSCoder) {
|
||||
super.init(coder: aDecoder)
|
||||
render()
|
||||
}
|
||||
|
||||
@@ -10,11 +10,12 @@ var log = {
|
||||
return Logger()
|
||||
}()
|
||||
|
||||
#if __FP_LOG
|
||||
struct Logger {
|
||||
private let osLog: OSLog
|
||||
private let s = DispatchSemaphore(value: 1)
|
||||
|
||||
enum Level: Int, Comparable {
|
||||
private enum Level: Int, Comparable {
|
||||
case debug = 0
|
||||
case info = 1
|
||||
case warning = 2
|
||||
@@ -55,17 +56,16 @@ struct Logger {
|
||||
}
|
||||
}
|
||||
|
||||
public static func < (lhs: Logger.Level, rhs: Logger.Level) -> Bool {
|
||||
static func < (lhs: Logger.Level, rhs: Logger.Level) -> Bool {
|
||||
return lhs.rawValue < rhs.rawValue
|
||||
}
|
||||
}
|
||||
|
||||
init() {
|
||||
fileprivate init() {
|
||||
osLog = OSLog(subsystem: "com.scenee.FloatingPanel", category: "FloatingPanel")
|
||||
}
|
||||
|
||||
private func log(_ level: Level, _ message: Any, _ arguments: [Any], function: String, line: UInt) {
|
||||
#if __FP_LOG
|
||||
_ = s.wait(timeout: .now() + 0.033)
|
||||
defer { s.signal() }
|
||||
|
||||
@@ -73,7 +73,6 @@ struct Logger {
|
||||
let log = "\(level.shortName) \(message) \(extraMessage) (\(function):\(line))"
|
||||
|
||||
os_log("%@", log: osLog, type: level.osLogType, log)
|
||||
#endif
|
||||
}
|
||||
|
||||
private func getPrettyFunction(_ function: String, _ file: String) -> String {
|
||||
@@ -104,3 +103,12 @@ struct Logger {
|
||||
self.log(.fault, log, arguments, function: getPrettyFunction(function, file), line: line)
|
||||
}
|
||||
}
|
||||
#else
|
||||
struct Logger {
|
||||
func debug(_ log: Any, _ arguments: Any...) { }
|
||||
func info(_ log: Any, _ arguments: Any...) { }
|
||||
func warning(_ log: Any, _ arguments: Any...) { }
|
||||
func error(_ log: Any, _ arguments: Any...) { }
|
||||
func fault(_ log: Any, _ arguments: Any...) { }
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -73,15 +73,16 @@ extension UIView {
|
||||
}
|
||||
}
|
||||
|
||||
extension UIGestureRecognizer.State: CustomDebugStringConvertible {
|
||||
|
||||
extension UIGestureRecognizerState: CustomDebugStringConvertible {
|
||||
public var debugDescription: String {
|
||||
switch self {
|
||||
case .began: return "Began"
|
||||
case .changed: return "Changed"
|
||||
case .failed: return "Failed"
|
||||
case .cancelled: return "Cancelled"
|
||||
case .ended: return "Endeded"
|
||||
case .possible: return "Possible"
|
||||
case .began: return "began"
|
||||
case .changed: return "changed"
|
||||
case .failed: return "failed"
|
||||
case .cancelled: return "cancelled"
|
||||
case .ended: return "endeded"
|
||||
case .possible: return "possible"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -100,3 +101,10 @@ extension UISpringTimingParameters {
|
||||
self.init(mass: mass, stiffness: stiffness, damping: damp, initialVelocity: initialVelocity)
|
||||
}
|
||||
}
|
||||
|
||||
extension CGPoint {
|
||||
static var nan: CGPoint {
|
||||
return CGPoint(x: CGFloat.nan,
|
||||
y: CGFloat.nan)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
[](https://cocoapods.org/pods/FloatingPanel)
|
||||
[](https://github.com/Carthage/Carthage)
|
||||
[](https://cocoapods.org/pods/FloatingPanel)
|
||||
[](https://swift.org/)
|
||||
[](https://swift.org/)
|
||||
|
||||
# FloatingPanel
|
||||
@@ -66,7 +67,7 @@ Examples are here.
|
||||
|
||||
## Requirements
|
||||
|
||||
FloatingPanel is written in Swift 4.2. Compatible with iOS 10.0+
|
||||
FloatingPanel is written in Swift. It can be built by Xcode 9.4.1 or later. Compatible with iOS 10.0+.
|
||||
|
||||
## Installation
|
||||
|
||||
@@ -138,9 +139,9 @@ fpc.isRemovalInteractionEnabled = true // Optional: Let it removable by a swipe-
|
||||
self.present(fpc, animated: true, completion: nil)
|
||||
```
|
||||
|
||||
You can show a floating panel over UINavigationController from the containnee view controllers as a modality of `.overCurrentContext` style.
|
||||
You can show a floating panel over UINavigationController from the container view controllers as a modality of `.overCurrentContext` style.
|
||||
|
||||
NOTE: FloatingPanelController has the custom presentation controller. If you would like to customize the presentation/dismissal, please see [FloatingPanelTransitioning](https://github.com/SCENEE/FloatingPanel/blob/feat-modality/Framework/Sources/FloatingPanelTransitioning.swift).
|
||||
NOTE: FloatingPanelController has the custom presentation controller. If you would like to customize the presentation/dismissal, please see [FloatingPanelTransitioning](https://github.com/SCENEE/FloatingPanel/blob/master/Framework/Sources/FloatingPanelTransitioning.swift).
|
||||
|
||||
## View hierarchy
|
||||
|
||||
@@ -250,7 +251,7 @@ class FloatingPanelLandscapeLayout: FloatingPanelLayout {
|
||||
|
||||
public func prepareLayout(surfaceView: UIView, in view: UIView) -> [NSLayoutConstraint] {
|
||||
return [
|
||||
surfaceView.leftAnchor.constraint(equalTo: view.safeAreaLayoutGuid.leftAnchor, constant: 8.0),
|
||||
surfaceView.leftAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leftAnchor, constant: 8.0),
|
||||
surfaceView.widthAnchor.constraint(equalToConstant: 291),
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user