Compare commits

...

50 Commits

Author SHA1 Message Date
Shin Yamamoto 8fd134512f Release v1.7.0 2019-09-28 23:02:45 +09:00
Shin Yamamoto f566fc6475 Add a sample of panel including a PageVC content 2019-09-28 23:02:45 +09:00
Shin Yamamoto 9cbcb48a9b Merge pull request #266 from SCENEE/add-containerinsets
Add FloatingPanelSurfaceView.containerMargins
2019-09-28 22:47:21 +09:00
Shin Yamamoto 817956cef3 Update README 2019-09-28 21:49:47 +09:00
Shin Yamamoto 3c1aa7aa42 Fix FloatingPanelFullScreenLayout is not working 2019-09-28 13:18:58 +09:00
Shin Yamamoto 7598e8f160 ci: Update xcode versions 2019-09-28 13:18:58 +09:00
Shin Yamamoto ba011e7242 Add 'Show with ContainerMargins' sample 2019-09-28 13:18:44 +09:00
Shin Yamamoto ecdf20db8f Add FloatingPanelSurfaceView.containerMargins
`FloatingPanelSurfaceView.containerTopInset` is replaced by the top
inset.
2019-09-28 13:18:44 +09:00
Shin Yamamoto 3a7f39321c Update allowsRubberBanding() sample 2019-09-28 13:18:44 +09:00
Shin Yamamoto e75d83e7a4 Merge pull request #270 from SCENEE/release-1.6.6
Release 1.6.6
2019-09-28 13:17:37 +09:00
Shin Yamamoto 0461c49d23 Release v1.6.6 2019-09-27 23:47:36 +09:00
Shin Yamamoto c4f7fa5332 Remove target-action for an untracked scroll view 2019-09-27 23:47:36 +09:00
Shin Yamamoto 3a9f304735 Suppress UITableViewAlertForLayoutOutsideViewHierarchy alert
Following the alert suggestion
> [TableView] Warning once only: UITableView was told to layout its visible
> cells and other contents without being in the view hierarchy (the table
> view or one of its superviews has not been added to a window).
2019-09-27 23:47:36 +09:00
Shin Yamamoto 1d5cb1744f CI: Add Xcode 11 & Swift 5.1 build
* Add a build on Xcode 11 image with SUPPORTS_MACCATALYST=NO because Xcode 11 runs on macOS 10.14 in Travis CI
* Add Swift 5.1 badge
2019-09-27 23:47:36 +09:00
Shin Yamamoto b61b0b5451 Update framework links of Example apps
Try to resolve an issue to need a clean-build of framework as following
this site,
https://developer.apple.com/library/archive/technotes/tn2435/_index.html
2019-09-27 23:47:36 +09:00
Shin Yamamoto e3a4631e44 Remove unused delegate conformances 2019-09-27 23:47:36 +09:00
Shin Yamamoto d7f798e9a0 Fix layout of the root table view in Samples 2019-09-27 23:47:36 +09:00
Shin Yamamoto ae2c83e32b Modify modal style of Samples.app for iOS 13 2019-09-27 23:47:36 +09:00
Shin Yamamoto 059b2ed4f0 Fix File 'Utils.swift' target 2019-09-27 23:47:36 +09:00
Shin Yamamoto 338658cd9f Clean up some tests
* test_surfaceView_constraintsUpdate()
* test_moveTo()
2019-09-25 22:16:45 +09:00
Shin Yamamoto 2cdb4a6bc2 Suppress UITableViewAlertForLayoutOutsideViewHierarchy alert
Following the alert suggestion
> [TableView] Warning once only: UITableView was told to layout its visible
> cells and other contents without being in the view hierarchy (the table
> view or one of its superviews has not been added to a window).
2019-09-16 21:58:28 +09:00
Shin Yamamoto 1a4d5a7954 CI: Add Xcode 11 & Swift 5.1 build
* Add a build on Xcode 11 image with SUPPORTS_MACCATALYST=NO because Xcode 11 runs on macOS 10.14 in Travis CI
* Add Swift 5.1 badge
2019-09-16 21:56:28 +09:00
Shin Yamamoto 22ef3e7cd9 Merge pull request #253 from SCENEE/release-1.6.5
Release 1.6.5
2019-08-31 13:48:32 +09:00
Shin Yamamoto 5325e707e6 Release v1.6.5 2019-08-31 12:51:01 +09:00
Nikolay Derkach 1dc0a6b76a Support bottom content inset for container view (#257)
and also fix height of a content view resized by the inset
Fix #256
2019-08-31 12:49:59 +09:00
David Hart 2689d68bab Improve floatingPanelDidChangePosition and tigger it on removal 2019-08-31 12:49:57 +09:00
Nikolay Derkach f8b8176988 Support bottom content inset for container view (#257)
and also fix height of a content view resized by the inset
Fix #256
2019-08-31 12:43:53 +09:00
Shin Yamamoto 0c5bf2bfe9 Merge pull request #252 from hartbit/updating-layout
Improve the documentation of floatingPanelDidChangePosition
2019-08-24 17:02:33 +09:00
Shin Yamamoto 218a12962f Don't unregister safeAreaInsetsObservation in hide()
Because the observation can't be active when a user makes a panel visible
by move(to:) instead of show().
2019-08-24 15:37:15 +09:00
Shin Yamamoto 916d2ec76a Add move-to-hidden tests 2019-08-24 15:37:15 +09:00
Shin Yamamoto 1b1ba5deef Update README for UISearchController issue 2019-08-24 15:37:15 +09:00
Shin Yamamoto 58b2df4996 Fix UISearchBar's _searchField access 2019-08-24 09:54:54 +09:00
David Hart 8b45517915 Improve floatingPanelDidChangePosition and tigger it on removal 2019-08-22 14:09:02 +02:00
Shin Yamamoto 3cca07fefd Merge pull request #251 from rikusouda/fix/call_did_end_remove_by_backdrop_view
Call floatingPanelDidEndRemove when dismiss with tap on backdrop view
2019-08-21 15:21:55 +09:00
Yuki Yoshioka 276ae23f13 Call floatingPanelDidEndRemove when dismiss with tap on backdrop view
By #205 added dismissing by backdrop view. But `floatingPanelDidEndRemove` is not called from it.
2019-08-19 14:34:11 +09:00
Shin Yamamoto c1b2ffeb78 Merge pull request #212 from joshuafinch/feature/cocoapods-1.7
Add support for specifying swift_versions in CocoaPods 1.7 and above
2019-08-15 13:47:44 +09:00
Shin Yamamoto 3b812be84e Return true for FloatingPanelSurfaceView.requiresConstraintBasedLayout 2019-08-13 15:27:27 +09:00
Shin Yamamoto 262ee34201 Merge pull request #247 from SCENEE/release-1.6.4
Release 1.6.4
2019-08-09 23:52:04 +09:00
Shin Yamamoto 5d86bd5d02 Release v1.6.4 2019-08-09 09:55:04 +09:00
Shin Yamamoto 3b6271c4f4 Fix stopping a panel b/w anchors after an interruption
The panel(surface view) could stop b/w anchors if the pan gesture doesn't
pass through `.changed` state after an interruptible animator is interrupted.

The possible reason is the constraints have never changed since the last animation
is committed so that `surfaceView.superview!.layoutIfNeeded()` doesn't trigger
a layout update by the constraint-based layout system in
`FloatingPanelLayoutAdapter.activateLayout(of:)`.

Thus the inserted code changes a panel interactive constraint by the least
positive number. It allows the constraint-based layout system to update the
surface layout expectedly.
2019-08-08 10:52:51 +09:00
Shin Yamamoto 1671a3d50f Always call startInteraction before endInteraction 2019-08-07 22:27:13 +09:00
Shin Yamamoto 0ab318e804 Fix not calling floatingPanelDidEndDecelerating delegate after interruption
If the decelerating animation is interrupted and
floatingPanelShouldBeginDragging delegate method returns false,
floatingPanelDidEndDecelerating delegate method will not be called
after calling floatingPanelWillBeginDecelerating method.

A panel have to run an animation after the interruption and also
floatingPanelDidEndDecelerating(_:) delegate should be called always
after calling floatingPanelWillBeginDecelerating method.

Therefore floatingPanelShouldBeginDragging delegate method shouldn't be
called in the panel decelerating.
2019-08-07 22:07:35 +09:00
Shin Yamamoto 53719bd94a Feat elastic layout (#145)
* Move the prepareLayout(in:) call
* Support 'fitToBounds' content mode
* Add NSLayoutConstraint.{de}activate(constraint:)
* Update README
2019-08-03 14:45:35 +09:00
Nikolay Derkach 935b7d9e10 Allow to disable tap on backdrop view for panel dismissal (#205)
* Add 'FloatingPanelBackdropView. dismissalTapGestureRecognizer'
* Enable tap on backdropview gesture recognizer only for the modal presentation
2019-08-03 12:37:39 +09:00
Shin Yamamoto e3bf19b972 Merge pull request #224 from SCENEE/feat-position-reference
Add FloatingPanelLayout.positionReference
2019-07-27 15:22:18 +09:00
Shin Yamamoto c36f09d3e9 Update README 2019-07-27 13:47:13 +09:00
Shin Yamamoto 9936a89118 Add FloatingPanelLayoutTests.test_positionReference() 2019-07-27 11:44:33 +09:00
Shin Yamamoto 562424cd8f Add FloatingPanelLayout.positionReference 2019-07-27 11:44:33 +09:00
Shin Yamamoto 0c3fb83d0a Merge pull request #241 from SCENEE/release-1.6.3
Release 1.6.3
2019-07-26 23:00:45 +09:00
Joshua Finch 23846dbf23 Add support for CocoaPods version 1.7 swift_version 2019-06-01 22:56:21 +01:00
22 changed files with 719 additions and 218 deletions
+18 -9
View File
@@ -22,33 +22,40 @@ jobs:
osx_image: xcode10
name: "Swift 4.2"
- script: xcodebuild -scheme FloatingPanel SWIFT_VERSION=5.0 clean build
osx_image: xcode10.2
osx_image: xcode10.3
name: "Swift 5.0"
- script: xcodebuild -scheme FloatingPanel SWIFT_VERSION=5.1 SUPPORTS_MACCATALYST=NO clean build
# SUPPORTS_MACCATALYST=NO because Xcode 11 runs on macOS 10.14 in Travis CI
osx_image: xcode11
name: "Swift 5.1"
- stage: "Tests"
osx_image: xcode10.2
osx_image: xcode10.3
script: xcodebuild clean test -scheme FloatingPanel -workspace FloatingPanel.xcworkspace -destination 'platform=iOS Simulator,OS=10.3.1,name=iPhone SE'
name: "iPhone SE (iOS 10.3)"
- script: xcodebuild clean test -scheme FloatingPanel -workspace FloatingPanel.xcworkspace -destination 'platform=iOS Simulator,OS=11.4,name=iPhone 7'
osx_image: xcode10.2
osx_image: xcode10.3
name: "iPhone 7 (iOS 11.4)"
- script: xcodebuild clean test -scheme FloatingPanel -workspace FloatingPanel.xcworkspace -destination 'platform=iOS Simulator,OS=12.2,name=iPhone X'
osx_image: xcode10.2
osx_image: xcode10.3
name: "iPhone X (iOS 12.2)"
- script: xcodebuild clean test -scheme FloatingPanel -workspace FloatingPanel.xcworkspace -destination 'platform=iOS Simulator,OS=13.0,name=iPhone 11'
osx_image: xcode11
name: "iPhone X (iOS 13.0)"
- stage: Build examples
osx_image: xcode10.2
osx_image: xcode11
script: xcodebuild -scheme Maps -sdk iphonesimulator clean build
name: "Maps"
- script: xcodebuild -scheme Stocks -sdk iphonesimulator clean build
osx_image: xcode10.2
osx_image: xcode11
name: "Stocks"
- script: xcodebuild -scheme Samples -sdk iphonesimulator clean build
osx_image: xcode10.2
osx_image: xcode11
name: "Samples"
- stage: Carthage
osx_image: xcode10.2
osx_image: xcode11
before_install:
- brew update
- brew outdated carthage || brew upgrade carthage
@@ -56,7 +63,9 @@ jobs:
- carthage build --no-skip-current
- stage: CocoaPods
osx_image: xcode10.2
osx_image: xcode11
before_install:
- gem install cocoapods
script:
- pod spec lint --allow-warnings
- pod lib lint --allow-warnings
+6 -6
View File
@@ -7,13 +7,13 @@
objects = {
/* Begin PBXBuildFile section */
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 */; };
54B5112C216C3D840033A6F3 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54B5112B216C3D840033A6F3 /* ViewController.swift */; };
54B5112F216C3D840033A6F3 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 54B5112D216C3D840033A6F3 /* Main.storyboard */; };
54B51131216C3D860033A6F3 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 54B51130216C3D860033A6F3 /* Assets.xcassets */; };
54B51134216C3D860033A6F3 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 54B51132216C3D860033A6F3 /* LaunchScreen.storyboard */; };
54B5113F216C407F0033A6F3 /* FloatingPanel.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 54B5113E216C407F0033A6F3 /* FloatingPanel.framework */; };
54B51140216C407F0033A6F3 /* FloatingPanel.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 54B5113E216C407F0033A6F3 /* FloatingPanel.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
/* End PBXBuildFile section */
/* Begin PBXCopyFilesBuildPhase section */
@@ -23,7 +23,7 @@
dstPath = "";
dstSubfolderSpec = 10;
files = (
54B51140216C407F0033A6F3 /* FloatingPanel.framework in Embed Frameworks */,
549D23D3233C77D5008EF4D7 /* FloatingPanel.framework in Embed Frameworks */,
);
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
@@ -31,6 +31,7 @@
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
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>"; };
54B5112B216C3D840033A6F3 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = "<group>"; };
@@ -38,7 +39,6 @@
54B51130216C3D860033A6F3 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
54B51133216C3D860033A6F3 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
54B51135216C3D860033A6F3 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
54B5113E216C407F0033A6F3 /* FloatingPanel.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = FloatingPanel.framework; sourceTree = BUILT_PRODUCTS_DIR; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@@ -46,7 +46,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
54B5113F216C407F0033A6F3 /* FloatingPanel.framework in Frameworks */,
549D23D2233C77D5008EF4D7 /* FloatingPanel.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -56,7 +56,7 @@
54B5111D216C3D840033A6F3 = {
isa = PBXGroup;
children = (
54B5113E216C407F0033A6F3 /* FloatingPanel.framework */,
549D23D1233C77D5008EF4D7 /* FloatingPanel.framework */,
54B51128216C3D840033A6F3 /* Maps */,
54B51127216C3D840033A6F3 /* Products */,
);
+13 -3
View File
@@ -148,11 +148,9 @@ class SearchPanelViewController: UIViewController, UITableViewDataSource, UITabl
tableView.dataSource = self
tableView.delegate = self
searchBar.placeholder = "Search for a place or address"
let textField = searchBar.value(forKey: "_searchField") as! UITextField
textField.font = UIFont(name: textField.font!.fontName, size: 15.0)
searchBar.setSearchText(fontSize: 15.0)
hideHeader()
}
override func viewDidLayoutSubviews() {
@@ -274,3 +272,15 @@ class SearchHeaderView: UIView {
self.clipsToBounds = true
}
}
extension UISearchBar {
func setSearchText(fontSize: CGFloat) {
#if swift(>=5.1) // Xcode 11 or later
let font = searchTextField.font
searchTextField.font = font?.withSize(fontSize)
#else
let textField = value(forKey: "_searchField") as! UITextField
textField.font = textField.font?.withSize(fontSize)
#endif
}
}
@@ -14,9 +14,9 @@
545DB9F821511E6400CA77B8 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 545DB9F621511E6400CA77B8 /* LaunchScreen.storyboard */; };
545DBA0321511E6400CA77B8 /* SampleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 545DBA0221511E6400CA77B8 /* SampleTests.swift */; };
545DBA0E21511E6400CA77B8 /* SampleUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 545DBA0D21511E6400CA77B8 /* SampleUITests.swift */; };
549D23CB233C7779008EF4D7 /* FloatingPanel.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 549D23CA233C7779008EF4D7 /* FloatingPanel.framework */; };
549D23CC233C7779008EF4D7 /* FloatingPanel.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 549D23CA233C7779008EF4D7 /* FloatingPanel.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
54B51116216AFE5F0033A6F3 /* UIExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54B51115216AFE5F0033A6F3 /* UIExtensions.swift */; };
54B5113C216C40670033A6F3 /* FloatingPanel.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 54B5113B216C40670033A6F3 /* FloatingPanel.framework */; };
54B5113D216C40670033A6F3 /* FloatingPanel.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 54B5113B216C40670033A6F3 /* FloatingPanel.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
54CDC5D8215BBE23007D205C /* UIComponents.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CDC5D7215BBE23007D205C /* UIComponents.swift */; };
/* End PBXBuildFile section */
@@ -38,13 +38,13 @@
/* End PBXContainerItemProxy section */
/* Begin PBXCopyFilesBuildPhase section */
54B5111C216C3B300033A6F3 /* Embed Frameworks */ = {
549D23CD233C7779008EF4D7 /* Embed Frameworks */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 10;
files = (
54B5113D216C40670033A6F3 /* FloatingPanel.framework in Embed Frameworks */,
549D23CC233C7779008EF4D7 /* FloatingPanel.framework in Embed Frameworks */,
);
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
@@ -65,8 +65,8 @@
545DBA0921511E6400CA77B8 /* SamplesUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SamplesUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
545DBA0D21511E6400CA77B8 /* SampleUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SampleUITests.swift; sourceTree = "<group>"; };
545DBA0F21511E6400CA77B8 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
549D23CA233C7779008EF4D7 /* FloatingPanel.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = FloatingPanel.framework; sourceTree = BUILT_PRODUCTS_DIR; };
54B51115216AFE5F0033A6F3 /* UIExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIExtensions.swift; sourceTree = "<group>"; };
54B5113B216C40670033A6F3 /* FloatingPanel.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = FloatingPanel.framework; sourceTree = BUILT_PRODUCTS_DIR; };
54CDC5D7215BBE23007D205C /* UIComponents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIComponents.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
@@ -75,7 +75,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
54B5113C216C40670033A6F3 /* FloatingPanel.framework in Frameworks */,
549D23CB233C7779008EF4D7 /* FloatingPanel.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -99,12 +99,11 @@
545DB9E121511E6300CA77B8 = {
isa = PBXGroup;
children = (
54B5113B216C40670033A6F3 /* FloatingPanel.framework */,
549D23CA233C7779008EF4D7 /* FloatingPanel.framework */,
545DB9EC21511E6300CA77B8 /* Sources */,
545DBA0121511E6400CA77B8 /* Tests */,
545DBA0C21511E6400CA77B8 /* UITests */,
545DB9EB21511E6300CA77B8 /* Products */,
545DBA1B2151CC1000CA77B8 /* Frameworks */,
);
sourceTree = "<group>";
};
@@ -151,13 +150,6 @@
path = UITests;
sourceTree = "<group>";
};
545DBA1B2151CC1000CA77B8 /* Frameworks */ = {
isa = PBXGroup;
children = (
);
name = Frameworks;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
@@ -169,7 +161,7 @@
545DB9E621511E6300CA77B8 /* Sources */,
545DB9E721511E6300CA77B8 /* Frameworks */,
545DB9E821511E6300CA77B8 /* Resources */,
54B5111C216C3B300033A6F3 /* Embed Frameworks */,
549D23CD233C7779008EF4D7 /* Embed Frameworks */,
);
buildRules = (
);
@@ -1,11 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="14460.31" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="RoN-h0-uBD">
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="14490.70" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="RoN-h0-uBD">
<device id="retina5_9" orientation="portrait">
<adaptation id="fullscreen"/>
</device>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14460.20"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14490.49"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
@@ -35,18 +35,18 @@
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<tableView clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="plain" separatorStyle="default" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="28" sectionFooterHeight="28" translatesAutoresizingMaskIntoConstraints="NO" id="7IS-PU-x0P">
<rect key="frame" x="0.0" y="0.0" width="375" height="778"/>
<rect key="frame" x="0.0" y="0.0" width="375" height="812"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<prototypes>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" reuseIdentifier="Cell" textLabel="M0G-C8-hAO" style="IBUITableViewCellStyleDefault" id="ySY-oA-g81">
<rect key="frame" x="0.0" y="28" width="375" height="44"/>
<rect key="frame" x="0.0" y="28" width="375" height="43.666667938232422"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="ySY-oA-g81" id="sXB-nH-2g2">
<rect key="frame" x="0.0" y="0.0" width="375" height="43.666666666666664"/>
<rect key="frame" x="0.0" y="0.0" width="375" height="43.666667938232422"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Title" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="M0G-C8-hAO">
<rect key="frame" x="15" y="0.0" width="345" height="43.666666666666664"/>
<rect key="frame" x="15" y="0.0" width="345" height="43.666667938232422"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/>
@@ -62,7 +62,7 @@
<constraints>
<constraint firstItem="7IS-PU-x0P" firstAttribute="top" secondItem="Smh-Bd-AAc" secondAttribute="top" id="6yd-jv-ey3"/>
<constraint firstItem="7IS-PU-x0P" firstAttribute="leading" secondItem="39L-Nq-qfp" secondAttribute="leading" id="Z6Y-Dc-cei"/>
<constraint firstItem="7IS-PU-x0P" firstAttribute="bottom" secondItem="39L-Nq-qfp" secondAttribute="bottom" id="fNW-DP-lhV"/>
<constraint firstItem="7IS-PU-x0P" firstAttribute="bottom" secondItem="Smh-Bd-AAc" secondAttribute="bottom" id="fNW-DP-lhV"/>
<constraint firstItem="7IS-PU-x0P" firstAttribute="trailing" secondItem="39L-Nq-qfp" secondAttribute="trailing" id="vfY-Rc-FOI"/>
</constraints>
<viewLayoutGuide key="safeArea" id="39L-Nq-qfp"/>
@@ -87,35 +87,35 @@
<objects>
<viewController storyboardIdentifier="SettingsViewController" id="C1X-9Z-TyQ" customClass="SettingsViewController" customModule="Samples" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" id="af9-Zr-Ppc">
<rect key="frame" x="0.0" y="0.0" width="375" height="197.33000000000001"/>
<rect key="frame" x="0.0" y="0.0" width="375" height="197.33333333333334"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" distribution="fillProportionally" alignment="center" spacing="16" translatesAutoresizingMaskIntoConstraints="NO" id="n93-ZL-fmC">
<rect key="frame" x="32" y="16" width="311" height="147.33333333333334"/>
<rect key="frame" x="32" y="16" width="311" height="181.33333333333334"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Version: 1.0" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="WmC-Tq-NDN">
<rect key="frame" x="118.33333333333334" y="0.0" width="74.333333333333343" height="17"/>
<rect key="frame" x="118.33333333333334" y="0.0" width="74.333333333333343" height="21"/>
<fontDescription key="fontDescription" type="system" pointSize="14"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="UINavigationBar" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Ulg-gS-ah0">
<rect key="frame" x="90.666666666666686" y="33" width="130" height="20.333333333333329"/>
<rect key="frame" x="90.666666666666686" y="37" width="130" height="25"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleHeadline"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<stackView opaque="NO" contentMode="scaleToFill" alignment="center" spacing="126" translatesAutoresizingMaskIntoConstraints="NO" id="uEf-g4-CeU">
<rect key="frame" x="23.333333333333343" y="69.333333333333329" width="264.33333333333326" height="31"/>
<rect key="frame" x="23.333333333333343" y="78" width="264.66666666666663" height="38"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Large Titles" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="ogl-S5-4tJ">
<rect key="frame" x="0.0" y="5.3333333333333428" width="89.333333333333329" height="20.333333333333332"/>
<rect key="frame" x="0.0" y="8.9999999999999982" width="89.666666666666671" height="20.333333333333329"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<switch opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" contentHorizontalAlignment="center" contentVerticalAlignment="center" translatesAutoresizingMaskIntoConstraints="NO" id="js8-Qv-lUC">
<rect key="frame" x="215.33333333333334" y="0.0" width="51.000000000000028" height="31"/>
<rect key="frame" x="215.66666666666666" y="3.6666666666666714" width="50.999999999999972" height="31"/>
<connections>
<action selector="toggleLargeTitle:" destination="C1X-9Z-TyQ" eventType="valueChanged" id="FJS-Ty-mCY"/>
</connections>
@@ -123,16 +123,16 @@
</subviews>
</stackView>
<stackView opaque="NO" contentMode="scaleToFill" spacing="126" translatesAutoresizingMaskIntoConstraints="NO" id="ZtZ-Dz-4cC">
<rect key="frame" x="23.333333333333343" y="116.33333333333334" width="264.66666666666663" height="31"/>
<rect key="frame" x="23.333333333333343" y="132" width="264.66666666666663" height="49.333333333333343"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Translucent" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Z5i-rm-QgL">
<rect key="frame" x="0.0" y="0.0" width="89.666666666666671" height="31"/>
<rect key="frame" x="0.0" y="0.0" width="89.666666666666671" height="49.333333333333336"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<switch opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" contentHorizontalAlignment="center" contentVerticalAlignment="center" translatesAutoresizingMaskIntoConstraints="NO" id="s6b-j9-8Kw">
<rect key="frame" x="215.66666666666666" y="0.0" width="50.999999999999972" height="31"/>
<rect key="frame" x="215.66666666666666" y="0.0" width="50.999999999999972" height="49.333333333333336"/>
<connections>
<action selector="toggleTranslucent:" destination="C1X-9Z-TyQ" eventType="valueChanged" id="nL4-3L-9hh"/>
</connections>
@@ -309,22 +309,22 @@
<objects>
<viewController storyboardIdentifier="ModalViewController" id="bYI-y3-Rzb" customClass="ModalViewController" customModule="Samples" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="qwo-GK-p1U">
<rect key="frame" x="0.0" y="0.0" width="375" height="778"/>
<rect key="frame" x="0.0" y="0.0" width="375" height="724"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="vut-mK-Y4t" customClass="SafeAreaView" customModule="Samples" customModuleProvider="target">
<rect key="frame" x="0.0" y="744" width="375" height="34"/>
<rect key="frame" x="0.0" y="724" width="375" height="0.0"/>
<color key="backgroundColor" white="0.66666666666666663" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</view>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="sbF-Az-7sy">
<rect key="frame" x="20" y="44" width="39" height="30"/>
<rect key="frame" x="20" y="0.0" width="39" height="30"/>
<state key="normal" title="Close"/>
<connections>
<action selector="closeWithSender:" destination="bYI-y3-Rzb" eventType="touchUpInside" id="MSC-ch-YJK"/>
</connections>
</button>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" alignment="top" spacing="44" translatesAutoresizingMaskIntoConstraints="NO" id="9p4-06-y2T">
<rect key="frame" x="139.66666666666666" y="132" width="96" height="252"/>
<rect key="frame" x="134.66666666666666" y="88" width="106" height="326"/>
<subviews>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="i9x-x5-n1q">
<rect key="frame" x="0.0" y="0.0" width="80" height="30"/>
@@ -347,8 +347,15 @@
<action selector="moveToTipWithSender:" destination="bYI-y3-Rzb" eventType="touchUpInside" id="BmL-91-9ai"/>
</connections>
</button>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="swr-XM-GzZ">
<rect key="frame" x="0.0" y="222" width="106" height="30"/>
<state key="normal" title="Move to hidden"/>
<connections>
<action selector="moveToHiddenWithSender:" destination="bYI-y3-Rzb" eventType="touchUpInside" id="jfJ-0f-fdk"/>
</connections>
</button>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="szf-HE-QTk">
<rect key="frame" x="0.0" y="222" width="96" height="30"/>
<rect key="frame" x="0.0" y="296" width="96" height="30"/>
<state key="normal" title="Update layout"/>
<connections>
<action selector="updateLayout:" destination="bYI-y3-Rzb" eventType="touchUpInside" id="Woz-a7-YMJ"/>
@@ -517,7 +524,7 @@
</constraints>
</view>
<view alpha="0.5" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="Kva-Z7-0qY" customClass="OnSafeAreaView" customModule="Samples" customModuleProvider="target">
<rect key="frame" x="0.0" y="44" width="375" height="700"/>
<rect key="frame" x="0.0" y="44" width="375" height="734"/>
<color key="backgroundColor" red="0.0078431372550000003" green="0.72156862749999995" blue="0.45882352939999999" alpha="1" colorSpace="calibratedRGB"/>
</view>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="noi-1a-5bZ" customClass="CloseButton" customModule="Samples" customModuleProvider="target">
@@ -530,6 +537,23 @@
<action selector="closeWithSender:" destination="YC8-ae-15L" eventType="touchUpInside" id="Z2v-19-S5k"/>
</connections>
</button>
<stackView opaque="NO" contentMode="scaleToFill" distribution="fillProportionally" alignment="center" spacing="8" translatesAutoresizingMaskIntoConstraints="NO" id="qux-uG-4o2">
<rect key="frame" x="8" y="52" width="148.33333333333334" height="31"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="fitToBounds" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="7lq-d3-PKi">
<rect key="frame" x="0.0" y="5.3333333333333357" width="91.333333333333329" height="20.333333333333332"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<switch opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" contentHorizontalAlignment="center" contentVerticalAlignment="center" on="YES" translatesAutoresizingMaskIntoConstraints="NO" id="0MA-lV-KjS">
<rect key="frame" x="99.333333333333329" y="0.0" width="50.999999999999986" height="31"/>
<connections>
<action selector="modeChanged:" destination="YC8-ae-15L" eventType="valueChanged" id="IQ8-u2-Rib"/>
</connections>
</switch>
</subviews>
</stackView>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="22" translatesAutoresizingMaskIntoConstraints="NO" id="tP3-oJ-4EB">
<rect key="frame" x="130.66666666666666" y="132" width="114" height="134"/>
<subviews>
@@ -570,9 +594,11 @@
<constraint firstItem="tP3-oJ-4EB" firstAttribute="top" secondItem="aOK-7l-cA6" secondAttribute="top" constant="88" id="Zhb-Ss-epe"/>
<constraint firstItem="Kva-Z7-0qY" firstAttribute="trailing" secondItem="aOK-7l-cA6" secondAttribute="trailing" id="kkp-Yo-FQW"/>
<constraint firstItem="aOK-7l-cA6" firstAttribute="trailing" secondItem="noi-1a-5bZ" secondAttribute="trailing" constant="12" id="lv9-Nf-HNB"/>
<constraint firstItem="qux-uG-4o2" firstAttribute="top" secondItem="aOK-7l-cA6" secondAttribute="top" constant="8" id="naa-cf-ZIc"/>
<constraint firstItem="Kva-Z7-0qY" firstAttribute="leading" secondItem="aOK-7l-cA6" secondAttribute="leading" id="oVC-i1-TwS"/>
<constraint firstItem="aOK-7l-cA6" firstAttribute="bottom" secondItem="Kva-Z7-0qY" secondAttribute="bottom" id="rW2-mF-5DR"/>
<constraint firstItem="8yw-OC-Ubk" firstAttribute="top" relation="greaterThanOrEqual" secondItem="tP3-oJ-4EB" secondAttribute="bottom" constant="88" id="vKQ-h9-uKt"/>
<constraint firstItem="qux-uG-4o2" firstAttribute="leading" secondItem="g7l-kO-y7q" secondAttribute="leading" constant="8" id="zXb-R9-bMO"/>
</constraints>
<viewLayoutGuide key="safeArea" id="aOK-7l-cA6"/>
<connections>
@@ -584,6 +610,8 @@
<size key="freeformSize" width="375" height="778"/>
<connections>
<outlet property="closeButton" destination="noi-1a-5bZ" id="eWQ-ha-8y7"/>
<outlet property="intrinsicHeightConstraint" destination="vKQ-h9-uKt" id="QpA-WD-b17"/>
<outlet property="modeChangeView" destination="qux-uG-4o2" id="1Nq-fE-dXw"/>
<segue destination="bYI-y3-Rzb" kind="show" identifier="ShowSegue" id="r1P-2i-NDe"/>
</connections>
</viewController>
@@ -604,7 +632,7 @@
</connections>
</pongPressGestureRecognizer>
</objects>
<point key="canvasLocation" x="655" y="734"/>
<point key="canvasLocation" x="653.60000000000002" y="733.74384236453204"/>
</scene>
<!--Debug Text View Controller-->
<scene sceneID="Bkq-O7-q4A">
+114 -30
View File
@@ -17,13 +17,15 @@ class SampleListViewController: UIViewController {
case trackingTextView
case showDetail
case showModal
case showFloatingPanelModal
case showPanelModal
case showTabBar
case showPageView
case showPageContentView
case showNestedScrollView
case showRemovablePanel
case showIntrinsicView
case showContentInset
case showContainerMargins
var name: String {
switch self {
@@ -31,13 +33,15 @@ class SampleListViewController: UIViewController {
case .trackingTextView: return "Scroll tracking(TextView)"
case .showDetail: return "Show Detail Panel"
case .showModal: return "Show Modal"
case .showFloatingPanelModal: return "Show Floating Panel Modal"
case .showPanelModal: return "Show Panel Modal"
case .showTabBar: return "Show Tab Bar"
case .showPageView: return "Show Page View"
case .showPageContentView: return "Show Page Content View"
case .showNestedScrollView: return "Show Nested ScrollView"
case .showRemovablePanel: return "Show Removable Panel"
case .showIntrinsicView: return "Show Intrinsic View"
case .showContentInset: return "Show with ContentInset"
case .showContainerMargins: return "Show with ContainerMargins"
}
}
@@ -47,13 +51,15 @@ class SampleListViewController: UIViewController {
case .trackingTextView: return "ConsoleViewController"
case .showDetail: return "DetailViewController"
case .showModal: return "ModalViewController"
case .showFloatingPanelModal: return nil
case .showPanelModal: return nil
case .showTabBar: return "TabBarViewController"
case .showPageView: return nil
case .showPageContentView: return nil
case .showNestedScrollView: return "NestedScrollViewController"
case .showRemovablePanel: return "DetailViewController"
case .showIntrinsicView: return "IntrinsicViewController"
case .showContentInset: return nil
case .showContainerMargins: return nil
}
}
}
@@ -67,18 +73,7 @@ class SampleListViewController: UIViewController {
var mainPanelObserves: [NSKeyValueObservation] = []
var settingsObserves: [NSKeyValueObservation] = []
lazy var pages: [UIViewController] = {
let page1 = FloatingPanelController(delegate: self)
page1.view.backgroundColor = .blue
page1.show()
let page2 = FloatingPanelController(delegate: self)
page2.view.backgroundColor = .red
page2.show()
let page3 = FloatingPanelController(delegate: self)
page3.view.backgroundColor = .green
page3.show()
return [page1, page2, page3]
}()
var pages: [UIViewController] = []
override func viewDidLoad() {
super.viewDidLoad()
@@ -119,6 +114,8 @@ class SampleListViewController: UIViewController {
func addMainPanel(with contentVC: UIViewController) {
mainPanelObserves.removeAll()
let oldMainPanelVC = mainPanelVC
// Initialize FloatingPanelController
mainPanelVC = FloatingPanelController()
mainPanelVC.delegate = self
@@ -137,6 +134,10 @@ class SampleListViewController: UIViewController {
tapGesture.cancelsTouchesInView = false
tapGesture.numberOfTapsRequired = 2
mainPanelVC.surfaceView.addGestureRecognizer(tapGesture)
case .showPageContentView:
if let page = (mainPanelVC.contentViewController as? UIPageViewController)?.viewControllers?.first {
mainPanelVC.track(scrollView: (page as! DebugTableViewController).tableView)
}
case .showRemovablePanel, .showIntrinsicView:
mainPanelVC.isRemovalInteractionEnabled = true
@@ -164,7 +165,13 @@ class SampleListViewController: UIViewController {
}
// Add FloatingPanel to self.view
mainPanelVC.addPanel(toParent: self, belowView: nil, animated: true)
if let oldMainPanelVC = oldMainPanelVC {
oldMainPanelVC.removePanelFromParent(animated: true, completion: {
self.mainPanelVC.addPanel(toParent: self, belowView: nil, animated: true)
})
} else {
mainPanelVC.addPanel(toParent: self, belowView: nil, animated: true)
}
}
@objc
@@ -251,6 +258,8 @@ extension SampleListViewController: UITableViewDelegate {
}()
self.currentMenu = menu
detailPanelVC?.removePanelFromParent(animated: true, completion: nil)
detailPanelVC = nil
switch menu {
case .showDetail:
@@ -258,6 +267,7 @@ extension SampleListViewController: UITableViewDelegate {
// Initialize FloatingPanelController
detailPanelVC = FloatingPanelController()
detailPanelVC.delegate = self
// Initialize FloatingPanelController and add the view
detailPanelVC.surfaceView.cornerRadius = 6.0
@@ -266,13 +276,24 @@ extension SampleListViewController: UITableViewDelegate {
// Set a content view controller
detailPanelVC.set(contentViewController: contentVC)
detailPanelVC.contentMode = .fitToBounds
(contentVC as? DetailViewController)?.intrinsicHeightConstraint.priority = .defaultLow
// Add FloatingPanel to self.view
detailPanelVC.addPanel(toParent: self, belowView: nil, animated: true)
case .showModal, .showTabBar:
let modalVC = contentVC
modalVC.modalPresentationStyle = .fullScreen
present(modalVC, animated: true, completion: nil)
case .showPageView:
pages = [UIColor.blue, .red, .green].compactMap({ (color) -> UIViewController in
let page = FloatingPanelController(delegate: self)
page.view.backgroundColor = color
page.show()
return page
})
let pageVC = UIPageViewController(transitionStyle: .scroll, navigationOrientation: .horizontal, options: [:])
let closeButton = UIButton(type: .custom)
pageVC.view.addSubview(closeButton)
@@ -285,11 +306,21 @@ extension SampleListViewController: UITableViewDelegate {
])
pageVC.dataSource = self
pageVC.setViewControllers([pages[0]], direction: .forward, animated: false, completion: nil)
pageVC.modalPresentationStyle = .fullScreen
present(pageVC, animated: true, completion: nil)
case .showFloatingPanelModal:
case .showPageContentView:
pages = [DebugTableViewController(), DebugTableViewController(), DebugTableViewController()]
let pageVC = UIPageViewController(transitionStyle: .scroll, navigationOrientation: .horizontal, options: [:])
pageVC.dataSource = self
pageVC.delegate = self
pageVC.setViewControllers([pages[0]], direction: .forward, animated: false, completion: nil)
self.addMainPanel(with: pageVC)
case .showPanelModal:
let fpc = FloatingPanelController()
let contentVC = self.storyboard!.instantiateViewController(withIdentifier: "DetailViewController")
contentVC.loadViewIfNeeded()
(contentVC as? DetailViewController)?.modeChangeView.isHidden = true
fpc.set(contentViewController: contentVC)
fpc.delegate = self
@@ -299,23 +330,35 @@ extension SampleListViewController: UITableViewDelegate {
fpc.isRemovalInteractionEnabled = true
self.present(fpc, animated: true, completion: nil)
case .showContentInset:
let contentViewController = UIViewController()
contentViewController.view.backgroundColor = .green
let fpc = FloatingPanelController()
fpc.set(contentViewController: contentViewController)
fpc.surfaceView.contentInsets = .init(top: 20, left: 20, bottom: 0, right: 20)
fpc.surfaceView.contentInsets = .init(top: 20, left: 20, bottom: 20, right: 20)
fpc.delegate = self
fpc.isRemovalInteractionEnabled = true
self.present(fpc, animated: true, completion: nil)
case .showContainerMargins:
let fpc = FloatingPanelController()
fpc.surfaceView.cornerRadius = 38.5
fpc.surfaceView.backgroundColor = .red
fpc.surfaceView.containerMargins = .init(top: 24.0, left: 8.0, bottom: layoutInsets.bottom, right: 8.0)
#if swift(>=5.1) // Actually Xcode 11 or later
if #available(iOS 13.0, *) {
fpc.surfaceView.layer.cornerCurve = .continuous
}
#endif
fpc.delegate = self
fpc.isRemovalInteractionEnabled = true
self.present(fpc, animated: true, completion: nil)
default:
detailPanelVC?.removePanelFromParent(animated: true, completion: nil)
mainPanelVC?.removePanelFromParent(animated: true) {
self.addMainPanel(with: contentVC)
}
self.addMainPanel(with: contentVC)
}
}
@@ -335,13 +378,15 @@ extension SampleListViewController: FloatingPanelControllerDelegate {
return newCollection.verticalSizeClass == .compact ? RemovablePanelLandscapeLayout() : RemovablePanelLayout()
case .showIntrinsicView:
return IntrinsicPanelLayout()
case .showFloatingPanelModal:
case .showPanelModal:
if vc != mainPanelVC && vc != detailPanelVC {
return ModalPanelLayout()
}
fallthrough
case .showContentInset:
return NoInteractionBufferPanelLayout()
default:
return (newCollection.verticalSizeClass == .compact) ? nil : self
return (newCollection.verticalSizeClass == .compact) ? nil : self
}
}
@@ -403,9 +448,40 @@ extension SampleListViewController: UIPageViewControllerDataSource {
return pages[index - 1]
}
}
extension SampleListViewController: UIPageViewControllerDelegate {
// For showPageContent
func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) {
if completed, let page = pageViewController.viewControllers?.first {
(pageViewController.parent as! FloatingPanelController).track(scrollView: (page as! DebugTableViewController).tableView)
}
}
}
class IntrinsicPanelLayout: FloatingPanelIntrinsicLayout { }
class NoInteractionBufferPanelLayout: FloatingPanelLayout {
var initialPosition: FloatingPanelPosition {
return .full
}
func insetFor(position: FloatingPanelPosition) -> CGFloat? {
switch position {
case .full: return 0
case .half: return 216
case .tip: return 60
case .hidden: return nil
}
}
var topInteractionBuffer: CGFloat {
return 0.0
}
var bottomInteractionBuffer: CGFloat {
return 0.0
}
}
class RemovablePanelLayout: FloatingPanelIntrinsicLayout {
var supportedPositions: Set<FloatingPanelPosition> {
return [.full, .half]
@@ -723,6 +799,8 @@ extension DebugTableViewController: UITableViewDelegate {
}
class DetailViewController: InspectableViewController {
@IBOutlet weak var modeChangeView: UIStackView!
@IBOutlet weak var intrinsicHeightConstraint: NSLayoutConstraint!
@IBOutlet weak var closeButton: UIButton!
@IBAction func close(sender: UIButton) {
// (self.parent as? FloatingPanelController)?.removePanelFromParent(animated: true, completion: nil)
@@ -739,6 +817,10 @@ class DetailViewController: InspectableViewController {
break
}
}
@IBAction func modeChanged(_ sender: Any) {
guard let fpc = parent as? FloatingPanelController else { return }
fpc.contentMode = (fpc.contentMode == .static) ? .fitToBounds : .static
}
@IBAction func tapped(_ sender: Any) {
print("Detail panel is tapped!")
@@ -799,7 +881,9 @@ class ModalViewController: UIViewController, FloatingPanelControllerDelegate {
@IBAction func moveToTip(sender: UIButton) {
fpc.move(to: .tip, animated: true)
}
@IBAction func moveToHidden(sender: UIButton) {
fpc.move(to: .hidden, animated: true)
}
@IBAction func updateLayout(_ sender: Any) {
isNewlayout = !isNewlayout
UIView.animate(withDuration: 0.5) {
@@ -1106,8 +1190,8 @@ class TwoTabBarPanelLayout: FloatingPanelLayout {
}
class TwoTabBarPanelBehavior: FloatingPanelBehavior {
func allowsRubberBanding(for edge: UIRectEdge) -> Bool {
return (edge == .bottom || edge == .top)
func allowsRubberBanding(for edges: UIRectEdge) -> Bool {
return [UIRectEdge.top, UIRectEdge.bottom].contains(edges)
}
}
@@ -7,13 +7,13 @@
objects = {
/* Begin PBXBuildFile section */
5433F24B21717EA300BDAA5D /* FloatingPanel.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5433F24A21717EA300BDAA5D /* FloatingPanel.framework */; };
5433F24C21717EA300BDAA5D /* FloatingPanel.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 5433F24A21717EA300BDAA5D /* FloatingPanel.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
548DF95421705BE00041922A /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 548DF95321705BE00041922A /* AppDelegate.swift */; };
548DF95621705BE00041922A /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 548DF95521705BE00041922A /* ViewController.swift */; };
548DF95921705BE00041922A /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 548DF95721705BE00041922A /* Main.storyboard */; };
548DF95B21705BE10041922A /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 548DF95A21705BE10041922A /* Assets.xcassets */; };
548DF95E21705BE10041922A /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 548DF95C21705BE10041922A /* LaunchScreen.storyboard */; };
549D23CF233C77CF008EF4D7 /* FloatingPanel.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 549D23CE233C77CF008EF4D7 /* FloatingPanel.framework */; };
549D23D0233C77CF008EF4D7 /* FloatingPanel.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 549D23CE233C77CF008EF4D7 /* FloatingPanel.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
/* End PBXBuildFile section */
/* Begin PBXCopyFilesBuildPhase section */
@@ -23,7 +23,7 @@
dstPath = "";
dstSubfolderSpec = 10;
files = (
5433F24C21717EA300BDAA5D /* FloatingPanel.framework in Embed Frameworks */,
549D23D0233C77CF008EF4D7 /* FloatingPanel.framework in Embed Frameworks */,
);
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
@@ -31,7 +31,6 @@
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
5433F24A21717EA300BDAA5D /* FloatingPanel.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = FloatingPanel.framework; sourceTree = BUILT_PRODUCTS_DIR; };
548DF95021705BE00041922A /* Stocks.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Stocks.app; sourceTree = BUILT_PRODUCTS_DIR; };
548DF95321705BE00041922A /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
548DF95521705BE00041922A /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = "<group>"; };
@@ -39,6 +38,7 @@
548DF95A21705BE10041922A /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
548DF95D21705BE10041922A /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
548DF95F21705BE10041922A /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
549D23CE233C77CF008EF4D7 /* FloatingPanel.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = FloatingPanel.framework; sourceTree = BUILT_PRODUCTS_DIR; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@@ -46,7 +46,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
5433F24B21717EA300BDAA5D /* FloatingPanel.framework in Frameworks */,
549D23CF233C77CF008EF4D7 /* FloatingPanel.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -56,7 +56,7 @@
548DF94721705BE00041922A = {
isa = PBXGroup;
children = (
5433F24A21717EA300BDAA5D /* FloatingPanel.framework */,
549D23CE233C77CF008EF4D7 /* FloatingPanel.framework */,
548DF95221705BE00041922A /* Stocks */,
548DF95121705BE00041922A /* Products */,
);
+2 -2
View File
@@ -1,7 +1,7 @@
Pod::Spec.new do |s|
s.name = "FloatingPanel"
s.version = "1.6.3"
s.version = "1.7.0"
s.summary = "FloatingPanel is a clean and easy-to-use UI component of a floating panel interface."
s.description = <<-DESC
FloatingPanel is a clean and easy-to-use UI component for a new interface introduced in Apple Maps, Shortcuts and Stocks app.
@@ -13,7 +13,7 @@ The new interface displays the related contents and utilities in parallel as a u
s.platform = :ios, "10.0"
s.source = { :git => "https://github.com/SCENEE/FloatingPanel.git", :tag => "v#{s.version}" }
s.source_files = "Framework/Sources/*.swift"
s.swift_version = "4.0"
s.swift_versions = ["4.0", "4.2", "5.0"]
s.framework = "UIKit"
@@ -8,7 +8,6 @@
/* Begin PBXBuildFile section */
542753C622C49A6E00D17955 /* FloatingPanelLayoutTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 542753C522C49A6E00D17955 /* FloatingPanelLayoutTests.swift */; };
542753C822C49A8F00D17955 /* Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 542753C722C49A8F00D17955 /* Utils.swift */; };
54352E9621A51A2500CBCA08 /* FloatingPanelTransitioning.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54352E9521A51A2500CBCA08 /* FloatingPanelTransitioning.swift */; };
54352E9821A521CA00CBCA08 /* FloatingPanelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54352E9721A521CA00CBCA08 /* FloatingPanelView.swift */; };
5450EEE421646DF500135936 /* FloatingPanelBehavior.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5450EEE321646DF500135936 /* FloatingPanelBehavior.swift */; };
@@ -18,6 +17,7 @@
545DB9DE215118C800CA77B8 /* UIExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 545DB9DD215118C800CA77B8 /* UIExtensions.swift */; };
545DB9E021511AC100CA77B8 /* FloatingPanelController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 545DB9DF21511AC100CA77B8 /* FloatingPanelController.swift */; };
545DBA2B2152383100CA77B8 /* GrabberHandleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 545DBA2A2152383100CA77B8 /* GrabberHandleView.swift */; };
546055BF2333C4740069F400 /* Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 542753C722C49A8F00D17955 /* Utils.swift */; };
549E944522CF295D0050AECF /* FloatingPanelPositionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 549E944422CF295D0050AECF /* FloatingPanelPositionTests.swift */; };
54A6B6B122968B530077F348 /* FloatingPanelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54A6B6B022968B530077F348 /* FloatingPanelTests.swift */; };
54A6B6B622968F710077F348 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 54A6B6B522968F710077F348 /* LaunchScreen.storyboard */; };
@@ -318,7 +318,6 @@
545DBA2B2152383100CA77B8 /* GrabberHandleView.swift in Sources */,
54352E9621A51A2500CBCA08 /* FloatingPanelTransitioning.swift in Sources */,
545DB9DE215118C800CA77B8 /* UIExtensions.swift in Sources */,
542753C822C49A8F00D17955 /* Utils.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -331,6 +330,7 @@
549E944522CF295D0050AECF /* FloatingPanelPositionTests.swift in Sources */,
542753C622C49A6E00D17955 /* FloatingPanelLayoutTests.swift in Sources */,
54A6B6B82296A8520077F348 /* FloatingPanelSurfaceViewTests.swift in Sources */,
546055BF2333C4740069F400 /* Utils.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -702,7 +702,7 @@
PRODUCT_BUNDLE_IDENTIFIER = com.scenee.FloatingPanel;
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
SKIP_INSTALL = YES;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG __FP_LOG";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "TEST DEBUG __FP_LOG";
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 4.0;
+51 -12
View File
@@ -19,8 +19,8 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate {
weak var scrollView: UIScrollView? {
didSet {
guard let scrollView = scrollView else { return }
scrollView.panGestureRecognizer.addTarget(self, action: #selector(handle(panGesture:)))
oldValue?.panGestureRecognizer.removeTarget(self, action: nil)
scrollView?.panGestureRecognizer.addTarget(self, action: #selector(handle(panGesture:)))
}
}
@@ -85,6 +85,12 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate {
surfaceView.addGestureRecognizer(panGestureRecognizer)
panGestureRecognizer.addTarget(self, action: #selector(handle(panGesture:)))
panGestureRecognizer.delegate = self
// Set tap-to-dismiss in the backdrop view
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleBackdrop(tapGesture:)))
tapGesture.isEnabled = false
backdropView.dismissalTapGestureRecognizer = tapGesture
backdropView.addGestureRecognizer(tapGesture)
}
func move(to: FloatingPanelPosition, animated: Bool, completion: (() -> Void)? = nil) {
@@ -146,7 +152,8 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate {
// MARK: - Layout update
private func updateLayout(to target: FloatingPanelPosition) {
self.layoutAdapter.activateLayout(of: target)
self.layoutAdapter.activateFixedLayout()
self.layoutAdapter.activateInteractiveLayout(of: target)
}
func getBackdropAlpha(at currentY: CGFloat, with translation: CGPoint) -> CGFloat {
@@ -255,6 +262,14 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate {
}
// MARK: - Gesture handling
@objc func handleBackdrop(tapGesture: UITapGestureRecognizer) {
viewcontroller?.dismiss(animated: true) { [weak self] in
guard let vc = self?.viewcontroller else { return }
vc.delegate?.floatingPanelDidEndRemove(vc)
}
}
@objc func handle(panGesture: UIPanGestureRecognizer) {
let velocity = panGesture.velocity(in: panGesture.view)
@@ -337,9 +352,14 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate {
log.debug("panel gesture(\(state):\(panGesture.state)) --",
"translation = \(translation.y), location = \(location.y), velocity = \(velocity.y)")
if interactionInProgress == false, isDecelerating == false,
let vc = viewcontroller, vc.delegate?.floatingPanelShouldBeginDragging(vc) == false {
return
}
if let animator = self.animator {
guard surfaceView.presentationFrame.minY >= layoutAdapter.topMaxY else { return }
log.debug("panel animation interrupted!!!")
log.debug("panel animation(interruptible: \(animator.isInterruptible)) interrupted!!!")
if animator.isInterruptible {
animator.stopAnimation(false)
// A user can stop a panel at the nearest Y of a target position so this fine-tunes
@@ -354,12 +374,6 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate {
}
}
if interactionInProgress == false,
let vc = viewcontroller,
vc.delegate?.floatingPanelShouldBeginDragging(vc) == false {
return
}
if panGesture.state == .began {
panningBegan(at: location)
return
@@ -376,6 +390,15 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate {
}
panningChange(with: translation)
case .ended, .cancelled, .failed:
if interactionInProgress == false {
startInteraction(with: translation, at: location)
// Workaround: Prevent stopping the surface view b/w anchors if the pan gesture
// doesn't pass through .changed state after an interruptible animator is interrupted.
let dy = translation.y - .leastNonzeroMagnitude
layoutAdapter.updateInteractiveTopConstraint(diff: dy,
allowsTopBuffer: true,
with: behavior)
}
panningEnd(with: translation, velocity: velocity)
default:
break
@@ -492,6 +515,8 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate {
// from the full position because SafeArea is global in a screen.
private func preserveContentVCLayoutIfNeeded() {
guard let vc = viewcontroller else { return }
guard vc.contentMode != .fitToBounds else { return }
// Must include topY
if (surfaceView.frame.minY <= layoutAdapter.topY) {
if !disabledBottomAutoLayout {
@@ -615,6 +640,7 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate {
let animator = behavior.removalInteractionAnimator(vc, with: velocityVector)
animator.addAnimations { [weak self] in
self?.state = .hidden
self?.updateLayout(to: .hidden)
}
animator.addCompletion({ _ in
@@ -698,9 +724,22 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate {
let velocityVector = (distance != 0) ? CGVector(dx: 0, dy: abs(velocity.y)/distance) : .zero
let animator = behavior.interactionAnimator(vc, to: targetPosition, with: velocityVector)
animator.addAnimations { [weak self] in
guard let `self` = self else { return }
guard let `self` = self, let vc = self.viewcontroller else { return }
self.state = targetPosition
self.updateLayout(to: targetPosition)
if animator.isInterruptible {
switch vc.contentMode {
case .fitToBounds:
UIView.performWithLinear(startTime: 0.0, relativeDuration: 0.75) {
self.layoutAdapter.activateFixedLayout()
self.surfaceView.superview!.layoutIfNeeded()
}
case .static:
self.layoutAdapter.activateFixedLayout()
}
} else {
self.layoutAdapter.activateFixedLayout()
}
self.layoutAdapter.activateInteractiveLayout(of: targetPosition)
}
animator.addCompletion { [weak self] pos in
// Prevent calling `finishAnimation(at:)` by the old animator whose `isInterruptive` is false
@@ -6,4 +6,6 @@
import UIKit
/// A view that presents a backdrop interface behind a floating panel.
public class FloatingPanelBackdropView: UIView { }
public class FloatingPanelBackdropView: UIView {
public var dismissalTapGestureRecognizer: UITapGestureRecognizer!
}
@@ -12,7 +12,9 @@ public protocol FloatingPanelControllerDelegate: class {
// if it returns nil, FloatingPanelController uses the default behavior
func floatingPanel(_ vc: FloatingPanelController, behaviorFor newCollection: UITraitCollection) -> FloatingPanelBehavior?
func floatingPanelDidChangePosition(_ vc: FloatingPanelController) // changed the settled position in the model layer
/// Called when the floating panel has changed to a new position. Can be called inside an animation block, so any
/// view properties set inside this function will be automatically animated alongside the panel.
func floatingPanelDidChangePosition(_ vc: FloatingPanelController)
/// Asks the delegate if dragging should begin by the pan gesture recognizer.
func floatingPanelShouldBeginDragging(_ vc: FloatingPanelController) -> Bool
@@ -107,13 +109,21 @@ public enum FloatingPanelPosition: Int {
///
/// A container view controller to display a floating panel to present contents in parallel as a user wants.
///
open class FloatingPanelController: UIViewController, UIScrollViewDelegate, UIGestureRecognizerDelegate {
open class FloatingPanelController: UIViewController {
/// Constants indicating how safe area insets are added to the adjusted content inset.
public enum ContentInsetAdjustmentBehavior: Int {
case always
case never
}
/// A flag used to determine how the controller object lays out the content view when the surface position changes.
public enum ContentMode: Int {
/// The option to fix the content to keep the height of the top most position.
case `static`
/// The option to scale the content to fit the bounds of the root view by changing the surface position.
case fitToBounds
}
/// The delegate of the floating panel controller object.
public weak var delegate: FloatingPanelControllerDelegate?{
didSet{
@@ -177,6 +187,14 @@ open class FloatingPanelController: UIViewController, UIScrollViewDelegate, UIGe
set { set(contentViewController: newValue) }
get { return _contentViewController }
}
public var contentMode: ContentMode = .static {
didSet {
guard position != .hidden else { return }
activateLayout()
}
}
private var _contentViewController: UIViewController?
private(set) var floatingPanel: FloatingPanel!
@@ -308,7 +326,6 @@ open class FloatingPanelController: UIViewController, UIScrollViewDelegate, UIGe
private func reloadLayout(for traitCollection: UITraitCollection) {
floatingPanel.layoutAdapter.layout = fetchLayout(for: traitCollection)
floatingPanel.layoutAdapter.prepareLayout(in: self)
if let parent = self.parent {
if let layout = layout as? UIViewController, layout == parent {
@@ -321,6 +338,8 @@ open class FloatingPanelController: UIViewController, UIScrollViewDelegate, UIGe
}
private func activateLayout() {
floatingPanel.layoutAdapter.prepareLayout(in: self)
// preserve the current content offset
let contentOffset = scrollView?.contentOffset
@@ -361,7 +380,6 @@ open class FloatingPanelController: UIViewController, UIScrollViewDelegate, UIGe
/// Hides the surface view to the hidden position
public func hide(animated: Bool = false, completion: (() -> Void)? = nil) {
safeAreaInsetsObservation = nil
move(to: .hidden,
animated: animated,
completion: completion)
+116 -56
View File
@@ -11,15 +11,24 @@ import UIKit
/// It can't be used with FloatingPanelIntrinsicLayout.
public protocol FloatingPanelFullScreenLayout: FloatingPanelLayout { }
public extension FloatingPanelFullScreenLayout {
var positionReference: FloatingPanelLayoutReference {
return .fromSuperview
}
}
/// FloatingPanelIntrinsicLayout
///
/// Use the layout protocol if you want to layout a panel using the intrinsic height.
/// It can't be used with FloatingPanelFullScreenLayout.
/// It can't be used with `FloatingPanelFullScreenLayout`.
///
/// - Attention:
/// `insetFor(position:)` must return `nil` for the full position. Because
/// the inset is determined automatically by the intrinsic height.
/// You can customize insets only for the half, tip and hidden positions.
///
/// - Note:
/// By default, the `positionReference` is set to `.fromSafeArea`.
public protocol FloatingPanelIntrinsicLayout: FloatingPanelLayout { }
public extension FloatingPanelIntrinsicLayout {
@@ -34,6 +43,15 @@ public extension FloatingPanelIntrinsicLayout {
func insetFor(position: FloatingPanelPosition) -> CGFloat? {
return nil
}
var positionReference: FloatingPanelLayoutReference {
return .fromSafeArea
}
}
public enum FloatingPanelLayoutReference: Int {
case fromSafeArea = 0
case fromSuperview = 1
}
public protocol FloatingPanelLayout: class {
@@ -74,6 +92,8 @@ public protocol FloatingPanelLayout: class {
///
/// Default is 0.3 at full position, otherwise 0.0.
func backdropAlphaFor(position: FloatingPanelPosition) -> CGFloat
var positionReference: FloatingPanelLayoutReference { get }
}
public extension FloatingPanelLayout {
@@ -94,6 +114,10 @@ public extension FloatingPanelLayout {
func backdropAlphaFor(position: FloatingPanelPosition) -> CGFloat {
return position == .full ? 0.3 : 0.0
}
var positionReference: FloatingPanelLayoutReference {
return .fromSafeArea
}
}
public class FloatingPanelDefaultLayout: FloatingPanelLayout {
@@ -160,6 +184,8 @@ class FloatingPanelLayoutAdapter {
private var tipConstraints: [NSLayoutConstraint] = []
private var offConstraints: [NSLayoutConstraint] = []
private var interactiveTopConstraint: NSLayoutConstraint?
private var bottomConstraint: NSLayoutConstraint?
private var heightConstraint: NSLayoutConstraint?
@@ -218,27 +244,28 @@ class FloatingPanelLayoutAdapter {
func positionY(for pos: FloatingPanelPosition) -> CGFloat {
switch pos {
case .full:
switch layout {
case is FloatingPanelIntrinsicLayout:
if layout is FloatingPanelIntrinsicLayout {
return surfaceView.superview!.bounds.height - surfaceView.bounds.height
case is FloatingPanelFullScreenLayout:
return fullInset
default:
}
switch layout.positionReference {
case .fromSafeArea:
return (safeAreaInsets.top + fullInset)
case .fromSuperview:
return fullInset
}
case .half:
switch layout {
case is FloatingPanelFullScreenLayout:
return surfaceView.superview!.bounds.height - halfInset
default:
switch layout.positionReference {
case .fromSafeArea:
return surfaceView.superview!.bounds.height - (safeAreaInsets.bottom + halfInset)
case .fromSuperview:
return surfaceView.superview!.bounds.height - halfInset
}
case .tip:
switch layout {
case is FloatingPanelFullScreenLayout:
return surfaceView.superview!.bounds.height - tipInset
default:
switch layout.positionReference {
case .fromSafeArea:
return surfaceView.superview!.bounds.height - (safeAreaInsets.bottom + tipInset)
case .fromSuperview:
return surfaceView.superview!.bounds.height - tipInset
}
case .hidden:
return surfaceView.superview!.bounds.height - hiddenInset
@@ -279,6 +306,10 @@ class FloatingPanelLayoutAdapter {
self.vc = vc
NSLayoutConstraint.deactivate(fixedConstraints + fullConstraints + halfConstraints + tipConstraints + offConstraints)
NSLayoutConstraint.deactivate(constraint: self.heightConstraint)
self.heightConstraint = nil
NSLayoutConstraint.deactivate(constraint: self.bottomConstraint)
self.bottomConstraint = nil
surfaceView.translatesAutoresizingMaskIntoConstraints = false
backdropView.translatesAutoresizingMaskIntoConstraints = false
@@ -294,9 +325,14 @@ class FloatingPanelLayoutAdapter {
fixedConstraints = surfaceConstraints + backdropConstraints
if vc.contentMode == .fitToBounds {
bottomConstraint = surfaceView.bottomAnchor.constraint(equalTo: vc.view.bottomAnchor,
constant: 0.0)
}
// Flexible surface constraints for full, half, tip and off
let topAnchor: NSLayoutYAxisAnchor = {
if layout is FloatingPanelFullScreenLayout {
if layout.positionReference == .fromSuperview {
return vc.view.topAnchor
} else {
return vc.layoutGuide.topAnchor
@@ -315,7 +351,7 @@ class FloatingPanelLayoutAdapter {
}
let bottomAnchor: NSLayoutYAxisAnchor = {
if layout is FloatingPanelFullScreenLayout {
if layout.positionReference == .fromSuperview {
return vc.view.bottomAnchor
} else {
return vc.layoutGuide.bottomAnchor
@@ -342,16 +378,15 @@ class FloatingPanelLayoutAdapter {
NSLayoutConstraint.deactivate(fullConstraints + halfConstraints + tipConstraints + offConstraints)
let interactiveTopConstraint: NSLayoutConstraint
switch layout {
case is FloatingPanelIntrinsicLayout,
is FloatingPanelFullScreenLayout:
initialConst = surfaceView.frame.minY + offset.y
interactiveTopConstraint = surfaceView.topAnchor.constraint(equalTo: vc.view.topAnchor,
constant: initialConst)
default:
switch layout.positionReference {
case .fromSafeArea:
initialConst = surfaceView.frame.minY - safeAreaInsets.top + offset.y
interactiveTopConstraint = surfaceView.topAnchor.constraint(equalTo: vc.layoutGuide.topAnchor,
constant: initialConst)
case .fromSuperview:
initialConst = surfaceView.frame.minY + offset.y
interactiveTopConstraint = surfaceView.topAnchor.constraint(equalTo: vc.view.topAnchor,
constant: initialConst)
}
NSLayoutConstraint.activate([interactiveTopConstraint])
self.interactiveTopConstraint = interactiveTopConstraint
@@ -360,66 +395,73 @@ class FloatingPanelLayoutAdapter {
func endInteraction(at state: FloatingPanelPosition) {
// Don't deactivate `interactiveTopConstraint` here because it leads to
// unsatisfiable constraints
if self.interactiveTopConstraint == nil {
// Actiavate `interactiveTopConstraint` for `fitToBounds` mode.
// It goes throught this path when the pan gesture state jumps
// from .begin to .end.
startInteraction(at: state)
}
}
// The method is separated from prepareLayout(to:) for the rotation support
// It must be called in FloatingPanelController.traitCollectionDidChange(_:)
func updateHeight() {
guard let vc = vc else { return }
NSLayoutConstraint.deactivate(constraint: heightConstraint)
heightConstraint = nil
if let const = self.heightConstraint {
NSLayoutConstraint.deactivate([const])
if layout is FloatingPanelIntrinsicLayout {
updateIntrinsicHeight()
}
defer {
if layout is FloatingPanelIntrinsicLayout {
NSLayoutConstraint.deactivate(fullConstraints)
fullConstraints = [
surfaceView.topAnchor.constraint(equalTo: vc.layoutGuide.bottomAnchor,
constant: -fullInset),
]
}
}
let heightConstraint: NSLayoutConstraint
guard vc.contentMode != .fitToBounds else { return }
switch layout {
case is FloatingPanelIntrinsicLayout:
updateIntrinsicHeight()
heightConstraint = surfaceView.heightAnchor.constraint(equalToConstant: intrinsicHeight + safeAreaInsets.bottom)
default:
let const = -(positionY(for: topMostState))
heightConstraint = surfaceView.heightAnchor.constraint(equalTo: vc.view.heightAnchor,
constant: const)
}
NSLayoutConstraint.activate([heightConstraint])
self.heightConstraint = heightConstraint
NSLayoutConstraint.activate(constraint: heightConstraint)
surfaceView.bottomOverflow = vc.view.bounds.height + layout.topInteractionBuffer
if layout is FloatingPanelIntrinsicLayout {
NSLayoutConstraint.deactivate(fullConstraints)
fullConstraints = [
surfaceView.topAnchor.constraint(equalTo: vc.layoutGuide.bottomAnchor,
constant: -fullInset),
]
}
}
func updateInteractiveTopConstraint(diff: CGFloat, allowsTopBuffer: Bool, with behavior: FloatingPanelBehavior) {
defer {
surfaceView.superview!.layoutIfNeeded() // MUST call here to update `surfaceView.frame`
layoutSurfaceIfNeeded() // MUST be called to update `surfaceView.frame`
}
let topMostConst: CGFloat = {
var ret: CGFloat = 0.0
switch layout {
case is FloatingPanelIntrinsicLayout, is FloatingPanelFullScreenLayout:
ret = topY
default:
switch layout.positionReference {
case .fromSafeArea:
ret = topY - safeAreaInsets.top
case .fromSuperview:
ret = topY
}
return max(ret, 0.0) // The top boundary is equal to the related topAnchor.
}()
let bottomMostConst: CGFloat = {
var ret: CGFloat = 0.0
let _bottomY = vc.isRemovalInteractionEnabled ? positionY(for: .hidden) : bottomY
switch layout {
case is FloatingPanelIntrinsicLayout, is FloatingPanelFullScreenLayout:
ret = _bottomY
default:
switch layout.positionReference {
case .fromSafeArea:
ret = _bottomY - safeAreaInsets.top
case .fromSuperview:
ret = _bottomY
}
return min(ret, surfaceView.superview!.bounds.height)
}()
@@ -451,22 +493,28 @@ class FloatingPanelLayoutAdapter {
return (1.0 - (1.0 / ((buffer * 0.55 / base) + 1.0))) * base
}
func activateLayout(of state: FloatingPanelPosition) {
func activateFixedLayout() {
// Must deactivate `interactiveTopConstraint` here
NSLayoutConstraint.deactivate(constraint: self.interactiveTopConstraint)
self.interactiveTopConstraint = nil
NSLayoutConstraint.activate(fixedConstraints)
if vc.contentMode == .fitToBounds {
NSLayoutConstraint.activate(constraint: self.bottomConstraint)
}
}
func activateInteractiveLayout(of state: FloatingPanelPosition) {
defer {
surfaceView.superview!.layoutIfNeeded()
layoutSurfaceIfNeeded()
log.debug("activateLayout -- surface.presentation = \(self.surfaceView.presentationFrame) surface.frame = \(self.surfaceView.frame)")
}
var state = state
setBackdropAlpha(of: state)
// Must deactivate `interactiveTopConstraint` here
if let interactiveTopConstraint = interactiveTopConstraint {
NSLayoutConstraint.deactivate([interactiveTopConstraint])
self.interactiveTopConstraint = nil
}
NSLayoutConstraint.activate(fixedConstraints)
if isValid(state) == false {
state = layout.initialPosition
}
@@ -484,10 +532,22 @@ class FloatingPanelLayoutAdapter {
}
}
func activateLayout(of state: FloatingPanelPosition) {
activateFixedLayout()
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 }
#endif
surfaceView.superview?.layoutIfNeeded()
}
private func setBackdropAlpha(of target: FloatingPanelPosition) {
if target == .hidden {
self.backdropView.alpha = 0.0
@@ -36,8 +36,6 @@ public class FloatingPanelSurfaceView: UIView {
public weak var contentView: UIView!
/// The content insets specifying the insets around the content view.
///
/// - important: Currently the `bottom` inset is ignored.
public var contentInsets: UIEdgeInsets = .zero {
didSet {
// Needs update constraints
@@ -83,8 +81,8 @@ public class FloatingPanelSurfaceView: UIView {
/// The color of the surface border.
public var borderWidth: CGFloat = 0.0 { didSet { setNeedsLayout() } }
/// Offset of the container view from the top
public var containerTopInset: CGFloat = 0.0 { didSet {
/// The margins to use when laying out the container view wrapping content.
public var containerMargins: UIEdgeInsets = .zero { didSet {
setNeedsUpdateConstraints()
} }
@@ -99,8 +97,10 @@ public class FloatingPanelSurfaceView: UIView {
@available(*, unavailable, renamed: "containerView")
public var backgroundView: UIView!
private lazy var containerViewTopInsetConstraint: NSLayoutConstraint = containerView.topAnchor.constraint(equalTo: topAnchor, constant: containerTopInset)
private lazy var containerViewHeightConstraint: NSLayoutConstraint = containerView.heightAnchor.constraint(equalTo: heightAnchor, multiplier: 1.0)
private lazy var containerViewTopConstraint = containerView.topAnchor.constraint(equalTo: topAnchor, constant: containerMargins.top)
private lazy var containerViewHeightConstraint = containerView.heightAnchor.constraint(equalTo: heightAnchor, multiplier: 1.0)
private lazy var containerViewLeftConstraint = containerView.leftAnchor.constraint(equalTo: leftAnchor, constant: 0.0)
private lazy var containerViewRightConstraint = containerView.rightAnchor.constraint(equalTo: rightAnchor, constant: 0.0)
/// The content view top constraint
private var contentViewTopConstraint: NSLayoutConstraint?
@@ -111,9 +111,11 @@ public class FloatingPanelSurfaceView: UIView {
/// The content height constraint
private var contentViewHeightConstraint: NSLayoutConstraint?
private lazy var grabberHandleWidthConstraint: NSLayoutConstraint = grabberHandle.widthAnchor.constraint(equalToConstant: grabberHandleWidth)
private lazy var grabberHandleHeightConstraint: NSLayoutConstraint = grabberHandle.heightAnchor.constraint(equalToConstant: grabberHandleHeight)
private lazy var grabberHandleTopConstraint: NSLayoutConstraint = grabberHandle.topAnchor.constraint(equalTo: topAnchor, constant: grabberTopPadding)
private lazy var grabberHandleWidthConstraint = grabberHandle.widthAnchor.constraint(equalToConstant: grabberHandleWidth)
private lazy var grabberHandleHeightConstraint = grabberHandle.heightAnchor.constraint(equalToConstant: grabberHandleHeight)
private lazy var grabberHandleTopConstraint = grabberHandle.topAnchor.constraint(equalTo: topAnchor, constant: grabberTopPadding)
public override class var requiresConstraintBasedLayout: Bool { return true }
override init(frame: CGRect) {
super.init(frame: frame)
@@ -132,9 +134,9 @@ public class FloatingPanelSurfaceView: UIView {
addSubview(containerView)
containerView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
containerViewTopInsetConstraint,
containerView.leftAnchor.constraint(equalTo: leftAnchor, constant: 0.0),
containerView.rightAnchor.constraint(equalTo: rightAnchor, constant: 0.0),
containerViewTopConstraint,
containerViewLeftConstraint,
containerViewRightConstraint,
containerViewHeightConstraint,
])
@@ -149,13 +151,15 @@ public class FloatingPanelSurfaceView: UIView {
}
public override func updateConstraints() {
containerViewTopInsetConstraint.constant = containerTopInset
containerViewHeightConstraint.constant = bottomOverflow
containerViewTopConstraint.constant = containerMargins.top
containerViewLeftConstraint.constant = containerMargins.left
containerViewRightConstraint.constant = -containerMargins.right
containerViewHeightConstraint.constant = (containerMargins.bottom == 0) ? bottomOverflow : -(containerMargins.top + containerMargins.bottom)
contentViewTopConstraint?.constant = contentInsets.top
contentViewLeftConstraint?.constant = contentInsets.left
contentViewRightConstraint?.constant = contentInsets.right
contentViewHeightConstraint?.constant = -containerTopInset
contentViewTopConstraint?.constant = containerMargins.top + contentInsets.top
contentViewLeftConstraint?.constant = containerMargins.left + contentInsets.left
contentViewRightConstraint?.constant = containerMargins.right + contentInsets.right
contentViewHeightConstraint?.constant = -(containerMargins.top + containerMargins.bottom + contentInsets.top + contentInsets.bottom)
grabberHandleTopConstraint.constant = grabberTopPadding
grabberHandleWidthConstraint.constant = grabberHandleWidth
@@ -196,6 +200,7 @@ public class FloatingPanelSurfaceView: UIView {
return
}
containerView.layer.masksToBounds = true
guard containerMargins.bottom == 0 else { return }
if #available(iOS 11, *) {
// Don't use `contentView.clipToBounds` because it prevents content view from expanding the height of a subview of it
// for the bottom overflow like Auto Layout settings of UIVisualEffectView in Main.storyboard of Example/Maps.
@@ -218,10 +223,11 @@ public class FloatingPanelSurfaceView: UIView {
/* contentView.frame = bounds */ // MUST NOT: Because the top safe area inset of a content VC will be incorrect.
contentView.translatesAutoresizingMaskIntoConstraints = false
let topConstraint = contentView.topAnchor.constraint(equalTo: topAnchor, constant: contentInsets.top)
let leftConstraint = contentView.leftAnchor.constraint(equalTo: leftAnchor, constant: contentInsets.left)
let rightConstraint = rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: contentInsets.right)
let heightConstraint = contentView.heightAnchor.constraint(equalTo: heightAnchor, constant: -containerTopInset)
let topConstraint = contentView.topAnchor.constraint(equalTo: topAnchor, constant: containerMargins.top + contentInsets.top)
let leftConstraint = contentView.leftAnchor.constraint(equalTo: leftAnchor, constant: containerMargins.left + contentInsets.left)
let rightConstraint = rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: containerMargins.right + contentInsets.right)
let heightPadding = containerMargins.top + containerMargins.bottom + contentInsets.top + contentInsets.bottom
let heightConstraint = contentView.heightAnchor.constraint(equalTo: heightAnchor, constant: -heightPadding)
NSLayoutConstraint.activate([
topConstraint,
leftConstraint,
@@ -59,9 +59,7 @@ class FloatingPanelPresentationController: UIPresentationController {
// Forward touch events to the presenting view controller
(fpc.view as? FloatingPanelPassThroughView)?.eventForwardingView = presentingViewController.view
// Set tap-to-dismiss in the backdrop view
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleBackdrop(tapGesture:)))
fpc.backdropView.addGestureRecognizer(tapGesture)
fpc.backdropView.dismissalTapGestureRecognizer.isEnabled = true
}
@objc func handleBackdrop(tapGesture: UITapGestureRecognizer) {
+1 -1
View File
@@ -15,7 +15,7 @@
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>1.6.3</string>
<string>1.7.0</string>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
</dict>
+18 -1
View File
@@ -21,7 +21,7 @@ class CustomLayoutGuide: LayoutGuideProvider {
}
extension UIViewController {
var layoutInsets: UIEdgeInsets {
@objc var layoutInsets: UIEdgeInsets {
if #available(iOS 11.0, *) {
return view.safeAreaInsets
} else {
@@ -75,6 +75,12 @@ extension UIView {
func enableAutoLayout() {
translatesAutoresizingMaskIntoConstraints = false
}
static func performWithLinear(startTime: Double = 0.0, relativeDuration: Double = 1.0, _ animations: @escaping (() -> Void)) {
UIView.animateKeyframes(withDuration: 0.0, delay: 0.0, options: [.calculationModeCubic], animations: {
UIView.addKeyframe(withRelativeStartTime: startTime, relativeDuration: relativeDuration, animations: animations)
}, completion: nil)
}
}
#if __FP_LOG
@@ -140,3 +146,14 @@ extension UITraitCollection {
|| previous.layoutDirection != layoutDirection
}
}
extension NSLayoutConstraint {
static func activate(constraint: NSLayoutConstraint?) {
guard let constraint = constraint else { return }
self.activate([constraint])
}
static func deactivate(constraint: NSLayoutConstraint?) {
guard let constraint = constraint else { return }
self.deactivate([constraint])
}
}
@@ -52,44 +52,54 @@ class FloatingPanelControllerTests: XCTestCase {
}
func test_moveTo() {
let fpc = FloatingPanelController(delegate: nil)
let delegate = FloatingPanelTestDelegate()
let fpc = FloatingPanelController(delegate: delegate)
XCTAssertEqual(delegate.position, .hidden)
fpc.showForTest()
XCTAssertEqual(delegate.position, .half)
fpc.hide()
XCTAssertEqual(delegate.position, .hidden)
fpc.move(to: .full, animated: false)
XCTAssertEqual(fpc.position, .full)
XCTAssertEqual(delegate.position, .full)
XCTAssertEqual(fpc.surfaceView.frame.minY, fpc.originYOfSurface(for: .full))
fpc.move(to: .half, animated: false)
XCTAssertEqual(fpc.position, .half)
XCTAssertEqual(delegate.position, .half)
XCTAssertEqual(fpc.surfaceView.frame.minY, fpc.originYOfSurface(for: .half))
fpc.move(to: .tip, animated: false)
XCTAssertEqual(fpc.position, .tip)
XCTAssertEqual(delegate.position, .tip)
XCTAssertEqual(fpc.surfaceView.frame.minY, fpc.originYOfSurface(for: .tip))
fpc.move(to: .hidden, animated: false)
XCTAssertEqual(fpc.position, .hidden)
XCTAssertEqual(delegate.position, .hidden)
XCTAssertEqual(fpc.surfaceView.frame.minY, fpc.originYOfSurface(for: .hidden))
fpc.move(to: .full, animated: true)
waitRunLoop(secs: 0.3)
XCTAssertEqual(fpc.position, .full)
XCTAssertEqual(delegate.position, .full)
XCTAssertEqual(fpc.surfaceView.frame.minY, fpc.originYOfSurface(for: .full))
fpc.move(to: .half, animated: true)
waitRunLoop(secs: 0.3)
XCTAssertEqual(fpc.position, .half)
XCTAssertEqual(delegate.position, .half)
XCTAssertEqual(fpc.surfaceView.frame.minY, fpc.originYOfSurface(for: .half))
fpc.move(to: .tip, animated: true)
waitRunLoop(secs: 0.3)
XCTAssertEqual(fpc.position, .tip)
XCTAssertEqual(delegate.position, .tip)
XCTAssertEqual(fpc.surfaceView.frame.minY, fpc.originYOfSurface(for: .tip))
fpc.move(to: .hidden, animated: true)
waitRunLoop(secs: 0.3)
XCTAssertEqual(fpc.position, .hidden)
XCTAssertEqual(delegate.position, .hidden)
XCTAssertEqual(fpc.surfaceView.frame.minY, fpc.originYOfSurface(for: .hidden))
}
func test_originSurfaceY() {
@@ -107,6 +117,31 @@ class FloatingPanelControllerTests: XCTestCase {
fpc.move(to: .hidden, animated: false)
XCTAssertEqual(fpc.surfaceView.frame.minY, fpc.originYOfSurface(for: .hidden))
}
func test_contentMode() {
let fpc = FloatingPanelController(delegate: nil)
fpc.loadViewIfNeeded()
fpc.view.frame = CGRect(x: 0, y: 0, width: 375, height: 667)
fpc.show(animated: false, completion: nil)
fpc.contentMode = .static
fpc.move(to: .full, animated: false)
XCTAssertEqual(fpc.surfaceView.frame.height, fpc.view.bounds.height - fpc.originYOfSurface(for: .full))
fpc.move(to: .half, animated: false)
XCTAssertEqual(fpc.surfaceView.frame.height, fpc.view.bounds.height - fpc.originYOfSurface(for: .full))
fpc.move(to: .tip, animated: false)
XCTAssertEqual(fpc.surfaceView.frame.height, fpc.view.bounds.height - fpc.originYOfSurface(for: .full))
fpc.contentMode = .fitToBounds
fpc.move(to: .full, animated: false)
XCTAssertEqual(fpc.surfaceView.frame.height, fpc.view.bounds.height - fpc.originYOfSurface(for: .full))
fpc.move(to: .half, animated: false)
XCTAssertEqual(fpc.surfaceView.frame.height, fpc.view.bounds.height - fpc.originYOfSurface(for: .half))
fpc.move(to: .tip, animated: false)
XCTAssertEqual(fpc.surfaceView.frame.height, fpc.view.bounds.height - fpc.originYOfSurface(for: .tip))
}
}
private class MyZombieViewController: UIViewController, FloatingPanelLayout, FloatingPanelBehavior, FloatingPanelControllerDelegate {
@@ -194,6 +194,47 @@ class FloatingPanelLayoutTests: XCTestCase {
fpc.floatingPanel.layoutAdapter.endInteraction(at: fpc.position)
}
func test_positionReference() {
fpc = CustomSafeAreaFloatingPanelController()
fpc.loadViewIfNeeded()
fpc.view.frame = CGRect(x: 0, y: 0, width: 375, height: 667)
class MyFloatingPanelFullLayout: FloatingPanelTestLayout {
var initialPosition: FloatingPanelPosition = .half
var positionReference: FloatingPanelLayoutReference {
return .fromSuperview
}
}
class MyFloatingPanelSafeAreaLayout: FloatingPanelTestLayout {
var initialPosition: FloatingPanelPosition = .half
var positionReference: FloatingPanelLayoutReference {
return .fromSafeArea
}
}
let fullLayout = MyFloatingPanelFullLayout()
let delegate = FloatingPanelTestDelegate()
delegate.layout = fullLayout
fpc.delegate = delegate
fpc.showForTest()
XCTAssertEqual(fpc.layout.positionReference, .fromSuperview)
XCTAssertEqual(fpc.originYOfSurface(for: .full), fullLayout.insetFor(position: .full))
XCTAssertEqual(fpc.originYOfSurface(for: .half), fpc.view!.frame.height - fullLayout.insetFor(position: .half)!)
XCTAssertEqual(fpc.originYOfSurface(for: .tip), fpc.view!.frame.height - fullLayout.insetFor(position: .tip)!)
let safeAreaLayout = MyFloatingPanelSafeAreaLayout()
delegate.layout = safeAreaLayout
fpc.delegate = delegate
XCTAssertEqual(fpc.layout.positionReference, .fromSafeArea)
XCTAssertEqual(fpc.originYOfSurface(for: .full),
fullLayout.insetFor(position: .full)! + fpc.layoutInsets.top)
XCTAssertEqual(fpc.originYOfSurface(for: .half),
fpc.view!.frame.height - (fullLayout.insetFor(position: .half)! + fpc.layoutInsets.bottom))
XCTAssertEqual(fpc.originYOfSurface(for: .tip),
fpc.view!.frame.height - (fullLayout.insetFor(position: .tip)! + fpc.layoutInsets.bottom))
}
}
private typealias LayoutSegmentTestParameter = (UInt, pos: CGFloat, forwardY: Bool, lower: FloatingPanelPosition?, upper: FloatingPanelPosition?)
@@ -204,3 +245,10 @@ private func assertLayoutSegment(_ floatingPanel: FloatingPanel, with params: [L
XCTAssertEqual(segument.upper, upper, line: line)
}
}
private class CustomSafeAreaFloatingPanelController: FloatingPanelController { }
extension CustomSafeAreaFloatingPanelController {
override var layoutInsets: UIEdgeInsets {
return UIEdgeInsets(top: 64.0, left: 0.0, bottom: 0.0, right: 34.0)
}
}
@@ -12,6 +12,7 @@ class FloatingPanelSurfaceViewTests: XCTestCase {
func test_surfaceView() {
let surface = FloatingPanelSurfaceView(frame: CGRect(x: 0.0, y: 0.0, width: 320.0, height: 480.0))
XCTAssertTrue(FloatingPanelSurfaceView.requiresConstraintBasedLayout)
XCTAssert(surface.contentView == nil)
surface.layoutIfNeeded()
XCTAssert(surface.grabberHandle.frame.minY == 6.0)
@@ -22,11 +23,8 @@ class FloatingPanelSurfaceViewTests: XCTestCase {
XCTAssert(surface.backgroundColor == surface.containerView.backgroundColor)
}
func test_surfaceView_constraintsUpdate() {
let window = UIWindow()
func test_surfaceView_grabberHandle() {
let surface = FloatingPanelSurfaceView(frame: CGRect(x: 0.0, y: 0.0, width: 320.0, height: 480.0))
window.addSubview(surface)
window.makeKeyAndVisible()
XCTAssert(surface.contentView == nil)
surface.layoutIfNeeded()
XCTAssert(surface.grabberHandle.frame.minY == 6.0)
@@ -35,11 +33,46 @@ class FloatingPanelSurfaceViewTests: XCTestCase {
surface.grabberHandleWidth = 44.0
surface.grabberHandleHeight = 12.0
surface.setNeedsLayout()
surface.layoutIfNeeded()
waitRunLoop(secs: 0.000_001)
XCTAssert(surface.grabberHandle.frame.width == surface.grabberHandleWidth, "\(surface.grabberHandle.frame.width) == \(surface.grabberHandleWidth)")
XCTAssert(surface.grabberHandle.frame.height == surface.grabberHandleHeight, "\(surface.grabberHandle.frame.height) == \(surface.grabberHandleHeight)")
window.resignKey()
}
func test_surfaceView_containerMargins() {
let surface = FloatingPanelSurfaceView(frame: CGRect(x: 0.0, y: 0.0, width: 320.0, height: 480.0))
surface.layoutIfNeeded()
XCTAssertEqual(surface.containerView.frame, surface.bounds)
surface.containerMargins = UIEdgeInsets(top: 16.0, left: 16.0, bottom: 16.0, right: 16.0)
surface.setNeedsLayout()
surface.layoutIfNeeded()
XCTAssertEqual(surface.containerView.frame, surface.bounds.inset(by: surface.containerMargins))
}
func test_surfaceView_contentInsets() {
let surface = FloatingPanelSurfaceView(frame: CGRect(x: 0.0, y: 0.0, width: 320.0, height: 480.0))
let contentView = UIView()
surface.add(contentView: contentView)
surface.layoutIfNeeded()
XCTAssertEqual(surface.contentView.frame, surface.bounds)
surface.contentInsets = UIEdgeInsets(top: 16.0, left: 16.0, bottom: 16.0, right: 16.0)
surface.setNeedsLayout()
surface.layoutIfNeeded()
XCTAssertEqual(surface.contentView.frame, surface.bounds.inset(by: surface.contentInsets))
}
func test_surfaceView_containerMargins_and_contentInsets() {
let surface = FloatingPanelSurfaceView(frame: CGRect(x: 0.0, y: 0.0, width: 320.0, height: 480.0))
let contentView = UIView()
surface.add(contentView: contentView)
surface.layoutIfNeeded()
XCTAssertEqual(surface.contentView.frame, surface.bounds)
surface.containerMargins = UIEdgeInsets(top: 16.0, left: 16.0, bottom: 16.0, right: 16.0)
surface.contentInsets = UIEdgeInsets(top: 16.0, left: 16.0, bottom: 16.0, right: 16.0)
surface.setNeedsLayout()
surface.layoutIfNeeded()
XCTAssertEqual(surface.containerView.frame, surface.bounds.inset(by: surface.containerMargins))
XCTAssertEqual(surface.contentView.frame, surface.containerView.bounds.inset(by: surface.contentInsets))
}
func test_surfaceView_cornderRaduis() {
+4
View File
@@ -21,12 +21,16 @@ extension FloatingPanelController {
class FloatingPanelTestDelegate: FloatingPanelControllerDelegate {
var layout: FloatingPanelLayout?
var behavior: FloatingPanelBehavior?
var position: FloatingPanelPosition = .hidden
func floatingPanel(_ vc: FloatingPanelController, layoutFor newCollection: UITraitCollection) -> FloatingPanelLayout? {
return layout
}
func floatingPanel(_ vc: FloatingPanelController, behaviorFor newCollection: UITraitCollection) -> FloatingPanelBehavior? {
return behavior
}
func floatingPanelDidChangePosition(_ vc: FloatingPanelController) {
position = vc.position
}
}
protocol FloatingPanelTestLayout: FloatingPanelFullScreenLayout {}
+125 -7
View File
@@ -5,9 +5,9 @@
[![Swift 4.1](https://img.shields.io/badge/Swift-4.1-orange.svg?style=flat)](https://swift.org/)
[![Swift 4.2](https://img.shields.io/badge/Swift-4.2-orange.svg?style=flat)](https://swift.org/)
[![Swift 5.0](https://img.shields.io/badge/Swift-5.0-orange.svg?style=flat)](https://swift.org/)
[![Swift 5.1](https://img.shields.io/badge/Swift-5.1-orange.svg?style=flat)](https://swift.org/)
# FloatingPanel
# FloatingPanel
FloatingPanel is a simple and easy-to-use UI component for a new interface introduced in Apple Maps, Shortcuts and Stocks app.
The new interface displays the related contents and utilities in parallel as a user wants.
@@ -24,25 +24,37 @@ The new interface displays the related contents and utilities in parallel as a u
- [Installation](#installation)
- [CocoaPods](#cocoapods)
- [Carthage](#carthage)
- [Swift Package Manager with Xcode 11](#swift-package-manager-with-xcode-11)
- [Getting Started](#getting-started)
- [Add a floating panel as a child view controller](#add-a-floating-panel-as-a-child-view-controller)
- [Present a floating panel as a modality](#present-a-floating-panel-as-a-modality)
- [View hierarchy](#view-hierarchy)
- [Usage](#usage)
- [Show/Hide a floating panel in a view with your view hierarchy](#showhide-a-floating-panel-in-a-view-with-your-view-hierarchy)
- [Scale the content view when the surface position changes](#scale-the-content-view-when-the-surface-position-changes)
- [Customize the layout with `FloatingPanelLayout` protocol](#customize-the-layout-with-floatingpanellayout-protocol)
- [Change the initial position and height](#change-the-initial-position-and-height)
- [Support your landscape layout](#support-your-landscape-layout)
- [Use Intrinsic height layout](#use-intrinsic-height-layout)
- [Specify position insets from the frame of `FloatingPanelContrller.view`, not the SafeArea](#specify-position-insets-from-the-frame-of-floatingpanelcontrllerview-not-the-safearea)
- [Customize the behavior with `FloatingPanelBehavior` protocol](#customize-the-behavior-with-floatingpanelbehavior-protocol)
- [Modify your floating panel's interaction](#modify-your-floating-panels-interaction)
- [Use a custom grabber handle](#use-a-custom-grabber-handle)
- [Add tap gestures to the surface or backdrop views](#add-tap-gestures-to-the-surface-or-backdrop-views)
- [Activate the rubber-band effect on the top/bottom edges](#activate-the-rubber-band-effect-on-the-topbottom-edges)
- [Manage the projection of a pan gesture momentum](#manage-the-projection-of-a-pan-gesture-momentum)
- [Customize the surface design](#customize-the-surface-design)
- [Use a custom grabber handle](#use-a-custom-grabber-handle)
- [Customize layout of the grabber handle](#customize-layout-of-the-grabber-handle)
- [Customize content padding from surface edges](#customize-content-padding-from-surface-edges)
- [Customize margins of the surface edges](#customize-margins-of-the-surface-edges)
- [Customize gestures](#customize-gestures)
- [Suppress the panel interaction](#suppress-the-panel-interaction)
- [Add tap gestures to the surface or backdrop views](#add-tap-gestures-to-the-surface-or-backdrop-views)
- [Create an additional floating panel for a detail](#create-an-additional-floating-panel-for-a-detail)
- [Move a position with an animation](#move-a-position-with-an-animation)
- [Work your contents together with a floating panel behavior](#work-your-contents-together-with-a-floating-panel-behavior)
- [Notes](#notes)
- ['Show' or 'Show Detail' Segues from `FloatingPanelController`'s content view controller](#show-or-show-detail-segues-from-floatingpanelcontrollers-content-view-controller)
- [UISearchController issue](#uisearchcontroller-issue)
- [FloatingPanelSurfaceView's issue on iOS 10](#floatingpanelsurfaceviews-issue-on-ios-10)
- [Author](#author)
- [License](#license)
@@ -83,7 +95,7 @@ it, simply add the following line to your Podfile:
pod 'FloatingPanel'
```
✏️ To suppress "Swift Conversion" warnings in Xcode, please set a Swift version to `SWIFT_VERSION` for the project in your Podfile. It will be resolved in CocoaPods v1.7.0.
✏️FloatingPanel v1.7.0 or later requires CocoaPods v1.7.0+ for `swift_versions` support.
### Carthage
@@ -205,6 +217,16 @@ fpc.hide(animated: true) {
NOTE: `FloatingPanelController` wraps `show`/`hide` with `addPanel`/`removePanelFromParent` for easy-to-use. But `show`/`hide` are more convenience for your app.
### Scale the content view when the surface position changes
Specify the `contentMode` to `.fitToBounds` if the surface height fits the bounds of `FloatingPanelController.view` when the surface position changes
```swift
fpc.contentMode = .fitToBounds
```
Otherwise, `FloatingPanelController` fixes the content by the height of the top most position.
### Customize the layout with `FloatingPanelLayout` protocol
#### Change the initial position and height
@@ -295,6 +317,27 @@ class RemovablePanelLayout: FloatingPanelIntrinsicLayout {
}
```
#### Specify position insets from the frame of `FloatingPanelContrller.view`, not the SafeArea
There are 2 ways. One is returning `.fromSuperview` for `FloatingPanelLayout.positionReference` in your layout.
```swift
class MyFullScreenLayout: FloatingPanelLayout {
...
var positionReference: FloatingPanelLayoutReference {
return .fromSuperview
}
}
```
Another is using `FloatingPanelFullScreenLayout` protocol.
```swift
class MyFullScreenLayout: FloatingPanelFullScreenLayout {
...
}
```
### Customize the behavior with `FloatingPanelBehavior` protocol
#### Modify your floating panel's interaction
@@ -317,7 +360,33 @@ class FloatingPanelStocksBehavior: FloatingPanelBehavior {
}
```
### Use a custom grabber handle
#### Activate the rubber-band effect on the top/bottom edges
```swift
class FloatingPanelBehavior: FloatingPanelBehavior {
...
func allowsRubberBanding(for edge: UIRectEdge) -> Bool {
return true
}
}
```
#### Manage the projection of a pan gesture momentum
This allows full projectional panel behavior. For example, a user can swipe up a panel from tip to full nearby the tip position.
```swift
class FloatingPanelBehavior: FloatingPanelBehavior {
...
func shouldProjectMomentum(_ fpc: FloatingPanelController, for proposedTargetPosition: FloatingPanelPosition) -> Bool
return true
}
}
```
### Customize the surface design
#### Use a custom grabber handle
```swift
let myGrabberHandleView = MyGrabberHandleView()
@@ -325,7 +394,50 @@ fpc.surfaceView.grabberHandle.isHidden = true
fpc.surfaceView.addSubview(myGrabberHandleView)
```
### Add tap gestures to the surface or backdrop views
#### Customize layout of the grabber handle
```swift
fpc.surfaceView.grabberTopPadding = 10.0
fpc.surfaceView.grabberHandleWidth = 44.0
fpc.surfaceView.grabberHandleHeight = 12.0
```
#### Customize content padding from surface edges
```swift
fpc.surfaceView.contentInsets = .init(top: 20, left: 20, bottom: 20, right: 20)
```
#### Customize margins of the surface edges
```swift
fpc.surfaceView.containerMargins = .init(top: 20.0, left: 16.0, bottom: 16.0, right: 16.0)
```
The feature can be used for these 2 kind panels
* Facebook/Slack-like panel whose surface top edge is separated from the grabber handle.
* iOS native panel to display AirPods information, for example.
### Customize gestures
#### Suppress the panel interaction
You can disable the pan gesture recognizer directly
```swift
fpc.panGestureRecognizer.isEnable = false
```
Or use this `FloatingPanelControllerDelegate` method.
```swift
func floatingPanelShouldBeginDragging(_ vc: FloatingPanelController) -> Bool {
return aCondition ? false : true
}
```
#### Add tap gestures to the surface or backdrop views
```swift
override func viewDidLoad() {
@@ -439,6 +551,12 @@ A `FloatingPanelController` object proxies an action for `show(_:sender)` to the
It's a great way to decouple between a floating panel and the content VC.
### UISearchController issue
`UISearchController` isn't able to be used with `FloatingPanelController` by the system design.
Because `UISearchController` automatically presents itself modally when a user interacts with the search bar, and then it swaps the superview of the search bar to the view managed by itself while it displays. As a result, `FloatingPanelController` can't control the search bar when it's active, as you can see from [the screen shot](https://github.com/SCENEE/FloatingPanel/issues/248#issuecomment-521263831).
### FloatingPanelSurfaceView's issue on iOS 10
* On iOS 10, `FloatingPanelSurfaceView.cornerRadius` isn't not automatically masked with the top rounded corners because of `UIVisualEffectView` issue. See https://forums.developer.apple.com/thread/50854.