Compare commits

...

55 Commits

Author SHA1 Message Date
Shin Yamamoto e4548b26bd Release 1.7.5 2020-06-03 22:33:02 +09:00
Shin Yamamoto d540b1ddde Fix the panel behavior in a sheet modal (#358)
* Add "Show Panel in Sheet Modal" sample
* Fix the behavior in a sheet modal
2020-06-03 22:31:30 +09:00
Shin Yamamoto aaeb752911 No need to recognize both of the pan gesture and dismiss gesuter of sheet modal 2020-05-30 10:11:12 +09:00
Grigory 966caad519 Fix the constraints break on fitToBounds mode (#359) 2020-05-30 09:44:58 +09:00
Shin Yamamoto a62c3a23dc Fix some view controllers in Samples.app for a sheet modal 2020-05-23 09:07:01 +09:00
Shin Yamamoto 7bbc3d5910 fix the behavior in a sheet modal 2020-05-23 08:12:01 +09:00
Shin Yamamoto e2afb1e22f Add "Show Panel in Sheet Modal" sample 2020-05-23 08:11:30 +09:00
Shin Yamamoto 8cca1178fd fix {top,bottom} constant's boundary in updating panel interactively (#352)
Because {top,bottom}Y can be {less,more} than the SafeArea/Superview bounds,
if a minus value is set to the inset for {top,bottom} most position.
2020-05-21 08:59:52 +09:00
Shin Yamamoto a09a0e9e32 fix the animation velocity's sign (#354)
Using a directional distance to calculate an animation velocity fixes an
issue where a panel's animation was wrong when a user swipes up a panel
at the top most position.
2020-05-16 09:18:59 +09:00
Shin Yamamoto 43c76faa20 fix invalid safearea insets in a table view with static cells (#353)
See also https://github.com/SCENEE/FloatingPanel/issues/330
2020-05-16 09:17:34 +09:00
Shin Yamamoto 5787a350ab fix the memory leak of FloatingPanelController object (#350) 2020-05-15 07:41:18 +09:00
Shin Yamamoto 9abb80de64 Fix an invalid indicator insets of the tracking scroll view (#346) 2020-04-29 14:22:34 +09:00
Shin Yamamoto 7d90458d99 Support the initial hidden position not including the supported positions (#345) 2020-04-29 14:20:49 +09:00
Shin Yamamoto 5d5f14acd8 fix a build error on Xcode 11.4 (#337) 2020-03-30 17:52:00 +09:00
Shin Yamamoto bed519f0c0 Merge pull request #326 from SCENEE/release-1.7.4
Release 1.7.4
2020-02-29 18:52:15 +09:00
Shin Yamamoto 7c47e2e20e Release 1.7.4 2020-02-29 13:25:11 +09:00
Federico Zanetello f909cd4101 ignore .swiftpm/ folder (#324) 2020-02-29 13:20:50 +09:00
Federico Zanetello 8c24aa3fc9 fix api typo (#325)
fixes floatingPanel(_ vc: FloatingPanelController, contentOffsetForPinning) name
2020-02-29 13:18:09 +09:00
Shin Yamamoto 7531d80f1c Merge pull request #323 from SCENEE/release-1.7.3
Release 1.7.3
2020-02-29 10:40:38 +09:00
Shin Yamamoto c4c1906cae Release 1.7.3 2020-02-28 21:00:44 +09:00
Shin Yamamoto 4f717c5840 update github issue template 2020-02-28 21:00:40 +09:00
Shin Yamamoto fbd83ef500 fix a run script error 2020-02-26 22:53:44 +09:00
Shin Yamamoto 2a91145366 Fix breaking content offset of the tracked scroll view (#315)
* Convert the tracked scroll view frame to the surface coordinate space
2020-02-26 22:49:51 +09:00
Shin Yamamoto f5d72aa0a5 Add failure requirements for multiple panels (#322)
* add multi panel sample
2020-02-26 22:47:52 +09:00
Shin Yamamoto 65f67c98f4 Add floatingPanel(_:contentOffsetForPinning:) delegate method (#314)
* add floatingPanel(_:contentOffsetForPinning:)
* add 'Show NavigationController' sample
* fix the initial content offset in a navigation bar with  a large text
    The content offset preservation should be applied only when
    `FloatingPanelController.contentInsetAdjustmentBehavior` is `.always`.
    This is because the library user loses control of the initial offset.
2020-02-24 11:16:10 +09:00
Shin Yamamoto 1f79c2573f Merge pull request #316 from jacksonjude/master
Minor README.md typo
2020-02-15 10:32:03 +09:00
jacksonjude bc840dde46 Minor README.md typo 2020-02-08 22:36:50 -08:00
Shin Yamamoto 57ed039857 Merge pull request #312 from SCENEE/release-1.7.2
Release 1.7.2
2020-01-30 09:29:00 +09:00
Shin Yamamoto 11f0e8c84e Release 1.7.2 2020-01-29 11:33:11 +09:00
Shin Yamamoto dd19c866d4 Merge pull request #311 from SCENEE/return-childvc-to-consult
Return the child view controller to consult
2020-01-29 11:31:01 +09:00
Shin Yamamoto 801fed9843 Return the child view controller to consult 2020-01-29 08:54:53 +09:00
Shin Yamamoto 847b5c0917 Merge pull request #310 from SCENEE/fix-didenddecelerating-call
Fix delegate calls
2020-01-28 22:39:37 +09:00
Shin Yamamoto c64056ca7b Make floatingPanelDidEndDragging's velocity zero when it won't animate
Ideally, it's better to define a delegate method like
scrollViewDidEndDragging(_:willDecelerate:) in FloatingPanelControllerDelegate
to notify whether a panel will be decelerated or not.
However it's a broken change so I add this change as workaround.
The delegate method definition will be improved on v2.0.
2020-01-28 11:40:31 +09:00
Shin Yamamoto 269c3e29b5 Call floatingPanelDidEndDecelerating even if an animation interrupted 2020-01-27 16:50:16 +09:00
Shin Yamamoto 002bbb4a4a Merge pull request #307 from SCENEE/iss-293
Fix a panel's move-up while dragging it down
2020-01-21 20:40:48 +09:00
Shin Yamamoto 14011a5bc2 Fix grabber area behavior
The grabber area was not working expectedly.
2020-01-18 17:27:58 +09:00
Shin Yamamoto 23f2242c9a Fix a panel's move-up in dragging it down
This issue is that a panel moved up while dragging it
down if content offset of the tracking scroll view in
a content view controller was greater than its top interaction buffer.

Ref. #293
2020-01-18 17:26:38 +09:00
Shin Yamamoto 4fd92a4002 Fix Maps.app's crash on device after the second launch (#306)
This seems to be Xcode 11's bug of linking frameworks.
2020-01-18 15:05:46 +09:00
Ramesh R C 9c57089b0e Add FloatingPanelController.nearbyPosition (#303)
* Added nearbyPosition : always a position of a user's finger.
* debugging nearby position in Maps.app.
* Added test cases move with nearby position.
2020-01-09 13:26:30 +09:00
Shin Yamamoto 3b11cdc72a Merge pull request #291 from SCENEE/release-1.7.1
Release 1.7.1
2019-11-27 21:59:57 +09:00
Shin Yamamoto 4edaad2cf4 Release 1.7.1 2019-11-21 21:17:11 +09:00
Shin Yamamoto 92fc0621e2 Merge pull request #292 from TadeasKriz/patch-1
Improve manual `show` and `hide` example.
2019-11-21 21:16:32 +09:00
Tadeas Kriz e9f4392c48 Improve manual show and hide example. 2019-11-20 15:56:17 +01:00
Shin Yamamoto 4df40becaf Merge pull request #290 from SCENEE/fix/addpanel
Pass parent to didMove(toParent:)
2019-11-20 10:36:22 +09:00
Shin Yamamoto ba11e7c7d7 Pass parent to didMove(toParent:)
4ad7f11 commit causes the wrong parameter.
2019-11-20 09:51:47 +09:00
Shin Yamamoto ae671f22c6 Merge pull request #288 from SCENEE/fix-swiftinterface-error
Rename the internal FloatingPanel object for .swiftinterface issue
2019-11-19 23:01:36 +09:00
Shin Yamamoto f22f58212b Clean up lines with only white spaces 2019-11-19 18:37:52 +09:00
Shin Yamamoto 54ff1c360d Rename 'FloatingPanel' type for '.swiftinterface' issue
See also https://forums.swift.org/t/frameworkname-is-not-a-member-type-of-frameworkname-errors-inside-swiftinterface/28962
2019-11-19 18:37:44 +09:00
dmytrofrolov1 772d6c3ef3 Improve the surface position evaluation and top scroll bouncing of content
* Evaluate the surface position approximately by 1px with a display scale
* Allow a top scroll bouncing of content without closing floating panel when a user scrolls it a lot
2019-11-19 18:29:20 +09:00
Shin Yamamoto a94c3b3c26 Merge pull request #287 from SCENEE/fix-module-stability
Enable the swift module interfaces
2019-11-15 22:07:01 +09:00
Shin Yamamoto d0ffc4ceb1 Enable the swift module interfaces 2019-11-15 14:32:33 +09:00
Shin Yamamoto 597ce487aa Merge pull request #275 from peka2/patch-1
Update README
2019-10-09 00:32:57 +09:00
peka2 87eb8d94fd Update README 2019-10-08 18:58:01 +09:00
Shin Yamamoto 4944fc516a Update README 2019-10-05 14:09:41 +09:00
Shin Yamamoto 7537384339 Merge pull request #273 from SCENEE/release-1.7.0
Release 1.7.0
2019-10-05 14:07:40 +09:00
19 changed files with 471 additions and 153 deletions
+18 -5
View File
@@ -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**
+3
View File
@@ -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>";
};
+6 -7
View File
@@ -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
@@ -275,12 +274,12 @@ class SearchHeaderView: UIView {
extension UISearchBar {
func setSearchText(fontSize: CGFloat) {
#if swift(>=5.1) // Xcode 11 or later
if #available(iOS 13, *) {
let font = searchTextField.font
searchTextField.font = font?.withSize(fontSize)
#else
} else {
let textField = value(forKey: "_searchField") as! UITextField
textField.font = textField.font?.withSize(fontSize)
#endif
}
}
}
@@ -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"/>
+134 -36
View File
@@ -18,6 +18,8 @@ class SampleListViewController: UIViewController {
case showDetail
case showModal
case showPanelModal
case showMultiPanelModal
case showPanelInSheetModal
case showTabBar
case showPageView
case showPageContentView
@@ -26,6 +28,7 @@ class SampleListViewController: UIViewController {
case showIntrinsicView
case showContentInset
case showContainerMargins
case showNavigationController
var name: String {
switch self {
@@ -34,6 +37,8 @@ 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 .showPanelInSheetModal: return "Show Panel in Sheet Modal"
case .showTabBar: return "Show Tab Bar"
case .showPageView: return "Show Page View"
case .showPageContentView: return "Show Page Content View"
@@ -42,6 +47,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 +57,8 @@ class SampleListViewController: UIViewController {
case .trackingTextView: return "ConsoleViewController"
case .showDetail: return "DetailViewController"
case .showModal: return "ModalViewController"
case .showMultiPanelModal: return nil
case .showPanelInSheetModal: return nil
case .showPanelModal: return nil
case .showTabBar: return "TabBarViewController"
case .showPageView: return nil
@@ -60,6 +68,7 @@ class SampleListViewController: UIViewController {
case .showIntrinsicView: return "IntrinsicViewController"
case .showContentInset: return nil
case .showContainerMargins: return nil
case .showNavigationController: return "RootNavigationController"
}
}
}
@@ -80,6 +89,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 +102,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 +129,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 +154,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 +173,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 +349,24 @@ extension SampleListViewController: UITableViewDelegate {
self.present(fpc, animated: true, completion: nil)
case .showMultiPanelModal:
let fpc = MultiPanelController()
self.present(fpc, animated: true, completion: nil)
case .showPanelInSheetModal:
let fpc = FloatingPanelController()
let contentVC = UIViewController()
fpc.set(contentViewController: contentVC)
fpc.delegate = self
fpc.surfaceView.cornerRadius = 38.5
fpc.surfaceView.shadowHidden = false
fpc.isRemovalInteractionEnabled = true
let mvc = UIViewController()
mvc.view.backgroundColor = UIColor(displayP3Red: 2/255, green: 184/255, blue: 117/255, alpha: 1.0)
fpc.addPanel(toParent: mvc)
self.present(mvc, animated: true, completion: nil)
case .showContentInset:
let contentViewController = UIViewController()
contentViewController.view.backgroundColor = .green
@@ -368,6 +404,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 +680,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 +695,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 +705,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 +731,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() {
@@ -841,8 +882,7 @@ class ModalViewController: UIViewController, FloatingPanelControllerDelegate {
var isNewlayout: Bool = false
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
override func viewDidLoad() {
// Initialize FloatingPanelController
fpc = FloatingPanelController()
fpc.delegate = self
@@ -862,8 +902,8 @@ class ModalViewController: UIViewController, FloatingPanelControllerDelegate {
fpc.addPanel(toParent: self, belowView: safeAreaView)
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
// Remove FloatingPanel from a view
fpc.removePanelFromParent(animated: false)
}
@@ -924,20 +964,15 @@ class TabBarContentViewController: UIViewController {
}
}
}
var fpc: FloatingPanelController!
lazy var fpc = FloatingPanelController()
var consoleVC: DebugTextViewController!
var threeLayout: ThreeTabBarPanelLayout!
var tab3Mode: Tab3Mode = .changeAutoLayout
var switcherLabel: UILabel!
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
// Initialize FloatingPanelController
fpc = FloatingPanelController()
override func viewDidLoad() {
fpc.delegate = self
// Initialize FloatingPanelController and add the view
fpc.surfaceView.cornerRadius = 6.0
fpc.surfaceView.shadowHidden = false
@@ -987,12 +1022,6 @@ class TabBarContentViewController: UIViewController {
fpc.updateLayout()
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
// Remove FloatingPanel from a view
fpc.removePanelFromParent(animated: false)
}
// MARK: - Action
@IBAction func close(sender: UIButton) {
@@ -1270,3 +1299,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 -1
View File
@@ -1,7 +1,7 @@
Pod::Spec.new do |s|
s.name = "FloatingPanel"
s.version = "1.7.0"
s.version = "1.7.5"
s.summary = "FloatingPanel is a clean and easy-to-use UI component of a floating panel interface."
s.description = <<-DESC
FloatingPanel is a clean and easy-to-use UI component for a new interface introduced in Apple Maps, Shortcuts and Stocks app.
@@ -26,7 +26,7 @@
54CDC5D3215B6D5A007D205C /* FloatingPanelSurfaceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CDC5D2215B6D5A007D205C /* FloatingPanelSurfaceView.swift */; };
54CDC5D5215B6D8D007D205C /* FloatingPanelBackdropView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CDC5D4215B6D8D007D205C /* FloatingPanelBackdropView.swift */; };
54CFBFC3215CD045006B5735 /* FloatingPanelLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CFBFC2215CD045006B5735 /* FloatingPanelLayout.swift */; };
54CFBFC5215CD09C006B5735 /* FloatingPanel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CFBFC4215CD09C006B5735 /* FloatingPanel.swift */; };
54CFBFC5215CD09C006B5735 /* FloatingPanelCore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CFBFC4215CD09C006B5735 /* FloatingPanelCore.swift */; };
54E740CD218AFD67005C1A34 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54E740CC218AFD67005C1A34 /* AppDelegate.swift */; };
/* End PBXBuildFile section */
@@ -70,7 +70,7 @@
54CDC5D2215B6D5A007D205C /* FloatingPanelSurfaceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FloatingPanelSurfaceView.swift; sourceTree = "<group>"; };
54CDC5D4215B6D8D007D205C /* FloatingPanelBackdropView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FloatingPanelBackdropView.swift; sourceTree = "<group>"; };
54CFBFC2215CD045006B5735 /* FloatingPanelLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FloatingPanelLayout.swift; sourceTree = "<group>"; };
54CFBFC4215CD09C006B5735 /* FloatingPanel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FloatingPanel.swift; sourceTree = "<group>"; };
54CFBFC4215CD09C006B5735 /* FloatingPanelCore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FloatingPanelCore.swift; sourceTree = "<group>"; };
54E740CA218AFD67005C1A34 /* FloatingPanelTesting.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = FloatingPanelTesting.app; sourceTree = BUILT_PRODUCTS_DIR; };
54E740CC218AFD67005C1A34 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
54E740D8218AFD6A005C1A34 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
@@ -129,7 +129,7 @@
545DB9C42151169500CA77B8 /* FloatingPanel.h */,
545DB9DF21511AC100CA77B8 /* FloatingPanelController.swift */,
54352E9521A51A2500CBCA08 /* FloatingPanelTransitioning.swift */,
54CFBFC4215CD09C006B5735 /* FloatingPanel.swift */,
54CFBFC4215CD09C006B5735 /* FloatingPanelCore.swift */,
54CFBFC2215CD045006B5735 /* FloatingPanelLayout.swift */,
5450EEE321646DF500135936 /* FloatingPanelBehavior.swift */,
54352E9721A521CA00CBCA08 /* FloatingPanelView.swift */,
@@ -311,7 +311,7 @@
54CFBFC3215CD045006B5735 /* FloatingPanelLayout.swift in Sources */,
54CDC5D5215B6D8D007D205C /* FloatingPanelBackdropView.swift in Sources */,
54352E9821A521CA00CBCA08 /* FloatingPanelView.swift in Sources */,
54CFBFC5215CD09C006B5735 /* FloatingPanel.swift in Sources */,
54CFBFC5215CD09C006B5735 /* FloatingPanelCore.swift in Sources */,
54ABD7AF216CCFF7002E6C13 /* Logger.swift in Sources */,
545DB9E021511AC100CA77B8 /* FloatingPanelController.swift in Sources */,
5450EEE421646DF500135936 /* FloatingPanelBehavior.swift in Sources */,
@@ -484,6 +484,7 @@
isa = XCBuildConfiguration;
buildSettings = {
APPLICATION_EXTENSION_API_ONLY = YES;
BUILD_LIBRARY_FOR_DISTRIBUTION = YES;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "";
CODE_SIGN_STYLE = Automatic;
@@ -514,6 +515,7 @@
isa = XCBuildConfiguration;
buildSettings = {
APPLICATION_EXTENSION_API_ONLY = YES;
BUILD_LIBRARY_FOR_DISTRIBUTION = YES;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "";
CODE_SIGN_STYLE = Automatic;
+74 -14
View File
@@ -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 }
@@ -197,7 +215,7 @@ open class FloatingPanelController: UIViewController {
private var _contentViewController: UIViewController?
private(set) var floatingPanel: FloatingPanel!
private(set) var floatingPanel: FloatingPanelCore!
private var preSafeAreaInsets: UIEdgeInsets = .zero // Capture the latest one
private var safeAreaInsetsObservation: NSKeyValueObservation?
private let modalTransition = FloatingPanelModalTransition()
@@ -220,7 +238,7 @@ open class FloatingPanelController: UIViewController {
modalPresentationStyle = .custom
transitioningDelegate = modalTransition
floatingPanel = FloatingPanel(self,
floatingPanel = FloatingPanelCore(self,
layout: fetchLayout(for: self.traitCollection),
behavior: fetchBehavior(for: self.traitCollection))
}
@@ -229,7 +247,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.
@@ -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 }
@@ -318,7 +371,6 @@ open class FloatingPanelController: UIViewController {
switch contentInsetAdjustmentBehavior {
case .always:
scrollView?.contentInset = adjustedContentInsets
scrollView?.scrollIndicatorInsets = adjustedContentInsets
default:
break
}
@@ -340,13 +392,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
@@ -364,9 +421,11 @@ open class FloatingPanelController: UIViewController {
// inset's update expectedly.
// 2. The safe area top inset can be variable on the large title navigation bar(iOS11+).
// That's why it needs the observation to keep `adjustedContentInsets` correct.
safeAreaInsetsObservation = self.observe(\.view.safeAreaInsets, options: [.initial, .new, .old]) { [weak self] (vc, change) in
guard change.oldValue != change.newValue else { return }
self?.update(safeAreaInsets: vc.layoutInsets)
safeAreaInsetsObservation = self.view.observe(\.safeAreaInsets, options: [.initial, .new, .old]) { [weak self] (_, change) in
// Use `self.view.safeAreaInsets` becauese `change.newValue` can be nil in particular case when
// is reported in https://github.com/SCENEE/FloatingPanel/issues/330
guard let `self` = self, change.oldValue != self.view.safeAreaInsets else { return }
self.update(safeAreaInsets: self.view.safeAreaInsets)
}
} else {
// KVOs for topLayoutGuide & bottomLayoutGuide are not effective.
@@ -425,9 +484,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
}
}
@@ -611,7 +670,8 @@ public extension UIViewController {
}
// Call dismiss(animated:completion:) to FloatingPanelController directly
if let fpc = self as? FloatingPanelController {
if fpc.presentingViewController != nil {
// When a panel is presented modally and it's not a child view controller of the presented view controller.
if fpc.presentingViewController != nil, fpc.parent == nil {
self.fp_original_dismiss(animated: flag, completion: completion)
} else {
fpc.removePanelFromParent(animated: flag, completion: completion)
@@ -8,7 +8,7 @@ import UIKit.UIGestureRecognizerSubclass // For Xcode 9.4.1
///
/// FloatingPanel presentation model
///
class FloatingPanel: NSObject, UIGestureRecognizerDelegate {
class FloatingPanelCore: NSObject, UIGestureRecognizerDelegate {
// MUST be a weak reference to prevent UI freeze on the presentation modally
weak var viewcontroller: FloatingPanelController?
@@ -81,7 +81,6 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate {
super.init()
panGestureRecognizer.floatingPanel = self
surfaceView.addGestureRecognizer(panGestureRecognizer)
panGestureRecognizer.addTarget(self, action: #selector(handle(panGesture:)))
panGestureRecognizer.delegate = self
@@ -98,7 +97,7 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate {
}
private func move(from: FloatingPanelPosition, to: FloatingPanelPosition, animated: Bool, completion: (() -> Void)? = nil) {
assert(layoutAdapter.isValid(to), "Can't move to '\(to)' position because it's not valid in the layout")
assert(layoutAdapter.validPositions.contains(to), "Can't move to '\(to)' position because it's not valid in the layout")
guard let vc = viewcontroller else {
completion?()
return
@@ -192,6 +191,9 @@ class FloatingPanel: 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,
@@ -203,13 +205,27 @@ 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)
}
}
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
}
}
if #available(iOS 11.0, *),
otherGestureRecognizer.name == "_UISheetInteractionBackgroundDismissRecognizer" {
// The dismiss gesture of a sheet modal should not begin until the pan gesture fails.
return true
}
return false
}
@@ -230,7 +246,15 @@ 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
}
return allowScrollPanGesture(for: scrollView)
default:
return false
}
}
}
@@ -240,11 +264,23 @@ class FloatingPanel: 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,
is UIScreenEdgePanGestureRecognizer,
is UIPinchGestureRecognizer:
if #available(iOS 11.0, *),
otherGestureRecognizer.name == "_UISheetInteractionBackgroundDismissRecognizer" {
// Should begin the pan gesture without waiting the dismiss gesture of a sheet modal.
return false
}
// Do not begin the pan gesture until these gestures fail
return true
default:
@@ -279,15 +315,16 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate {
let location = panGesture.location(in: surfaceView)
let belowTop = surfaceView.presentationFrame.minY > layoutAdapter.topY
let offset = scrollView.contentOffset.y - scrollView.contentOffsetZero.y
let surfaceMinY = surfaceView.presentationFrame.minY
let adapterTopY = layoutAdapter.topY
let belowTop = surfaceMinY > (adapterTopY + (1.0 / surfaceView.traitCollection.displayScale))
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
@@ -330,11 +367,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(for: scrollView) {
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(for: scrollView) {
unlockScrollView()
}
@@ -370,7 +407,7 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate {
}
animator.finishAnimation(at: .current)
} else {
self.animator = nil
self.endAnimation(false) // Must call it manually
}
}
@@ -437,14 +474,15 @@ class FloatingPanel: 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 {
@@ -565,7 +603,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 }
@@ -592,19 +630,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 {
@@ -670,9 +711,12 @@ class FloatingPanel: 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)
}
@@ -721,7 +765,7 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate {
vc.delegate?.floatingPanelWillBeginDecelerating(vc)
let velocityVector = (distance != 0) ? CGVector(dx: 0, dy: abs(velocity.y)/distance) : .zero
let velocityVector = (distance != 0) ? CGVector(dx: 0, dy: velocity.y / distance) : .zero
let animator = behavior.interactionAnimator(vc, to: targetPosition, with: velocityVector)
animator.addAnimations { [weak self] in
guard let `self` = self, let vc = self.viewcontroller else { return }
@@ -745,15 +789,14 @@ class FloatingPanel: 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
@@ -768,7 +811,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()
}
}
@@ -776,7 +819,7 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate {
private func distance(to targetPosition: FloatingPanelPosition) -> CGFloat {
let currentY = surfaceView.frame.minY
let targetY = layoutAdapter.positionY(for: targetPosition)
return CGFloat(abs(currentY - targetY))
return CGFloat(targetY - currentY)
}
// Distance travelled after decelerating to zero velocity at a constant rate.
@@ -870,10 +913,25 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate {
// Must use setContentOffset(_:animated) to force-stop deceleration
scrollView?.setContentOffset(contentOffset, animated: false)
}
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
}
return false
}
}
class FloatingPanelPanGestureRecognizer: UIPanGestureRecognizer {
fileprivate weak var floatingPanel: FloatingPanel?
fileprivate weak var floatingPanel: FloatingPanelCore?
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesBegan(touches, with: event)
if floatingPanel?.animator != nil {
@@ -885,7 +943,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)
+10 -9
View File
@@ -94,6 +94,7 @@ public protocol FloatingPanelLayout: class {
func backdropAlphaFor(position: FloatingPanelPosition) -> CGFloat
var positionReference: FloatingPanelLayoutReference { get }
}
public extension FloatingPanelLayout {
@@ -210,6 +211,10 @@ class FloatingPanelLayoutAdapter {
return layout.supportedPositions
}
var validPositions: Set<FloatingPanelPosition> {
return supportedPositions.union([.hidden])
}
var topMostState: FloatingPanelPosition {
return supportedPositions.sorted(by: { $0.rawValue < $1.rawValue }).first ?? .hidden
}
@@ -452,7 +457,7 @@ class FloatingPanelLayoutAdapter {
case .fromSuperview:
ret = topY
}
return max(ret, 0.0) // The top boundary is equal to the related topAnchor.
return ret
}()
let bottomMostConst: CGFloat = {
var ret: CGFloat = 0.0
@@ -463,7 +468,7 @@ class FloatingPanelLayoutAdapter {
case .fromSuperview:
ret = _bottomY
}
return min(ret, surfaceView.superview!.bounds.height)
return ret
}()
let minConst = allowsTopBuffer ? topMostConst - layout.topInteractionBuffer : topMostConst
let maxConst = bottomMostConst + layout.bottomInteractionBuffer
@@ -515,7 +520,7 @@ class FloatingPanelLayoutAdapter {
setBackdropAlpha(of: state)
if isValid(state) == false {
if validPositions.contains(state) == false {
state = layout.initialPosition
}
@@ -537,10 +542,6 @@ class FloatingPanelLayoutAdapter {
activateInteractiveLayout(of: state)
}
func isValid(_ state: FloatingPanelPosition) -> Bool {
return supportedPositions.union([.hidden]).contains(state)
}
private func layoutSurfaceIfNeeded() {
#if !TEST
guard surfaceView.window != nil else { return }
@@ -559,8 +560,8 @@ class FloatingPanelLayoutAdapter {
private func checkLayoutConsistance() {
// Verify layout configurations
assert(supportedPositions.count > 0)
assert(supportedPositions.contains(layout.initialPosition),
"Does not include an initial position (\(layout.initialPosition)) in supportedPositions (\(supportedPositions))")
assert(validPositions.contains(layout.initialPosition),
"Does not include an initial position (\(layout.initialPosition)) in (\(validPositions))")
if layout is FloatingPanelIntrinsicLayout {
assert(layout.insetFor(position: .full) == nil, "Return `nil` for full position on FloatingPanelIntrinsicLayout")
@@ -34,7 +34,7 @@ public class FloatingPanelSurfaceView: UIView {
/// A root view of a content view controller
public weak var contentView: UIView!
/// The content insets specifying the insets around the content view.
public var contentInsets: UIEdgeInsets = .zero {
didSet {
@@ -101,7 +101,7 @@ public class FloatingPanelSurfaceView: UIView {
private lazy var containerViewHeightConstraint = containerView.heightAnchor.constraint(equalTo: heightAnchor, multiplier: 1.0)
private lazy var containerViewLeftConstraint = containerView.leftAnchor.constraint(equalTo: leftAnchor, constant: 0.0)
private lazy var containerViewRightConstraint = containerView.rightAnchor.constraint(equalTo: rightAnchor, constant: 0.0)
/// The content view top constraint
private var contentViewTopConstraint: NSLayoutConstraint?
/// The content view left constraint
@@ -228,6 +228,7 @@ public class FloatingPanelSurfaceView: UIView {
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)
heightConstraint.priority = UILayoutPriority(999)
NSLayoutConstraint.activate([
topConstraint,
leftConstraint,
+1 -1
View File
@@ -15,7 +15,7 @@
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>1.7.0</string>
<string>1.7.5</string>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
</dict>
+4 -5
View File
@@ -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)
+38 -1
View File
@@ -195,6 +195,43 @@ class FloatingPanelLayoutTests: XCTestCase {
fpc.floatingPanel.layoutAdapter.endInteraction(at: fpc.position)
}
func test_updateInteractiveTopConstraintWithMinusInsets() {
class FloatingPanelLayoutMinusInsets: FloatingPanelTestLayout {
let initialPosition: FloatingPanelPosition = .full
let supportedPositions: Set<FloatingPanelPosition> = [.tip, .full]
func insetFor(position: FloatingPanelPosition) -> CGFloat? {
switch position {
case .full, .tip: return -200
default: return nil
}
}
}
let delegate = FloatingPanelTestDelegate()
delegate.layout = FloatingPanelLayoutMinusInsets()
fpc.delegate = delegate
fpc.showForTest()
fpc.floatingPanel.layoutAdapter.startInteraction(at: fpc.position)
let fullPos = fpc.originYOfSurface(for: .full)
let tipPos = fpc.originYOfSurface(for: .tip)
let current = fpc.surfaceView.frame.minY
var next: CGFloat
fpc.floatingPanel.layoutAdapter.updateInteractiveTopConstraint(diff: -100.0, allowsTopBuffer: false, with: fpc.behavior)
next = fpc.surfaceView.frame.minY
XCTAssertEqual(next, current)
fpc.floatingPanel.layoutAdapter.updateInteractiveTopConstraint(diff: -100.0, allowsTopBuffer: true, with: fpc.behavior)
next = fpc.surfaceView.frame.minY
XCTAssertEqual(next, fullPos - fpc.layout.topInteractionBuffer)
fpc.floatingPanel.layoutAdapter.updateInteractiveTopConstraint(diff: tipPos - fullPos + 100.0, allowsTopBuffer: true, with: fpc.behavior)
next = fpc.surfaceView.frame.minY
XCTAssertEqual(next, tipPos + fpc.layout.bottomInteractionBuffer)
fpc.floatingPanel.layoutAdapter.endInteraction(at: fpc.position)
}
func test_positionReference() {
fpc = CustomSafeAreaFloatingPanelController()
fpc.loadViewIfNeeded()
@@ -238,7 +275,7 @@ class FloatingPanelLayoutTests: XCTestCase {
}
private typealias LayoutSegmentTestParameter = (UInt, pos: CGFloat, forwardY: Bool, lower: FloatingPanelPosition?, upper: FloatingPanelPosition?)
private func assertLayoutSegment(_ floatingPanel: FloatingPanel, with params: [LayoutSegmentTestParameter]) {
private func assertLayoutSegment(_ floatingPanel: FloatingPanelCore, with params: [LayoutSegmentTestParameter]) {
params.forEach { (line, pos, forwardY, lowr, upper) in
let segument = floatingPanel.layoutAdapter.segument(at: pos, forward: forwardY)
XCTAssertEqual(segument.lower, lowr, line: line)
+1 -1
View File
@@ -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)
+50 -29
View File
@@ -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)
@@ -181,41 +181,60 @@ FloatingPanelController.view (FloatingPanelPassThroughView)
### Show/Hide a floating panel in a view with your view hierarchy
If you need more control over showing and hiding the floating panel, you can forgo the `addPanel` and `removePanelFromParent` methods. These methods are a convenience wrapper for **FloatingPanel**'s `show` and `hide` methods along with some required setup.
There are two ways to work with the `FloatingPanelController`:
1. Add it to the hierarchy once and then call `show` and `hide` methods to make it appear/disappear.
2. Add it to the hierarchy when needed and remove afterwards.
The following example shows how to add the controller to your `UIViewController` and how to remove it. Make sure that you never add the same `FloatingPanelController` to the hierarchy before removing it.
**NOTE**: `self.` prefix is not required, nor recommended. It's used here to make it clearer where do the functions used come from. `self` is an instance of a custom UIViewController in your code.
```swift
// Add the controller and the managed views to a view controller.
// From the second time, just call `show(animated:completion)`.
view.addSubview(fpc.view)
// Add the floating panel view to the controller's view on top of other views.
self.view.addSubview(fpc.view)
// REQUIRED. It makes the floating panel view have the same size as the controller's view.
fpc.view.frame = self.view.bounds
fpc.view.frame = view.bounds // MUST
// In addition, Auto Layout constraints are highly recommended.
// Because it makes the layout more robust on trait collection change.
//
// fpc.view.translatesAutoresizingMaskIntoConstraints = false
// NSLayoutConstraint.activate([...])
//
// Constraint the fpc.view to all four edges of your controller's view.
// It makes the layout more robust on trait collection change.
fpc.view.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
fpc.view.topAnchor.constraint(equalTo: self.view.topAnchor, constant: 0.0),
fpc.view.leftAnchor.constraint(equalTo: self.view.leftAnchor, constant: 0.0),
fpc.view.rightAnchor.constraint(equalTo: self.view.rightAnchor, constant: 0.0),
fpc.view.bottomAnchor.constraint(equalTo: self.view.bottomAnchor, constant: 0.0),
])
parent.addChild(fpc)
// Add the floating panel controller to the controller hierarchy.
self.addChild(fpc)
// Show a floating panel to the initial position defined in your `FloatingPanelLayout` object.
// Show the floating panel at the initial position defined in your `FloatingPanelLayout` object.
fpc.show(animated: true) {
// Only for the first time
self.didMove(toParent: self)
}
...
// Hide it
fpc.hide(animated: true) {
// Remove it if needed
self.willMove(toParent: nil)
self.view.removeFromSuperview()
self.removeFromParent()
// Inform the floating panel controller that the transition to the controller hierarchy has completed.
fpc.didMove(toParent: self)
}
```
NOTE: `FloatingPanelController` wraps `show`/`hide` with `addPanel`/`removePanelFromParent` for easy-to-use. But `show`/`hide` are more convenience for your app.
After you add the `FloatingPanelController` as seen above, you can call `fpc.show(animated: true) { }` to show the panel and `fpc.hide(animated: true) { }` to hide it.
To remove the `FloatingPanelController` from the hierarchy, follow the example below.
```swift
// Inform the panel controller that it will be removed from the hierarchy.
fpc.willMove(toParent: nil)
// Hide the floating panel.
fpc.hide(animated: true) {
// Remove the floating panel view from your controller's view.
fpc.view.removeFromSuperview()
// Remove the floating panel controller from the controller hierarchy.
fpc.removeFromParent()
}
```
### Scale the content view when the surface position changes
@@ -227,6 +246,8 @@ fpc.contentMode = .fitToBounds
Otherwise, `FloatingPanelController` fixes the content by the height of the top most position.
✏️ In `.fitToBounds` mode, the surface height changes as following a user interaction so that you have a responsibility to configure Auto Layout constrains not to break the layout of a content view by the elastic surface height.
### Customize the layout with `FloatingPanelLayout` protocol
#### Change the initial position and height
@@ -317,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.
@@ -378,7 +399,7 @@ This allows full projectional panel behavior. For example, a user can swipe up a
```swift
class FloatingPanelBehavior: FloatingPanelBehavior {
...
func shouldProjectMomentum(_ fpc: FloatingPanelController, for proposedTargetPosition: FloatingPanelPosition) -> Bool
func shouldProjectMomentum(_ fpc: FloatingPanelController, for proposedTargetPosition: FloatingPanelPosition) -> Bool {
return true
}
}