Compare commits
24 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f909cd4101 | |||
| 8c24aa3fc9 | |||
| 7531d80f1c | |||
| c4c1906cae | |||
| 4f717c5840 | |||
| fbd83ef500 | |||
| 2a91145366 | |||
| f5d72aa0a5 | |||
| 65f67c98f4 | |||
| 1f79c2573f | |||
| bc840dde46 | |||
| 57ed039857 | |||
| 11f0e8c84e | |||
| dd19c866d4 | |||
| 801fed9843 | |||
| 847b5c0917 | |||
| c64056ca7b | |||
| 269c3e29b5 | |||
| 002bbb4a4a | |||
| 14011a5bc2 | |||
| 23f2242c9a | |||
| 4fd92a4002 | |||
| 9c57089b0e | |||
| 3b11cdc72a |
@@ -2,7 +2,7 @@
|
||||
>
|
||||
> Please remove this line and everything above it before submitting.
|
||||
|
||||
### Short description
|
||||
### Description
|
||||
|
||||
### Expected behavior
|
||||
|
||||
@@ -12,16 +12,29 @@
|
||||
|
||||
**Code example that reproduces the issue**
|
||||
|
||||
|
||||
**How do you display panel(s)?**
|
||||
|
||||
* Add as child view controllers
|
||||
* Present modally
|
||||
|
||||
**How many panels do you displays?**
|
||||
|
||||
* 1
|
||||
* 2+
|
||||
|
||||
### Environment
|
||||
|
||||
**Library version**
|
||||
|
||||
**Installation method**
|
||||
|
||||
- [ ] CocoaPods
|
||||
- [ ] Carthage
|
||||
- [ ] Git submodules
|
||||
|
||||
* CocoaPods
|
||||
* Carthage
|
||||
* Swift Package Manager
|
||||
|
||||
**iOS version(s)**
|
||||
|
||||
|
||||
**Xcode version**
|
||||
|
||||
|
||||
@@ -30,6 +30,9 @@ xcuserdata/
|
||||
*.dSYM.zip
|
||||
*.dSYM
|
||||
|
||||
## Swift Package Manager Specific
|
||||
.swiftpm/
|
||||
|
||||
## Playgrounds
|
||||
timeline.xctimeline
|
||||
playground.xcworkspace
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -294,7 +294,7 @@
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "/usr/libexec/PlistBuddy -c \"Set :CFBundleVersion $(git rev-parse --abbrev-ref HEAD)($(git rev-parse --short HEAD))\" $SRCROOT/$INFOPLIST_FILE\n";
|
||||
shellScript = "/usr/libexec/PlistBuddy -c \"Set :CFBundleVersion $(git rev-parse --abbrev-ref HEAD)($(git rev-parse --short HEAD))\" \"$SRCROOT/$INFOPLIST_FILE\"\n";
|
||||
};
|
||||
/* End PBXShellScriptBuildPhase section */
|
||||
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<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>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="15705" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="RoN-h0-uBD">
|
||||
<device id="retina5_9" orientation="portrait" appearance="light"/>
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14490.49"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="15706"/>
|
||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
@@ -13,7 +11,7 @@
|
||||
<!--Navigation Controller-->
|
||||
<scene sceneID="Cjh-iX-VQw">
|
||||
<objects>
|
||||
<navigationController id="RoN-h0-uBD" sceneMemberID="viewController">
|
||||
<navigationController storyboardIdentifier="RootNavigationController" id="RoN-h0-uBD" sceneMemberID="viewController">
|
||||
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" largeTitles="YES" id="hNW-5m-Omi">
|
||||
<rect key="frame" x="0.0" y="44" width="375" height="96"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
|
||||
@@ -18,6 +18,7 @@ class SampleListViewController: UIViewController {
|
||||
case showDetail
|
||||
case showModal
|
||||
case showPanelModal
|
||||
case showMultiPanelModal
|
||||
case showTabBar
|
||||
case showPageView
|
||||
case showPageContentView
|
||||
@@ -26,6 +27,7 @@ class SampleListViewController: UIViewController {
|
||||
case showIntrinsicView
|
||||
case showContentInset
|
||||
case showContainerMargins
|
||||
case showNavigationController
|
||||
|
||||
var name: String {
|
||||
switch self {
|
||||
@@ -34,6 +36,7 @@ class SampleListViewController: UIViewController {
|
||||
case .showDetail: return "Show Detail Panel"
|
||||
case .showModal: return "Show Modal"
|
||||
case .showPanelModal: return "Show Panel Modal"
|
||||
case .showMultiPanelModal: return "Show Multi Panel Modal"
|
||||
case .showTabBar: return "Show Tab Bar"
|
||||
case .showPageView: return "Show Page View"
|
||||
case .showPageContentView: return "Show Page Content View"
|
||||
@@ -42,6 +45,7 @@ class SampleListViewController: UIViewController {
|
||||
case .showIntrinsicView: return "Show Intrinsic View"
|
||||
case .showContentInset: return "Show with ContentInset"
|
||||
case .showContainerMargins: return "Show with ContainerMargins"
|
||||
case .showNavigationController: return "Show Navigation Controller"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,6 +55,7 @@ class SampleListViewController: UIViewController {
|
||||
case .trackingTextView: return "ConsoleViewController"
|
||||
case .showDetail: return "DetailViewController"
|
||||
case .showModal: return "ModalViewController"
|
||||
case .showMultiPanelModal: return nil
|
||||
case .showPanelModal: return nil
|
||||
case .showTabBar: return "TabBarViewController"
|
||||
case .showPageView: return nil
|
||||
@@ -60,6 +65,7 @@ class SampleListViewController: UIViewController {
|
||||
case .showIntrinsicView: return "IntrinsicViewController"
|
||||
case .showContentInset: return nil
|
||||
case .showContainerMargins: return nil
|
||||
case .showNavigationController: return "RootNavigationController"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -80,6 +86,7 @@ class SampleListViewController: UIViewController {
|
||||
tableView.dataSource = self
|
||||
tableView.delegate = self
|
||||
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "Cell")
|
||||
automaticallyAdjustsScrollViewInsets = false
|
||||
|
||||
let searchController = UISearchController(searchResultsController: nil)
|
||||
if #available(iOS 11.0, *) {
|
||||
@@ -92,11 +99,14 @@ class SampleListViewController: UIViewController {
|
||||
|
||||
let contentVC = DebugTableViewController()
|
||||
addMainPanel(with: contentVC)
|
||||
|
||||
var insets = UIEdgeInsets.zero
|
||||
insets.bottom += 69.0
|
||||
tableView.contentInset = insets
|
||||
}
|
||||
|
||||
override func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
|
||||
if #available(iOS 11.0, *) {
|
||||
if let observation = navigationController?.navigationBar.observe(\.prefersLargeTitles, changeHandler: { (bar, _) in
|
||||
self.tableView.reloadData()
|
||||
@@ -116,15 +126,13 @@ class SampleListViewController: UIViewController {
|
||||
|
||||
let oldMainPanelVC = mainPanelVC
|
||||
|
||||
// Initialize FloatingPanelController
|
||||
mainPanelVC = FloatingPanelController()
|
||||
mainPanelVC.delegate = self
|
||||
mainPanelVC.contentInsetAdjustmentBehavior = .always
|
||||
|
||||
// Initialize FloatingPanelController and add the view
|
||||
mainPanelVC.surfaceView.cornerRadius = 6.0
|
||||
mainPanelVC.surfaceView.shadowHidden = false
|
||||
|
||||
// Set a content view controller
|
||||
mainPanelVC.set(contentViewController: contentVC)
|
||||
|
||||
// Enable tap-to-hide and removal interaction
|
||||
@@ -143,6 +151,8 @@ class SampleListViewController: UIViewController {
|
||||
|
||||
let backdropTapGesture = UITapGestureRecognizer(target: self, action: #selector(handleBackdrop(tapGesture:)))
|
||||
mainPanelVC.backdropView.addGestureRecognizer(backdropTapGesture)
|
||||
case .showNavigationController:
|
||||
mainPanelVC.contentInsetAdjustmentBehavior = .never
|
||||
default:
|
||||
break
|
||||
}
|
||||
@@ -160,6 +170,11 @@ class SampleListViewController: UIViewController {
|
||||
mainPanelVC.track(scrollView: contentVC.tableView)
|
||||
case let contentVC as NestedScrollViewController:
|
||||
mainPanelVC.track(scrollView: contentVC.scrollView)
|
||||
case let navVC as UINavigationController:
|
||||
if let rootVC = (navVC.topViewController as? SampleListViewController) {
|
||||
rootVC.loadViewIfNeeded()
|
||||
mainPanelVC.track(scrollView: rootVC.tableView)
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
@@ -331,6 +346,10 @@ extension SampleListViewController: UITableViewDelegate {
|
||||
|
||||
self.present(fpc, animated: true, completion: nil)
|
||||
|
||||
case .showMultiPanelModal:
|
||||
let fpc = MultiPanelController()
|
||||
self.present(fpc, animated: true, completion: nil)
|
||||
|
||||
case .showContentInset:
|
||||
let contentViewController = UIViewController()
|
||||
contentViewController.view.backgroundColor = .green
|
||||
@@ -368,6 +387,14 @@ extension SampleListViewController: UITableViewDelegate {
|
||||
}
|
||||
|
||||
extension SampleListViewController: FloatingPanelControllerDelegate {
|
||||
func floatingPanel(_ vc: FloatingPanelController, contentOffsetForPinning trackedScrollView: UIScrollView) -> CGPoint {
|
||||
if currentMenu == .showNavigationController, #available(iOSApplicationExtension 11.0, *) {
|
||||
// 148.0 is the SafeArea's top value for a navigation bar with a large title.
|
||||
return CGPoint(x: 0.0, y: 0.0 - trackedScrollView.contentInset.top - 148.0)
|
||||
}
|
||||
return CGPoint(x: 0.0, y: 0.0 - trackedScrollView.contentInset.top)
|
||||
}
|
||||
|
||||
func floatingPanel(_ vc: FloatingPanelController, layoutFor newCollection: UITraitCollection) -> FloatingPanelLayout? {
|
||||
if vc == settingsPanelVC {
|
||||
return IntrinsicPanelLayout()
|
||||
@@ -636,7 +663,8 @@ class InspectableViewController: UIViewController {
|
||||
}
|
||||
|
||||
class DebugTableViewController: InspectableViewController {
|
||||
weak var tableView: UITableView!
|
||||
lazy var tableView = UITableView(frame: .zero, style: .plain)
|
||||
lazy var buttonStackView = UIStackView()
|
||||
var items: [String] = []
|
||||
var itemHeight: CGFloat = 66.0
|
||||
|
||||
@@ -650,8 +678,6 @@ class DebugTableViewController: InspectableViewController {
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
let tableView = UITableView(frame: .zero,
|
||||
style: .plain)
|
||||
view.addSubview(tableView)
|
||||
tableView.translatesAutoresizingMaskIntoConstraints = false
|
||||
NSLayoutConstraint.activate([
|
||||
@@ -662,18 +688,17 @@ class DebugTableViewController: InspectableViewController {
|
||||
])
|
||||
tableView.dataSource = self
|
||||
tableView.delegate = self
|
||||
self.tableView = tableView
|
||||
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "Cell")
|
||||
|
||||
let stackView = UIStackView()
|
||||
view.addSubview(stackView)
|
||||
stackView.axis = .vertical
|
||||
stackView.distribution = .fillEqually
|
||||
stackView.alignment = .trailing
|
||||
stackView.spacing = 10.0
|
||||
stackView.translatesAutoresizingMaskIntoConstraints = false
|
||||
view.addSubview(buttonStackView)
|
||||
buttonStackView.axis = .vertical
|
||||
buttonStackView.distribution = .fillEqually
|
||||
buttonStackView.alignment = .trailing
|
||||
buttonStackView.spacing = 10.0
|
||||
buttonStackView.translatesAutoresizingMaskIntoConstraints = false
|
||||
NSLayoutConstraint.activate([
|
||||
stackView.topAnchor.constraint(equalTo: view.topAnchor, constant: 22.0),
|
||||
stackView.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -22.0),
|
||||
buttonStackView.topAnchor.constraint(equalTo: view.topAnchor, constant: 22.0),
|
||||
buttonStackView.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -22.0),
|
||||
])
|
||||
|
||||
for menu in Menu.allCases {
|
||||
@@ -689,13 +714,12 @@ class DebugTableViewController: InspectableViewController {
|
||||
button.addTarget(self, action: #selector(reorderItems), for: .touchUpInside)
|
||||
reorderButton = button
|
||||
}
|
||||
stackView.addArrangedSubview(button)
|
||||
buttonStackView.addArrangedSubview(button)
|
||||
}
|
||||
|
||||
for i in 0...100 {
|
||||
items.append("Items \(i)")
|
||||
}
|
||||
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "Cell")
|
||||
}
|
||||
|
||||
@objc func animateScroll() {
|
||||
@@ -1270,3 +1294,72 @@ class SettingsViewController: InspectableViewController {
|
||||
navigationController?.navigationBar.isTranslucent = sender.isOn
|
||||
}
|
||||
}
|
||||
|
||||
// MARK -: Multi Panel
|
||||
|
||||
import WebKit
|
||||
final class MultiPanelController: FloatingPanelController, FloatingPanelControllerDelegate {
|
||||
|
||||
private final class FirstPanelContentViewController: UIViewController {
|
||||
|
||||
lazy var webView: WKWebView = WKWebView()
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
view.addSubview(webView)
|
||||
webView.frame = view.bounds
|
||||
webView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
||||
webView.load(URLRequest(url: URL(string: "https://www.apple.com")!))
|
||||
|
||||
let vc = MultiSecondPanelController()
|
||||
vc.setUpContent()
|
||||
vc.addPanel(toParent: self)
|
||||
}
|
||||
}
|
||||
|
||||
private final class MultiSecondPanelController: FloatingPanelController {
|
||||
|
||||
private final class SecondPanelContentViewController: DebugTableViewController {}
|
||||
|
||||
func setUpContent() {
|
||||
contentInsetAdjustmentBehavior = .never
|
||||
let vc = SecondPanelContentViewController()
|
||||
vc.loadViewIfNeeded()
|
||||
vc.title = "Second Panel"
|
||||
vc.buttonStackView.isHidden = true
|
||||
let navigationController = UINavigationController(rootViewController: vc)
|
||||
navigationController.navigationBar.barTintColor = .white
|
||||
navigationController.navigationBar.titleTextAttributes = [
|
||||
.foregroundColor: UIColor.black
|
||||
]
|
||||
set(contentViewController: navigationController)
|
||||
self.track(scrollView: vc.tableView)
|
||||
surfaceView.containerMargins = .init(top: 24.0, left: 0.0, bottom: layoutInsets.bottom, right: 0.0)
|
||||
}
|
||||
}
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
delegate = self
|
||||
isRemovalInteractionEnabled = true
|
||||
|
||||
let vc = FirstPanelContentViewController()
|
||||
set(contentViewController: vc)
|
||||
track(scrollView: vc.webView.scrollView)
|
||||
}
|
||||
|
||||
private final class FirstViewLayout: FloatingPanelLayout {
|
||||
let initialPosition: FloatingPanelPosition = .full
|
||||
let supportedPositions: Set<FloatingPanelPosition> = [.full]
|
||||
func insetFor(position: FloatingPanelPosition) -> CGFloat? {
|
||||
switch position {
|
||||
case .full: return 40.0
|
||||
default: return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func floatingPanel(_ vc: FloatingPanelController, layoutFor newCollection: UITraitCollection) -> FloatingPanelLayout? {
|
||||
return FirstViewLayout()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
Pod::Spec.new do |s|
|
||||
|
||||
s.name = "FloatingPanel"
|
||||
s.version = "1.7.1"
|
||||
s.version = "1.7.3"
|
||||
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.
|
||||
|
||||
@@ -37,6 +37,15 @@ public protocol FloatingPanelControllerDelegate: class {
|
||||
///
|
||||
/// By default, any tap and long gesture recognizers are allowed to recognize gestures simultaneously.
|
||||
func floatingPanel(_ vc: FloatingPanelController, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool
|
||||
|
||||
/// Asks the delegate for a content offset of the tracked scroll view to be pinned when a floating panel moves
|
||||
///
|
||||
/// If you do not implement this method, the controller uses a value of the content offset plus the content insets
|
||||
/// of the tracked scroll view. Your implementation of this method can return a value for a navigation bar with a large
|
||||
/// title, for example.
|
||||
///
|
||||
/// This method will not be called if the controller doesn't track any scroll view.
|
||||
func floatingPanel(_ vc: FloatingPanelController, contentOffsetForPinning trackedScrollView: UIScrollView) -> CGPoint
|
||||
}
|
||||
|
||||
public extension FloatingPanelControllerDelegate {
|
||||
@@ -62,6 +71,9 @@ public extension FloatingPanelControllerDelegate {
|
||||
func floatingPanel(_ vc: FloatingPanelController, shouldRecognizeSimultaneouslyWith gestureRecognizer: UIGestureRecognizer) -> Bool {
|
||||
return false
|
||||
}
|
||||
func floatingPanel(_ vc: FloatingPanelController, contentOffsetForPinning trackedScrollView: UIScrollView) -> CGPoint {
|
||||
return CGPoint(x: 0.0, y: 0.0 - trackedScrollView.contentInset.top)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -187,7 +199,13 @@ 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 }
|
||||
@@ -279,6 +297,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 }
|
||||
@@ -340,13 +393,18 @@ open class FloatingPanelController: UIViewController {
|
||||
private func activateLayout() {
|
||||
floatingPanel.layoutAdapter.prepareLayout(in: self)
|
||||
|
||||
// preserve the current content offset
|
||||
let contentOffset = scrollView?.contentOffset
|
||||
// preserve the current content offset if contentInsetAdjustmentBehavior is `.always`
|
||||
var contentOffset: CGPoint?
|
||||
if contentInsetAdjustmentBehavior == .always {
|
||||
contentOffset = scrollView?.contentOffset
|
||||
}
|
||||
|
||||
floatingPanel.layoutAdapter.updateHeight()
|
||||
floatingPanel.layoutAdapter.activateLayout(of: floatingPanel.state)
|
||||
|
||||
scrollView?.contentOffset = contentOffset ?? .zero
|
||||
if let contentOffset = contentOffset {
|
||||
scrollView?.contentOffset = contentOffset
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Container view controller interface
|
||||
|
||||
@@ -191,6 +191,9 @@ class FloatingPanelCore: NSObject, UIGestureRecognizerDelegate {
|
||||
}
|
||||
|
||||
switch otherGestureRecognizer {
|
||||
case is FloatingPanelPanGestureRecognizer:
|
||||
// All visiable panels' pan gesture should be recognized simultaneously.
|
||||
return true
|
||||
case is UIPanGestureRecognizer,
|
||||
is UISwipeGestureRecognizer,
|
||||
is UIRotationGestureRecognizer,
|
||||
@@ -210,8 +213,14 @@ class FloatingPanelCore: NSObject, UIGestureRecognizerDelegate {
|
||||
}
|
||||
|
||||
public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldBeRequiredToFailBy otherGestureRecognizer: UIGestureRecognizer) -> Bool {
|
||||
guard gestureRecognizer == panGestureRecognizer else { return false }
|
||||
/* log.debug("shouldBeRequiredToFailBy", otherGestureRecognizer) */
|
||||
if otherGestureRecognizer is FloatingPanelPanGestureRecognizer {
|
||||
// If this panel is the farthest descendant of visiable panels,
|
||||
// its ancestors' pan gesture must wait for its pan gesture to fail
|
||||
if let view = otherGestureRecognizer.view, surfaceView.isDescendant(of: view) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -234,8 +243,10 @@ class FloatingPanelCore: NSObject, UIGestureRecognizerDelegate {
|
||||
scrollGestureRecognizers.contains(otherGestureRecognizer) {
|
||||
switch otherGestureRecognizer {
|
||||
case scrollView.panGestureRecognizer:
|
||||
let offset = scrollView.contentOffset.y - scrollView.contentOffsetZero.y
|
||||
return allowScrollPanGesture(at: CGPoint(x: 0.0, y: offset))
|
||||
if grabberAreaFrame.contains(gestureRecognizer.location(in: gestureRecognizer.view)) {
|
||||
return false
|
||||
}
|
||||
return allowScrollPanGesture(for: scrollView)
|
||||
default:
|
||||
return false
|
||||
}
|
||||
@@ -248,6 +259,13 @@ class FloatingPanelCore: NSObject, UIGestureRecognizerDelegate {
|
||||
}
|
||||
|
||||
switch otherGestureRecognizer {
|
||||
case is FloatingPanelPanGestureRecognizer:
|
||||
// If this panel is the farthest descendant of visiable panels,
|
||||
// its pan gesture does not require its ancestors' pan gesture to fail
|
||||
if let view = otherGestureRecognizer.view, surfaceView.isDescendant(of: view) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
case is UIPanGestureRecognizer,
|
||||
is UISwipeGestureRecognizer,
|
||||
is UIRotationGestureRecognizer,
|
||||
@@ -290,14 +308,13 @@ class FloatingPanelCore: NSObject, UIGestureRecognizerDelegate {
|
||||
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)) --",
|
||||
"belowTop = \(belowTop),",
|
||||
"interactionInProgress = \(interactionInProgress),",
|
||||
"scroll offset = \(offset),",
|
||||
"scroll offset = \(scrollView.contentOffset.y),",
|
||||
"location = \(location.y), velocity = \(velocity.y)")
|
||||
|
||||
let offset = scrollView.contentOffset.y - contentOrigin(of: scrollView).y
|
||||
|
||||
if belowTop {
|
||||
// Scroll offset pinning
|
||||
@@ -340,11 +357,11 @@ class FloatingPanelCore: NSObject, UIGestureRecognizerDelegate {
|
||||
} else {
|
||||
if state == layoutAdapter.topMostState {
|
||||
// Hide a scroll indicator just before starting an interaction by swiping a panel down.
|
||||
if velocity.y > 0, !allowScrollPanGesture(at: CGPoint(x: 0.0, y: offset)) {
|
||||
if velocity.y > 0, !allowScrollPanGesture(for: scrollView) {
|
||||
lockScrollView()
|
||||
}
|
||||
// Show a scroll indicator when an animation is interrupted at the top and content is scrolled up
|
||||
if velocity.y < 0, allowScrollPanGesture(at: CGPoint(x: 0.0, y: offset)) {
|
||||
if velocity.y < 0, allowScrollPanGesture(for: scrollView) {
|
||||
unlockScrollView()
|
||||
}
|
||||
|
||||
@@ -380,7 +397,7 @@ class FloatingPanelCore: NSObject, UIGestureRecognizerDelegate {
|
||||
}
|
||||
animator.finishAnimation(at: .current)
|
||||
} else {
|
||||
self.animator = nil
|
||||
self.endAnimation(false) // Must call it manually
|
||||
}
|
||||
}
|
||||
|
||||
@@ -447,14 +464,15 @@ class FloatingPanelCore: NSObject, UIGestureRecognizerDelegate {
|
||||
return true
|
||||
}
|
||||
|
||||
let scrollViewFrame = scrollView.convert(scrollView.bounds, to: surfaceView)
|
||||
guard
|
||||
scrollView.frame.contains(initialLocation), // When initialLocation not in scrollView, don't scroll.
|
||||
scrollViewFrame.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
|
||||
let offset = scrollView.contentOffset.y - contentOrigin(of: scrollView).y
|
||||
// The zero offset must be excluded because the offset is usually zero
|
||||
// after a panel moves from half/tip to full.
|
||||
if offset > 0.0 {
|
||||
@@ -602,19 +620,22 @@ class FloatingPanelCore: 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 {
|
||||
@@ -680,9 +701,12 @@ class FloatingPanelCore: NSObject, UIGestureRecognizerDelegate {
|
||||
if grabberAreaFrame.contains(location) {
|
||||
initialScrollOffset = scrollView.contentOffset
|
||||
} else {
|
||||
initialScrollOffset = contentOrigin(of: scrollView)
|
||||
// Fit the surface bounds to a scroll offset content by startInteraction(at:offset:)
|
||||
offset = CGPoint(x: -scrollView.contentOffset.x, y: -scrollView.contentOffset.y)
|
||||
initialScrollOffset = scrollView.contentOffsetZero
|
||||
let scrollOffsetY = (scrollView.contentOffset.y - contentOrigin(of: scrollView).y)
|
||||
if scrollOffsetY < 0 {
|
||||
offset = CGPoint(x: -scrollView.contentOffset.x, y: -scrollOffsetY)
|
||||
}
|
||||
}
|
||||
log.debug("initial scroll offset --", initialScrollOffset)
|
||||
}
|
||||
@@ -755,15 +779,14 @@ class FloatingPanelCore: NSObject, UIGestureRecognizerDelegate {
|
||||
// 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
|
||||
|
||||
@@ -778,7 +801,7 @@ class FloatingPanelCore: 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()
|
||||
}
|
||||
}
|
||||
@@ -881,7 +904,15 @@ class FloatingPanelCore: NSObject, UIGestureRecognizerDelegate {
|
||||
scrollView?.setContentOffset(contentOffset, animated: false)
|
||||
}
|
||||
|
||||
private func allowScrollPanGesture(at contentOffset: CGPoint) -> Bool {
|
||||
private func contentOrigin(of scrollView: UIScrollView) -> CGPoint {
|
||||
if let vc = viewcontroller, let origin = vc.delegate?.floatingPanel(vc, contentOffsetForPinning: scrollView) {
|
||||
return origin
|
||||
}
|
||||
return CGPoint(x: 0.0, y: 0.0 - scrollView.contentInset.top)
|
||||
}
|
||||
|
||||
private func allowScrollPanGesture(for scrollView: UIScrollView) -> Bool {
|
||||
let contentOffset = scrollView.contentOffset - contentOrigin(of: scrollView)
|
||||
if state == layoutAdapter.topMostState {
|
||||
return contentOffset.y <= -30.0 || contentOffset.y > 0
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>FMWK</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.7.1</string>
|
||||
<string>1.7.3</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||
</dict>
|
||||
|
||||
@@ -114,9 +114,6 @@ extension UIGestureRecognizerState: CustomDebugStringConvertible {
|
||||
#endif
|
||||
|
||||
extension UIScrollView {
|
||||
var contentOffsetZero: CGPoint {
|
||||
return CGPoint(x: 0.0, y: 0.0 - contentInset.top)
|
||||
}
|
||||
var isLocked: Bool {
|
||||
return !showsVerticalScrollIndicator && !bounces && isDirectionalLockEnabled
|
||||
}
|
||||
@@ -133,8 +130,10 @@ extension UISpringTimingParameters {
|
||||
|
||||
extension CGPoint {
|
||||
static var nan: CGPoint {
|
||||
return CGPoint(x: CGFloat.nan,
|
||||
y: CGFloat.nan)
|
||||
return CGPoint(x: CGFloat.nan, y: CGFloat.nan)
|
||||
}
|
||||
static func - (left: CGPoint, right: CGPoint) -> CGPoint {
|
||||
return CGPoint(x: left.x - right.x, y: left.y - right.y)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -36,7 +36,7 @@ The new interface displays the related contents and utilities in parallel as a u
|
||||
- [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)
|
||||
- [Specify position insets from the frame of `FloatingPanelController.view`, not the SafeArea](#specify-position-insets-from-the-frame-of-floatingpanelcontrollerview-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)
|
||||
- [Activate the rubber-band effect on the top/bottom edges](#activate-the-rubber-band-effect-on-the-topbottom-edges)
|
||||
@@ -338,7 +338,7 @@ class RemovablePanelLayout: FloatingPanelIntrinsicLayout {
|
||||
}
|
||||
```
|
||||
|
||||
#### Specify position insets from the frame of `FloatingPanelContrller.view`, not the SafeArea
|
||||
#### Specify position insets from the frame of `FloatingPanelController.view`, not the SafeArea
|
||||
|
||||
There are 2 ways. One is returning `.fromSuperview` for `FloatingPanelLayout.positionReference` in your layout.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user