Compare commits
55 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 11f0e8c84e | |||
| dd19c866d4 | |||
| 801fed9843 | |||
| 847b5c0917 | |||
| c64056ca7b | |||
| 269c3e29b5 | |||
| 002bbb4a4a | |||
| 14011a5bc2 | |||
| 23f2242c9a | |||
| 4fd92a4002 | |||
| 9c57089b0e | |||
| 3b11cdc72a | |||
| 4edaad2cf4 | |||
| 92fc0621e2 | |||
| e9f4392c48 | |||
| 4df40becaf | |||
| ba11e7c7d7 | |||
| ae671f22c6 | |||
| f22f58212b | |||
| 54ff1c360d | |||
| 772d6c3ef3 | |||
| a94c3b3c26 | |||
| d0ffc4ceb1 | |||
| 597ce487aa | |||
| 87eb8d94fd | |||
| 4944fc516a | |||
| 7537384339 | |||
| 8fd134512f | |||
| f566fc6475 | |||
| 9cbcb48a9b | |||
| 817956cef3 | |||
| 3c1aa7aa42 | |||
| 7598e8f160 | |||
| ba011e7242 | |||
| ecdf20db8f | |||
| 3a7f39321c | |||
| e75d83e7a4 | |||
| 2cdb4a6bc2 | |||
| 1a4d5a7954 | |||
| 22ef3e7cd9 | |||
| f8b8176988 | |||
| 0c5bf2bfe9 | |||
| 8b45517915 | |||
| 3cca07fefd | |||
| 276ae23f13 | |||
| c1b2ffeb78 | |||
| 262ee34201 | |||
| 53719bd94a | |||
| 935b7d9e10 | |||
| e3bf19b972 | |||
| c36f09d3e9 | |||
| 9936a89118 | |||
| 562424cd8f | |||
| 0c3fb83d0a | |||
| 23846dbf23 |
+14
-9
@@ -22,7 +22,7 @@ jobs:
|
||||
osx_image: xcode10
|
||||
name: "Swift 4.2"
|
||||
- script: xcodebuild -scheme FloatingPanel SWIFT_VERSION=5.0 clean build
|
||||
osx_image: xcode10.2
|
||||
osx_image: xcode10.3
|
||||
name: "Swift 5.0"
|
||||
- script: xcodebuild -scheme FloatingPanel SWIFT_VERSION=5.1 SUPPORTS_MACCATALYST=NO clean build
|
||||
# SUPPORTS_MACCATALYST=NO because Xcode 11 runs on macOS 10.14 in Travis CI
|
||||
@@ -30,29 +30,32 @@ jobs:
|
||||
name: "Swift 5.1"
|
||||
|
||||
- stage: "Tests"
|
||||
osx_image: xcode10.2
|
||||
osx_image: xcode10.3
|
||||
script: xcodebuild clean test -scheme FloatingPanel -workspace FloatingPanel.xcworkspace -destination 'platform=iOS Simulator,OS=10.3.1,name=iPhone SE'
|
||||
name: "iPhone SE (iOS 10.3)"
|
||||
- script: xcodebuild clean test -scheme FloatingPanel -workspace FloatingPanel.xcworkspace -destination 'platform=iOS Simulator,OS=11.4,name=iPhone 7'
|
||||
osx_image: xcode10.2
|
||||
osx_image: xcode10.3
|
||||
name: "iPhone 7 (iOS 11.4)"
|
||||
- script: xcodebuild clean test -scheme FloatingPanel -workspace FloatingPanel.xcworkspace -destination 'platform=iOS Simulator,OS=12.2,name=iPhone X'
|
||||
osx_image: xcode10.2
|
||||
osx_image: xcode10.3
|
||||
name: "iPhone X (iOS 12.2)"
|
||||
- script: xcodebuild clean test -scheme FloatingPanel -workspace FloatingPanel.xcworkspace -destination 'platform=iOS Simulator,OS=13.0,name=iPhone 11'
|
||||
osx_image: xcode11
|
||||
name: "iPhone X (iOS 13.0)"
|
||||
|
||||
- stage: Build examples
|
||||
osx_image: xcode10.2
|
||||
osx_image: xcode11
|
||||
script: xcodebuild -scheme Maps -sdk iphonesimulator clean build
|
||||
name: "Maps"
|
||||
- script: xcodebuild -scheme Stocks -sdk iphonesimulator clean build
|
||||
osx_image: xcode10.2
|
||||
osx_image: xcode11
|
||||
name: "Stocks"
|
||||
- script: xcodebuild -scheme Samples -sdk iphonesimulator clean build
|
||||
osx_image: xcode10.2
|
||||
osx_image: xcode11
|
||||
name: "Samples"
|
||||
|
||||
- stage: Carthage
|
||||
osx_image: xcode10.2
|
||||
osx_image: xcode11
|
||||
before_install:
|
||||
- brew update
|
||||
- brew outdated carthage || brew upgrade carthage
|
||||
@@ -60,7 +63,9 @@ jobs:
|
||||
- carthage build --no-skip-current
|
||||
|
||||
- stage: CocoaPods
|
||||
osx_image: xcode10.2
|
||||
osx_image: xcode11
|
||||
before_install:
|
||||
- gem install cocoapods
|
||||
script:
|
||||
- pod spec lint --allow-warnings
|
||||
- pod lib lint --allow-warnings
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
543844BD23D2BE2000D5EDE4 /* MapKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 543844BC23D2BE2000D5EDE4 /* MapKit.framework */; };
|
||||
549D23D2233C77D5008EF4D7 /* FloatingPanel.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 549D23D1233C77D5008EF4D7 /* FloatingPanel.framework */; };
|
||||
549D23D3233C77D5008EF4D7 /* FloatingPanel.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 549D23D1233C77D5008EF4D7 /* FloatingPanel.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
|
||||
54B5112A216C3D840033A6F3 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54B51129216C3D840033A6F3 /* AppDelegate.swift */; };
|
||||
@@ -31,6 +32,7 @@
|
||||
/* End PBXCopyFilesBuildPhase section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
543844BC23D2BE2000D5EDE4 /* MapKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MapKit.framework; path = System/Library/Frameworks/MapKit.framework; sourceTree = SDKROOT; };
|
||||
549D23D1233C77D5008EF4D7 /* FloatingPanel.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = FloatingPanel.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
54B51126216C3D840033A6F3 /* Maps.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Maps.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
54B51129216C3D840033A6F3 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||
@@ -46,6 +48,7 @@
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
543844BD23D2BE2000D5EDE4 /* MapKit.framework in Frameworks */,
|
||||
549D23D2233C77D5008EF4D7 /* FloatingPanel.framework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
@@ -53,12 +56,21 @@
|
||||
/* End PBXFrameworksBuildPhase section */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
543844BB23D2BE1F00D5EDE4 /* Frameworks */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
543844BC23D2BE2000D5EDE4 /* MapKit.framework */,
|
||||
);
|
||||
name = Frameworks;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
54B5111D216C3D840033A6F3 = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
549D23D1233C77D5008EF4D7 /* FloatingPanel.framework */,
|
||||
54B51128216C3D840033A6F3 /* Maps */,
|
||||
54B51127216C3D840033A6F3 /* Products */,
|
||||
543844BB23D2BE1F00D5EDE4 /* Frameworks */,
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
|
||||
@@ -107,6 +107,7 @@ class ViewController: UIViewController, MKMapViewDelegate, UISearchBarDelegate,
|
||||
let progress = max(0.0, min((tipY - y) / 44.0, 1.0))
|
||||
self.searchVC.tableView.alpha = progress
|
||||
}
|
||||
debugPrint("NearbyPosition : ",vc.nearbyPosition)
|
||||
}
|
||||
|
||||
func floatingPanelWillBeginDragging(_ vc: FloatingPanelController) {
|
||||
@@ -177,7 +178,7 @@ class SearchPanelViewController: UIViewController, UITableViewDataSource, UITabl
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
return 2
|
||||
return 100
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||
@@ -188,12 +189,10 @@ class SearchPanelViewController: UIViewController, UITableViewDataSource, UITabl
|
||||
cell.iconImageView.image = UIImage(named: "mark")
|
||||
cell.titleLabel.text = "Marked Location"
|
||||
cell.subTitleLabel.text = "Golden Gate Bridge, San Francisco"
|
||||
case 1:
|
||||
default:
|
||||
cell.iconImageView.image = UIImage(named: "like")
|
||||
cell.titleLabel.text = "Favorites"
|
||||
cell.subTitleLabel.text = "0 Places"
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
return cell
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="14460.31" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="RoN-h0-uBD">
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="14490.70" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="RoN-h0-uBD">
|
||||
<device id="retina5_9" orientation="portrait">
|
||||
<adaptation id="fullscreen"/>
|
||||
</device>
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14460.20"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14490.49"/>
|
||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
@@ -537,6 +537,23 @@
|
||||
<action selector="closeWithSender:" destination="YC8-ae-15L" eventType="touchUpInside" id="Z2v-19-S5k"/>
|
||||
</connections>
|
||||
</button>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" distribution="fillProportionally" alignment="center" spacing="8" translatesAutoresizingMaskIntoConstraints="NO" id="qux-uG-4o2">
|
||||
<rect key="frame" x="8" y="52" width="148.33333333333334" height="31"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="fitToBounds" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="7lq-d3-PKi">
|
||||
<rect key="frame" x="0.0" y="5.3333333333333357" width="91.333333333333329" height="20.333333333333332"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<switch opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" contentHorizontalAlignment="center" contentVerticalAlignment="center" on="YES" translatesAutoresizingMaskIntoConstraints="NO" id="0MA-lV-KjS">
|
||||
<rect key="frame" x="99.333333333333329" y="0.0" width="50.999999999999986" height="31"/>
|
||||
<connections>
|
||||
<action selector="modeChanged:" destination="YC8-ae-15L" eventType="valueChanged" id="IQ8-u2-Rib"/>
|
||||
</connections>
|
||||
</switch>
|
||||
</subviews>
|
||||
</stackView>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="22" translatesAutoresizingMaskIntoConstraints="NO" id="tP3-oJ-4EB">
|
||||
<rect key="frame" x="130.66666666666666" y="132" width="114" height="134"/>
|
||||
<subviews>
|
||||
@@ -577,9 +594,11 @@
|
||||
<constraint firstItem="tP3-oJ-4EB" firstAttribute="top" secondItem="aOK-7l-cA6" secondAttribute="top" constant="88" id="Zhb-Ss-epe"/>
|
||||
<constraint firstItem="Kva-Z7-0qY" firstAttribute="trailing" secondItem="aOK-7l-cA6" secondAttribute="trailing" id="kkp-Yo-FQW"/>
|
||||
<constraint firstItem="aOK-7l-cA6" firstAttribute="trailing" secondItem="noi-1a-5bZ" secondAttribute="trailing" constant="12" id="lv9-Nf-HNB"/>
|
||||
<constraint firstItem="qux-uG-4o2" firstAttribute="top" secondItem="aOK-7l-cA6" secondAttribute="top" constant="8" id="naa-cf-ZIc"/>
|
||||
<constraint firstItem="Kva-Z7-0qY" firstAttribute="leading" secondItem="aOK-7l-cA6" secondAttribute="leading" id="oVC-i1-TwS"/>
|
||||
<constraint firstItem="aOK-7l-cA6" firstAttribute="bottom" secondItem="Kva-Z7-0qY" secondAttribute="bottom" id="rW2-mF-5DR"/>
|
||||
<constraint firstItem="8yw-OC-Ubk" firstAttribute="top" relation="greaterThanOrEqual" secondItem="tP3-oJ-4EB" secondAttribute="bottom" constant="88" id="vKQ-h9-uKt"/>
|
||||
<constraint firstItem="qux-uG-4o2" firstAttribute="leading" secondItem="g7l-kO-y7q" secondAttribute="leading" constant="8" id="zXb-R9-bMO"/>
|
||||
</constraints>
|
||||
<viewLayoutGuide key="safeArea" id="aOK-7l-cA6"/>
|
||||
<connections>
|
||||
@@ -591,6 +610,8 @@
|
||||
<size key="freeformSize" width="375" height="778"/>
|
||||
<connections>
|
||||
<outlet property="closeButton" destination="noi-1a-5bZ" id="eWQ-ha-8y7"/>
|
||||
<outlet property="intrinsicHeightConstraint" destination="vKQ-h9-uKt" id="QpA-WD-b17"/>
|
||||
<outlet property="modeChangeView" destination="qux-uG-4o2" id="1Nq-fE-dXw"/>
|
||||
<segue destination="bYI-y3-Rzb" kind="show" identifier="ShowSegue" id="r1P-2i-NDe"/>
|
||||
</connections>
|
||||
</viewController>
|
||||
@@ -611,7 +632,7 @@
|
||||
</connections>
|
||||
</pongPressGestureRecognizer>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="655" y="734"/>
|
||||
<point key="canvasLocation" x="653.60000000000002" y="733.74384236453204"/>
|
||||
</scene>
|
||||
<!--Debug Text View Controller-->
|
||||
<scene sceneID="Bkq-O7-q4A">
|
||||
|
||||
@@ -17,13 +17,15 @@ class SampleListViewController: UIViewController {
|
||||
case trackingTextView
|
||||
case showDetail
|
||||
case showModal
|
||||
case showFloatingPanelModal
|
||||
case showPanelModal
|
||||
case showTabBar
|
||||
case showPageView
|
||||
case showPageContentView
|
||||
case showNestedScrollView
|
||||
case showRemovablePanel
|
||||
case showIntrinsicView
|
||||
case showContentInset
|
||||
case showContainerMargins
|
||||
|
||||
var name: String {
|
||||
switch self {
|
||||
@@ -31,13 +33,15 @@ class SampleListViewController: UIViewController {
|
||||
case .trackingTextView: return "Scroll tracking(TextView)"
|
||||
case .showDetail: return "Show Detail Panel"
|
||||
case .showModal: return "Show Modal"
|
||||
case .showFloatingPanelModal: return "Show Floating Panel Modal"
|
||||
case .showPanelModal: return "Show Panel Modal"
|
||||
case .showTabBar: return "Show Tab Bar"
|
||||
case .showPageView: return "Show Page View"
|
||||
case .showPageContentView: return "Show Page Content View"
|
||||
case .showNestedScrollView: return "Show Nested ScrollView"
|
||||
case .showRemovablePanel: return "Show Removable Panel"
|
||||
case .showIntrinsicView: return "Show Intrinsic View"
|
||||
case .showContentInset: return "Show with ContentInset"
|
||||
case .showContainerMargins: return "Show with ContainerMargins"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,13 +51,15 @@ class SampleListViewController: UIViewController {
|
||||
case .trackingTextView: return "ConsoleViewController"
|
||||
case .showDetail: return "DetailViewController"
|
||||
case .showModal: return "ModalViewController"
|
||||
case .showFloatingPanelModal: return nil
|
||||
case .showPanelModal: return nil
|
||||
case .showTabBar: return "TabBarViewController"
|
||||
case .showPageView: return nil
|
||||
case .showPageContentView: return nil
|
||||
case .showNestedScrollView: return "NestedScrollViewController"
|
||||
case .showRemovablePanel: return "DetailViewController"
|
||||
case .showIntrinsicView: return "IntrinsicViewController"
|
||||
case .showContentInset: return nil
|
||||
case .showContainerMargins: return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -67,18 +73,7 @@ class SampleListViewController: UIViewController {
|
||||
var mainPanelObserves: [NSKeyValueObservation] = []
|
||||
var settingsObserves: [NSKeyValueObservation] = []
|
||||
|
||||
lazy var pages: [UIViewController] = {
|
||||
let page1 = FloatingPanelController(delegate: self)
|
||||
page1.view.backgroundColor = .blue
|
||||
page1.show()
|
||||
let page2 = FloatingPanelController(delegate: self)
|
||||
page2.view.backgroundColor = .red
|
||||
page2.show()
|
||||
let page3 = FloatingPanelController(delegate: self)
|
||||
page3.view.backgroundColor = .green
|
||||
page3.show()
|
||||
return [page1, page2, page3]
|
||||
}()
|
||||
var pages: [UIViewController] = []
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
@@ -119,6 +114,8 @@ class SampleListViewController: UIViewController {
|
||||
func addMainPanel(with contentVC: UIViewController) {
|
||||
mainPanelObserves.removeAll()
|
||||
|
||||
let oldMainPanelVC = mainPanelVC
|
||||
|
||||
// Initialize FloatingPanelController
|
||||
mainPanelVC = FloatingPanelController()
|
||||
mainPanelVC.delegate = self
|
||||
@@ -137,6 +134,10 @@ class SampleListViewController: UIViewController {
|
||||
tapGesture.cancelsTouchesInView = false
|
||||
tapGesture.numberOfTapsRequired = 2
|
||||
mainPanelVC.surfaceView.addGestureRecognizer(tapGesture)
|
||||
case .showPageContentView:
|
||||
if let page = (mainPanelVC.contentViewController as? UIPageViewController)?.viewControllers?.first {
|
||||
mainPanelVC.track(scrollView: (page as! DebugTableViewController).tableView)
|
||||
}
|
||||
case .showRemovablePanel, .showIntrinsicView:
|
||||
mainPanelVC.isRemovalInteractionEnabled = true
|
||||
|
||||
@@ -164,7 +165,13 @@ class SampleListViewController: UIViewController {
|
||||
}
|
||||
|
||||
// Add FloatingPanel to self.view
|
||||
mainPanelVC.addPanel(toParent: self, belowView: nil, animated: true)
|
||||
if let oldMainPanelVC = oldMainPanelVC {
|
||||
oldMainPanelVC.removePanelFromParent(animated: true, completion: {
|
||||
self.mainPanelVC.addPanel(toParent: self, belowView: nil, animated: true)
|
||||
})
|
||||
} else {
|
||||
mainPanelVC.addPanel(toParent: self, belowView: nil, animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
@objc
|
||||
@@ -251,6 +258,8 @@ extension SampleListViewController: UITableViewDelegate {
|
||||
}()
|
||||
|
||||
self.currentMenu = menu
|
||||
detailPanelVC?.removePanelFromParent(animated: true, completion: nil)
|
||||
detailPanelVC = nil
|
||||
|
||||
switch menu {
|
||||
case .showDetail:
|
||||
@@ -258,6 +267,7 @@ extension SampleListViewController: UITableViewDelegate {
|
||||
|
||||
// Initialize FloatingPanelController
|
||||
detailPanelVC = FloatingPanelController()
|
||||
detailPanelVC.delegate = self
|
||||
|
||||
// Initialize FloatingPanelController and add the view
|
||||
detailPanelVC.surfaceView.cornerRadius = 6.0
|
||||
@@ -266,6 +276,9 @@ extension SampleListViewController: UITableViewDelegate {
|
||||
// Set a content view controller
|
||||
detailPanelVC.set(contentViewController: contentVC)
|
||||
|
||||
detailPanelVC.contentMode = .fitToBounds
|
||||
(contentVC as? DetailViewController)?.intrinsicHeightConstraint.priority = .defaultLow
|
||||
|
||||
// Add FloatingPanel to self.view
|
||||
detailPanelVC.addPanel(toParent: self, belowView: nil, animated: true)
|
||||
case .showModal, .showTabBar:
|
||||
@@ -274,6 +287,13 @@ extension SampleListViewController: UITableViewDelegate {
|
||||
present(modalVC, animated: true, completion: nil)
|
||||
|
||||
case .showPageView:
|
||||
pages = [UIColor.blue, .red, .green].compactMap({ (color) -> UIViewController in
|
||||
let page = FloatingPanelController(delegate: self)
|
||||
page.view.backgroundColor = color
|
||||
page.show()
|
||||
return page
|
||||
})
|
||||
|
||||
let pageVC = UIPageViewController(transitionStyle: .scroll, navigationOrientation: .horizontal, options: [:])
|
||||
let closeButton = UIButton(type: .custom)
|
||||
pageVC.view.addSubview(closeButton)
|
||||
@@ -289,9 +309,18 @@ extension SampleListViewController: UITableViewDelegate {
|
||||
pageVC.modalPresentationStyle = .fullScreen
|
||||
present(pageVC, animated: true, completion: nil)
|
||||
|
||||
case .showFloatingPanelModal:
|
||||
case .showPageContentView:
|
||||
pages = [DebugTableViewController(), DebugTableViewController(), DebugTableViewController()]
|
||||
let pageVC = UIPageViewController(transitionStyle: .scroll, navigationOrientation: .horizontal, options: [:])
|
||||
pageVC.dataSource = self
|
||||
pageVC.delegate = self
|
||||
pageVC.setViewControllers([pages[0]], direction: .forward, animated: false, completion: nil)
|
||||
self.addMainPanel(with: pageVC)
|
||||
case .showPanelModal:
|
||||
let fpc = FloatingPanelController()
|
||||
let contentVC = self.storyboard!.instantiateViewController(withIdentifier: "DetailViewController")
|
||||
contentVC.loadViewIfNeeded()
|
||||
(contentVC as? DetailViewController)?.modeChangeView.isHidden = true
|
||||
fpc.set(contentViewController: contentVC)
|
||||
fpc.delegate = self
|
||||
|
||||
@@ -301,11 +330,11 @@ extension SampleListViewController: UITableViewDelegate {
|
||||
fpc.isRemovalInteractionEnabled = true
|
||||
|
||||
self.present(fpc, animated: true, completion: nil)
|
||||
|
||||
|
||||
case .showContentInset:
|
||||
let contentViewController = UIViewController()
|
||||
contentViewController.view.backgroundColor = .green
|
||||
|
||||
|
||||
let fpc = FloatingPanelController()
|
||||
fpc.set(contentViewController: contentViewController)
|
||||
fpc.surfaceView.contentInsets = .init(top: 20, left: 20, bottom: 20, right: 20)
|
||||
@@ -313,11 +342,23 @@ extension SampleListViewController: UITableViewDelegate {
|
||||
fpc.delegate = self
|
||||
fpc.isRemovalInteractionEnabled = true
|
||||
self.present(fpc, animated: true, completion: nil)
|
||||
default:
|
||||
detailPanelVC?.removePanelFromParent(animated: true, completion: nil)
|
||||
mainPanelVC?.removePanelFromParent(animated: true) {
|
||||
self.addMainPanel(with: contentVC)
|
||||
|
||||
case .showContainerMargins:
|
||||
let fpc = FloatingPanelController()
|
||||
fpc.surfaceView.cornerRadius = 38.5
|
||||
fpc.surfaceView.backgroundColor = .red
|
||||
fpc.surfaceView.containerMargins = .init(top: 24.0, left: 8.0, bottom: layoutInsets.bottom, right: 8.0)
|
||||
#if swift(>=5.1) // Actually Xcode 11 or later
|
||||
if #available(iOS 13.0, *) {
|
||||
fpc.surfaceView.layer.cornerCurve = .continuous
|
||||
}
|
||||
#endif
|
||||
|
||||
fpc.delegate = self
|
||||
fpc.isRemovalInteractionEnabled = true
|
||||
self.present(fpc, animated: true, completion: nil)
|
||||
default:
|
||||
self.addMainPanel(with: contentVC)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -337,7 +378,7 @@ extension SampleListViewController: FloatingPanelControllerDelegate {
|
||||
return newCollection.verticalSizeClass == .compact ? RemovablePanelLandscapeLayout() : RemovablePanelLayout()
|
||||
case .showIntrinsicView:
|
||||
return IntrinsicPanelLayout()
|
||||
case .showFloatingPanelModal:
|
||||
case .showPanelModal:
|
||||
if vc != mainPanelVC && vc != detailPanelVC {
|
||||
return ModalPanelLayout()
|
||||
}
|
||||
@@ -407,6 +448,14 @@ extension SampleListViewController: UIPageViewControllerDataSource {
|
||||
return pages[index - 1]
|
||||
}
|
||||
}
|
||||
extension SampleListViewController: UIPageViewControllerDelegate {
|
||||
// For showPageContent
|
||||
func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) {
|
||||
if completed, let page = pageViewController.viewControllers?.first {
|
||||
(pageViewController.parent as! FloatingPanelController).track(scrollView: (page as! DebugTableViewController).tableView)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class IntrinsicPanelLayout: FloatingPanelIntrinsicLayout { }
|
||||
|
||||
@@ -750,6 +799,8 @@ extension DebugTableViewController: UITableViewDelegate {
|
||||
}
|
||||
|
||||
class DetailViewController: InspectableViewController {
|
||||
@IBOutlet weak var modeChangeView: UIStackView!
|
||||
@IBOutlet weak var intrinsicHeightConstraint: NSLayoutConstraint!
|
||||
@IBOutlet weak var closeButton: UIButton!
|
||||
@IBAction func close(sender: UIButton) {
|
||||
// (self.parent as? FloatingPanelController)?.removePanelFromParent(animated: true, completion: nil)
|
||||
@@ -766,6 +817,10 @@ class DetailViewController: InspectableViewController {
|
||||
break
|
||||
}
|
||||
}
|
||||
@IBAction func modeChanged(_ sender: Any) {
|
||||
guard let fpc = parent as? FloatingPanelController else { return }
|
||||
fpc.contentMode = (fpc.contentMode == .static) ? .fitToBounds : .static
|
||||
}
|
||||
|
||||
@IBAction func tapped(_ sender: Any) {
|
||||
print("Detail panel is tapped!")
|
||||
@@ -1135,8 +1190,8 @@ class TwoTabBarPanelLayout: FloatingPanelLayout {
|
||||
}
|
||||
|
||||
class TwoTabBarPanelBehavior: FloatingPanelBehavior {
|
||||
func allowsRubberBanding(for edge: UIRectEdge) -> Bool {
|
||||
return (edge == .bottom || edge == .top)
|
||||
func allowsRubberBanding(for edges: UIRectEdge) -> Bool {
|
||||
return [UIRectEdge.top, UIRectEdge.bottom].contains(edges)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
Pod::Spec.new do |s|
|
||||
|
||||
s.name = "FloatingPanel"
|
||||
s.version = "1.6.6"
|
||||
s.version = "1.7.2"
|
||||
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.0"
|
||||
s.swift_versions = ["4.0", "4.2", "5.0"]
|
||||
|
||||
s.framework = "UIKit"
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
54CDC5D3215B6D5A007D205C /* FloatingPanelSurfaceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CDC5D2215B6D5A007D205C /* FloatingPanelSurfaceView.swift */; };
|
||||
54CDC5D5215B6D8D007D205C /* FloatingPanelBackdropView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CDC5D4215B6D8D007D205C /* FloatingPanelBackdropView.swift */; };
|
||||
54CFBFC3215CD045006B5735 /* FloatingPanelLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CFBFC2215CD045006B5735 /* FloatingPanelLayout.swift */; };
|
||||
54CFBFC5215CD09C006B5735 /* FloatingPanel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CFBFC4215CD09C006B5735 /* FloatingPanel.swift */; };
|
||||
54CFBFC5215CD09C006B5735 /* FloatingPanelCore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CFBFC4215CD09C006B5735 /* FloatingPanelCore.swift */; };
|
||||
54E740CD218AFD67005C1A34 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54E740CC218AFD67005C1A34 /* AppDelegate.swift */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
@@ -70,7 +70,7 @@
|
||||
54CDC5D2215B6D5A007D205C /* FloatingPanelSurfaceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FloatingPanelSurfaceView.swift; sourceTree = "<group>"; };
|
||||
54CDC5D4215B6D8D007D205C /* FloatingPanelBackdropView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FloatingPanelBackdropView.swift; sourceTree = "<group>"; };
|
||||
54CFBFC2215CD045006B5735 /* FloatingPanelLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FloatingPanelLayout.swift; sourceTree = "<group>"; };
|
||||
54CFBFC4215CD09C006B5735 /* FloatingPanel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FloatingPanel.swift; sourceTree = "<group>"; };
|
||||
54CFBFC4215CD09C006B5735 /* FloatingPanelCore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FloatingPanelCore.swift; sourceTree = "<group>"; };
|
||||
54E740CA218AFD67005C1A34 /* FloatingPanelTesting.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = FloatingPanelTesting.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
54E740CC218AFD67005C1A34 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||
54E740D8218AFD6A005C1A34 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
@@ -129,7 +129,7 @@
|
||||
545DB9C42151169500CA77B8 /* FloatingPanel.h */,
|
||||
545DB9DF21511AC100CA77B8 /* FloatingPanelController.swift */,
|
||||
54352E9521A51A2500CBCA08 /* FloatingPanelTransitioning.swift */,
|
||||
54CFBFC4215CD09C006B5735 /* FloatingPanel.swift */,
|
||||
54CFBFC4215CD09C006B5735 /* FloatingPanelCore.swift */,
|
||||
54CFBFC2215CD045006B5735 /* FloatingPanelLayout.swift */,
|
||||
5450EEE321646DF500135936 /* FloatingPanelBehavior.swift */,
|
||||
54352E9721A521CA00CBCA08 /* FloatingPanelView.swift */,
|
||||
@@ -311,7 +311,7 @@
|
||||
54CFBFC3215CD045006B5735 /* FloatingPanelLayout.swift in Sources */,
|
||||
54CDC5D5215B6D8D007D205C /* FloatingPanelBackdropView.swift in Sources */,
|
||||
54352E9821A521CA00CBCA08 /* FloatingPanelView.swift in Sources */,
|
||||
54CFBFC5215CD09C006B5735 /* FloatingPanel.swift in Sources */,
|
||||
54CFBFC5215CD09C006B5735 /* FloatingPanelCore.swift in Sources */,
|
||||
54ABD7AF216CCFF7002E6C13 /* Logger.swift in Sources */,
|
||||
545DB9E021511AC100CA77B8 /* FloatingPanelController.swift in Sources */,
|
||||
5450EEE421646DF500135936 /* FloatingPanelBehavior.swift in Sources */,
|
||||
@@ -484,6 +484,7 @@
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
APPLICATION_EXTENSION_API_ONLY = YES;
|
||||
BUILD_LIBRARY_FOR_DISTRIBUTION = YES;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_IDENTITY = "";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
@@ -514,6 +515,7 @@
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
APPLICATION_EXTENSION_API_ONLY = YES;
|
||||
BUILD_LIBRARY_FOR_DISTRIBUTION = YES;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_IDENTITY = "";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
|
||||
@@ -6,4 +6,6 @@
|
||||
import UIKit
|
||||
|
||||
/// A view that presents a backdrop interface behind a floating panel.
|
||||
public class FloatingPanelBackdropView: UIView { }
|
||||
public class FloatingPanelBackdropView: UIView {
|
||||
public var dismissalTapGestureRecognizer: UITapGestureRecognizer!
|
||||
}
|
||||
|
||||
@@ -116,6 +116,14 @@ open class FloatingPanelController: UIViewController {
|
||||
case never
|
||||
}
|
||||
|
||||
/// A flag used to determine how the controller object lays out the content view when the surface position changes.
|
||||
public enum ContentMode: Int {
|
||||
/// The option to fix the content to keep the height of the top most position.
|
||||
case `static`
|
||||
/// The option to scale the content to fit the bounds of the root view by changing the surface position.
|
||||
case fitToBounds
|
||||
}
|
||||
|
||||
/// The delegate of the floating panel controller object.
|
||||
public weak var delegate: FloatingPanelControllerDelegate?{
|
||||
didSet{
|
||||
@@ -179,9 +187,23 @@ open class FloatingPanelController: UIViewController {
|
||||
set { set(contentViewController: newValue) }
|
||||
get { return _contentViewController }
|
||||
}
|
||||
|
||||
/// The NearbyPosition determines that finger's nearby position.
|
||||
public var nearbyPosition: FloatingPanelPosition {
|
||||
let currentY = surfaceView.frame.minY
|
||||
return floatingPanel.targetPosition(from: currentY, with: .zero)
|
||||
}
|
||||
|
||||
public var contentMode: ContentMode = .static {
|
||||
didSet {
|
||||
guard position != .hidden else { return }
|
||||
activateLayout()
|
||||
}
|
||||
}
|
||||
|
||||
private var _contentViewController: UIViewController?
|
||||
|
||||
private(set) var floatingPanel: FloatingPanel!
|
||||
private(set) var floatingPanel: FloatingPanelCore!
|
||||
private var preSafeAreaInsets: UIEdgeInsets = .zero // Capture the latest one
|
||||
private var safeAreaInsetsObservation: NSKeyValueObservation?
|
||||
private let modalTransition = FloatingPanelModalTransition()
|
||||
@@ -204,7 +226,7 @@ open class FloatingPanelController: UIViewController {
|
||||
modalPresentationStyle = .custom
|
||||
transitioningDelegate = modalTransition
|
||||
|
||||
floatingPanel = FloatingPanel(self,
|
||||
floatingPanel = FloatingPanelCore(self,
|
||||
layout: fetchLayout(for: self.traitCollection),
|
||||
behavior: fetchBehavior(for: self.traitCollection))
|
||||
}
|
||||
@@ -213,7 +235,7 @@ open class FloatingPanelController: UIViewController {
|
||||
floatingPanel.layoutAdapter.layout = fetchLayout(for: traitCollection)
|
||||
floatingPanel.behavior = fetchBehavior(for: self.traitCollection)
|
||||
}
|
||||
|
||||
|
||||
// MARK:- Overrides
|
||||
|
||||
/// Creates the view that the controller manages.
|
||||
@@ -263,6 +285,41 @@ open class FloatingPanelController: UIViewController {
|
||||
safeAreaInsetsObservation = nil
|
||||
}
|
||||
|
||||
// MARK:- Child view controller to consult
|
||||
#if swift(>=4.2)
|
||||
open override var childForStatusBarStyle: UIViewController? {
|
||||
return contentViewController
|
||||
}
|
||||
|
||||
open override var childForStatusBarHidden: UIViewController? {
|
||||
return contentViewController
|
||||
}
|
||||
|
||||
open override var childForScreenEdgesDeferringSystemGestures: UIViewController? {
|
||||
return contentViewController
|
||||
}
|
||||
|
||||
open override var childForHomeIndicatorAutoHidden: UIViewController? {
|
||||
return contentViewController
|
||||
}
|
||||
#else
|
||||
open override var childViewControllerForStatusBarStyle: UIViewController? {
|
||||
return contentViewController
|
||||
}
|
||||
|
||||
open override var childViewControllerForStatusBarHidden: UIViewController? {
|
||||
return contentViewController
|
||||
}
|
||||
|
||||
open override func childViewControllerForScreenEdgesDeferringSystemGestures() -> UIViewController? {
|
||||
return contentViewController
|
||||
}
|
||||
|
||||
open override func childViewControllerForHomeIndicatorAutoHidden() -> UIViewController? {
|
||||
return contentViewController
|
||||
}
|
||||
#endif
|
||||
|
||||
// MARK:- Internals
|
||||
func prepare(for newCollection: UITraitCollection) {
|
||||
guard newCollection.shouldUpdateLayout(from: traitCollection) else { return }
|
||||
@@ -310,7 +367,6 @@ open class FloatingPanelController: UIViewController {
|
||||
|
||||
private func reloadLayout(for traitCollection: UITraitCollection) {
|
||||
floatingPanel.layoutAdapter.layout = fetchLayout(for: traitCollection)
|
||||
floatingPanel.layoutAdapter.prepareLayout(in: self)
|
||||
|
||||
if let parent = self.parent {
|
||||
if let layout = layout as? UIViewController, layout == parent {
|
||||
@@ -323,6 +379,8 @@ open class FloatingPanelController: UIViewController {
|
||||
}
|
||||
|
||||
private func activateLayout() {
|
||||
floatingPanel.layoutAdapter.prepareLayout(in: self)
|
||||
|
||||
// preserve the current content offset
|
||||
let contentOffset = scrollView?.contentOffset
|
||||
|
||||
@@ -408,9 +466,9 @@ open class FloatingPanelController: UIViewController {
|
||||
show(animated: animated) { [weak self] in
|
||||
guard let `self` = self else { return }
|
||||
#if swift(>=4.2)
|
||||
self.didMove(toParent: self)
|
||||
self.didMove(toParent: parent)
|
||||
#else
|
||||
self.didMove(toParentViewController: self)
|
||||
self.didMove(toParentViewController: parent)
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import UIKit.UIGestureRecognizerSubclass // For Xcode 9.4.1
|
||||
///
|
||||
/// FloatingPanel presentation model
|
||||
///
|
||||
class FloatingPanel: NSObject, UIGestureRecognizerDelegate {
|
||||
class FloatingPanelCore: NSObject, UIGestureRecognizerDelegate {
|
||||
// MUST be a weak reference to prevent UI freeze on the presentation modally
|
||||
weak var viewcontroller: FloatingPanelController?
|
||||
|
||||
@@ -81,10 +81,15 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate {
|
||||
super.init()
|
||||
|
||||
panGestureRecognizer.floatingPanel = self
|
||||
|
||||
surfaceView.addGestureRecognizer(panGestureRecognizer)
|
||||
panGestureRecognizer.addTarget(self, action: #selector(handle(panGesture:)))
|
||||
panGestureRecognizer.delegate = self
|
||||
|
||||
// Set tap-to-dismiss in the backdrop view
|
||||
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleBackdrop(tapGesture:)))
|
||||
tapGesture.isEnabled = false
|
||||
backdropView.dismissalTapGestureRecognizer = tapGesture
|
||||
backdropView.addGestureRecognizer(tapGesture)
|
||||
}
|
||||
|
||||
func move(to: FloatingPanelPosition, animated: Bool, completion: (() -> Void)? = nil) {
|
||||
@@ -146,7 +151,8 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate {
|
||||
// MARK: - Layout update
|
||||
|
||||
private func updateLayout(to target: FloatingPanelPosition) {
|
||||
self.layoutAdapter.activateLayout(of: target)
|
||||
self.layoutAdapter.activateFixedLayout()
|
||||
self.layoutAdapter.activateInteractiveLayout(of: target)
|
||||
}
|
||||
|
||||
func getBackdropAlpha(at currentY: CGFloat, with translation: CGPoint) -> CGFloat {
|
||||
@@ -196,7 +202,10 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate {
|
||||
default:
|
||||
// Should recognize tap/long press gestures in parallel when the surface view is at an anchor position.
|
||||
let surfaceFrame = surfaceView.layer.presentation()?.frame ?? surfaceView.frame
|
||||
return surfaceFrame.minY == layoutAdapter.positionY(for: state)
|
||||
let surfaceY = surfaceFrame.minY
|
||||
let adapterY = layoutAdapter.positionY(for: state)
|
||||
|
||||
return abs(surfaceY - adapterY) < (1.0 / surfaceView.traitCollection.displayScale)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -223,7 +232,16 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate {
|
||||
// the panel's pan gesture if not returns false
|
||||
if let scrollGestureRecognizers = scrollView.gestureRecognizers,
|
||||
scrollGestureRecognizers.contains(otherGestureRecognizer) {
|
||||
return false
|
||||
switch otherGestureRecognizer {
|
||||
case scrollView.panGestureRecognizer:
|
||||
if grabberAreaFrame.contains(gestureRecognizer.location(in: gestureRecognizer.view)) {
|
||||
return false
|
||||
}
|
||||
let offset = scrollView.contentOffset.y - scrollView.contentOffsetZero.y
|
||||
return allowScrollPanGesture(at: CGPoint(x: 0.0, y: offset))
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -255,6 +273,14 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate {
|
||||
}
|
||||
|
||||
// MARK: - Gesture handling
|
||||
|
||||
@objc func handleBackdrop(tapGesture: UITapGestureRecognizer) {
|
||||
viewcontroller?.dismiss(animated: true) { [weak self] in
|
||||
guard let vc = self?.viewcontroller else { return }
|
||||
vc.delegate?.floatingPanelDidEndRemove(vc)
|
||||
}
|
||||
}
|
||||
|
||||
@objc func handle(panGesture: UIPanGestureRecognizer) {
|
||||
let velocity = panGesture.velocity(in: panGesture.view)
|
||||
|
||||
@@ -264,7 +290,9 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate {
|
||||
|
||||
let location = panGesture.location(in: surfaceView)
|
||||
|
||||
let belowTop = surfaceView.presentationFrame.minY > layoutAdapter.topY
|
||||
let surfaceMinY = surfaceView.presentationFrame.minY
|
||||
let adapterTopY = layoutAdapter.topY
|
||||
let belowTop = surfaceMinY > (adapterTopY + (1.0 / surfaceView.traitCollection.displayScale))
|
||||
let offset = scrollView.contentOffset.y - scrollView.contentOffsetZero.y
|
||||
|
||||
log.debug("scroll gesture(\(state):\(panGesture.state)) --",
|
||||
@@ -315,11 +343,11 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate {
|
||||
} else {
|
||||
if state == layoutAdapter.topMostState {
|
||||
// Hide a scroll indicator just before starting an interaction by swiping a panel down.
|
||||
if offset < 0, velocity.y > 0 {
|
||||
if velocity.y > 0, !allowScrollPanGesture(at: CGPoint(x: 0.0, y: offset)) {
|
||||
lockScrollView()
|
||||
}
|
||||
// Show a scroll indicator when an animation is interrupted at the top and content is scrolled up
|
||||
if offset > 0, velocity.y < 0 {
|
||||
if velocity.y < 0, allowScrollPanGesture(at: CGPoint(x: 0.0, y: offset)) {
|
||||
unlockScrollView()
|
||||
}
|
||||
|
||||
@@ -355,7 +383,7 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate {
|
||||
}
|
||||
animator.finishAnimation(at: .current)
|
||||
} else {
|
||||
self.animator = nil
|
||||
self.endAnimation(false) // Must call it manually
|
||||
}
|
||||
}
|
||||
|
||||
@@ -500,6 +528,8 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate {
|
||||
// from the full position because SafeArea is global in a screen.
|
||||
private func preserveContentVCLayoutIfNeeded() {
|
||||
guard let vc = viewcontroller else { return }
|
||||
guard vc.contentMode != .fitToBounds else { return }
|
||||
|
||||
// Must include topY
|
||||
if (surfaceView.frame.minY <= layoutAdapter.topY) {
|
||||
if !disabledBottomAutoLayout {
|
||||
@@ -548,7 +578,7 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate {
|
||||
return
|
||||
}
|
||||
|
||||
stopScrollDeceleration = (surfaceView.frame.minY > layoutAdapter.topY) // Projecting the dragging to the scroll dragging or not
|
||||
stopScrollDeceleration = surfaceView.frame.minY > (layoutAdapter.topY + (1.0 / surfaceView.traitCollection.displayScale)) // Projecting the dragging to the scroll dragging or not
|
||||
if stopScrollDeceleration {
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
guard let `self` = self else { return }
|
||||
@@ -575,19 +605,22 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
if let vc = viewcontroller {
|
||||
vc.delegate?.floatingPanelDidEndDragging(vc, withVelocity: velocity, targetPosition: targetPosition)
|
||||
}
|
||||
|
||||
if scrollView != nil, !stopScrollDeceleration,
|
||||
surfaceView.frame.minY == layoutAdapter.topY,
|
||||
targetPosition == layoutAdapter.topMostState {
|
||||
self.state = targetPosition
|
||||
self.updateLayout(to: targetPosition)
|
||||
self.unlockScrollView()
|
||||
if let vc = viewcontroller {
|
||||
vc.delegate?.floatingPanelDidEndDragging(vc, withVelocity: .zero, targetPosition: targetPosition)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if let vc = viewcontroller {
|
||||
vc.delegate?.floatingPanelDidEndDragging(vc, withVelocity: velocity, targetPosition: targetPosition)
|
||||
}
|
||||
|
||||
// Workaround: Disable a tracking scroll to prevent bouncing a scroll content in a panel animating
|
||||
let isScrollEnabled = scrollView?.isScrollEnabled
|
||||
if let scrollView = scrollView, targetPosition != .full {
|
||||
@@ -650,12 +683,15 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate {
|
||||
|
||||
initialFrame = surfaceView.frame
|
||||
if state == layoutAdapter.topMostState, let scrollView = scrollView {
|
||||
if grabberAreaFrame.contains(location) {
|
||||
if grabberAreaFrame.contains(location) || scrollView.isTracking == false {
|
||||
initialScrollOffset = scrollView.contentOffset
|
||||
} else {
|
||||
// Fit the surface bounds to a scroll offset content by startInteraction(at:offset:)
|
||||
offset = CGPoint(x: -scrollView.contentOffset.x, y: -scrollView.contentOffset.y)
|
||||
initialScrollOffset = scrollView.contentOffsetZero
|
||||
// Fit the surface bounds to a scroll offset content by startInteraction(at:offset:)
|
||||
let scrollOffsetY = (scrollView.contentOffset.y - scrollView.contentOffsetZero.y)
|
||||
if scrollOffsetY < 0 {
|
||||
offset = CGPoint(x: -scrollView.contentOffset.x, y: -scrollOffsetY)
|
||||
}
|
||||
}
|
||||
log.debug("initial scroll offset --", initialScrollOffset)
|
||||
}
|
||||
@@ -707,23 +743,35 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate {
|
||||
let velocityVector = (distance != 0) ? CGVector(dx: 0, dy: abs(velocity.y)/distance) : .zero
|
||||
let animator = behavior.interactionAnimator(vc, to: targetPosition, with: velocityVector)
|
||||
animator.addAnimations { [weak self] in
|
||||
guard let `self` = self else { return }
|
||||
guard let `self` = self, let vc = self.viewcontroller else { return }
|
||||
self.state = targetPosition
|
||||
self.updateLayout(to: targetPosition)
|
||||
if animator.isInterruptible {
|
||||
switch vc.contentMode {
|
||||
case .fitToBounds:
|
||||
UIView.performWithLinear(startTime: 0.0, relativeDuration: 0.75) {
|
||||
self.layoutAdapter.activateFixedLayout()
|
||||
self.surfaceView.superview!.layoutIfNeeded()
|
||||
}
|
||||
case .static:
|
||||
self.layoutAdapter.activateFixedLayout()
|
||||
}
|
||||
} else {
|
||||
self.layoutAdapter.activateFixedLayout()
|
||||
}
|
||||
self.layoutAdapter.activateInteractiveLayout(of: targetPosition)
|
||||
}
|
||||
animator.addCompletion { [weak self] pos in
|
||||
// Prevent calling `finishAnimation(at:)` by the old animator whose `isInterruptive` is false
|
||||
// when a new animator has been started after the old one is interrupted.
|
||||
guard let `self` = self, self.animator == animator else { return }
|
||||
self.finishAnimation(at: targetPosition)
|
||||
log.debug("finishAnimation to \(targetPosition)")
|
||||
self.endAnimation(pos == .end)
|
||||
}
|
||||
self.animator = animator
|
||||
animator.startAnimation()
|
||||
}
|
||||
|
||||
private func finishAnimation(at targetPosition: FloatingPanelPosition) {
|
||||
log.debug("finishAnimation to \(targetPosition)")
|
||||
|
||||
private func endAnimation(_ finished: Bool) {
|
||||
self.isDecelerating = false
|
||||
self.animator = nil
|
||||
|
||||
@@ -738,7 +786,7 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate {
|
||||
stopScrollDeceleration = false
|
||||
|
||||
log.debug("finishAnimation -- state = \(state) surface.minY = \(surfaceView.presentationFrame.minY) topY = \(layoutAdapter.topY)")
|
||||
if state == layoutAdapter.topMostState, abs(surfaceView.presentationFrame.minY - layoutAdapter.topY) <= 1.0 {
|
||||
if finished, state == layoutAdapter.topMostState, abs(surfaceView.presentationFrame.minY - layoutAdapter.topY) <= 1.0 {
|
||||
unlockScrollView()
|
||||
}
|
||||
}
|
||||
@@ -840,10 +888,17 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate {
|
||||
// Must use setContentOffset(_:animated) to force-stop deceleration
|
||||
scrollView?.setContentOffset(contentOffset, animated: false)
|
||||
}
|
||||
|
||||
private func allowScrollPanGesture(at contentOffset: CGPoint) -> Bool {
|
||||
if state == layoutAdapter.topMostState {
|
||||
return contentOffset.y <= -30.0 || contentOffset.y > 0
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
class FloatingPanelPanGestureRecognizer: UIPanGestureRecognizer {
|
||||
fileprivate weak var floatingPanel: FloatingPanel?
|
||||
fileprivate weak var floatingPanel: FloatingPanelCore?
|
||||
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
|
||||
super.touchesBegan(touches, with: event)
|
||||
if floatingPanel?.animator != nil {
|
||||
@@ -855,7 +910,7 @@ class FloatingPanelPanGestureRecognizer: UIPanGestureRecognizer {
|
||||
return super.delegate
|
||||
}
|
||||
set {
|
||||
guard newValue is FloatingPanel else {
|
||||
guard newValue is FloatingPanelCore else {
|
||||
let exception = NSException(name: .invalidArgumentException,
|
||||
reason: "FloatingPanelController's built-in pan gesture recognizer must have its controller as its delegate.",
|
||||
userInfo: nil)
|
||||
@@ -11,15 +11,24 @@ import UIKit
|
||||
/// It can't be used with FloatingPanelIntrinsicLayout.
|
||||
public protocol FloatingPanelFullScreenLayout: FloatingPanelLayout { }
|
||||
|
||||
public extension FloatingPanelFullScreenLayout {
|
||||
var positionReference: FloatingPanelLayoutReference {
|
||||
return .fromSuperview
|
||||
}
|
||||
}
|
||||
|
||||
/// FloatingPanelIntrinsicLayout
|
||||
///
|
||||
/// Use the layout protocol if you want to layout a panel using the intrinsic height.
|
||||
/// It can't be used with FloatingPanelFullScreenLayout.
|
||||
/// It can't be used with `FloatingPanelFullScreenLayout`.
|
||||
///
|
||||
/// - Attention:
|
||||
/// `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.
|
||||
///
|
||||
/// - Note:
|
||||
/// By default, the `positionReference` is set to `.fromSafeArea`.
|
||||
public protocol FloatingPanelIntrinsicLayout: FloatingPanelLayout { }
|
||||
|
||||
public extension FloatingPanelIntrinsicLayout {
|
||||
@@ -34,6 +43,15 @@ public extension FloatingPanelIntrinsicLayout {
|
||||
func insetFor(position: FloatingPanelPosition) -> CGFloat? {
|
||||
return nil
|
||||
}
|
||||
|
||||
var positionReference: FloatingPanelLayoutReference {
|
||||
return .fromSafeArea
|
||||
}
|
||||
}
|
||||
|
||||
public enum FloatingPanelLayoutReference: Int {
|
||||
case fromSafeArea = 0
|
||||
case fromSuperview = 1
|
||||
}
|
||||
|
||||
public protocol FloatingPanelLayout: class {
|
||||
@@ -74,6 +92,9 @@ public protocol FloatingPanelLayout: class {
|
||||
///
|
||||
/// Default is 0.3 at full position, otherwise 0.0.
|
||||
func backdropAlphaFor(position: FloatingPanelPosition) -> CGFloat
|
||||
|
||||
var positionReference: FloatingPanelLayoutReference { get }
|
||||
|
||||
}
|
||||
|
||||
public extension FloatingPanelLayout {
|
||||
@@ -94,6 +115,10 @@ public extension FloatingPanelLayout {
|
||||
func backdropAlphaFor(position: FloatingPanelPosition) -> CGFloat {
|
||||
return position == .full ? 0.3 : 0.0
|
||||
}
|
||||
|
||||
var positionReference: FloatingPanelLayoutReference {
|
||||
return .fromSafeArea
|
||||
}
|
||||
}
|
||||
|
||||
public class FloatingPanelDefaultLayout: FloatingPanelLayout {
|
||||
@@ -160,6 +185,8 @@ class FloatingPanelLayoutAdapter {
|
||||
private var tipConstraints: [NSLayoutConstraint] = []
|
||||
private var offConstraints: [NSLayoutConstraint] = []
|
||||
private var interactiveTopConstraint: NSLayoutConstraint?
|
||||
private var bottomConstraint: NSLayoutConstraint?
|
||||
|
||||
|
||||
private var heightConstraint: NSLayoutConstraint?
|
||||
|
||||
@@ -218,27 +245,28 @@ class FloatingPanelLayoutAdapter {
|
||||
func positionY(for pos: FloatingPanelPosition) -> CGFloat {
|
||||
switch pos {
|
||||
case .full:
|
||||
switch layout {
|
||||
case is FloatingPanelIntrinsicLayout:
|
||||
if layout is FloatingPanelIntrinsicLayout {
|
||||
return surfaceView.superview!.bounds.height - surfaceView.bounds.height
|
||||
case is FloatingPanelFullScreenLayout:
|
||||
return fullInset
|
||||
default:
|
||||
}
|
||||
switch layout.positionReference {
|
||||
case .fromSafeArea:
|
||||
return (safeAreaInsets.top + fullInset)
|
||||
case .fromSuperview:
|
||||
return fullInset
|
||||
}
|
||||
case .half:
|
||||
switch layout {
|
||||
case is FloatingPanelFullScreenLayout:
|
||||
return surfaceView.superview!.bounds.height - halfInset
|
||||
default:
|
||||
switch layout.positionReference {
|
||||
case .fromSafeArea:
|
||||
return surfaceView.superview!.bounds.height - (safeAreaInsets.bottom + halfInset)
|
||||
case .fromSuperview:
|
||||
return surfaceView.superview!.bounds.height - halfInset
|
||||
}
|
||||
case .tip:
|
||||
switch layout {
|
||||
case is FloatingPanelFullScreenLayout:
|
||||
return surfaceView.superview!.bounds.height - tipInset
|
||||
default:
|
||||
switch layout.positionReference {
|
||||
case .fromSafeArea:
|
||||
return surfaceView.superview!.bounds.height - (safeAreaInsets.bottom + tipInset)
|
||||
case .fromSuperview:
|
||||
return surfaceView.superview!.bounds.height - tipInset
|
||||
}
|
||||
case .hidden:
|
||||
return surfaceView.superview!.bounds.height - hiddenInset
|
||||
@@ -279,6 +307,10 @@ class FloatingPanelLayoutAdapter {
|
||||
self.vc = vc
|
||||
|
||||
NSLayoutConstraint.deactivate(fixedConstraints + fullConstraints + halfConstraints + tipConstraints + offConstraints)
|
||||
NSLayoutConstraint.deactivate(constraint: self.heightConstraint)
|
||||
self.heightConstraint = nil
|
||||
NSLayoutConstraint.deactivate(constraint: self.bottomConstraint)
|
||||
self.bottomConstraint = nil
|
||||
|
||||
surfaceView.translatesAutoresizingMaskIntoConstraints = false
|
||||
backdropView.translatesAutoresizingMaskIntoConstraints = false
|
||||
@@ -294,9 +326,14 @@ class FloatingPanelLayoutAdapter {
|
||||
|
||||
fixedConstraints = surfaceConstraints + backdropConstraints
|
||||
|
||||
if vc.contentMode == .fitToBounds {
|
||||
bottomConstraint = surfaceView.bottomAnchor.constraint(equalTo: vc.view.bottomAnchor,
|
||||
constant: 0.0)
|
||||
}
|
||||
|
||||
// Flexible surface constraints for full, half, tip and off
|
||||
let topAnchor: NSLayoutYAxisAnchor = {
|
||||
if layout is FloatingPanelFullScreenLayout {
|
||||
if layout.positionReference == .fromSuperview {
|
||||
return vc.view.topAnchor
|
||||
} else {
|
||||
return vc.layoutGuide.topAnchor
|
||||
@@ -315,7 +352,7 @@ class FloatingPanelLayoutAdapter {
|
||||
}
|
||||
|
||||
let bottomAnchor: NSLayoutYAxisAnchor = {
|
||||
if layout is FloatingPanelFullScreenLayout {
|
||||
if layout.positionReference == .fromSuperview {
|
||||
return vc.view.bottomAnchor
|
||||
} else {
|
||||
return vc.layoutGuide.bottomAnchor
|
||||
@@ -342,16 +379,15 @@ class FloatingPanelLayoutAdapter {
|
||||
NSLayoutConstraint.deactivate(fullConstraints + halfConstraints + tipConstraints + offConstraints)
|
||||
|
||||
let interactiveTopConstraint: NSLayoutConstraint
|
||||
switch layout {
|
||||
case is FloatingPanelIntrinsicLayout,
|
||||
is FloatingPanelFullScreenLayout:
|
||||
initialConst = surfaceView.frame.minY + offset.y
|
||||
interactiveTopConstraint = surfaceView.topAnchor.constraint(equalTo: vc.view.topAnchor,
|
||||
constant: initialConst)
|
||||
default:
|
||||
switch layout.positionReference {
|
||||
case .fromSafeArea:
|
||||
initialConst = surfaceView.frame.minY - safeAreaInsets.top + offset.y
|
||||
interactiveTopConstraint = surfaceView.topAnchor.constraint(equalTo: vc.layoutGuide.topAnchor,
|
||||
constant: initialConst)
|
||||
case .fromSuperview:
|
||||
initialConst = surfaceView.frame.minY + offset.y
|
||||
interactiveTopConstraint = surfaceView.topAnchor.constraint(equalTo: vc.view.topAnchor,
|
||||
constant: initialConst)
|
||||
}
|
||||
NSLayoutConstraint.activate([interactiveTopConstraint])
|
||||
self.interactiveTopConstraint = interactiveTopConstraint
|
||||
@@ -360,41 +396,48 @@ class FloatingPanelLayoutAdapter {
|
||||
func endInteraction(at state: FloatingPanelPosition) {
|
||||
// Don't deactivate `interactiveTopConstraint` here because it leads to
|
||||
// unsatisfiable constraints
|
||||
|
||||
if self.interactiveTopConstraint == nil {
|
||||
// Actiavate `interactiveTopConstraint` for `fitToBounds` mode.
|
||||
// It goes throught this path when the pan gesture state jumps
|
||||
// from .begin to .end.
|
||||
startInteraction(at: state)
|
||||
}
|
||||
}
|
||||
|
||||
// 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(constraint: heightConstraint)
|
||||
heightConstraint = nil
|
||||
|
||||
if let const = self.heightConstraint {
|
||||
NSLayoutConstraint.deactivate([const])
|
||||
if layout is FloatingPanelIntrinsicLayout {
|
||||
updateIntrinsicHeight()
|
||||
}
|
||||
defer {
|
||||
if layout is FloatingPanelIntrinsicLayout {
|
||||
NSLayoutConstraint.deactivate(fullConstraints)
|
||||
fullConstraints = [
|
||||
surfaceView.topAnchor.constraint(equalTo: vc.layoutGuide.bottomAnchor,
|
||||
constant: -fullInset),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
let heightConstraint: NSLayoutConstraint
|
||||
guard vc.contentMode != .fitToBounds else { return }
|
||||
|
||||
switch layout {
|
||||
case is FloatingPanelIntrinsicLayout:
|
||||
updateIntrinsicHeight()
|
||||
heightConstraint = surfaceView.heightAnchor.constraint(equalToConstant: intrinsicHeight + safeAreaInsets.bottom)
|
||||
default:
|
||||
let const = -(positionY(for: topMostState))
|
||||
heightConstraint = surfaceView.heightAnchor.constraint(equalTo: vc.view.heightAnchor,
|
||||
constant: const)
|
||||
}
|
||||
|
||||
NSLayoutConstraint.activate([heightConstraint])
|
||||
self.heightConstraint = heightConstraint
|
||||
NSLayoutConstraint.activate(constraint: heightConstraint)
|
||||
|
||||
surfaceView.bottomOverflow = vc.view.bounds.height + layout.topInteractionBuffer
|
||||
|
||||
if layout is FloatingPanelIntrinsicLayout {
|
||||
NSLayoutConstraint.deactivate(fullConstraints)
|
||||
fullConstraints = [
|
||||
surfaceView.topAnchor.constraint(equalTo: vc.layoutGuide.bottomAnchor,
|
||||
constant: -fullInset),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
func updateInteractiveTopConstraint(diff: CGFloat, allowsTopBuffer: Bool, with behavior: FloatingPanelBehavior) {
|
||||
@@ -404,22 +447,22 @@ class FloatingPanelLayoutAdapter {
|
||||
|
||||
let topMostConst: CGFloat = {
|
||||
var ret: CGFloat = 0.0
|
||||
switch layout {
|
||||
case is FloatingPanelIntrinsicLayout, is FloatingPanelFullScreenLayout:
|
||||
ret = topY
|
||||
default:
|
||||
switch layout.positionReference {
|
||||
case .fromSafeArea:
|
||||
ret = topY - safeAreaInsets.top
|
||||
case .fromSuperview:
|
||||
ret = topY
|
||||
}
|
||||
return max(ret, 0.0) // The top boundary is equal to the related topAnchor.
|
||||
}()
|
||||
let bottomMostConst: CGFloat = {
|
||||
var ret: CGFloat = 0.0
|
||||
let _bottomY = vc.isRemovalInteractionEnabled ? positionY(for: .hidden) : bottomY
|
||||
switch layout {
|
||||
case is FloatingPanelIntrinsicLayout, is FloatingPanelFullScreenLayout:
|
||||
ret = _bottomY
|
||||
default:
|
||||
switch layout.positionReference {
|
||||
case .fromSafeArea:
|
||||
ret = _bottomY - safeAreaInsets.top
|
||||
case .fromSuperview:
|
||||
ret = _bottomY
|
||||
}
|
||||
return min(ret, surfaceView.superview!.bounds.height)
|
||||
}()
|
||||
@@ -451,7 +494,19 @@ class FloatingPanelLayoutAdapter {
|
||||
return (1.0 - (1.0 / ((buffer * 0.55 / base) + 1.0))) * base
|
||||
}
|
||||
|
||||
func activateLayout(of state: FloatingPanelPosition) {
|
||||
func activateFixedLayout() {
|
||||
// Must deactivate `interactiveTopConstraint` here
|
||||
NSLayoutConstraint.deactivate(constraint: self.interactiveTopConstraint)
|
||||
self.interactiveTopConstraint = nil
|
||||
|
||||
NSLayoutConstraint.activate(fixedConstraints)
|
||||
|
||||
if vc.contentMode == .fitToBounds {
|
||||
NSLayoutConstraint.activate(constraint: self.bottomConstraint)
|
||||
}
|
||||
}
|
||||
|
||||
func activateInteractiveLayout(of state: FloatingPanelPosition) {
|
||||
defer {
|
||||
layoutSurfaceIfNeeded()
|
||||
log.debug("activateLayout -- surface.presentation = \(self.surfaceView.presentationFrame) surface.frame = \(self.surfaceView.frame)")
|
||||
@@ -461,13 +516,6 @@ class FloatingPanelLayoutAdapter {
|
||||
|
||||
setBackdropAlpha(of: state)
|
||||
|
||||
// Must deactivate `interactiveTopConstraint` here
|
||||
if let interactiveTopConstraint = interactiveTopConstraint {
|
||||
NSLayoutConstraint.deactivate([interactiveTopConstraint])
|
||||
self.interactiveTopConstraint = nil
|
||||
}
|
||||
NSLayoutConstraint.activate(fixedConstraints)
|
||||
|
||||
if isValid(state) == false {
|
||||
state = layout.initialPosition
|
||||
}
|
||||
@@ -485,6 +533,11 @@ class FloatingPanelLayoutAdapter {
|
||||
}
|
||||
}
|
||||
|
||||
func activateLayout(of state: FloatingPanelPosition) {
|
||||
activateFixedLayout()
|
||||
activateInteractiveLayout(of: state)
|
||||
}
|
||||
|
||||
func isValid(_ state: FloatingPanelPosition) -> Bool {
|
||||
return supportedPositions.union([.hidden]).contains(state)
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ public class FloatingPanelSurfaceView: UIView {
|
||||
|
||||
/// A root view of a content view controller
|
||||
public weak var contentView: UIView!
|
||||
|
||||
|
||||
/// The content insets specifying the insets around the content view.
|
||||
public var contentInsets: UIEdgeInsets = .zero {
|
||||
didSet {
|
||||
@@ -81,8 +81,8 @@ public class FloatingPanelSurfaceView: UIView {
|
||||
/// The color of the surface border.
|
||||
public var borderWidth: CGFloat = 0.0 { didSet { setNeedsLayout() } }
|
||||
|
||||
/// Offset of the container view from the top
|
||||
public var containerTopInset: CGFloat = 0.0 { didSet {
|
||||
/// The margins to use when laying out the container view wrapping content.
|
||||
public var containerMargins: UIEdgeInsets = .zero { didSet {
|
||||
setNeedsUpdateConstraints()
|
||||
} }
|
||||
|
||||
@@ -97,9 +97,11 @@ public class FloatingPanelSurfaceView: UIView {
|
||||
@available(*, unavailable, renamed: "containerView")
|
||||
public var backgroundView: UIView!
|
||||
|
||||
private lazy var containerViewTopInsetConstraint: NSLayoutConstraint = containerView.topAnchor.constraint(equalTo: topAnchor, constant: containerTopInset)
|
||||
private lazy var containerViewHeightConstraint: NSLayoutConstraint = containerView.heightAnchor.constraint(equalTo: heightAnchor, multiplier: 1.0)
|
||||
|
||||
private lazy var containerViewTopConstraint = containerView.topAnchor.constraint(equalTo: topAnchor, constant: containerMargins.top)
|
||||
private lazy var containerViewHeightConstraint = containerView.heightAnchor.constraint(equalTo: heightAnchor, multiplier: 1.0)
|
||||
private lazy var containerViewLeftConstraint = containerView.leftAnchor.constraint(equalTo: leftAnchor, constant: 0.0)
|
||||
private lazy var containerViewRightConstraint = containerView.rightAnchor.constraint(equalTo: rightAnchor, constant: 0.0)
|
||||
|
||||
/// The content view top constraint
|
||||
private var contentViewTopConstraint: NSLayoutConstraint?
|
||||
/// The content view left constraint
|
||||
@@ -109,9 +111,9 @@ public class FloatingPanelSurfaceView: UIView {
|
||||
/// The content height constraint
|
||||
private var contentViewHeightConstraint: NSLayoutConstraint?
|
||||
|
||||
private lazy var grabberHandleWidthConstraint: NSLayoutConstraint = grabberHandle.widthAnchor.constraint(equalToConstant: grabberHandleWidth)
|
||||
private lazy var grabberHandleHeightConstraint: NSLayoutConstraint = grabberHandle.heightAnchor.constraint(equalToConstant: grabberHandleHeight)
|
||||
private lazy var grabberHandleTopConstraint: NSLayoutConstraint = grabberHandle.topAnchor.constraint(equalTo: topAnchor, constant: grabberTopPadding)
|
||||
private lazy var grabberHandleWidthConstraint = grabberHandle.widthAnchor.constraint(equalToConstant: grabberHandleWidth)
|
||||
private lazy var grabberHandleHeightConstraint = grabberHandle.heightAnchor.constraint(equalToConstant: grabberHandleHeight)
|
||||
private lazy var grabberHandleTopConstraint = grabberHandle.topAnchor.constraint(equalTo: topAnchor, constant: grabberTopPadding)
|
||||
|
||||
public override class var requiresConstraintBasedLayout: Bool { return true }
|
||||
|
||||
@@ -132,9 +134,9 @@ public class FloatingPanelSurfaceView: UIView {
|
||||
addSubview(containerView)
|
||||
containerView.translatesAutoresizingMaskIntoConstraints = false
|
||||
NSLayoutConstraint.activate([
|
||||
containerViewTopInsetConstraint,
|
||||
containerView.leftAnchor.constraint(equalTo: leftAnchor, constant: 0.0),
|
||||
containerView.rightAnchor.constraint(equalTo: rightAnchor, constant: 0.0),
|
||||
containerViewTopConstraint,
|
||||
containerViewLeftConstraint,
|
||||
containerViewRightConstraint,
|
||||
containerViewHeightConstraint,
|
||||
])
|
||||
|
||||
@@ -149,13 +151,15 @@ public class FloatingPanelSurfaceView: UIView {
|
||||
}
|
||||
|
||||
public override func updateConstraints() {
|
||||
containerViewTopInsetConstraint.constant = containerTopInset
|
||||
containerViewHeightConstraint.constant = bottomOverflow
|
||||
containerViewTopConstraint.constant = containerMargins.top
|
||||
containerViewLeftConstraint.constant = containerMargins.left
|
||||
containerViewRightConstraint.constant = -containerMargins.right
|
||||
containerViewHeightConstraint.constant = (containerMargins.bottom == 0) ? bottomOverflow : -(containerMargins.top + containerMargins.bottom)
|
||||
|
||||
contentViewTopConstraint?.constant = contentInsets.top
|
||||
contentViewLeftConstraint?.constant = contentInsets.left
|
||||
contentViewRightConstraint?.constant = contentInsets.right
|
||||
contentViewHeightConstraint?.constant = -(containerTopInset + contentInsets.top + contentInsets.bottom)
|
||||
contentViewTopConstraint?.constant = containerMargins.top + contentInsets.top
|
||||
contentViewLeftConstraint?.constant = containerMargins.left + contentInsets.left
|
||||
contentViewRightConstraint?.constant = containerMargins.right + contentInsets.right
|
||||
contentViewHeightConstraint?.constant = -(containerMargins.top + containerMargins.bottom + contentInsets.top + contentInsets.bottom)
|
||||
|
||||
grabberHandleTopConstraint.constant = grabberTopPadding
|
||||
grabberHandleWidthConstraint.constant = grabberHandleWidth
|
||||
@@ -196,6 +200,7 @@ public class FloatingPanelSurfaceView: UIView {
|
||||
return
|
||||
}
|
||||
containerView.layer.masksToBounds = true
|
||||
guard containerMargins.bottom == 0 else { return }
|
||||
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.storyboard of Example/Maps.
|
||||
@@ -218,10 +223,11 @@ public class FloatingPanelSurfaceView: UIView {
|
||||
/* contentView.frame = bounds */ // MUST NOT: Because the top safe area inset of a content VC will be incorrect.
|
||||
contentView.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
let topConstraint = contentView.topAnchor.constraint(equalTo: topAnchor, constant: contentInsets.top)
|
||||
let leftConstraint = contentView.leftAnchor.constraint(equalTo: leftAnchor, constant: contentInsets.left)
|
||||
let rightConstraint = rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: contentInsets.right)
|
||||
let heightConstraint = contentView.heightAnchor.constraint(equalTo: heightAnchor, constant: -(containerTopInset + contentInsets.top + contentInsets.bottom))
|
||||
let topConstraint = contentView.topAnchor.constraint(equalTo: topAnchor, constant: containerMargins.top + contentInsets.top)
|
||||
let leftConstraint = contentView.leftAnchor.constraint(equalTo: leftAnchor, constant: containerMargins.left + contentInsets.left)
|
||||
let rightConstraint = rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: containerMargins.right + contentInsets.right)
|
||||
let heightPadding = containerMargins.top + containerMargins.bottom + contentInsets.top + contentInsets.bottom
|
||||
let heightConstraint = contentView.heightAnchor.constraint(equalTo: heightAnchor, constant: -heightPadding)
|
||||
NSLayoutConstraint.activate([
|
||||
topConstraint,
|
||||
leftConstraint,
|
||||
|
||||
@@ -59,9 +59,7 @@ class FloatingPanelPresentationController: UIPresentationController {
|
||||
// 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)
|
||||
fpc.backdropView.dismissalTapGestureRecognizer.isEnabled = true
|
||||
}
|
||||
|
||||
@objc func handleBackdrop(tapGesture: UITapGestureRecognizer) {
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>FMWK</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.6.6</string>
|
||||
<string>1.7.2</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||
</dict>
|
||||
|
||||
@@ -21,7 +21,7 @@ class CustomLayoutGuide: LayoutGuideProvider {
|
||||
}
|
||||
|
||||
extension UIViewController {
|
||||
var layoutInsets: UIEdgeInsets {
|
||||
@objc var layoutInsets: UIEdgeInsets {
|
||||
if #available(iOS 11.0, *) {
|
||||
return view.safeAreaInsets
|
||||
} else {
|
||||
@@ -75,6 +75,12 @@ extension UIView {
|
||||
func enableAutoLayout() {
|
||||
translatesAutoresizingMaskIntoConstraints = false
|
||||
}
|
||||
|
||||
static func performWithLinear(startTime: Double = 0.0, relativeDuration: Double = 1.0, _ animations: @escaping (() -> Void)) {
|
||||
UIView.animateKeyframes(withDuration: 0.0, delay: 0.0, options: [.calculationModeCubic], animations: {
|
||||
UIView.addKeyframe(withRelativeStartTime: startTime, relativeDuration: relativeDuration, animations: animations)
|
||||
}, completion: nil)
|
||||
}
|
||||
}
|
||||
|
||||
#if __FP_LOG
|
||||
@@ -140,3 +146,14 @@ extension UITraitCollection {
|
||||
|| previous.layoutDirection != layoutDirection
|
||||
}
|
||||
}
|
||||
|
||||
extension NSLayoutConstraint {
|
||||
static func activate(constraint: NSLayoutConstraint?) {
|
||||
guard let constraint = constraint else { return }
|
||||
self.activate([constraint])
|
||||
}
|
||||
static func deactivate(constraint: NSLayoutConstraint?) {
|
||||
guard let constraint = constraint else { return }
|
||||
self.deactivate([constraint])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,6 +101,22 @@ class FloatingPanelControllerTests: XCTestCase {
|
||||
XCTAssertEqual(delegate.position, .hidden)
|
||||
XCTAssertEqual(fpc.surfaceView.frame.minY, fpc.originYOfSurface(for: .hidden))
|
||||
}
|
||||
|
||||
func test_moveWithNearbyPosition() {
|
||||
let delegate = FloatingPanelTestDelegate()
|
||||
let fpc = FloatingPanelController(delegate: delegate)
|
||||
XCTAssertEqual(delegate.position, .hidden)
|
||||
fpc.showForTest()
|
||||
|
||||
XCTAssertEqual(fpc.nearbyPosition, .half)
|
||||
|
||||
fpc.hide()
|
||||
XCTAssertEqual(fpc.nearbyPosition, .tip)
|
||||
|
||||
fpc.move(to: .full, animated: false)
|
||||
XCTAssertEqual(fpc.nearbyPosition, .full)
|
||||
XCTAssertEqual(fpc.surfaceView.frame.minY, fpc.originYOfSurface(for: .full))
|
||||
}
|
||||
|
||||
func test_originSurfaceY() {
|
||||
let fpc = FloatingPanelController(delegate: nil)
|
||||
@@ -117,6 +133,31 @@ class FloatingPanelControllerTests: XCTestCase {
|
||||
fpc.move(to: .hidden, animated: false)
|
||||
XCTAssertEqual(fpc.surfaceView.frame.minY, fpc.originYOfSurface(for: .hidden))
|
||||
}
|
||||
|
||||
func test_contentMode() {
|
||||
let fpc = FloatingPanelController(delegate: nil)
|
||||
fpc.loadViewIfNeeded()
|
||||
fpc.view.frame = CGRect(x: 0, y: 0, width: 375, height: 667)
|
||||
fpc.show(animated: false, completion: nil)
|
||||
|
||||
fpc.contentMode = .static
|
||||
|
||||
fpc.move(to: .full, animated: false)
|
||||
XCTAssertEqual(fpc.surfaceView.frame.height, fpc.view.bounds.height - fpc.originYOfSurface(for: .full))
|
||||
fpc.move(to: .half, animated: false)
|
||||
XCTAssertEqual(fpc.surfaceView.frame.height, fpc.view.bounds.height - fpc.originYOfSurface(for: .full))
|
||||
fpc.move(to: .tip, animated: false)
|
||||
XCTAssertEqual(fpc.surfaceView.frame.height, fpc.view.bounds.height - fpc.originYOfSurface(for: .full))
|
||||
|
||||
fpc.contentMode = .fitToBounds
|
||||
|
||||
fpc.move(to: .full, animated: false)
|
||||
XCTAssertEqual(fpc.surfaceView.frame.height, fpc.view.bounds.height - fpc.originYOfSurface(for: .full))
|
||||
fpc.move(to: .half, animated: false)
|
||||
XCTAssertEqual(fpc.surfaceView.frame.height, fpc.view.bounds.height - fpc.originYOfSurface(for: .half))
|
||||
fpc.move(to: .tip, animated: false)
|
||||
XCTAssertEqual(fpc.surfaceView.frame.height, fpc.view.bounds.height - fpc.originYOfSurface(for: .tip))
|
||||
}
|
||||
}
|
||||
|
||||
private class MyZombieViewController: UIViewController, FloatingPanelLayout, FloatingPanelBehavior, FloatingPanelControllerDelegate {
|
||||
|
||||
@@ -194,13 +194,61 @@ class FloatingPanelLayoutTests: XCTestCase {
|
||||
|
||||
fpc.floatingPanel.layoutAdapter.endInteraction(at: fpc.position)
|
||||
}
|
||||
|
||||
func test_positionReference() {
|
||||
fpc = CustomSafeAreaFloatingPanelController()
|
||||
fpc.loadViewIfNeeded()
|
||||
fpc.view.frame = CGRect(x: 0, y: 0, width: 375, height: 667)
|
||||
|
||||
class MyFloatingPanelFullLayout: FloatingPanelTestLayout {
|
||||
var initialPosition: FloatingPanelPosition = .half
|
||||
var positionReference: FloatingPanelLayoutReference {
|
||||
return .fromSuperview
|
||||
}
|
||||
}
|
||||
class MyFloatingPanelSafeAreaLayout: FloatingPanelTestLayout {
|
||||
var initialPosition: FloatingPanelPosition = .half
|
||||
var positionReference: FloatingPanelLayoutReference {
|
||||
return .fromSafeArea
|
||||
}
|
||||
}
|
||||
let fullLayout = MyFloatingPanelFullLayout()
|
||||
let delegate = FloatingPanelTestDelegate()
|
||||
delegate.layout = fullLayout
|
||||
fpc.delegate = delegate
|
||||
fpc.showForTest()
|
||||
|
||||
XCTAssertEqual(fpc.layout.positionReference, .fromSuperview)
|
||||
XCTAssertEqual(fpc.originYOfSurface(for: .full), fullLayout.insetFor(position: .full))
|
||||
XCTAssertEqual(fpc.originYOfSurface(for: .half), fpc.view!.frame.height - fullLayout.insetFor(position: .half)!)
|
||||
XCTAssertEqual(fpc.originYOfSurface(for: .tip), fpc.view!.frame.height - fullLayout.insetFor(position: .tip)!)
|
||||
|
||||
let safeAreaLayout = MyFloatingPanelSafeAreaLayout()
|
||||
delegate.layout = safeAreaLayout
|
||||
fpc.delegate = delegate
|
||||
|
||||
XCTAssertEqual(fpc.layout.positionReference, .fromSafeArea)
|
||||
XCTAssertEqual(fpc.originYOfSurface(for: .full),
|
||||
fullLayout.insetFor(position: .full)! + fpc.layoutInsets.top)
|
||||
XCTAssertEqual(fpc.originYOfSurface(for: .half),
|
||||
fpc.view!.frame.height - (fullLayout.insetFor(position: .half)! + fpc.layoutInsets.bottom))
|
||||
XCTAssertEqual(fpc.originYOfSurface(for: .tip),
|
||||
fpc.view!.frame.height - (fullLayout.insetFor(position: .tip)! + fpc.layoutInsets.bottom))
|
||||
}
|
||||
}
|
||||
|
||||
private typealias LayoutSegmentTestParameter = (UInt, pos: CGFloat, forwardY: Bool, lower: FloatingPanelPosition?, upper: FloatingPanelPosition?)
|
||||
private func assertLayoutSegment(_ floatingPanel: FloatingPanel, with params: [LayoutSegmentTestParameter]) {
|
||||
private func assertLayoutSegment(_ floatingPanel: FloatingPanelCore, with params: [LayoutSegmentTestParameter]) {
|
||||
params.forEach { (line, pos, forwardY, lowr, upper) in
|
||||
let segument = floatingPanel.layoutAdapter.segument(at: pos, forward: forwardY)
|
||||
XCTAssertEqual(segument.lower, lowr, line: line)
|
||||
XCTAssertEqual(segument.upper, upper, line: line)
|
||||
}
|
||||
}
|
||||
|
||||
private class CustomSafeAreaFloatingPanelController: FloatingPanelController { }
|
||||
extension CustomSafeAreaFloatingPanelController {
|
||||
override var layoutInsets: UIEdgeInsets {
|
||||
return UIEdgeInsets(top: 64.0, left: 0.0, bottom: 0.0, right: 34.0)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ class FloatingPanelSurfaceViewTests: XCTestCase {
|
||||
XCTAssert(surface.backgroundColor == surface.containerView.backgroundColor)
|
||||
}
|
||||
|
||||
func test_surfaceView_constraintsUpdate() {
|
||||
func test_surfaceView_grabberHandle() {
|
||||
let surface = FloatingPanelSurfaceView(frame: CGRect(x: 0.0, y: 0.0, width: 320.0, height: 480.0))
|
||||
XCTAssert(surface.contentView == nil)
|
||||
surface.layoutIfNeeded()
|
||||
@@ -39,6 +39,42 @@ class FloatingPanelSurfaceViewTests: XCTestCase {
|
||||
XCTAssert(surface.grabberHandle.frame.height == surface.grabberHandleHeight, "\(surface.grabberHandle.frame.height) == \(surface.grabberHandleHeight)")
|
||||
}
|
||||
|
||||
func test_surfaceView_containerMargins() {
|
||||
let surface = FloatingPanelSurfaceView(frame: CGRect(x: 0.0, y: 0.0, width: 320.0, height: 480.0))
|
||||
surface.layoutIfNeeded()
|
||||
XCTAssertEqual(surface.containerView.frame, surface.bounds)
|
||||
surface.containerMargins = UIEdgeInsets(top: 16.0, left: 16.0, bottom: 16.0, right: 16.0)
|
||||
surface.setNeedsLayout()
|
||||
surface.layoutIfNeeded()
|
||||
XCTAssertEqual(surface.containerView.frame, surface.bounds.inset(by: surface.containerMargins))
|
||||
}
|
||||
|
||||
func test_surfaceView_contentInsets() {
|
||||
let surface = FloatingPanelSurfaceView(frame: CGRect(x: 0.0, y: 0.0, width: 320.0, height: 480.0))
|
||||
let contentView = UIView()
|
||||
surface.add(contentView: contentView)
|
||||
surface.layoutIfNeeded()
|
||||
XCTAssertEqual(surface.contentView.frame, surface.bounds)
|
||||
surface.contentInsets = UIEdgeInsets(top: 16.0, left: 16.0, bottom: 16.0, right: 16.0)
|
||||
surface.setNeedsLayout()
|
||||
surface.layoutIfNeeded()
|
||||
XCTAssertEqual(surface.contentView.frame, surface.bounds.inset(by: surface.contentInsets))
|
||||
}
|
||||
|
||||
func test_surfaceView_containerMargins_and_contentInsets() {
|
||||
let surface = FloatingPanelSurfaceView(frame: CGRect(x: 0.0, y: 0.0, width: 320.0, height: 480.0))
|
||||
let contentView = UIView()
|
||||
surface.add(contentView: contentView)
|
||||
surface.layoutIfNeeded()
|
||||
XCTAssertEqual(surface.contentView.frame, surface.bounds)
|
||||
surface.containerMargins = UIEdgeInsets(top: 16.0, left: 16.0, bottom: 16.0, right: 16.0)
|
||||
surface.contentInsets = UIEdgeInsets(top: 16.0, left: 16.0, bottom: 16.0, right: 16.0)
|
||||
surface.setNeedsLayout()
|
||||
surface.layoutIfNeeded()
|
||||
XCTAssertEqual(surface.containerView.frame, surface.bounds.inset(by: surface.containerMargins))
|
||||
XCTAssertEqual(surface.contentView.frame, surface.containerView.bounds.inset(by: surface.contentInsets))
|
||||
}
|
||||
|
||||
func test_surfaceView_cornderRaduis() {
|
||||
let surface = FloatingPanelSurfaceView(frame: CGRect(x: 0.0, y: 0.0, width: 320.0, height: 480.0))
|
||||
XCTAssert(surface.cornerRadius == 0.0)
|
||||
|
||||
@@ -523,7 +523,7 @@ private class FloatingPanelLayout3Positions: FloatingPanelTestLayout {
|
||||
}
|
||||
|
||||
private typealias TestParameter = (UInt, CGFloat,CGPoint, FloatingPanelPosition)
|
||||
private func assertTargetPosition(_ floatingPanel: FloatingPanel, with params: [TestParameter]) {
|
||||
private func assertTargetPosition(_ floatingPanel: FloatingPanelCore, with params: [TestParameter]) {
|
||||
params.forEach { (line, pos, velocity, result) in
|
||||
floatingPanel.surfaceView.frame.origin.y = pos
|
||||
XCTAssertEqual(floatingPanel.targetPosition(from: pos, with: velocity), result, line: line)
|
||||
|
||||
@@ -7,8 +7,7 @@
|
||||
[](https://swift.org/)
|
||||
[](https://swift.org/)
|
||||
|
||||
# FloatingPanel
|
||||
|
||||
# FloatingPanel
|
||||
|
||||
FloatingPanel is a simple and easy-to-use UI component for a new interface introduced in Apple Maps, Shortcuts and Stocks app.
|
||||
The new interface displays the related contents and utilities in parallel as a user wants.
|
||||
@@ -25,20 +24,31 @@ The new interface displays the related contents and utilities in parallel as a u
|
||||
- [Installation](#installation)
|
||||
- [CocoaPods](#cocoapods)
|
||||
- [Carthage](#carthage)
|
||||
- [Swift Package Manager with Xcode 11](#swift-package-manager-with-xcode-11)
|
||||
- [Getting Started](#getting-started)
|
||||
- [Add a floating panel as a child view controller](#add-a-floating-panel-as-a-child-view-controller)
|
||||
- [Present a floating panel as a modality](#present-a-floating-panel-as-a-modality)
|
||||
- [View hierarchy](#view-hierarchy)
|
||||
- [Usage](#usage)
|
||||
- [Show/Hide a floating panel in a view with your view hierarchy](#showhide-a-floating-panel-in-a-view-with-your-view-hierarchy)
|
||||
- [Scale the content view when the surface position changes](#scale-the-content-view-when-the-surface-position-changes)
|
||||
- [Customize the layout with `FloatingPanelLayout` protocol](#customize-the-layout-with-floatingpanellayout-protocol)
|
||||
- [Change the initial position and height](#change-the-initial-position-and-height)
|
||||
- [Support your landscape layout](#support-your-landscape-layout)
|
||||
- [Use Intrinsic height layout](#use-intrinsic-height-layout)
|
||||
- [Specify position insets from the frame of `FloatingPanelContrller.view`, not the SafeArea](#specify-position-insets-from-the-frame-of-floatingpanelcontrllerview-not-the-safearea)
|
||||
- [Customize the behavior with `FloatingPanelBehavior` protocol](#customize-the-behavior-with-floatingpanelbehavior-protocol)
|
||||
- [Modify your floating panel's interaction](#modify-your-floating-panels-interaction)
|
||||
- [Use a custom grabber handle](#use-a-custom-grabber-handle)
|
||||
- [Add tap gestures to the surface or backdrop views](#add-tap-gestures-to-the-surface-or-backdrop-views)
|
||||
- [Activate the rubber-band effect on the top/bottom edges](#activate-the-rubber-band-effect-on-the-topbottom-edges)
|
||||
- [Manage the projection of a pan gesture momentum](#manage-the-projection-of-a-pan-gesture-momentum)
|
||||
- [Customize the surface design](#customize-the-surface-design)
|
||||
- [Use a custom grabber handle](#use-a-custom-grabber-handle)
|
||||
- [Customize layout of the grabber handle](#customize-layout-of-the-grabber-handle)
|
||||
- [Customize content padding from surface edges](#customize-content-padding-from-surface-edges)
|
||||
- [Customize margins of the surface edges](#customize-margins-of-the-surface-edges)
|
||||
- [Customize gestures](#customize-gestures)
|
||||
- [Suppress the panel interaction](#suppress-the-panel-interaction)
|
||||
- [Add tap gestures to the surface or backdrop views](#add-tap-gestures-to-the-surface-or-backdrop-views)
|
||||
- [Create an additional floating panel for a detail](#create-an-additional-floating-panel-for-a-detail)
|
||||
- [Move a position with an animation](#move-a-position-with-an-animation)
|
||||
- [Work your contents together with a floating panel behavior](#work-your-contents-together-with-a-floating-panel-behavior)
|
||||
@@ -85,7 +95,7 @@ it, simply add the following line to your Podfile:
|
||||
pod 'FloatingPanel'
|
||||
```
|
||||
|
||||
✏️ To suppress "Swift Conversion" warnings in Xcode, please set a Swift version to `SWIFT_VERSION` for the project in your Podfile. It will be resolved in CocoaPods v1.7.0.
|
||||
✏️FloatingPanel v1.7.0 or later requires CocoaPods v1.7.0+ for `swift_versions` support.
|
||||
|
||||
### Carthage
|
||||
|
||||
@@ -171,41 +181,72 @@ FloatingPanelController.view (FloatingPanelPassThroughView)
|
||||
|
||||
### Show/Hide a floating panel in a view with your view hierarchy
|
||||
|
||||
If you need more control over showing and hiding the floating panel, you can forgo the `addPanel` and `removePanelFromParent` methods. These methods are a convenience wrapper for **FloatingPanel**'s `show` and `hide` methods along with some required setup.
|
||||
|
||||
There are two ways to work with the `FloatingPanelController`:
|
||||
1. Add it to the hierarchy once and then call `show` and `hide` methods to make it appear/disappear.
|
||||
2. Add it to the hierarchy when needed and remove afterwards.
|
||||
|
||||
The following example shows how to add the controller to your `UIViewController` and how to remove it. Make sure that you never add the same `FloatingPanelController` to the hierarchy before removing it.
|
||||
|
||||
**NOTE**: `self.` prefix is not required, nor recommended. It's used here to make it clearer where do the functions used come from. `self` is an instance of a custom UIViewController in your code.
|
||||
|
||||
```swift
|
||||
// Add the controller and the managed views to a view controller.
|
||||
// From the second time, just call `show(animated:completion)`.
|
||||
view.addSubview(fpc.view)
|
||||
// Add the floating panel view to the controller's view on top of other views.
|
||||
self.view.addSubview(fpc.view)
|
||||
|
||||
// REQUIRED. It makes the floating panel view have the same size as the controller's view.
|
||||
fpc.view.frame = self.view.bounds
|
||||
|
||||
fpc.view.frame = view.bounds // MUST
|
||||
// In addition, Auto Layout constraints are highly recommended.
|
||||
// Because it makes the layout more robust on trait collection change.
|
||||
//
|
||||
// fpc.view.translatesAutoresizingMaskIntoConstraints = false
|
||||
// NSLayoutConstraint.activate([...])
|
||||
//
|
||||
// Constraint the fpc.view to all four edges of your controller's view.
|
||||
// It makes the layout more robust on trait collection change.
|
||||
fpc.view.translatesAutoresizingMaskIntoConstraints = false
|
||||
NSLayoutConstraint.activate([
|
||||
fpc.view.topAnchor.constraint(equalTo: self.view.topAnchor, constant: 0.0),
|
||||
fpc.view.leftAnchor.constraint(equalTo: self.view.leftAnchor, constant: 0.0),
|
||||
fpc.view.rightAnchor.constraint(equalTo: self.view.rightAnchor, constant: 0.0),
|
||||
fpc.view.bottomAnchor.constraint(equalTo: self.view.bottomAnchor, constant: 0.0),
|
||||
])
|
||||
|
||||
parent.addChild(fpc)
|
||||
// Add the floating panel controller to the controller hierarchy.
|
||||
self.addChild(fpc)
|
||||
|
||||
// Show a floating panel to the initial position defined in your `FloatingPanelLayout` object.
|
||||
// Show the floating panel at the initial position defined in your `FloatingPanelLayout` object.
|
||||
fpc.show(animated: true) {
|
||||
|
||||
// Only for the first time
|
||||
self.didMove(toParent: self)
|
||||
}
|
||||
|
||||
...
|
||||
|
||||
// Hide it
|
||||
fpc.hide(animated: true) {
|
||||
|
||||
// Remove it if needed
|
||||
self.willMove(toParent: nil)
|
||||
self.view.removeFromSuperview()
|
||||
self.removeFromParent()
|
||||
// Inform the floating panel controller that the transition to the controller hierarchy has completed.
|
||||
fpc.didMove(toParent: self)
|
||||
}
|
||||
```
|
||||
|
||||
NOTE: `FloatingPanelController` wraps `show`/`hide` with `addPanel`/`removePanelFromParent` for easy-to-use. But `show`/`hide` are more convenience for your app.
|
||||
After you add the `FloatingPanelController` as seen above, you can call `fpc.show(animated: true) { }` to show the panel and `fpc.hide(animated: true) { }` to hide it.
|
||||
|
||||
To remove the `FloatingPanelController` from the hierarchy, follow the example below.
|
||||
|
||||
```swift
|
||||
// Inform the panel controller that it will be removed from the hierarchy.
|
||||
fpc.willMove(toParent: nil)
|
||||
|
||||
// Hide the floating panel.
|
||||
fpc.hide(animated: true) {
|
||||
// Remove the floating panel view from your controller's view.
|
||||
fpc.view.removeFromSuperview()
|
||||
// Remove the floating panel controller from the controller hierarchy.
|
||||
fpc.removeFromParent()
|
||||
}
|
||||
```
|
||||
|
||||
### Scale the content view when the surface position changes
|
||||
|
||||
Specify the `contentMode` to `.fitToBounds` if the surface height fits the bounds of `FloatingPanelController.view` when the surface position changes
|
||||
|
||||
```swift
|
||||
fpc.contentMode = .fitToBounds
|
||||
```
|
||||
|
||||
Otherwise, `FloatingPanelController` fixes the content by the height of the top most position.
|
||||
|
||||
✏️ In `.fitToBounds` mode, the surface height changes as following a user interaction so that you have a responsibility to configure Auto Layout constrains not to break the layout of a content view by the elastic surface height.
|
||||
|
||||
### Customize the layout with `FloatingPanelLayout` protocol
|
||||
|
||||
@@ -297,6 +338,27 @@ class RemovablePanelLayout: FloatingPanelIntrinsicLayout {
|
||||
}
|
||||
```
|
||||
|
||||
#### Specify position insets from the frame of `FloatingPanelContrller.view`, not the SafeArea
|
||||
|
||||
There are 2 ways. One is returning `.fromSuperview` for `FloatingPanelLayout.positionReference` in your layout.
|
||||
|
||||
```swift
|
||||
class MyFullScreenLayout: FloatingPanelLayout {
|
||||
...
|
||||
var positionReference: FloatingPanelLayoutReference {
|
||||
return .fromSuperview
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Another is using `FloatingPanelFullScreenLayout` protocol.
|
||||
|
||||
```swift
|
||||
class MyFullScreenLayout: FloatingPanelFullScreenLayout {
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
### Customize the behavior with `FloatingPanelBehavior` protocol
|
||||
|
||||
#### Modify your floating panel's interaction
|
||||
@@ -319,7 +381,33 @@ class FloatingPanelStocksBehavior: FloatingPanelBehavior {
|
||||
}
|
||||
```
|
||||
|
||||
### Use a custom grabber handle
|
||||
#### Activate the rubber-band effect on the top/bottom edges
|
||||
|
||||
```swift
|
||||
class FloatingPanelBehavior: FloatingPanelBehavior {
|
||||
...
|
||||
func allowsRubberBanding(for edge: UIRectEdge) -> Bool {
|
||||
return true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Manage the projection of a pan gesture momentum
|
||||
|
||||
This allows full projectional panel behavior. For example, a user can swipe up a panel from tip to full nearby the tip position.
|
||||
|
||||
```swift
|
||||
class FloatingPanelBehavior: FloatingPanelBehavior {
|
||||
...
|
||||
func shouldProjectMomentum(_ fpc: FloatingPanelController, for proposedTargetPosition: FloatingPanelPosition) -> Bool {
|
||||
return true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Customize the surface design
|
||||
|
||||
#### Use a custom grabber handle
|
||||
|
||||
```swift
|
||||
let myGrabberHandleView = MyGrabberHandleView()
|
||||
@@ -327,7 +415,50 @@ fpc.surfaceView.grabberHandle.isHidden = true
|
||||
fpc.surfaceView.addSubview(myGrabberHandleView)
|
||||
```
|
||||
|
||||
### Add tap gestures to the surface or backdrop views
|
||||
#### Customize layout of the grabber handle
|
||||
|
||||
```swift
|
||||
fpc.surfaceView.grabberTopPadding = 10.0
|
||||
fpc.surfaceView.grabberHandleWidth = 44.0
|
||||
fpc.surfaceView.grabberHandleHeight = 12.0
|
||||
```
|
||||
|
||||
#### Customize content padding from surface edges
|
||||
|
||||
```swift
|
||||
fpc.surfaceView.contentInsets = .init(top: 20, left: 20, bottom: 20, right: 20)
|
||||
```
|
||||
|
||||
#### Customize margins of the surface edges
|
||||
|
||||
```swift
|
||||
fpc.surfaceView.containerMargins = .init(top: 20.0, left: 16.0, bottom: 16.0, right: 16.0)
|
||||
```
|
||||
|
||||
The feature can be used for these 2 kind panels
|
||||
|
||||
* Facebook/Slack-like panel whose surface top edge is separated from the grabber handle.
|
||||
* iOS native panel to display AirPods information, for example.
|
||||
|
||||
### Customize gestures
|
||||
|
||||
#### Suppress the panel interaction
|
||||
|
||||
You can disable the pan gesture recognizer directly
|
||||
|
||||
```swift
|
||||
fpc.panGestureRecognizer.isEnable = false
|
||||
```
|
||||
|
||||
Or use this `FloatingPanelControllerDelegate` method.
|
||||
|
||||
```swift
|
||||
func floatingPanelShouldBeginDragging(_ vc: FloatingPanelController) -> Bool {
|
||||
return aCondition ? false : true
|
||||
}
|
||||
```
|
||||
|
||||
#### Add tap gestures to the surface or backdrop views
|
||||
|
||||
```swift
|
||||
override func viewDidLoad() {
|
||||
|
||||
Reference in New Issue
Block a user