Compare commits

...

117 Commits

Author SHA1 Message Date
Shin Yamamoto 2bfea00c72 Release v1.3.0 2018-12-15 08:48:00 +09:00
Shin Yamamoto 81441a724b Fix a crash in checkLayoutConsistance()
The crash happened on orientation change from landscape to portrait in
"Show Tab Bar" scene.
2018-12-12 11:47:02 +09:00
Shin Yamamoto 82c8d8dd9a Fix the boundary condition for top/bottom buffers 2018-12-12 09:59:19 +09:00
Shin Yamamoto 8751a9fe53 Fix the broken landscape layout 2018-12-12 09:59:19 +09:00
Shin Yamamoto 9cf561fcf1 Update README 2018-12-12 09:59:19 +09:00
Shin Yamamoto 332559c67d Update Samples
- Make "Show Intrinsic View" panel dismissable
- Add tap-to-hide sample
2018-12-12 09:59:19 +09:00
Shin Yamamoto 7f419b7e78 Merge pull request #79 from SCENEE/improve-intrinsic-height
Improve intrinsic height
2018-12-07 16:23:07 +09:00
Shin Yamamoto 973a90e071 Update README 2018-12-07 15:31:59 +09:00
Shin Yamamoto 66a8ca36e4 Fix FloatingPanelIntrinsicLayout
- `.full` position's height must be the intrinsic height.
- Work it for a safe area bottom anchor
- Remove FloatingPanelIntrinsicLayout.contentViewController because it
isn't actually needed
2018-12-07 14:37:48 +09:00
Shin Yamamoto 3715007156 Merge pull request #63 from SCENEE/feat-modality
FloatingPanelController as a Modality
2018-12-07 14:36:43 +09:00
Shin Yamamoto 5158685c02 Update README
- Show/Hide usage
- Modality usage
2018-12-07 13:25:17 +09:00
Shin Yamamoto 61d3371ea5 Fix a bug in Samples app 2018-12-06 09:11:25 +09:00
Shin Yamamoto 0491507e67 Build 'next' branch on Travis CI 2018-12-06 09:11:25 +09:00
Shin Yamamoto fcd4ad874a Use autoresizing masks instead of Auto Layout constraints 2018-12-06 09:11:25 +09:00
Shin Yamamoto e2668fcdf2 Fix the condition of removal interaction 2018-12-06 09:11:25 +09:00
Shin Yamamoto f3ac2b2cec Add the modal dismissal on backdrop tap 2018-12-06 09:11:25 +09:00
Shin Yamamoto 8b84391e36 Fix FloatingPanelModalTransitioning 2018-12-06 09:11:25 +09:00
Shin Yamamoto 1b3ca347f5 Update comment of FloatingPanelSurfaceView.grabberHandle 2018-12-06 09:11:25 +09:00
Shin Yamamoto 2dced7bfbf Improve FloatingPanelController implementation 2018-12-06 09:11:25 +09:00
Shin Yamamoto ac9f8fe89c Update Samples App for .hidden 2018-12-04 22:25:22 +09:00
Shin Yamamoto 6817990555 Add .hidden position 2018-12-04 22:25:22 +09:00
Shin Yamamoto d395cde316 Fix a backdrop's cut off on orientation change 2018-12-04 22:25:22 +09:00
Shin Yamamoto 20272eccb8 Add FloatingPanelTransitioning to present as Modality 2018-12-04 22:25:22 +09:00
Shin Yamamoto 091ae8abff Add 'Show Floating Panel Modal' in Samples app 2018-12-04 22:25:22 +09:00
Shin Yamamoto 6e87690649 FloatingPanelController as a Modality
* Change a floating panel view hierarchy
* Add FloatingPanelController.{show,hide}(animated:completion)
2018-12-04 22:24:26 +09:00
Shin Yamamoto d5a1bd3859 Update Samples App to use dismiss(animated:completion) 2018-12-04 22:21:50 +09:00
Shin Yamamoto a1e4643a25 Swizzling UIViewController.dismiss(animated:completion) for a content VC 2018-12-04 22:21:50 +09:00
Shin Yamamoto 71c0450614 Merge pull request #74 from SCENEE/feat-intrinsic-height
Feat intrinsic height
2018-12-04 22:20:45 +09:00
Shin Yamamoto d469caad69 Fix unexpected assertion failure 2018-12-04 08:19:41 +09:00
Derek Schade 5cc3d4fbfb Enabled inset checks again 2018-12-04 08:14:46 +09:00
Derek Schade a8691ee3a5 Update layout when layout is intrinsiclayout 2018-12-04 08:14:46 +09:00
Derek Schade 91d7941921 Add IntrinsicPanelLayout 2018-12-04 08:14:46 +09:00
Derek Schade 0bc7a0953e Intrinsic layout protocol 2018-12-04 08:14:46 +09:00
Derek Schade c60bea5952 add intrinsic viewcontroller to storyboard 2018-12-04 08:14:46 +09:00
Shin Yamamoto 4db648ad25 Merge pull request #72 from SCENEE/release-v1.2.2
Release v1.2.2
2018-12-03 12:27:47 +09:00
Shin Yamamoto 0bb8342873 Release v1.2.2 2018-12-03 09:25:09 +09:00
Shin Yamamoto d4f2a88fdf Merge pull request #71 from SCENEE/fix-bugs
Fix scroll tracking bugs
2018-12-01 15:45:41 +09:00
Shin Yamamoto af45d39841 Fix panning at grabber Area 2018-12-01 12:36:47 +09:00
Shin Yamamoto 66f5b0b210 Fix an invalid content offset on height change 2018-12-01 11:08:33 +09:00
Shin Yamamoto 4a3b79f1b4 Update README 2018-11-29 13:06:27 +09:00
Shin Yamamoto 12a100def8 Merge pull request #68 from SCENEE/release-1.2.1
Release v1.2.1
2018-11-26 13:20:24 +09:00
Shin Yamamoto 47971f607a Release v1.2.1 2018-11-26 09:40:14 +09:00
Shin Yamamoto 03a4d342a3 Merge pull request #66 from Galeas/fix-remove-backdrop-view-on-dismiss
Fix remove backdrop view on dismiss
2018-11-25 11:11:22 +09:00
Evgeniy Branitsky 4f5abfefec Removing backdrop view 2018-11-24 11:21:56 +03:00
Shin Yamamoto e1ee3c06e8 Merge pull request #62 from SCENEE/feat-make-animator-interruptible
Make the animated interaction interruptible
2018-11-22 20:21:11 +09:00
Shin Yamamoto 17ba704472 Make the animated interaction interruptible 2018-11-22 14:26:18 +09:00
Shin Yamamoto e5391fa1f4 Merge pull request #61 from SCENEE/improve-delegate
Improve delegate
2018-11-22 13:54:02 +09:00
Shin Yamamoto c0647017b5 Add FloatingPanelControllerDelegate.floatingPanelDidChangePosition(_:) 2018-11-21 09:10:50 +09:00
Shin Yamamoto 3686bb4b44 Clean up code for scrollGestureRecognizers 2018-11-20 11:08:23 +09:00
Shin Yamamoto 76c8ca4b20 Merge pull request #56 from SCENEE/fix-gesture-handling
Fix the gesture handling
2018-11-20 11:07:14 +09:00
Shin Yamamoto c53e64027b Merge pull request #45 from SCENEE/release-1.2.0
Release v1.2.0
2018-11-17 10:12:10 +09:00
Shin Yamamoto c15d236320 Release v1.2.0 2018-11-17 09:40:34 +09:00
Shin Yamamoto 281504c9c6 Fix the gesture handling
* Fix a detection of a long press gesture in content VC
* Fix a SwipeActionPanGesture is not working in the tracking scroll
    * Update DebugTableViewController to test it
2018-11-17 09:09:07 +09:00
Shin Yamamoto eaf0ebe62b Merge pull request #52 from futuretap/master
Fixed infinite recursion in FloatingPanel.responds(to:)
2018-11-16 10:25:34 +09:00
Ortwin Gentz a4a08662be Fixed infinite recursion in FloatingPanel.responds(to:)
if track(scrollView:) is called twice
2018-11-15 22:11:40 +01:00
Shin Yamamoto e8a0ffeca5 Merge pull request #48 from SCENEE/fix-scrolling-short-contents
Fix scrolling short contents
2018-11-16 00:19:41 +09:00
Shin Yamamoto 1399cc6fbd Fix a layout bug on orientation changed 2018-11-15 10:33:46 +09:00
Shin Yamamoto 8b44ad4b08 Fix unexpected bounciness on short contents
This issue happens in dragging from half to full position.
2018-11-15 10:33:46 +09:00
Shin Yamamoto e4a1a6e293 Improve Scroll tracking(TableView) sample 2018-11-15 10:33:46 +09:00
Shin Yamamoto ba00786b91 Merge pull request #51 from futuretap/improved-shadow
Improved shadow, not darkening content background
2018-11-15 09:33:55 +09:00
Ortwin Gentz 5e7529d1e6 Improved shadow, not darkening content background
By removing shadowPath and putting the shadow directly onto the surface view layer instead of a sublayer, the shadow doesn't affect the content view background
2018-11-14 15:33:38 +01:00
Shin Yamamoto e75108113a Fix comment 2018-11-14 20:16:49 +09:00
Shin Yamamoto 6d51b0d420 Fix failure requirements for scroll view's gesture recognizers 2018-11-14 11:50:41 +09:00
Shin Yamamoto c3b199755e Add short contents scroll sample 2018-11-14 11:50:41 +09:00
Shin Yamamoto d57d8e9da5 Fix scrolling contents shorter than scroll view size 2018-11-14 11:38:23 +09:00
Shin Yamamoto d24e1c5355 Prevent unexpected assertion failure
The assertion always fails on orientation change from landspace to
portrait with the default layouts.
2018-11-14 11:18:54 +09:00
Shin Yamamoto 95c94560be Merge pull request #44 from SCENEE/fix-scroll-indicator-hidden
Fix scroll indicator hidden
2018-11-14 07:48:50 +09:00
Shin Yamamoto 7b4ed52eb1 Add comments and fix typos 2018-11-12 13:35:36 +09:00
Shin Yamamoto cab8c15474 Fix scroll view indicators hidden at the top once 2018-11-12 13:35:36 +09:00
Shin Yamamoto 72f5d59a75 Merge pull request #43 from SCENEE/add-update-layout-api
Add FloatingPanelController.updateLayout()
2018-11-12 13:33:25 +09:00
Shin Yamamoto 273adc8d1b Fix FloatingPanel's initial state value 2018-11-12 13:03:54 +09:00
Shin Yamamoto 5f0f28cb0e Add update layout sample 2018-11-12 13:03:54 +09:00
Shin Yamamoto e1c9fe120b Add FloatingPanelController.updateLayout() 2018-11-12 13:03:54 +09:00
Shin Yamamoto 630580beb6 Merge pull request #38 from SCENEE/fix-scroll-view-bounce
Fix scroll top bounce bugs
2018-11-09 10:20:35 +09:00
Shin Yamamoto 68a2c43580 Merge pull request #37 from SCENEE/fix-orientation-change-on-removal
Fix orientation change on removal
2018-11-09 10:20:20 +09:00
Shin Yamamoto 817fce6d10 Fix scroll top bounce bugs
Disable scroll top bouncing if a user scroll down contents(no
deceleration) and the scroll offset Y is less than 10.0, instead
of the velocity condition(greater than 2500.0).

This change prevents potential bugs on scroll bouncing so that the
scroll view tracking is more robust.
2018-11-08 11:32:54 +09:00
Shin Yamamoto 6e85afaee6 Update checkLayoutConsistance 2018-11-08 09:52:04 +09:00
Shin Yamamoto 98c5096f67 Merge pull request #36 from SCENEE/fix-scroll-jump-after-scroll-animation
Fix scrollview jumps after it moved programmatically
2018-11-08 09:28:17 +09:00
Shin Yamamoto 57c7ced59d Add RemovablePanelLandscapeLayout 2018-11-07 13:38:35 +09:00
Shin Yamamoto 73e6d38344 Introduce FloatingPanelBehavior.redirectionalProgress(_:from:to:) 2018-11-07 13:38:35 +09:00
Shin Yamamoto bd128bf8b0 Fix removal interaction 2018-11-07 13:11:38 +09:00
Shin Yamamoto 16e8808ce5 Modify the backdrop default behavior 2018-11-07 11:18:05 +09:00
Shin Yamamoto f4088fcb6b Fix backdrop handling 2018-11-07 11:06:18 +09:00
Shin Yamamoto 63b8aa24e8 Improve removal interaction impl 2018-11-07 10:54:28 +09:00
Shin Yamamoto 5744491606 Fix the surface height on orientation change 2018-11-07 10:45:32 +09:00
Shin Yamamoto f5ecbef724 Add 'Animate Scroll' button in Samples app 2018-11-07 10:44:35 +09:00
Shin Yamamoto f176a2c70e Fix scrollview jumps after it moved programmatically 2018-11-07 09:49:00 +09:00
Shin Yamamoto 51c124d3e4 Merge pull request #33 from SCENEE/fix-unexpected-stop-deleration
Reset 'stopScrollDeceleration' on animation finish
2018-11-07 09:19:16 +09:00
Shin Yamamoto 5f6c97336e Merge pull request #34 from ffittschen/fix-moving-without-scroll-top
Fix moving floating panel without scrolling to top of scroll view
2018-11-07 09:18:59 +09:00
Florian Fittschen 894eb77d5d Revert scrollview offset changes 2018-11-06 10:01:57 +01:00
Shin Yamamoto d1b5a1f517 Merge pull request #29 from SCENEE/fix-navigation-bar-problem
Fix navigation bar problem
2018-11-06 10:10:45 +09:00
Shin Yamamoto 300d5f8d91 Reset 'stopScrollDeceleration' on animation finish 2018-11-06 10:04:41 +09:00
Shin Yamamoto bbc885f783 Merge pull request #32 from SCENEE/fix-moving-without-scroll-top
Fix a bug in moving interaction from full position
2018-11-06 09:19:18 +09:00
Florian Fittschen 6badeeebe5 Fix moving without scrolling to top 2018-11-05 11:15:54 +01:00
Shin Yamamoto e282806422 Merge pull request #31 from SCENEE/fix-removal-interaction
Fix the removal interaction
2018-11-05 18:22:08 +09:00
Shin Yamamoto 629807584b Fix the removal interaction 2018-11-05 08:50:39 +09:00
Shin Yamamoto 922c0e53d2 Fix a bug in moving interaction from full position
The floating panel must work well if the tracking scroll view's
content offset isn't the top.
2018-11-04 11:55:30 +09:00
Shin Yamamoto ec9fcd473a Fix the surface view height on non traslucent nav bar 2018-11-02 19:51:06 +09:00
Shin Yamamoto de9f415ded Observe parent's view.safeAreaInsets 2018-11-02 17:00:34 +09:00
Shin Yamamoto fd5ca2c2fc Clean up FloatingPanelSurfaceView and FloatingPanelLayout 2018-11-02 16:19:53 +09:00
Shin Yamamoto c0b9ddc4a3 Revert a sample code in README to prevent a confusion for v1.1.0 2018-11-02 12:19:49 +09:00
Shin Yamamoto 43f33083f1 Fix README 2018-11-02 12:17:44 +09:00
Shin Yamamoto 2b483e6adb Merge pull request #21 from SCENEE/fix-show-segue-problem
Fix show segue problem
2018-11-02 12:12:50 +09:00
Shin Yamamoto 0cf0f42ca4 Replace FloatingPanelController.show(_:sender:) with the set(contentViewController:)
And add a sample code to test show(_:sender:) in ContentVC.

`FloatingPanelController.show(_:sender:)` can block 'Show' segue in a
content view controller. 'Show' segue should not be handled by
'FloatingPanelController`. So I replace this method.
2018-11-02 10:33:12 +09:00
Shin Yamamoto c9ccea3f84 Merge pull request #27 from SCENEE/fix-backdrop-bugs
Fix backdrop bugs
2018-11-02 10:29:27 +09:00
Shin Yamamoto c2dee28132 Improve FloatingPanelSurfaceView.bottomOverflow handling 2018-11-02 10:07:49 +09:00
Shin Yamamoto 4fd4709182 Fix FloatingPanelSurfaceView.backgroundColor 2018-11-02 10:07:49 +09:00
Shin Yamamoto 00ccc0eb6a Fix backdrop alpha issue 2018-11-02 10:07:49 +09:00
Shin Yamamoto ed91f51482 Merge pull request #25 from SCENEE/add-removable-interaction
Add a removable interaction
2018-11-02 09:42:57 +09:00
Shin Yamamoto c2cea95aa5 Clean up FloatingPanelLayout consistance check 2018-11-02 09:17:58 +09:00
Shin Yamamoto 274027cb64 Allow one supported position 2018-11-02 09:17:58 +09:00
Shin Yamamoto b4a26344d9 Add a removable interaction 2018-11-02 09:17:58 +09:00
Shin Yamamoto 580c708788 Merge pull request #24 from SCENEE/add-preconditions-of-parent
Add and Update preconditions of the parent VC
2018-11-01 12:20:50 +09:00
Shin Yamamoto f4d6380094 Add and Update preconditions of the parent VC 2018-11-01 11:19:32 +09:00
Shin Yamamoto e44dc06a61 Merge pull request #18 from kingcos/master
Add missing constraints of the title
2018-10-29 22:09:59 +09:00
kingcos b6184f5b41 Add missing constraint of the title 2018-10-29 11:16:59 +08:00
Shin Yamamoto e6fc2f397e Merge pull request #17 from SCENEE/release-1.1.0
Release v1.1.0
2018-10-29 08:38:12 +09:00
18 changed files with 1634 additions and 373 deletions
+1
View File
@@ -2,6 +2,7 @@ language: swift
branches:
only:
- master
- next
cache:
directories:
- build
+6 -2
View File
@@ -26,8 +26,8 @@ class ViewController: UIViewController, MKMapViewDelegate, UISearchBarDelegate,
searchVC = storyboard?.instantiateViewController(withIdentifier: "SearchPanel") as? SearchPanelViewController
// Add a content view controller
fpc.show(searchVC, sender: self)
// Set a content view controller
fpc.set(contentViewController: searchVC)
fpc.track(scrollView: searchVC.tableView)
setupMapView()
@@ -234,6 +234,10 @@ public class SearchPanelLandscapeLayout: FloatingPanelLayout {
]
}
}
public func backdropAlphaFor(position: FloatingPanelPosition) -> CGFloat {
return 0.0
}
}
class SearchCell: UITableViewCell {
@@ -1,11 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="14313.18" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="RoN-h0-uBD">
<device id="retina4_7" orientation="portrait">
<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">
<device id="retina5_9" orientation="portrait">
<adaptation id="fullscreen"/>
</device>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14283.14"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14460.20"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
@@ -15,7 +15,7 @@
<objects>
<navigationController id="RoN-h0-uBD" sceneMemberID="viewController">
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" id="hNW-5m-Omi">
<rect key="frame" x="0.0" y="20" width="375" height="44"/>
<rect key="frame" x="0.0" y="44" width="375" height="44"/>
<autoresizingMask key="autoresizingMask"/>
</navigationBar>
<connections>
@@ -31,22 +31,22 @@
<objects>
<viewController id="jF4-A0-Eq6" customClass="SampleListViewController" customModule="Samples" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="Smh-Bd-AAc">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<rect key="frame" x="0.0" y="0.0" width="375" height="812"/>
<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="667"/>
<rect key="frame" x="0.0" y="0.0" width="375" height="778"/>
<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"/>
<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.5"/>
<rect key="frame" x="0.0" y="0.0" width="375" height="43.666666666666664"/>
<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.5"/>
<rect key="frame" x="15" y="0.0" width="345" height="43.666666666666664"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/>
@@ -81,17 +81,17 @@
<objects>
<viewController id="RpE-lI-27a" customClass="TabBarContentViewController" customModule="Samples" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="JER-jz-KSq">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<rect key="frame" x="0.0" y="0.0" width="375" height="812"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Item 2" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="AiP-dx-mFn">
<rect key="frame" x="163.5" y="323" width="48" height="21"/>
<rect key="frame" x="163.66666666666666" y="395.66666666666669" width="48" height="21"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="IvG-yp-yzI">
<rect key="frame" x="20" y="20" width="39" height="30"/>
<rect key="frame" x="20" y="44" width="39" height="30"/>
<state key="normal" title="Close"/>
<connections>
<action selector="closeWithSender:" destination="RpE-lI-27a" eventType="touchUpInside" id="hj3-Xv-6Gq"/>
@@ -119,17 +119,17 @@
<objects>
<viewController id="lto-Zc-Vtp" customClass="TabBarContentViewController" customModule="Samples" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="ji9-Ez-N7i">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<rect key="frame" x="0.0" y="0.0" width="375" height="812"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Item 1" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="uoW-c8-9wx">
<rect key="frame" x="164.5" y="323" width="46" height="21"/>
<rect key="frame" x="164.66666666666666" y="395.66666666666669" width="46" height="21"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="eFN-tN-4Ct">
<rect key="frame" x="20" y="20" width="39" height="30"/>
<rect key="frame" x="20" y="44" width="39" height="30"/>
<state key="normal" title="Close"/>
<connections>
<action selector="closeWithSender:" destination="bYI-y3-Rzb" eventType="touchUpInside" id="YL4-GP-ZEZ"/>
@@ -152,6 +152,35 @@
</objects>
<point key="canvasLocation" x="-380" y="1576"/>
</scene>
<!--Intrinsic View Controller-->
<scene sceneID="wtJ-qZ-aCl">
<objects>
<viewController storyboardIdentifier="IntrinsicViewController" title="Intrinsic View Controller" id="aK0-kv-mTu" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="eLM-xc-d9e">
<rect key="frame" x="0.0" y="0.0" width="375" height="812"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="1000" verticalHuggingPriority="1000" horizontalCompressionResistancePriority="1000" verticalCompressionResistancePriority="1000" text="Change this text" textAlignment="center" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="ge4-RW-Gmz">
<rect key="frame" x="24" y="68" width="327" height="20.333333333333329"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
</subviews>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstItem="vtu-Jb-oOn" firstAttribute="trailing" secondItem="ge4-RW-Gmz" secondAttribute="trailing" constant="24" id="V59-MD-Lcg"/>
<constraint firstItem="ge4-RW-Gmz" firstAttribute="leading" secondItem="eLM-xc-d9e" secondAttribute="leading" constant="24" id="hAO-P0-7Kw"/>
<constraint firstItem="ge4-RW-Gmz" firstAttribute="top" secondItem="vtu-Jb-oOn" secondAttribute="top" constant="24" id="j0s-fd-MYj"/>
<constraint firstAttribute="bottom" relation="greaterThanOrEqual" secondItem="ge4-RW-Gmz" secondAttribute="bottom" constant="24" id="tEn-PO-nVD"/>
</constraints>
<viewLayoutGuide key="safeArea" id="vtu-Jb-oOn"/>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="DfE-fL-zy5" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="-940" y="805"/>
</scene>
<!--Tab Bar View Controller-->
<scene sceneID="nQ5-PV-qFw">
<objects>
@@ -175,22 +204,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="667"/>
<rect key="frame" x="0.0" y="0.0" width="375" height="812"/>
<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="667" width="375" height="0.0"/>
<rect key="frame" x="0.0" y="778" width="375" height="34"/>
<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="20" width="39" height="30"/>
<rect key="frame" x="20" y="44" 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="145" y="108" width="85" height="178"/>
<rect key="frame" x="139.66666666666666" y="132" width="96" height="252"/>
<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"/>
@@ -213,6 +242,13 @@
<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="szf-HE-QTk">
<rect key="frame" x="0.0" y="222" width="96" height="30"/>
<state key="normal" title="Update layout"/>
<connections>
<action selector="updateLayout:" destination="bYI-y3-Rzb" eventType="touchUpInside" id="Woz-a7-YMJ"/>
</connections>
</button>
</subviews>
</stackView>
</subviews>
@@ -356,7 +392,7 @@
</connections>
</swipeGestureRecognizer>
</objects>
<point key="canvasLocation" x="1239" y="806"/>
<point key="canvasLocation" x="1311" y="806"/>
</scene>
<!--Detail View Controller-->
<scene sceneID="b6k-zi-3wn">
@@ -384,24 +420,47 @@
</constraints>
</view>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="Kva-Z7-0qY" customClass="OnSafeAreaView" customModule="Samples" customModuleProvider="target">
<rect key="frame" x="0.0" y="734" width="375" height="44"/>
<rect key="frame" x="0.0" y="700" width="375" height="44"/>
<color key="backgroundColor" red="0.0078431372550000003" green="0.72156862749999995" blue="0.45882352939999999" alpha="1" colorSpace="calibratedRGB"/>
<constraints>
<constraint firstAttribute="height" constant="44" id="DQJ-cY-cKx"/>
</constraints>
</view>
<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="82"/>
<subviews>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="c5r-jU-haj">
<rect key="frame" x="0.0" y="0.0" width="114" height="30"/>
<state key="normal" title="Show"/>
<connections>
<action selector="buttonPressed:" destination="YC8-ae-15L" eventType="touchUpInside" id="Mi1-o6-TWt"/>
</connections>
</button>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="wmd-ab-Nz3">
<rect key="frame" x="0.0" y="52" width="114" height="30"/>
<state key="normal" title="Present Modallly"/>
<connections>
<action selector="buttonPressed:" destination="YC8-ae-15L" eventType="touchUpInside" id="tjH-Ev-kpx"/>
<segue destination="bYI-y3-Rzb" kind="presentation" identifier="PresentModallySegue" id="3yq-HE-Tgn"/>
</connections>
</button>
</subviews>
</stackView>
</subviews>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<color key="backgroundColor" red="1" green="0.83234566450000003" blue="0.47320586440000001" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<gestureRecognizers/>
<constraints>
<constraint firstItem="noi-1a-5bZ" firstAttribute="top" secondItem="g7l-kO-y7q" secondAttribute="top" constant="12" id="EQy-cr-F2Y"/>
<constraint firstItem="tP3-oJ-4EB" firstAttribute="centerX" secondItem="g7l-kO-y7q" secondAttribute="centerX" id="EsD-Vf-dNZ"/>
<constraint firstItem="8yw-OC-Ubk" firstAttribute="bottom" secondItem="g7l-kO-y7q" secondAttribute="bottom" id="JOL-wC-w74"/>
<constraint firstItem="8yw-OC-Ubk" firstAttribute="leading" secondItem="tAi-nk-rDB" secondAttribute="leading" id="RiJ-Hb-OOZ"/>
<constraint firstItem="8yw-OC-Ubk" firstAttribute="trailing" secondItem="tAi-nk-rDB" secondAttribute="trailing" id="Sof-yL-mwK"/>
<constraint firstItem="tP3-oJ-4EB" firstAttribute="top" secondItem="tAi-nk-rDB" secondAttribute="top" constant="88" id="Zhb-Ss-epe"/>
<constraint firstItem="Kva-Z7-0qY" firstAttribute="trailing" secondItem="tAi-nk-rDB" secondAttribute="trailing" id="kkp-Yo-FQW"/>
<constraint firstItem="tAi-nk-rDB" firstAttribute="trailing" secondItem="noi-1a-5bZ" secondAttribute="trailing" constant="12" id="lv9-Nf-HNB"/>
<constraint firstItem="Kva-Z7-0qY" firstAttribute="leading" secondItem="tAi-nk-rDB" secondAttribute="leading" id="oVC-i1-TwS"/>
<constraint firstItem="tAi-nk-rDB" 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="8" symbolic="YES" id="vKQ-h9-uKt"/>
</constraints>
<viewLayoutGuide key="safeArea" id="tAi-nk-rDB"/>
<connections>
@@ -413,6 +472,7 @@
<size key="freeformSize" width="375" height="778"/>
<connections>
<outlet property="closeButton" destination="noi-1a-5bZ" id="eWQ-ha-8y7"/>
<segue destination="bYI-y3-Rzb" kind="show" identifier="ShowSegue" id="r1P-2i-NDe"/>
</connections>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="Wqk-xl-O3I" userLabel="First Responder" sceneMemberID="firstResponder"/>
@@ -432,7 +492,7 @@
</connections>
</pongPressGestureRecognizer>
</objects>
<point key="canvasLocation" x="1442" y="-23"/>
<point key="canvasLocation" x="1440.8" y="-23.388305847076463"/>
</scene>
<!--Debug Text View Controller-->
<scene sceneID="Bkq-O7-q4A">
@@ -507,4 +567,7 @@ Section 1.10.33 of "de Finibus Bonorum et Malorum", written by Cicero in 45 BC
<point key="canvasLocation" x="729" y="-23"/>
</scene>
</scenes>
<inferredMetricsTieBreakers>
<segue reference="3yq-HE-Tgn"/>
</inferredMetricsTieBreakers>
</document>
+288 -31
View File
@@ -9,7 +9,7 @@
import UIKit
import FloatingPanel
class SampleListViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
class SampleListViewController: UIViewController, UITableViewDataSource, UITableViewDelegate, FloatingPanelControllerDelegate, FloatingPanelLayout {
@IBOutlet weak var tableView: UITableView!
enum Menu: Int, CaseIterable {
@@ -17,17 +17,23 @@ class SampleListViewController: UIViewController, UITableViewDataSource, UITable
case trackingTextView
case showDetail
case showModal
case showFloatingPanelModal
case showTabBar
case showNestedScrollView
case showRemovablePanel
case showIntrinsicView
var name: String {
switch self {
case .trackingTableView: return "Scroll tracking (UITableView)"
case .trackingTextView: return "Scroll tracking (UITextView)"
case .trackingTableView: return "Scroll tracking(TableView)"
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 .showTabBar: return "Show Tab Bar"
case .showNestedScrollView: return "Show Nested ScrollView"
case .showRemovablePanel: return "Show Removable Panel"
case .showIntrinsicView: return "Show Intrinsic View"
}
}
@@ -37,14 +43,18 @@ class SampleListViewController: UIViewController, UITableViewDataSource, UITable
case .trackingTextView: return "ConsoleViewController"
case .showDetail: return "DetailViewController"
case .showModal: return "ModalViewController"
case .showFloatingPanelModal: return nil
case .showTabBar: return "TabBarViewController"
case .showNestedScrollView: return "NestedScrollViewController"
case .showRemovablePanel: return "DetailViewController"
case .showIntrinsicView: return "IntrinsicViewController"
}
}
}
var mainPanelVC: FloatingPanelController!
var detailPanelVC: FloatingPanelController!
var currentMenu: Menu = .trackingTableView
override func viewDidLoad() {
super.viewDidLoad()
@@ -52,21 +62,38 @@ class SampleListViewController: UIViewController, UITableViewDataSource, UITable
tableView.delegate = self
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "Cell")
let contentVC = DebugTableViewController(style: .plain)
let contentVC = DebugTableViewController()
addMainPanel(with: contentVC)
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
}
func addMainPanel(with contentVC: UIViewController) {
// Initialize FloatingPanelController
mainPanelVC = FloatingPanelController()
mainPanelVC.delegate = self
// Initialize FloatingPanelController and add the view
mainPanelVC.surfaceView.cornerRadius = 6.0
mainPanelVC.surfaceView.shadowHidden = false
// Add a content view controller and connect with the scroll view
mainPanelVC.show(contentVC, sender: self)
// Set a content view controller
mainPanelVC.set(contentViewController: contentVC)
// Enable tap-to-hide and removal interaction
switch currentMenu {
case .showRemovablePanel, .showIntrinsicView:
mainPanelVC.isRemovalInteractionEnabled = true
let backdropTapGesture = UITapGestureRecognizer(target: self, action: #selector(handleBackdrop(tapGesture:)))
mainPanelVC.backdropView.addGestureRecognizer(backdropTapGesture)
default:
break
}
// Track a scroll view
switch contentVC {
case let consoleVC as DebugTextViewController:
mainPanelVC.track(scrollView: consoleVC.textView)
@@ -78,6 +105,7 @@ class SampleListViewController: UIViewController, UITableViewDataSource, UITable
default:
break
}
// Add FloatingPanel to self.view
mainPanelVC.addPanel(toParent: self, belowView: nil, animated: true)
}
@@ -86,6 +114,10 @@ class SampleListViewController: UIViewController, UITableViewDataSource, UITable
detailPanelVC.removePanelFromParent(animated: true, completion: nil)
}
@objc func handleBackdrop(tapGesture: UITapGestureRecognizer) {
mainPanelVC.hide(animated: true, completion: nil)
}
// MARK:- TableViewDatasource
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
@@ -104,15 +136,17 @@ class SampleListViewController: UIViewController, UITableViewDataSource, UITable
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let menu = Menu.allCases[indexPath.row]
let contentVC: UIViewController = {
guard let storyboardID = menu.storyboardID else { return DebugTableViewController(style: .plain) }
guard let storyboardID = menu.storyboardID else { return DebugTableViewController() }
guard let vc = self.storyboard?.instantiateViewController(withIdentifier: storyboardID) else { fatalError() }
return vc
}()
self.currentMenu = menu
switch menu {
case .showDetail:
detailPanelVC?.removeFromParent()
detailPanelVC?.removePanelFromParent(animated: false)
// Initialize FloatingPanelController
detailPanelVC = FloatingPanelController()
@@ -120,16 +154,26 @@ class SampleListViewController: UIViewController, UITableViewDataSource, UITable
detailPanelVC.surfaceView.cornerRadius = 6.0
detailPanelVC.surfaceView.shadowHidden = false
// Add a content view controller and connect with the scroll view
detailPanelVC.show(contentVC, sender: self)
// (contentVC as? DetailViewController)?.closeButton?.addTarget(self, action: #selector(dismissDetailPanelVC), for: .touchUpInside)
// Set a content view controller
detailPanelVC.set(contentViewController: contentVC)
// Add FloatingPanel to self.view
detailPanelVC.addPanel(toParent: self, belowView: nil, animated: true)
case .showModal, .showTabBar:
let modalVC = contentVC
present(modalVC, animated: true, completion: nil)
case .showFloatingPanelModal:
let fpc = FloatingPanelController()
let contentVC = self.storyboard!.instantiateViewController(withIdentifier: "DetailViewController")
fpc.set(contentViewController: contentVC)
fpc.delegate = self
fpc.surfaceView.cornerRadius = 38.5
fpc.surfaceView.shadowHidden = false
fpc.isRemovalInteractionEnabled = true
self.present(fpc, animated: true, completion: nil)
default:
detailPanelVC?.removePanelFromParent(animated: true, completion: nil)
mainPanelVC?.removePanelFromParent(animated: true) {
@@ -137,6 +181,86 @@ class SampleListViewController: UIViewController, UITableViewDataSource, UITable
}
}
}
func floatingPanel(_ vc: FloatingPanelController, layoutFor newCollection: UITraitCollection) -> FloatingPanelLayout? {
switch currentMenu {
case .showRemovablePanel:
return newCollection.verticalSizeClass == .compact ? RemovablePanelLandscapeLayout() : RemovablePanelLayout()
case .showIntrinsicView:
return IntrinsicPanelLayout()
case .showFloatingPanelModal:
if vc != mainPanelVC && vc != detailPanelVC {
return ModalPanelLayout()
}
fallthrough
default:
return (newCollection.verticalSizeClass == .compact) ? nil : self
}
}
var initialPosition: FloatingPanelPosition {
return .half
}
func insetFor(position: FloatingPanelPosition) -> CGFloat? {
switch position {
case .full: return UIScreen.main.bounds.height == 667.0 ? 18.0 : 16.0
case .half: return 262.0
case .tip: return 69.0
case .hidden: return nil
}
}
}
class IntrinsicPanelLayout: FloatingPanelIntrinsicLayout { }
class RemovablePanelLayout: FloatingPanelIntrinsicLayout {
var supportedPositions: Set<FloatingPanelPosition> {
return [.full, .half]
}
var topInteractionBuffer: CGFloat {
return 200.0
}
var bottomInteractionBuffer: CGFloat {
return 261.0 - 22.0
}
func insetFor(position: FloatingPanelPosition) -> CGFloat? {
switch position {
case .half: return 130.0
default: return nil
}
}
func backdropAlphaFor(position: FloatingPanelPosition) -> CGFloat {
return 0.3
}
}
class RemovablePanelLandscapeLayout: FloatingPanelIntrinsicLayout {
var supportedPositions: Set<FloatingPanelPosition> {
return [.full, .half]
}
var bottomInteractionBuffer: CGFloat {
return 261.0 - 22.0
}
func insetFor(position: FloatingPanelPosition) -> CGFloat? {
switch position {
case .half: return 261.0
default: return nil
}
}
func backdropAlphaFor(position: FloatingPanelPosition) -> CGFloat {
return 0.3
}
}
class ModalPanelLayout: FloatingPanelIntrinsicLayout {
var topInteractionBuffer: CGFloat {
return 100.0
}
func backdropAlphaFor(position: FloatingPanelPosition) -> CGFloat {
return 0.3
}
}
class NestedScrollViewController: UIViewController {
@@ -173,22 +297,103 @@ class DebugTextViewController: UIViewController, UITextViewDelegate {
}
@IBAction func close(sender: UIButton) {
// Now impossible
// dismiss(animated: true, completion: nil)
(self.parent as? FloatingPanelController)?.removePanelFromParent(animated: true, completion: nil)
// (self.parent as? FloatingPanelController)?.removePanelFromParent(animated: true, completion: nil)
dismiss(animated: true, completion: nil)
}
}
class DebugTableViewController: UITableViewController {
class DebugTableViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
weak var tableView: UITableView!
var items: [String] = []
var itemHeight: CGFloat = 66.0
override func viewDidLoad() {
super.viewDidLoad()
let tableView = UITableView(frame: .zero,
style: .plain)
view.addSubview(tableView)
tableView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
tableView.topAnchor.constraint(equalTo: view.topAnchor),
tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
tableView.leftAnchor.constraint(equalTo: view.leftAnchor),
tableView.rightAnchor.constraint(equalTo: view.rightAnchor)
])
tableView.dataSource = self
tableView.delegate = self
self.tableView = tableView
let stackView = UIStackView()
view.addSubview(stackView)
stackView.axis = .vertical
stackView.distribution = .fillEqually
stackView.alignment = .trailing
stackView.spacing = 10.0
stackView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
stackView.topAnchor.constraint(equalTo: view.topAnchor, constant: 22.0),
stackView.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -22.0),
])
let button = UIButton()
button.setTitle("Animate Scroll", for: .normal)
button.setTitleColor(view.tintColor, for: .normal)
button.addTarget(self, action: #selector(animateScroll), for: .touchUpInside)
stackView.addArrangedSubview(button)
let button2 = UIButton()
button2.setTitle("Change content size", for: .normal)
button2.setTitleColor(view.tintColor, for: .normal)
button2.addTarget(self, action: #selector(changeContentSize), for: .touchUpInside)
stackView.addArrangedSubview(button2)
for i in 0...100 {
items.append("Items \(i)")
}
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "Cell")
}
@objc func animateScroll() {
tableView.scrollToRow(at: IndexPath(row: lround(Double(items.count) / 2.0),
section: 0),
at: .top, animated: true)
}
@objc func changeContentSize() {
let actionSheet = UIAlertController(title: "Change content size", message: "", preferredStyle: .actionSheet)
actionSheet.addAction(UIAlertAction(title: "Large", style: .default, handler: { (_) in
self.itemHeight = 66.0
self.changeItems(100)
}))
actionSheet.addAction(UIAlertAction(title: "Match", style: .default, handler: { (_) in
switch self.tableView.bounds.height {
case 585: // iPhone 6,7,8
self.itemHeight = self.tableView.bounds.height / 13.0
self.changeItems(13)
case 656: // iPhone {6,7,8} Plus
self.itemHeight = self.tableView.bounds.height / 16.0
self.changeItems(16)
default: // iPhone X family
self.itemHeight = self.tableView.bounds.height / 12.0
self.changeItems(12)
}
}))
actionSheet.addAction(UIAlertAction(title: "Short", style: .default, handler: { (_) in
self.itemHeight = 66.0
self.changeItems(3)
}))
self.present(actionSheet, animated: true, completion: nil)
}
func changeItems(_ count: Int) {
items.removeAll()
for i in 0..<count {
items.append("Items \(i)")
}
tableView.reloadData()
}
@objc func close(sender: UIButton) {
// Remove FloatingPanel from a view
(self.parent as! FloatingPanelController).removePanelFromParent(animated: true, completion: nil)
@@ -238,28 +443,48 @@ class DebugTableViewController: UITableViewController {
print("Content View: willTransition(to: \(newCollection), with: \(coordinator))")
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return items.count
}
override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return 66.0
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return itemHeight
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
cell.textLabel?.text = items[indexPath.row]
return cell
}
func tableView(_ tableView: UITableView, editActionsForRowAt indexPath: IndexPath) -> [UITableViewRowAction]? {
return [
UITableViewRowAction(style: .destructive, title: "Delete", handler: { (action, path) in
self.items.remove(at: path.row)
tableView.deleteRows(at: [path], with: .automatic)
}),
]
}
}
class DetailViewController: UIViewController {
@IBOutlet weak var closeButton: UIButton!
@IBAction func close(sender: UIButton) {
// Now impossible
// dismiss(animated: true, completion: nil)
(self.parent as? FloatingPanelController)?.removePanelFromParent(animated: true, completion: nil)
// (self.parent as? FloatingPanelController)?.removePanelFromParent(animated: true, completion: nil)
dismiss(animated: true, completion: nil)
}
@IBAction func buttonPressed(_ sender: UIButton) {
switch sender.titleLabel?.text {
case "Show":
performSegue(withIdentifier: "ShowSegue", sender: self)
case "Present Modally":
performSegue(withIdentifier: "PresentModallySegue", sender: self)
default:
break
}
}
@IBAction func tapped(_ sender: Any) {
print("Detail panel is tapped!")
}
@@ -271,25 +496,31 @@ class DetailViewController: UIViewController {
}
}
class ModalViewController: UIViewController {
class ModalViewController: UIViewController, FloatingPanelControllerDelegate {
var fpc: FloatingPanelController!
var consoleVC: DebugTextViewController!
@IBOutlet weak var safeAreaView: UIView!
var isNewlayout: Bool = false
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
// Initialize FloatingPanelController
fpc = FloatingPanelController()
fpc.delegate = self
// Initialize FloatingPanelController and add the view
fpc.surfaceView.cornerRadius = 6.0
fpc.surfaceView.shadowHidden = false
// Add a content view controller and connect with the scroll view
// Set a content view controller and track the scroll view
let consoleVC = storyboard?.instantiateViewController(withIdentifier: "ConsoleViewController") as! DebugTextViewController
fpc.show(consoleVC, sender: self)
self.consoleVC = consoleVC
fpc.set(contentViewController: consoleVC)
fpc.track(scrollView: consoleVC.textView)
self.consoleVC = consoleVC
// Add FloatingPanel to self.view
fpc.addPanel(toParent: self, belowView: safeAreaView)
}
@@ -313,6 +544,32 @@ class ModalViewController: UIViewController {
@IBAction func moveToTip(sender: UIButton) {
fpc.move(to: .tip, animated: true)
}
@IBAction func updateLayout(_ sender: Any) {
isNewlayout = !isNewlayout
UIView.animate(withDuration: 0.5) {
self.fpc.updateLayout()
}
}
func floatingPanel(_ vc: FloatingPanelController, layoutFor newCollection: UITraitCollection) -> FloatingPanelLayout? {
return (isNewlayout) ? ModalSecondLayout() : nil
}
}
class ModalSecondLayout: FloatingPanelLayout {
var initialPosition: FloatingPanelPosition {
return .half
}
func insetFor(position: FloatingPanelPosition) -> CGFloat? {
switch position {
case .full: return 18.0
case .half: return 262.0
case .tip: return 44.0
case .hidden: return nil
}
}
}
class TabBarViewController: UITabBarController {}
@@ -331,11 +588,11 @@ class TabBarContentViewController: UIViewController, FloatingPanelControllerDele
fpc.surfaceView.cornerRadius = 6.0
fpc.surfaceView.shadowHidden = false
// Add a content view controller and connect with the scroll view
// Set a content view controller and track the scroll view
let consoleVC = storyboard?.instantiateViewController(withIdentifier: "ConsoleViewController") as! DebugTextViewController
fpc.show(consoleVC, sender: self)
self.consoleVC = consoleVC
fpc.set(contentViewController: consoleVC)
fpc.track(scrollView: consoleVC.textView)
self.consoleVC = consoleVC
// Add FloatingPanel to self.view
fpc.addPanel(toParent: self)
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="14313.18" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="BYZ-38-t0r">
<device id="retina5_9" orientation="portrait">
<device id="retina4_7" orientation="portrait">
<adaptation id="fullscreen"/>
</device>
<dependencies>
@@ -15,11 +15,11 @@
<objects>
<viewController id="BYZ-38-t0r" customClass="ViewController" customModule="Stocks" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
<rect key="frame" x="0.0" y="0.0" width="375" height="812"/>
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<scrollView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="Uop-sw-I6p">
<rect key="frame" x="0.0" y="109" width="375" height="624.66666666666663"/>
<rect key="frame" x="0.0" y="85" width="375" height="537.5"/>
<subviews>
<imageView userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" placeholderIntrinsicWidth="375" placeholderIntrinsicHeight="625" image="stocks_list" translatesAutoresizingMaskIntoConstraints="NO" id="XJR-iK-fem">
<rect key="frame" x="0.0" y="0.0" width="375" height="625"/>
@@ -34,10 +34,10 @@
</constraints>
</scrollView>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="dFl-81-6ok">
<rect key="frame" x="0.0" y="733.66666666666663" width="375" height="78.333333333333371"/>
<rect key="frame" x="0.0" y="622.5" width="375" height="44.5"/>
<subviews>
<imageView userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="yahoo_bottom_bar" translatesAutoresizingMaskIntoConstraints="NO" id="NKr-gS-mpx">
<rect key="frame" x="0.0" y="0.0" width="375" height="44.333333333333336"/>
<rect key="frame" x="0.0" y="0.0" width="375" height="44.5"/>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstAttribute="height" relation="greaterThanOrEqual" constant="44.5" id="B5t-ZF-qUj"/>
@@ -52,18 +52,17 @@
<constraint firstItem="NKr-gS-mpx" firstAttribute="leading" secondItem="dFl-81-6ok" secondAttribute="leading" id="T2r-kY-JYy"/>
</constraints>
</view>
<stackView opaque="NO" contentMode="scaleToFill" fixedFrame="YES" axis="vertical" alignment="top" spacing="-8" translatesAutoresizingMaskIntoConstraints="NO" id="f7r-Al-pIN">
<rect key="frame" x="16" y="44.000000000000014" width="153.33333333333334" height="56.666666666666664"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" alignment="top" spacing="-8" translatesAutoresizingMaskIntoConstraints="NO" id="f7r-Al-pIN">
<rect key="frame" x="16" y="20" width="153.5" height="57"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="STOCKS" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="PCG-Wl-fXa">
<rect key="frame" x="0.0" y="0.0" width="111.66666666666667" height="32.333333333333336"/>
<rect key="frame" x="0.0" y="0.0" width="111.5" height="32.5"/>
<fontDescription key="fontDescription" type="system" weight="heavy" pointSize="27"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="OCTOBER 5" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="XmK-pu-9g4">
<rect key="frame" x="0.0" y="24.333333333333332" width="153.33333333333334" height="32.333333333333343"/>
<rect key="frame" x="0.0" y="24.5" width="153.5" height="32.5"/>
<fontDescription key="fontDescription" type="system" weight="heavy" pointSize="27"/>
<color key="textColor" red="0.55308091640472412" green="0.55657511949539185" blue="0.57255202531814575" alpha="1" colorSpace="custom" customColorSpace="displayP3"/>
<nil key="highlightedColor"/>
@@ -75,10 +74,12 @@
<constraints>
<constraint firstItem="6Tk-OE-BBY" firstAttribute="trailing" secondItem="dFl-81-6ok" secondAttribute="trailing" id="20i-yz-AaQ"/>
<constraint firstItem="Uop-sw-I6p" firstAttribute="leading" secondItem="6Tk-OE-BBY" secondAttribute="leading" id="44w-r8-vYl"/>
<constraint firstItem="f7r-Al-pIN" firstAttribute="leading" secondItem="6Tk-OE-BBY" secondAttribute="leading" constant="16" id="4Bq-Km-eET"/>
<constraint firstItem="Uop-sw-I6p" firstAttribute="top" secondItem="6Tk-OE-BBY" secondAttribute="top" constant="65" id="CXL-Dk-8MM"/>
<constraint firstItem="Uop-sw-I6p" firstAttribute="trailing" secondItem="6Tk-OE-BBY" secondAttribute="trailing" id="CsO-WF-T8L"/>
<constraint firstItem="dFl-81-6ok" firstAttribute="top" secondItem="Uop-sw-I6p" secondAttribute="bottom" id="Cz0-dW-r9H"/>
<constraint firstAttribute="bottom" secondItem="dFl-81-6ok" secondAttribute="bottom" id="KGl-8W-5ja"/>
<constraint firstItem="f7r-Al-pIN" firstAttribute="top" secondItem="6Tk-OE-BBY" secondAttribute="top" constant="1.4210854715202004e-14" id="Qvt-vQ-PpT"/>
<constraint firstItem="dFl-81-6ok" firstAttribute="leading" secondItem="6Tk-OE-BBY" secondAttribute="leading" id="nlX-Ab-1aI"/>
<constraint firstItem="6Tk-OE-BBY" firstAttribute="bottom" secondItem="NKr-gS-mpx" secondAttribute="bottom" id="yeu-NH-Pmp"/>
</constraints>
+6 -3
View File
@@ -34,8 +34,8 @@ class ViewController: UIViewController, FloatingPanelControllerDelegate {
newsVC = storyboard?.instantiateViewController(withIdentifier: "News") as? NewsViewController
// Add a content view controller
fpc.show(newsVC, sender: self)
// Set a content view controller
fpc.set(contentViewController: newsVC)
fpc.track(scrollView: newsVC.scrollView)
fpc.addPanel(toParent: self, belowView: bottomToolView, animated: false)
@@ -114,10 +114,13 @@ class FloatingPanelStocksLayout: FloatingPanelLayout {
case .full: return 56.0
case .half: return 262.0
case .tip: return 85.0 + 44.0 // Visible + ToolView
default: return nil
}
}
var backdropAlpha: CGFloat = 0.0
func backdropAlphaFor(position: FloatingPanelPosition) -> CGFloat {
return 0.0
}
}
// MARK: My custom behavior
+3 -3
View File
@@ -1,10 +1,10 @@
Pod::Spec.new do |s|
s.name = "FloatingPanel"
s.version = "1.1.0"
s.summary = "FloatingPanel is a simple and easy-to-use UI component of a floating panel interface"
s.version = "1.3.0"
s.summary = "FloatingPanel is a clean and easy-to-use UI component of a floating panel interface."
s.description = <<-DESC
FloatingPanel is a simple and easy-to-use UI component for a new interface introduced in Apple Maps, Shortcuts and Stocks app.
FloatingPanel is a clean 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.
DESC
s.homepage = "https://github.com/SCENEE/FloatingPanel"
@@ -7,6 +7,8 @@
objects = {
/* Begin PBXBuildFile section */
54352E9821A521CA00CBCA08 /* FloatingPanelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54352E9721A521CA00CBCA08 /* FloatingPanelView.swift */; };
54352E9621A51A2500CBCA08 /* FloatingPanelTransitioning.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54352E9521A51A2500CBCA08 /* FloatingPanelTransitioning.swift */; };
5450EEE421646DF500135936 /* FloatingPanelBehavior.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5450EEE321646DF500135936 /* FloatingPanelBehavior.swift */; };
545DB9CB2151169500CA77B8 /* FloatingPanel.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 545DB9C12151169500CA77B8 /* FloatingPanel.framework */; };
545DB9D02151169500CA77B8 /* ViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 545DB9CF2151169500CA77B8 /* ViewTests.swift */; };
@@ -32,6 +34,8 @@
/* End PBXContainerItemProxy section */
/* Begin PBXFileReference section */
54352E9721A521CA00CBCA08 /* FloatingPanelView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FloatingPanelView.swift; sourceTree = "<group>"; };
54352E9521A51A2500CBCA08 /* FloatingPanelTransitioning.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FloatingPanelTransitioning.swift; sourceTree = "<group>"; };
5450EEE321646DF500135936 /* FloatingPanelBehavior.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FloatingPanelBehavior.swift; sourceTree = "<group>"; };
545DB9C12151169500CA77B8 /* FloatingPanel.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = FloatingPanel.framework; sourceTree = BUILT_PRODUCTS_DIR; };
545DB9C42151169500CA77B8 /* FloatingPanelController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FloatingPanelController.h; sourceTree = "<group>"; };
@@ -92,9 +96,11 @@
545DB9C52151169500CA77B8 /* Info.plist */,
545DB9C42151169500CA77B8 /* FloatingPanelController.h */,
545DB9DF21511AC100CA77B8 /* FloatingPanelController.swift */,
54352E9521A51A2500CBCA08 /* FloatingPanelTransitioning.swift */,
54CFBFC4215CD09C006B5735 /* FloatingPanel.swift */,
54CFBFC2215CD045006B5735 /* FloatingPanelLayout.swift */,
5450EEE321646DF500135936 /* FloatingPanelBehavior.swift */,
54352E9721A521CA00CBCA08 /* FloatingPanelView.swift */,
54CDC5D2215B6D5A007D205C /* FloatingPanelSurfaceView.swift */,
54CDC5D4215B6D8D007D205C /* FloatingPanelBackdropView.swift */,
545DBA2A2152383100CA77B8 /* GrabberHandleView.swift */,
@@ -225,11 +231,13 @@
54CDC5D3215B6D5A007D205C /* FloatingPanelSurfaceView.swift in Sources */,
54CFBFC3215CD045006B5735 /* FloatingPanelLayout.swift in Sources */,
54CDC5D5215B6D8D007D205C /* FloatingPanelBackdropView.swift in Sources */,
54352E9821A521CA00CBCA08 /* FloatingPanelView.swift in Sources */,
54CFBFC5215CD09C006B5735 /* FloatingPanel.swift in Sources */,
54ABD7AF216CCFF7002E6C13 /* Logger.swift in Sources */,
545DB9E021511AC100CA77B8 /* FloatingPanelController.swift in Sources */,
5450EEE421646DF500135936 /* FloatingPanelBehavior.swift in Sources */,
545DBA2B2152383100CA77B8 /* GrabberHandleView.swift in Sources */,
54352E9621A51A2500CBCA08 /* FloatingPanelTransitioning.swift in Sources */,
545DB9DE215118C800CA77B8 /* UIExtensions.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
+413 -93
View File
@@ -33,12 +33,21 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
unowned let viewcontroller: FloatingPanelController
private(set) var state: FloatingPanelPosition = .tip
private(set) var state: FloatingPanelPosition = .hidden {
didSet { viewcontroller.delegate?.floatingPanelDidChangePosition(viewcontroller) }
}
private var isBottomState: Bool {
let remains = layoutAdapter.supportedPositions.filter { $0.rawValue > state.rawValue }
return remains.count == 0
}
let panGesture: FloatingPanelPanGestureRecognizer
var isRemovalInteractionEnabled: Bool = false
private var animator: UIViewPropertyAnimator?
private var initialFrame: CGRect = .zero
private var initialScrollOffset: CGPoint = .zero
private var transOffsetY: CGFloat = 0
private var interactionInProgress: Bool = false
@@ -51,7 +60,10 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
init(_ vc: FloatingPanelController, layout: FloatingPanelLayout, behavior: FloatingPanelBehavior) {
viewcontroller = vc
surfaceView = vc.view as! FloatingPanelSurfaceView
surfaceView = FloatingPanelSurfaceView()
surfaceView.backgroundColor = .white
backdropView = FloatingPanelBackdropView()
backdropView.backgroundColor = .black
backdropView.alpha = 0.0
@@ -74,29 +86,36 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
panGesture.delegate = self
}
func layoutViews(in vc: UIViewController) {
func setUpViews(in vc: UIViewController) {
unowned let view = vc.view!
view.insertSubview(backdropView, belowSubview: surfaceView)
backdropView.frame = view.bounds
// FloatingPanelSurfaceWrapperView is needed to update the surface's height
// without animation and prevent the backdrop's cut-off on orientation change.
let surfaceWrapperView = FloatingPanelSurfaceWrapperView()
surfaceWrapperView.frame = view.bounds
surfaceWrapperView.backgroundColor = .clear
layoutAdapter.prepareLayout(toParent: vc)
view.addSubview(surfaceWrapperView)
surfaceWrapperView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
surfaceWrapperView.topAnchor.constraint(equalTo: view.topAnchor, constant: 0.0),
surfaceWrapperView.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 0.0),
surfaceWrapperView.rightAnchor.constraint(equalTo: view.rightAnchor, constant: 0.0),
surfaceWrapperView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: 0.0),
])
surfaceWrapperView.addSubview(surfaceView)
view.insertSubview(backdropView, belowSubview: surfaceWrapperView)
backdropView.frame = view.bounds
}
func move(to: FloatingPanelPosition, animated: Bool, completion: (() -> Void)? = nil) {
move(from: state, to: to, animated: animated, completion: completion)
}
func present(animated: Bool, completion: (() -> Void)? = nil) {
self.layoutAdapter.activateLayout(of: nil)
move(from: nil, to: layoutAdapter.layout.initialPosition, animated: animated, completion: completion)
}
func dismiss(animated: Bool, completion: (() -> Void)? = nil) {
move(from: state, to: nil, animated: animated, completion: completion)
}
private func move(from: FloatingPanelPosition?, to: FloatingPanelPosition?, animated: Bool, completion: (() -> Void)? = nil) {
private func move(from: FloatingPanelPosition, to: FloatingPanelPosition, animated: Bool, completion: (() -> Void)? = nil) {
if to != .full {
lockScrollView()
}
@@ -104,23 +123,19 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
if animated {
let animator: UIViewPropertyAnimator
switch (from, to) {
case (nil, let to?):
case (.hidden, let to):
animator = behavior.addAnimator(self.viewcontroller, to: to)
case (let from?, let to?):
animator = behavior.moveAnimator(self.viewcontroller, from: from, to: to)
case (let from?, nil):
case (let from, .hidden):
animator = behavior.removeAnimator(self.viewcontroller, from: from)
case (nil, nil):
fatalError()
case (let from, let to):
animator = behavior.moveAnimator(self.viewcontroller, from: from, to: to)
}
animator.addAnimations { [weak self] in
guard let self = self else { return }
self.updateLayout(to: to)
if let to = to {
self.state = to
}
self.state = to
}
animator.addCompletion { _ in
completion?()
@@ -128,34 +143,33 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
animator.startAnimation()
} else {
self.updateLayout(to: to)
if let to = to {
self.state = to
}
self.state = to
completion?()
}
}
// MARK: - Layout update
private func updateLayout(to target: FloatingPanelPosition?) {
private func updateLayout(to target: FloatingPanelPosition) {
self.layoutAdapter.activateLayout(of: target)
self.setBackdropAlpha(of: target)
}
private func setBackdropAlpha(of target: FloatingPanelPosition?) {
switch target {
case .full?:
self.backdropView.alpha = layoutAdapter.layout.backdropAlpha
default:
self.backdropView.alpha = 0.0
}
}
private func getBackdropAlpha(with translation: CGPoint) -> CGFloat {
let topY = layoutAdapter.topY
let middleY = layoutAdapter.middleY
let currentY = getCurrentY(from: initialFrame, with: translation)
return (1 - (currentY - topY) / (middleY - topY)) * layoutAdapter.layout.backdropAlpha
let next = directionalPosition(with: translation)
let pre = redirectionalPosition(with: translation)
let nextY = layoutAdapter.positionY(for: next)
let preY = layoutAdapter.positionY(for: pre)
let nextAlpha = layoutAdapter.layout.backdropAlphaFor(position: next)
let preAlpha = layoutAdapter.layout.backdropAlphaFor(position: pre)
if preY == nextY {
return preAlpha
} else {
return preAlpha + max(min(1.0, 1.0 - (nextY - currentY) / (nextY - preY) ), 0.0) * (nextAlpha - preAlpha)
}
}
// MARK: - UIGestureRecognizerDelegate
@@ -164,77 +178,157 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
guard gestureRecognizer == panGesture else { return false }
log.debug("shouldRecognizeSimultaneouslyWith", otherGestureRecognizer)
/* log.debug("shouldRecognizeSimultaneouslyWith", otherGestureRecognizer) */
return otherGestureRecognizer == scrollView?.panGestureRecognizer
// all gestures of the tracking scroll view should be recognized in parallel
// and handle them in self.handle(panGesture:)
return scrollView?.gestureRecognizers?.contains(otherGestureRecognizer) ?? false
}
public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldBeRequiredToFailBy otherGestureRecognizer: UIGestureRecognizer) -> Bool {
guard gestureRecognizer == panGesture else { return false }
// Do not begin any gestures excluding the tracking scrollView's pan gesture until the pan gesture fails
if otherGestureRecognizer == scrollView?.panGestureRecognizer {
/* log.debug("shouldBeRequiredToFailBy", otherGestureRecognizer) */
// The tracking scroll view's gestures should begin without waiting for the pan gesture failure.
// `scrollView.gestureRecognizers` can contains the following gestures
// * UIScrollViewDelayedTouchesBeganGestureRecognizer
// * UIScrollViewPanGestureRecognizer (scrollView.panGestureRecognizer)
// * _UIDragAutoScrollGestureRecognizer
// * _UISwipeActionPanGestureRecognizer
// * UISwipeDismissalGestureRecognizer
if let scrollView = scrollView,
let scrollGestureRecognizers = scrollView.gestureRecognizers,
scrollGestureRecognizers.contains(otherGestureRecognizer) {
return false
} else {
return true
}
// Long press gesture should begin without waiting for the pan gesture failure.
if otherGestureRecognizer is UILongPressGestureRecognizer {
return false
}
// Do not begin any other gestures until the pan gesture fails.
return true
}
public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRequireFailureOf otherGestureRecognizer: UIGestureRecognizer) -> Bool {
guard gestureRecognizer == panGesture else { return false }
// Do not begin the pan gesture until any other gestures fail except fo the tracking scrollView's pan gesture.
log.debug("shouldRequireFailureOf", otherGestureRecognizer)
// Should begin the pan gesture without waiting for the tracking scroll view's gestures.
// `scrollView.gestureRecognizers` can contains the following gestures
// * UIScrollViewDelayedTouchesBeganGestureRecognizer
// * UIScrollViewPanGestureRecognizer (scrollView.panGestureRecognizer)
// * _UIDragAutoScrollGestureRecognizer
// * _UISwipeActionPanGestureRecognizer
// * UISwipeDismissalGestureRecognizer
if let scrollView = scrollView {
// On short contents scroll, `_UISwipeActionPanGestureRecognizer` blocks
// the panel's pan gesture if not returns false
if let scrollGestureRecognizers = scrollView.gestureRecognizers,
scrollGestureRecognizers.contains(otherGestureRecognizer) {
return false
}
}
switch otherGestureRecognizer {
case scrollView?.panGestureRecognizer:
return false
case is UIPanGestureRecognizer,
is UISwipeGestureRecognizer,
is UIRotationGestureRecognizer,
is UIScreenEdgePanGestureRecognizer,
is UIPinchGestureRecognizer:
// Do not begin the pan gesture until these gestures fail
return true
default:
// Should begin the pan gesture witout waiting tap/long press gestures fail
return false
}
}
// MARK: - Gesture handling
var grabberAreaFrame: CGRect {
let grabberAreaFrame = CGRect(x: surfaceView.bounds.origin.x,
y: surfaceView.bounds.origin.y,
width: surfaceView.bounds.width,
height: FloatingPanelSurfaceView.topGrabberBarHeight * 2)
return grabberAreaFrame
}
// MARK: - Gesture handling
private let offsetThreshold: CGFloat = 5.0 // Optimal value from testing
@objc func handle(panGesture: UIPanGestureRecognizer) {
log.debug("Gesture >>>>", panGesture)
let velocity = panGesture.velocity(in: panGesture.view)
switch panGesture {
case scrollView?.panGestureRecognizer:
guard let scrollView = scrollView else { return }
log.debug("SrollPanGesture ScrollView.contentOffset >>>", scrollView.contentOffset.y, scrollView.contentSize, scrollView.bounds.size)
// Prevent scoll slip by the top bounce when the scroll view's height is
// less than the content's height
if scrollView.isDecelerating == false, scrollView.contentSize.height > scrollView.bounds.height {
scrollView.bounces = (scrollView.contentOffset.y > offsetThreshold)
}
if surfaceView.frame.minY > layoutAdapter.topY {
scrollView.contentOffset.y = scrollView.contentOffsetZero.y
switch state {
case .full:
let point = panGesture.location(in: surfaceView)
if grabberAreaFrame.contains(point) {
// Preserve the current content offset in moving from full.
scrollView.contentOffset.y = initialScrollOffset.y
} else {
// Prevent over scrolling in moving from full.
scrollView.contentOffset.y = scrollView.contentOffsetZero.y
}
case .half, .tip:
guard scrollView.isDecelerating == false else {
// Don't fix the scroll offset in animating the panel to half and tip.
// It causes a buggy scrolling deceleration because `state` becomes
// a target position in animating the panel on the interaction from full.
return
}
// Fix the scroll offset in moving the panel from half and tip.
scrollView.contentOffset.y = initialScrollOffset.y
case .hidden:
fatalError("A floating panel hidden must not be used by a user")
}
// Always hide a scroll indicator at the non-top.
if interactionInProgress {
lockScrollView()
}
} else {
// Always show a scroll indicator at the top.
if interactionInProgress {
unlockScrollView()
}
}
case panGesture:
let translation = panGesture.translation(in: panGesture.view!.superview)
let velocity = panGesture.velocity(in: panGesture.view)
let location = panGesture.location(in: panGesture.view)
log.debug(panGesture.state, ">>>", "{ translation: \(translation), velocity: \(velocity) }")
log.debug(panGesture.state, ">>>", "translation: \(translation.y), velocity: \(velocity.y)")
if let scrollView = scrollView, scrollView.frame.contains(location) {
log.debug("ScrollView.contentOffset >>>", scrollView.contentOffset)
if state == .full {
if scrollView.contentOffset.y - scrollView.contentOffsetZero.y > 0 {
return
}
if scrollView.isDecelerating {
return
}
if interactionInProgress == false, velocity.y < 0 || velocity.y > 2500.0 {
return
}
}
if shouldScrollViewHandleTouch(scrollView, point: location, velocity: velocity) {
return
}
if let animator = self.animator {
animator.stopAnimation(true)
self.animator = nil
}
switch panGesture.state {
case .began:
panningBegan()
case .changed:
if interactionInProgress == false {
startInteraction(with: translation)
}
panningChange(with: translation)
case .ended, .cancelled, .failed:
panningEnd(with: translation, velocity: velocity)
@@ -246,18 +340,56 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
}
}
private func shouldScrollViewHandleTouch(_ scrollView: UIScrollView?, point: CGPoint, velocity: CGPoint) -> Bool {
// When no scrollView, nothing to handle.
guard let scrollView = scrollView else { return false }
// For _UISwipeActionPanGestureRecognizer
if let scrollGestureRecognizers = scrollView.gestureRecognizers {
for gesture in scrollGestureRecognizers {
guard gesture.state == .began || gesture.state == .changed
else { continue }
if gesture != scrollView.panGestureRecognizer {
return true
}
}
}
guard
state == .full, // When not .full, don't scroll.
interactionInProgress == false, // When interaction already in progress, don't scroll.
scrollView.frame.contains(point), // When point not in scrollView, don't scroll.
!grabberAreaFrame.contains(point) // When point within grabber area, don't scroll.
else {
return false
}
log.debug("ScrollView.contentOffset >>>", scrollView.contentOffset.y)
let offset = scrollView.contentOffset.y - scrollView.contentOffsetZero.y
if abs(offset) > offsetThreshold {
return true
}
if scrollView.isDecelerating {
return true
}
if velocity.y < 0 {
return true
}
return false
}
private func panningBegan() {
// A user interaction does not always start from Began state of the pan gesture
// because it can be recognized in scrolling a content in a content view controller.
// So I do nothing here.
log.debug("panningBegan \(initialFrame)")
// So do nothing here.
log.debug("panningBegan")
}
private func panningChange(with translation: CGPoint) {
log.debug("panningChange")
if interactionInProgress == false {
startInteraction(with: translation)
}
let currentY = getCurrentY(from: initialFrame, with: translation)
@@ -275,36 +407,111 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
initialFrame = surfaceView.frame
}
stopScrollDeceleration = (surfaceView.frame.minY > layoutAdapter.topY) // Projecting the dragging to the scroll dragging
stopScrollDeceleration = (surfaceView.frame.minY > layoutAdapter.topY) // Projecting the dragging to the scroll dragging or not
let targetPosition = self.targetPosition(with: translation, velocity: velocity)
let distance = self.distance(to: targetPosition, with: translation)
endInteraction(for: targetPosition)
if isRemovalInteractionEnabled, isBottomState {
let velocityVector = (distance != 0) ? CGVector(dx: 0,
dy: max(min(velocity.y/distance, behavior.removalVelocity), 0.0)) : .zero
if shouldStartRemovalAnimation(with: translation, velocityVector: velocityVector) {
viewcontroller.delegate?.floatingPanelDidEndDraggingToRemove(viewcontroller, withVelocity: velocity)
self.startRemovalAnimation(with: velocityVector) { [weak self] in
guard let self = self else { return }
self.viewcontroller.dismiss(animated: false)
self.viewcontroller.delegate?.floatingPanelDidEndRemove(self.viewcontroller)
}
return
}
}
viewcontroller.delegate?.floatingPanelDidEndDragging(viewcontroller, withVelocity: velocity, targetPosition: targetPosition)
viewcontroller.delegate?.floatingPanelWillBeginDecelerating(viewcontroller)
startAnimation(to: targetPosition, at: distance, with: velocity)
}
private func shouldStartRemovalAnimation(with translation: CGPoint, velocityVector: CGVector) -> Bool {
let posY = layoutAdapter.positionY(for: state)
let currentY = getCurrentY(from: initialFrame, with: translation)
let safeAreaBottomY = layoutAdapter.safeAreaBottomY
let vth = behavior.removalVelocity
let pth = max(min(behavior.removalProgress, 1.0), 0.0)
let num = (currentY - posY)
let den = (safeAreaBottomY - posY)
guard num >= 0, den != 0, (num / den >= pth || velocityVector.dy == vth)
else { return false }
return true
}
private func startRemovalAnimation(with velocityVector: CGVector, completion: (() -> Void)?) {
let animator = self.behavior.removalInteractionAnimator(self.viewcontroller, with: velocityVector)
animator.addAnimations { [weak self] in
self?.updateLayout(to: .hidden)
}
animator.addCompletion({ _ in
completion?()
})
animator.startAnimation()
}
private func startInteraction(with translation: CGPoint) {
/* Don't lock a scroll view to show a scroll indicator after hitting the top */
log.debug("startInteraction")
initialFrame = surfaceView.frame
if let scrollView = scrollView {
initialScrollOffset = scrollView.contentOffset
}
transOffsetY = translation.y
viewcontroller.delegate?.floatingPanelWillBeginDragging(viewcontroller)
lockScrollView()
if layoutAdapter.layout is FloatingPanelIntrinsicLayout {
viewcontroller.contentViewController?.view?.constraints.forEach({ (const) in
switch viewcontroller.contentViewController?.layoutGuide.bottomAnchor {
case const.firstAnchor:
(const.secondItem as? UIView)?.disableAutoLayout()
case const.secondAnchor:
(const.firstItem as? UIView)?.disableAutoLayout()
default:
break
}
})
}
interactionInProgress = true
}
private func endInteraction(for targetPosition: FloatingPanelPosition) {
log.debug("endInteraction for \(targetPosition)")
if targetPosition != .full {
lockScrollView(withBounce: true)
}
interactionInProgress = false
// Prevent to keep a scoll view indicator visible at the half/tip position
if targetPosition != .full {
lockScrollView()
}
if layoutAdapter.layout is FloatingPanelIntrinsicLayout {
viewcontroller.contentViewController?.view?.constraints.forEach({ (const) in
switch viewcontroller.contentViewController?.layoutGuide.bottomAnchor {
case const.firstAnchor:
(const.secondItem as? UIView)?.enableAutoLayout()
case const.secondAnchor:
(const.firstItem as? UIView)?.enableAutoLayout()
default:
break
}
})
}
}
private func getCurrentY(from rect: CGRect, with translation: CGPoint) -> CGFloat {
@@ -322,19 +529,20 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
return max(topY, min(bottomY, y))
}
}
return max(topY - topBuffer, min(bottomY + bottomBuffer, y))
let topMax = layoutAdapter.topMaxY
let bottomMax = layoutAdapter.bottomMaxY
return max(max(topY - topBuffer, topMax), min(min(bottomY + bottomBuffer, bottomMax), y))
}
private func startAnimation(to targetPosition: FloatingPanelPosition, at distance: CGFloat, with velocity: CGPoint) {
let targetY = layoutAdapter.positionY(for: targetPosition)
let velocityVector = (distance != 0) ? CGVector(dx: 0, dy: max(min(velocity.y/distance, 30.0), -30.0)) : .zero
let animator = behavior.interactionAnimator(self.viewcontroller, to: targetPosition, with: velocityVector)
animator.isInterruptible = false // To prevent a backdrop color's punk
animator.addAnimations { [weak self] in
guard let self = self else { return }
if self.state == targetPosition {
self.surfaceView.frame.origin.y = targetY
self.setBackdropAlpha(of: targetPosition)
self.layoutAdapter.setBackdropAlpha(of: targetPosition)
} else {
self.updateLayout(to: targetPosition)
}
@@ -358,8 +566,11 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
self.animator = nil
self.viewcontroller.delegate?.floatingPanelDidEndDecelerating(self.viewcontroller)
stopScrollDeceleration = false
// Don't unlock scroll view in animating view when presentation layer != model layer
unlockScrollView()
if targetPosition == .full {
unlockScrollView()
}
}
private func distance(to targetPosition: FloatingPanelPosition, with translation: CGPoint) -> CGFloat {
@@ -367,6 +578,7 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
let middleY = layoutAdapter.middleY
let bottomY = layoutAdapter.bottomY
let currentY = getCurrentY(from: initialFrame, with: translation)
switch targetPosition {
case .full:
return CGFloat(fabs(Double(currentY - topY)))
@@ -374,6 +586,72 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
return CGFloat(fabs(Double(currentY - middleY)))
case .tip:
return CGFloat(fabs(Double(currentY - bottomY)))
case .hidden:
fatalError("A floating panel hidden must not be used by a user")
}
}
private func directionalPosition(with translation: CGPoint) -> FloatingPanelPosition {
let currentY = getCurrentY(from: initialFrame, with: translation)
let supportedPositions = layoutAdapter.supportedPositions
if supportedPositions.count == 1 {
return state
}
switch supportedPositions {
case [.full, .half]: return translation.y >= 0 ? .half : .full
case [.half, .tip]: return translation.y >= 0 ? .tip : .half
case [.full, .tip]: return translation.y >= 0 ? .tip : .full
default:
let middleY = layoutAdapter.middleY
switch state {
case .full:
if translation.y <= 0 {
return .full
}
return currentY > middleY ? .tip : .half
case .half:
return translation.y >= 0 ? .tip : .full
case .tip:
if translation.y >= 0 {
return .tip
}
return currentY > middleY ? .half : .full
case .hidden:
fatalError("A floating panel hidden must not be used by a user")
}
}
}
private func redirectionalPosition(with translation: CGPoint) -> FloatingPanelPosition {
let currentY = getCurrentY(from: initialFrame, with: translation)
let supportedPositions = layoutAdapter.supportedPositions
if supportedPositions.count == 1 {
return state
}
switch supportedPositions {
case [.full, .half]: return translation.y >= 0 ? .full : .half
case [.half, .tip]: return translation.y >= 0 ? .half : .tip
case [.full, .tip]: return translation.y >= 0 ? .full : .tip
default:
let middleY = layoutAdapter.middleY
switch state {
case .full:
return currentY > middleY ? .half : .full
case .half:
return .half
case .tip:
return currentY > middleY ? .tip : .half
case .hidden:
fatalError("A floating panel hidden must not be used by a user")
}
}
}
@@ -386,9 +664,11 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
private func targetPosition(with translation: CGPoint, velocity: CGPoint) -> (FloatingPanelPosition) {
let currentY = getCurrentY(from: initialFrame, with: translation)
let supportedPositions: Set = layoutAdapter.layout.supportedPositions
let supportedPositions = layoutAdapter.supportedPositions
assert(supportedPositions.count > 1)
if supportedPositions.count == 1 {
return state
}
switch supportedPositions {
case [.full, .half]:
@@ -399,14 +679,46 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
return targetPosition(from: [.full, .tip], at: currentY, velocity: velocity)
default:
/*
[topY|full]---[th1]---[middleY|default]---[th2]---[bottomY|collapsed]
[topY|full]---[th1]---[middleY|half]---[th2]---[bottomY|tip]
*/
let topY = layoutAdapter.topY
let middleY = layoutAdapter.middleY
let bottomY = layoutAdapter.bottomY
let th1 = (topY + middleY) / 2
let th2 = (middleY + bottomY) / 2
let target: FloatingPanelPosition
let forwardYDirection: Bool
switch state {
case .full:
target = .half
forwardYDirection = true
case .half:
if (currentY < middleY) {
target = .full
forwardYDirection = false
} else {
target = .tip
forwardYDirection = true
}
case .tip:
target = .half
forwardYDirection = false
case .hidden:
fatalError("A floating panel hidden must not be used by a user")
}
let redirectionalProgress = max(min(behavior.redirectionalProgress(viewcontroller, from: state, to: target), 1.0), 0.0)
let th1: CGFloat
let th2: CGFloat
if forwardYDirection {
th1 = topY + (middleY - topY) * redirectionalProgress
th2 = middleY + (bottomY - middleY) * redirectionalProgress
} else {
th1 = middleY - (middleY - topY) * redirectionalProgress
th2 = bottomY - (bottomY - middleY) * redirectionalProgress
}
switch currentY {
case ..<th1:
@@ -446,7 +758,10 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
let topY = layoutAdapter.positionY(for: top)
let bottomY = layoutAdapter.positionY(for: bottom)
let th = (topY + bottomY) / 2
let target = top == state ? bottom : top
let redirectionalProgress = max(min(behavior.redirectionalProgress(viewcontroller, from: state, to: target), 1.0), 0.0)
let th = topY + (bottomY - topY) * redirectionalProgress
switch currentY {
case ..<th:
@@ -466,17 +781,15 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
// MARK: - ScrollView handling
func lockScrollView(withBounce bounce: Bool = false) {
private func lockScrollView() {
guard let scrollView = scrollView else { return }
scrollView.isDirectionalLockEnabled = true
if bounce {
scrollView.bounces = false
}
scrollView.bounces = false
scrollView.showsVerticalScrollIndicator = false
}
func unlockScrollView() {
private func unlockScrollView() {
guard let scrollView = scrollView else { return }
scrollView.isDirectionalLockEnabled = false
@@ -498,6 +811,13 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
}
}
func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) {
if state != .full {
initialScrollOffset = scrollView.contentOffset
}
userScrollViewDelegate?.scrollViewDidEndScrollingAnimation?(scrollView)
}
func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
if stopScrollDeceleration {
targetContentOffset.pointee = scrollView.contentOffset
+43 -3
View File
@@ -6,7 +6,12 @@
import UIKit
public protocol FloatingPanelBehavior {
/// Returns a UIViewPropertyAnimator object for interacting with a floating panel by a user pan gesture
/// Returns the progress to redirect to the previous position
///
/// The progress is represented by a floating-point value between 0.0 and 1.0, inclusive, where 1.0 indicates the floating panel is impossible to move to the next posiiton. The default value is 0.5. Values less than 0.0 and greater than 1.0 are pinned to those limits.
func redirectionalProgress(_ fpc: FloatingPanelController, from: FloatingPanelPosition, to: FloatingPanelPosition) -> CGFloat
/// Returns a UIViewPropertyAnimator object to project a floating panel to a position on finger up if the user dragged.
func interactionAnimator(_ fpc: FloatingPanelController, to targetPosition: FloatingPanelPosition, with velocity: CGVector) -> UIViewPropertyAnimator
/// Returns a UIViewPropertyAnimator object to add a floating panel to a position.
@@ -26,9 +31,28 @@ public protocol FloatingPanelBehavior {
/// Its animator instance will be used to animate the surface view in `FloatingPanelController.move(to:animated:completion:)`.
/// Default is an animator with ease-in-out curve and 0.25 sec duration.
func moveAnimator(_ fpc: FloatingPanelController, from: FloatingPanelPosition, to: FloatingPanelPosition) -> UIViewPropertyAnimator
/// Returns a y-axis velocity to invoke a removal interaction at the bottom position.
///
/// Default is 10.0. This method is called when FloatingPanelController.isRemovalInteractionEnabled is true.
var removalVelocity: CGFloat { get }
/// Returns the threshold of the transition to invoke a removal interaction at the bottom position.
///
/// The progress is represented by a floating-point value between 0.0 and 1.0, inclusive, where 1.0 indicates the floating panel is impossible to invoke the removal interaction. The default value is 0.5. Values less than 0.0 and greater than 1.0 are pinned to those limits. This method is called when FloatingPanelController.isRemovalInteractionEnabled is true.
var removalProgress: CGFloat { get }
/// Returns a UIViewPropertyAnimator object to remove a floating panel with a velocity interactively at the bottom position.
///
/// Default is a spring animator with 1.0 damping ratio. This method is called when FloatingPanelController.isRemovalInteractionEnabled is true.
func removalInteractionAnimator(_ fpc: FloatingPanelController, with velocity: CGVector) -> UIViewPropertyAnimator
}
public extension FloatingPanelBehavior {
func redirectionalProgress(_ fpc: FloatingPanelController, from: FloatingPanelPosition, to: FloatingPanelPosition) -> CGFloat {
return 0.5
}
func addAnimator(_ fpc: FloatingPanelController, to: FloatingPanelPosition) -> UIViewPropertyAnimator {
return UIViewPropertyAnimator(duration: 0.25, curve: .easeInOut)
}
@@ -40,15 +64,31 @@ public extension FloatingPanelBehavior {
func moveAnimator(_ fpc: FloatingPanelController, from: FloatingPanelPosition, to: FloatingPanelPosition) -> UIViewPropertyAnimator {
return UIViewPropertyAnimator(duration: 0.25, curve: .easeInOut)
}
var removalVelocity: CGFloat {
return 10.0
}
var removalProgress: CGFloat {
return 0.5
}
func removalInteractionAnimator(_ fpc: FloatingPanelController, with velocity: CGVector) -> UIViewPropertyAnimator {
log.debug("velocity", velocity)
let timing = UISpringTimingParameters(dampingRatio: 1.0,
frequencyResponse: 0.3,
initialVelocity: velocity)
return UIViewPropertyAnimator(duration: 0, timingParameters: timing)
}
}
class FloatingPanelDefaultBehavior: FloatingPanelBehavior {
func interactionAnimator(_ fpc: FloatingPanelController, to targetPosition: FloatingPanelPosition, with velocity: CGVector) -> UIViewPropertyAnimator {
let timing = timeingCurve(to: targetPosition, with: velocity)
let timing = timeingCurve(with: velocity)
return UIViewPropertyAnimator(duration: 0, timingParameters: timing)
}
private func timeingCurve(to: FloatingPanelPosition, with velocity: CGVector) -> UITimingCurveProvider {
private func timeingCurve(with velocity: CGVector) -> UITimingCurveProvider {
log.debug("velocity", velocity)
let damping = self.getDamping(with: velocity)
return UISpringTimingParameters(dampingRatio: damping,
+206 -50
View File
@@ -12,6 +12,8 @@ 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
func floatingPanelDidMove(_ vc: FloatingPanelController) // any offset changes
// called on start of dragging (may require some time and or distance to move)
@@ -20,6 +22,11 @@ public protocol FloatingPanelControllerDelegate: class {
func floatingPanelDidEndDragging(_ vc: FloatingPanelController, withVelocity velocity: CGPoint, targetPosition: FloatingPanelPosition)
func floatingPanelWillBeginDecelerating(_ vc: FloatingPanelController) // called on finger up as we are moving
func floatingPanelDidEndDecelerating(_ vc: FloatingPanelController) // called when scroll view grinds to a halt
// called on start of dragging to remove its views from a parent view controller
func floatingPanelDidEndDraggingToRemove(_ vc: FloatingPanelController, withVelocity velocity: CGPoint)
// called when its views are removed from a parent view controller
func floatingPanelDidEndRemove(_ vc: FloatingPanelController)
}
public extension FloatingPanelControllerDelegate {
@@ -29,24 +36,28 @@ public extension FloatingPanelControllerDelegate {
func floatingPanel(_ vc: FloatingPanelController, behaviorFor newCollection: UITraitCollection) -> FloatingPanelBehavior? {
return nil
}
func floatingPanelDidChangePosition(_ vc: FloatingPanelController) {}
func floatingPanelDidMove(_ vc: FloatingPanelController) {}
func floatingPanelWillBeginDragging(_ vc: FloatingPanelController) {}
func floatingPanelDidEndDragging(_ vc: FloatingPanelController, withVelocity velocity: CGPoint, targetPosition: FloatingPanelPosition) {}
func floatingPanelWillBeginDecelerating(_ vc: FloatingPanelController) {}
func floatingPanelDidEndDecelerating(_ vc: FloatingPanelController) {}
func floatingPanelDidEndDraggingToRemove(_ vc: FloatingPanelController, withVelocity velocity: CGPoint) {}
func floatingPanelDidEndRemove(_ vc: FloatingPanelController) {}
}
public enum FloatingPanelPosition: Int, CaseIterable {
case full
case half
case tip
case hidden
}
///
/// A container view controller to display a floating panel to present contents in parallel as a user wants.
///
public class FloatingPanelController: UIViewController, UIScrollViewDelegate, UIGestureRecognizerDelegate {
/// Constants indicating how safe area insets are added to the adjusted content inset.
public enum ContentInsetAdjustmentBehavior: Int {
case always
@@ -58,7 +69,7 @@ public class FloatingPanelController: UIViewController, UIScrollViewDelegate, UI
/// Returns the surface view managed by the controller object. It's the same as `self.view`.
public var surfaceView: FloatingPanelSurfaceView! {
return view as? FloatingPanelSurfaceView
return floatingPanel.surfaceView
}
/// Returns the backdrop view managed by the controller object.
@@ -81,7 +92,17 @@ public class FloatingPanelController: UIViewController, UIScrollViewDelegate, UI
return floatingPanel.state
}
/// The content insets of the tracking scroll view derived from the safe area of the parent view
/// The layout object managed by the controller
public var layout: FloatingPanelLayout {
return floatingPanel.layoutAdapter.layout
}
/// The behavior object managed by the controller
public var behavior: FloatingPanelBehavior {
return floatingPanel.behavior
}
/// The content insets of the tracking scroll view derived from this safe area
public var adjustedContentInsets: UIEdgeInsets {
return floatingPanel.layoutAdapter.adjustedContentInsets
}
@@ -91,69 +112,114 @@ public class FloatingPanelController: UIViewController, UIScrollViewDelegate, UI
/// This property specifies how the content area of the tracking scroll view is modified using `adjustedContentInsets`. The default value of this property is FloatingPanelController.ContentInsetAdjustmentBehavior.always.
public var contentInsetAdjustmentBehavior: ContentInsetAdjustmentBehavior = .always
/// A Boolean value that determines whether the removal interaction is enabled.
public var isRemovalInteractionEnabled: Bool {
set { floatingPanel.isRemovalInteractionEnabled = newValue }
get { return floatingPanel.isRemovalInteractionEnabled }
}
/// The view controller responsible for the content portion of the floating panel.
public var contentViewController: UIViewController? {
set { set(contentViewController: newValue) }
get { return _contentViewController }
}
private var _contentViewController: UIViewController?
private var floatingPanel: FloatingPanel!
private var safeAreaInsetsObservation: NSKeyValueObservation?
private let modalTransition = FloatingPanelModalTransition()
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
floatingPanel = FloatingPanel(self,
layout: fetchLayout(for: self.traitCollection),
behavior: fetchBehavior(for: self.traitCollection))
setUp()
}
/// Initialize a newly created floating panel controller.
public init() {
super.init(nibName: nil, bundle: nil)
setUp()
}
private func setUp() {
_ = FloatingPanelController.dismissSwizzling
modalPresentationStyle = .custom
transitioningDelegate = modalTransition
floatingPanel = FloatingPanel(self,
layout: fetchLayout(for: self.traitCollection),
behavior: fetchBehavior(for: self.traitCollection))
}
// MARK:- Overrides
/// Creates the view that the controller manages.
override public func loadView() {
assert(self.storyboard == nil, "Storyboard isn't supported")
let view = FloatingPanelSurfaceView()
view.backgroundColor = .white
let view = FloatingPanelPassThroughView()
view.backgroundColor = .clear
self.view = view as UIView
}
public override func viewDidLoad() {
super.viewDidLoad()
floatingPanel.setUpViews(in: self)
}
public override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
view.frame.size = size
view.layoutIfNeeded()
floatingPanel.layoutAdapter.checkLayoutConsistance()
}
public override func willTransition(to newCollection: UITraitCollection, with coordinator: UIViewControllerTransitionCoordinator) {
super.willTransition(to: newCollection, with: coordinator)
// Change layout for a new trait collection
floatingPanel.layoutAdapter.layout = fetchLayout(for: newCollection)
updateLayout(for: newCollection)
floatingPanel.behavior = fetchBehavior(for: newCollection)
guard let parent = parent else { fatalError() }
floatingPanel.layoutAdapter.prepareLayout(toParent: parent)
floatingPanel.layoutAdapter.activateLayout(of: floatingPanel.state)
}
public override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
super.traitCollectionDidChange(previousTraitCollection)
guard previousTraitCollection != traitCollection else { return }
if let parent = parent {
self.update(safeAreaInsets: parent.layoutInsets)
}
floatingPanel.backdropView.isHidden = (traitCollection.verticalSizeClass == .compact)
self.update(safeAreaInsets: layoutInsets)
}
public override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
// Need to update safeAreaInsets here to ensure that the `adjustedContentInsets` has a correct value.
// Because the parent VC does not call viewSafeAreaInsetsDidChange() expectedly and
// `view.safeAreaInsets` has a correct value of the bottom inset here.
if let parent = parent {
self.update(safeAreaInsets: parent.layoutInsets)
// Must track safeAreaInsets/{top,bottom}LayoutGuide of the `self.view`
// to update floatingPanel.safeAreaInsets`. There are 2 reasons.
// 1. This or the parent VC doesn't call viewSafeAreaInsetsDidChange() on the bottom
// inset's update expectedly.
// 2. The safe area top inset can be variable on the large title navigation bar(iOS11+).
// That's why it needs the observation to keep `adjustedContentInsets` correct.
if #available(iOS 11.0, *) {
safeAreaInsetsObservation = self.observe(\.view.safeAreaInsets) { [weak self] (vc, chaneg) in
guard let self = self else { return }
self.update(safeAreaInsets: vc.layoutInsets)
}
} else {
// KVOs for topLayoutGuide & bottomLayoutGuide are not effective.
// Instead, safeAreaInsets is updated here
self.update(safeAreaInsets: layoutInsets)
}
}
public override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
safeAreaInsetsObservation = nil
}
// MARK:- Privates
private func fetchLayout(for traitCollection: UITraitCollection) -> FloatingPanelLayout {
switch traitCollection.verticalSizeClass {
case .compact:
@@ -168,7 +234,13 @@ public class FloatingPanelController: UIViewController, UIScrollViewDelegate, UI
}
private func update(safeAreaInsets: UIEdgeInsets) {
// preserve the current content offset
let contentOffset = scrollView?.contentOffset
floatingPanel.safeAreaInsets = safeAreaInsets
scrollView?.contentOffset = contentOffset ?? .zero
switch contentInsetAdjustmentBehavior {
case .always:
scrollView?.contentInset = adjustedContentInsets
@@ -178,8 +250,30 @@ public class FloatingPanelController: UIViewController, UIScrollViewDelegate, UI
}
}
private func updateLayout(for traitCollection: UITraitCollection) {
floatingPanel.layoutAdapter.layout = fetchLayout(for: traitCollection)
floatingPanel.layoutAdapter.prepareLayout(in: self)
floatingPanel.layoutAdapter.activateLayout(of: floatingPanel.state)
}
// MARK: - Container view controller interface
/// Shows the surface view at the initial position defined by the current layout
public func show(animated: Bool = false, completion: (() -> Void)? = nil) {
// Must apply the current layout here
updateLayout(for: traitCollection)
move(to: floatingPanel.layoutAdapter.layout.initialPosition,
animated: animated,
completion: completion)
}
/// Hides the surface view to the hidden position
public func hide(animated: Bool = false, completion: (() -> Void)? = nil) {
move(to: .hidden,
animated: animated,
completion: completion)
}
/// Adds the view managed by the controller as a child of the specified view controller.
/// - Parameters:
/// - parent: A parent view controller object that displays FloatingPanelController's view. A container view controller object isn't applicable.
@@ -191,22 +285,22 @@ public class FloatingPanelController: UIViewController, UIScrollViewDelegate, UI
return
}
precondition((parent is UINavigationController) == false, "UINavigationController displays only one child view controller at a time.")
precondition((parent is UITableViewController) == false, "UITableViewController should not be the parent because the view hierarchy will be break in reusing cells.")
precondition((parent is UICollectionViewController) == false, "UICollectionViewController should not be the parent because the view hierarchy will be break in reusing cells.")
precondition((parent is UITabBarController) == false, "UITabBarController displays child view controllers with a radio-style selection interface")
precondition((parent is UISplitViewController) == false, "UISplitViewController manages two child view controllers in a master-detail interface")
precondition((parent is UITableViewController) == false, "UITableViewController should not be the parent because the view is a table view so that a floating panel doens't work well")
precondition((parent is UICollectionViewController) == false, "UICollectionViewController should not be the parent because the view is a collection view so that a floating panel doens't work well")
view.frame = parent.view.bounds
if let belowView = belowView {
parent.view.insertSubview(self.view, belowSubview: belowView)
} else {
parent.view.addSubview(self.view)
}
view.frame = parent.view.bounds // MUST
parent.addChild(self)
// Must set a layout again here because `self.traitCollection` is applied correctly once it's added to a parent VC
floatingPanel.layoutAdapter.layout = fetchLayout(for: traitCollection)
floatingPanel.layoutViews(in: parent)
floatingPanel.present(animated: animated) { [weak self] in
show(animated: true) { [weak self] in
guard let self = self else { return }
self.didMove(toParent: parent)
}
@@ -222,9 +316,8 @@ public class FloatingPanelController: UIViewController, UIScrollViewDelegate, UI
return
}
floatingPanel.dismiss(animated: animated) { [weak self] in
hide(animated: animated) { [weak self] in
guard let self = self else { return }
self.willMove(toParent: nil)
self.view.removeFromSuperview()
self.removeFromParent()
@@ -238,23 +331,40 @@ public class FloatingPanelController: UIViewController, UIScrollViewDelegate, UI
/// - animated: Pass true to animate the presentation; otherwise, pass false.
/// - completion: The block to execute after the view controller has finished moving. This block has no return value and takes no parameters. You may specify nil for this parameter.
public func move(to: FloatingPanelPosition, animated: Bool, completion: (() -> Void)? = nil) {
precondition(floatingPanel.layoutAdapter.vc != nil, "Use show(animated:completion)")
floatingPanel.move(to: to, animated: animated, completion: completion)
}
/// Presents the specified view controller as the content view controller in the surface view interface.
/// Sets the view controller responsible for the content portion of the floating panel..
public func set(contentViewController: UIViewController?) {
if let vc = _contentViewController {
vc.willMove(toParent: nil)
vc.view.removeFromSuperview()
vc.removeFromParent()
}
if let vc = contentViewController {
let surfaceView = floatingPanel.surfaceView
surfaceView.add(childView: vc.view)
addChild(vc)
vc.didMove(toParent: self)
}
_contentViewController = contentViewController
}
@available(*, unavailable, renamed: "set(contentViewController:)")
public override func show(_ vc: UIViewController, sender: Any?) {
let surfaceView = self.view as! FloatingPanelSurfaceView
surfaceView.contentView.addSubview(vc.view)
vc.view.frame = surfaceView.contentView.bounds
vc.view.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
vc.view.topAnchor.constraint(equalTo: surfaceView.contentView.topAnchor, constant: 0.0),
vc.view.leftAnchor.constraint(equalTo: surfaceView.contentView.leftAnchor, constant: 0.0),
vc.view.rightAnchor.constraint(equalTo: surfaceView.contentView.rightAnchor, constant: 0.0),
vc.view.bottomAnchor.constraint(equalTo: surfaceView.contentView.bottomAnchor, constant: 0.0),
])
addChild(vc)
vc.didMove(toParent: self)
if let target = self.parent?.targetViewController(forAction: #selector(UIViewController.show(_:sender:)), sender: sender) {
target.show(vc, sender: sender)
}
}
@available(*, unavailable, renamed: "set(contentViewController:)")
public override func showDetailViewController(_ vc: UIViewController, sender: Any?) {
if let target = self.parent?.targetViewController(forAction: #selector(UIViewController.showDetailViewController(_:sender:)), sender: sender) {
target.showDetailViewController(vc, sender: sender)
}
}
// MARK: - Scroll view tracking
@@ -266,8 +376,10 @@ public class FloatingPanelController: UIViewController, UIScrollViewDelegate, UI
///
public func track(scrollView: UIScrollView) {
floatingPanel.scrollView = scrollView
floatingPanel.userScrollViewDelegate = scrollView.delegate
scrollView.delegate = floatingPanel
if scrollView.delegate !== floatingPanel {
floatingPanel.userScrollViewDelegate = scrollView.delegate
scrollView.delegate = floatingPanel
}
switch contentInsetAdjustmentBehavior {
case .always:
if #available(iOS 11.0, *) {
@@ -282,7 +394,19 @@ public class FloatingPanelController: UIViewController, UIScrollViewDelegate, UI
}
}
// MARK: - Helpers
// MARK: - Utilities
/// Updates the layout object from the delegate and lays out the views managed
/// by the controller immediately.
///
/// This method updates the `FloatingPanelLayout` object from the delegate and
/// then it calls `layoutIfNeeded()` of the root view to force the view
/// to update the floating panel's layout immediately. It can be called in an
/// animation block.
public func updateLayout() {
updateLayout(for: view.traitCollection)
floatingPanel.layoutAdapter.checkLayoutConsistance()
}
/// Returns the y-coordinate of the point at the origin of the surface view
public func originYOfSurface(for pos: FloatingPanelPosition) -> CGFloat {
@@ -293,6 +417,38 @@ public class FloatingPanelController: UIViewController, UIScrollViewDelegate, UI
return floatingPanel.layoutAdapter.middleY
case .tip:
return floatingPanel.layoutAdapter.bottomY
case .hidden:
return floatingPanel.layoutAdapter.hiddenY
}
}
}
extension FloatingPanelController {
private static let dismissSwizzling: Any? = {
let aClass: AnyClass! = UIViewController.self //object_getClass(vc)
if let imp = class_getMethodImplementation(aClass, #selector(dismiss(animated:completion:))),
let originalAltMethod = class_getInstanceMethod(aClass, #selector(fp_original_dismiss(animated:completion:))) {
method_setImplementation(originalAltMethod, imp)
}
let originalMethod = class_getInstanceMethod(aClass, #selector(dismiss(animated:completion:)))
let swizzledMethod = class_getInstanceMethod(aClass, #selector(fp_dismiss(animated:completion:)))
if let originalMethod = originalMethod, let swizzledMethod = swizzledMethod {
// switch implementation..
method_exchangeImplementations(originalMethod, swizzledMethod)
}
return nil
}()
}
public extension UIViewController {
@objc public func fp_original_dismiss(animated flag: Bool, completion: (() -> Void)? = nil) {
// Implementation will be replaced by IMP of self.dismiss(animated:completion:)
}
@objc public func fp_dismiss(animated flag: Bool, completion: (() -> Void)? = nil) {
if let fpc = parent as? FloatingPanelController, fpc.parent != nil {
fpc.removePanelFromParent(animated: flag, completion: completion)
} else {
self.fp_original_dismiss(animated: flag, completion: completion)
}
}
}
+180 -73
View File
@@ -5,11 +5,37 @@
import UIKit
/// FloatingPanelIntrinsicLayout
///
/// - Attention:
/// `insetFor(position:)` must return `nil` for full position because the inset is determined automatically.
/// You can customize insets only for half, tip and hidden positions
/// on FloatingPanelIntrinsicLayout.
public protocol FloatingPanelIntrinsicLayout: FloatingPanelLayout { }
public extension FloatingPanelIntrinsicLayout {
var initialPosition: FloatingPanelPosition {
return .full
}
var supportedPositions: Set<FloatingPanelPosition> {
return [.full]
}
func insetFor(position: FloatingPanelPosition) -> CGFloat? {
return nil
}
}
public protocol FloatingPanelLayout: class {
/// Returns the initial position of a floating panel.
var initialPosition: FloatingPanelPosition { get }
/// Returns a set of FloatingPanelPosition objects to tell the applicable positions of the floating panel controller. Default is all of them.
/// Returns a set of FloatingPanelPosition objects to tell the applicable
/// positions of the floating panel controller.
///
/// By default, it returns all position exepct for `hidden` position. Because
/// it's always supported by `FloatingPanelController` so you don't need to return it.
var supportedPositions: Set<FloatingPanelPosition> { get }
/// Return the interaction buffer to the top from the top position. Default is 6.0.
@@ -18,10 +44,13 @@ public protocol FloatingPanelLayout: class {
/// Return the interaction buffer to the bottom from the bottom position. Default is 6.0.
var bottomInteractionBuffer: CGFloat { get }
/// Returns a CGFloat value to determine a floating panel height for each position(full, half and tip).
/// A value for full position indicates a top inset from a safe area.
/// On the other hand, values for half and tip positions indicate bottom insets from a safe area.
/// If a position doesn't contain the supported positions, return nil.
/// Returns a CGFloat value to determine a Y coordinate of a floating panel for each position(full, half, tip and hidden).
///
/// Its returning value indicates a different inset for each positiion.
/// For full position, a top inset from a safe area in `FloatingPanelController.view`.
/// For half or tip position, a bottom inset from the safe area.
/// For hidden position, a bottom inset from `FloatingPanelController.view`.
/// If a position isn't supported or the default value is used, return nil.
func insetFor(position: FloatingPanelPosition) -> CGFloat?
/// Returns X-axis and width layout constraints of the surface view of a floating panel.
@@ -30,17 +59,18 @@ public protocol FloatingPanelLayout: class {
/// By default, the width of a surface view fits a safe area.
func prepareLayout(surfaceView: UIView, in view: UIView) -> [NSLayoutConstraint]
/// Return the backdrop alpha of black color in full position. Default is 0.3.
var backdropAlpha: CGFloat { get }
/// Returns a CGFloat value to determine the backdrop view's alpha for a position.
///
/// Default is 0.3 at full position, otherwise 0.0.
func backdropAlphaFor(position: FloatingPanelPosition) -> CGFloat
}
public extension FloatingPanelLayout {
var backdropAlpha: CGFloat { return 0.3 }
var topInteractionBuffer: CGFloat { return 6.0 }
var bottomInteractionBuffer: CGFloat { return 6.0 }
public var supportedPositions: Set<FloatingPanelPosition> {
return Set(FloatingPanelPosition.allCases)
var supportedPositions: Set<FloatingPanelPosition> {
return Set([.full, .half, .tip])
}
func prepareLayout(surfaceView: UIView, in view: UIView) -> [NSLayoutConstraint] {
@@ -49,9 +79,15 @@ public extension FloatingPanelLayout {
surfaceView.rightAnchor.constraint(equalTo: view.sideLayoutGuide.rightAnchor, constant: 0.0),
]
}
func backdropAlphaFor(position: FloatingPanelPosition) -> CGFloat {
return position == .full ? 0.3 : 0.0
}
}
public class FloatingPanelDefaultLayout: FloatingPanelLayout {
public var contentViewController: UIViewController?
public var initialPosition: FloatingPanelPosition {
return .half
}
@@ -61,11 +97,14 @@ public class FloatingPanelDefaultLayout: FloatingPanelLayout {
case .full: return 18.0
case .half: return 262.0
case .tip: return 69.0
case .hidden: return nil
}
}
}
public class FloatingPanelDefaultLandscapeLayout: FloatingPanelLayout {
public var contentViewController: UIViewController?
public var initialPosition: FloatingPanelPosition {
return .tip
}
@@ -80,27 +119,21 @@ public class FloatingPanelDefaultLandscapeLayout: FloatingPanelLayout {
default: return nil
}
}
public func prepareLayout(surfaceView: UIView, in view: UIView) -> [NSLayoutConstraint] {
return [
surfaceView.leftAnchor.constraint(equalTo: view.sideLayoutGuide.leftAnchor, constant: 0.0),
surfaceView.rightAnchor.constraint(equalTo: view.sideLayoutGuide.rightAnchor, constant: 0.0),
]
}
}
class FloatingPanelLayoutAdapter {
weak var vc: UIViewController!
private weak var surfaceView: FloatingPanelSurfaceView!
private weak var backdropVIew: FloatingPanelBackdropView!
private weak var backdropView: FloatingPanelBackdropView!
var layout: FloatingPanelLayout {
didSet { checkConsistance(of: layout) }
}
var layout: FloatingPanelLayout
var safeAreaInsets: UIEdgeInsets = .zero {
didSet {
updateHeight()
if oldValue != safeAreaInsets {
updateHeight()
}
}
}
@@ -110,10 +143,14 @@ class FloatingPanelLayoutAdapter {
private var halfConstraints: [NSLayoutConstraint] = []
private var tipConstraints: [NSLayoutConstraint] = []
private var offConstraints: [NSLayoutConstraint] = []
private var heightConstraints: NSLayoutConstraint? = nil
private var heightConstraints: [NSLayoutConstraint] = []
private var fullInset: CGFloat {
return layout.insetFor(position: .full) ?? 0.0
if layout is FloatingPanelIntrinsicLayout {
return intrinsicHeight
} else {
return layout.insetFor(position: .full) ?? 0.0
}
}
private var halfInset: CGFloat {
return layout.insetFor(position: .half) ?? 0.0
@@ -121,10 +158,23 @@ class FloatingPanelLayoutAdapter {
private var tipInset: CGFloat {
return layout.insetFor(position: .tip) ?? 0.0
}
private var hiddenInset: CGFloat {
return layout.insetFor(position: .hidden) ?? 0.0
}
var supportedPositions: Set<FloatingPanelPosition> {
var supportedPositions = layout.supportedPositions
supportedPositions.remove(.hidden)
return supportedPositions
}
var topY: CGFloat {
if layout.supportedPositions.contains(.full) {
return (safeAreaInsets.top + fullInset)
if supportedPositions.contains(.full) {
if layout is FloatingPanelIntrinsicLayout {
return surfaceView.superview!.bounds.height - (safeAreaInsets.bottom + fullInset)
} else {
return (safeAreaInsets.top + fullInset)
}
} else {
return middleY
}
@@ -135,13 +185,24 @@ class FloatingPanelLayoutAdapter {
}
var bottomY: CGFloat {
if layout.supportedPositions.contains(.tip) {
if supportedPositions.contains(.tip) {
return surfaceView.superview!.bounds.height - (safeAreaInsets.bottom + tipInset)
} else {
return middleY
}
}
var hiddenY: CGFloat {
return surfaceView.superview!.bounds.height
}
var safeAreaBottomY: CGFloat {
return surfaceView.superview!.bounds.height - (safeAreaInsets.bottom + hiddenInset)
}
var topMaxY: CGFloat { return -safeAreaInsets.top }
var bottomMaxY: CGFloat { return safeAreaBottomY }
var adjustedContentInsets: UIEdgeInsets {
return UIEdgeInsets(top: 0.0,
left: 0.0,
@@ -157,88 +218,122 @@ class FloatingPanelLayoutAdapter {
return middleY
case .tip:
return bottomY
case .hidden:
return hiddenY
}
}
var intrinsicHeight: CGFloat = 0.0
init(surfaceView: FloatingPanelSurfaceView, backdropView: FloatingPanelBackdropView, layout: FloatingPanelLayout) {
self.layout = layout
self.surfaceView = surfaceView
self.backdropVIew = backdropView
self.backdropView = backdropView
}
func prepareLayout(toParent parent: UIViewController) {
func updateIntrinsicHeight() {
let fittingSize = UIView.layoutFittingCompressedSize
intrinsicHeight = surfaceView.contentView.systemLayoutSizeFitting(fittingSize).height
}
func prepareLayout(in vc: UIViewController) {
self.vc = vc
surfaceView.translatesAutoresizingMaskIntoConstraints = false
backdropVIew.translatesAutoresizingMaskIntoConstraints = false
backdropView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.deactivate(fixedConstraints + fullConstraints + halfConstraints + tipConstraints + offConstraints)
// Fixed constraints of surface and backdrop views
let surfaceConstraints = layout.prepareLayout(surfaceView: surfaceView, in: parent.view!)
let backdroptConstraints = [
backdropVIew.topAnchor.constraint(equalTo: parent.view.topAnchor,
constant: 0.0),
backdropVIew.leftAnchor.constraint(equalTo: parent.view.leftAnchor,
constant: 0.0),
backdropVIew.rightAnchor.constraint(equalTo: parent.view.rightAnchor,
constant: 0.0),
backdropVIew.bottomAnchor.constraint(equalTo: parent.view.bottomAnchor,
constant: 0.0),
let surfaceConstraints = layout.prepareLayout(surfaceView: surfaceView, in: vc.view!)
let backdropConstraints = [
backdropView.topAnchor.constraint(equalTo: vc.view.topAnchor, constant: 0.0),
backdropView.leftAnchor.constraint(equalTo: vc.view.leftAnchor,constant: 0.0),
backdropView.rightAnchor.constraint(equalTo: vc.view.rightAnchor, constant: 0.0),
backdropView.bottomAnchor.constraint(equalTo: vc.view.bottomAnchor, constant: 0.0),
]
fixedConstraints = surfaceConstraints + backdroptConstraints
fixedConstraints = surfaceConstraints + backdropConstraints
// Flexible surface constarints for full, half, tip and off
fullConstraints = [
surfaceView.topAnchor.constraint(equalTo: parent.layoutGuide.topAnchor,
constant: fullInset),
]
if layout is FloatingPanelIntrinsicLayout {
updateIntrinsicHeight()
fullConstraints = [
surfaceView.topAnchor.constraint(equalTo: vc.layoutGuide.bottomAnchor,
constant: -fullInset),
]
} else {
fullConstraints = [
surfaceView.topAnchor.constraint(equalTo: vc.layoutGuide.topAnchor,
constant: fullInset),
]
}
halfConstraints = [
surfaceView.topAnchor.constraint(equalTo: parent.layoutGuide.bottomAnchor,
surfaceView.topAnchor.constraint(equalTo: vc.layoutGuide.bottomAnchor,
constant: -halfInset),
]
tipConstraints = [
surfaceView.topAnchor.constraint(equalTo: parent.layoutGuide.bottomAnchor,
surfaceView.topAnchor.constraint(equalTo: vc.layoutGuide.bottomAnchor,
constant: -tipInset),
]
offConstraints = [
surfaceView.topAnchor.constraint(equalTo: parent.layoutGuide.bottomAnchor, constant: 0.0),
surfaceView.topAnchor.constraint(equalTo: vc.view.bottomAnchor,
constant: -hiddenInset),
]
}
// 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 }
defer {
UIView.performWithoutAnimation {
surfaceView.superview!.layoutIfNeeded()
}
}
if let consts = self.heightConstraints {
NSLayoutConstraint.deactivate([consts])
NSLayoutConstraint.deactivate(heightConstraints)
let height: CGFloat
if layout is FloatingPanelIntrinsicLayout {
updateIntrinsicHeight()
height = intrinsicHeight + safeAreaInsets.bottom
} else {
// Must use the`vc` height, not the screen height because safe area insets
// of `vc` are relative values. For example, a view controller in
// Navigation controller's safe area insets and frame can be changed whether
// the navigation bar is translucent or not.
height = vc.view.bounds.height - (safeAreaInsets.top + fullInset)
}
let height = UIScreen.main.bounds.height - (safeAreaInsets.top + fullInset)
let consts = surfaceView.heightAnchor.constraint(equalToConstant: height)
heightConstraints = [
surfaceView.heightAnchor.constraint(equalToConstant: height)
]
NSLayoutConstraint.activate(heightConstraints)
surfaceView.set(bottomOverflow: heightBuffer + layout.topInteractionBuffer)
NSLayoutConstraint.activate([consts])
heightConstraints = consts
surfaceView.bottomOverflow = heightBuffer
if layout is FloatingPanelIntrinsicLayout {
NSLayoutConstraint.deactivate(fullConstraints)
fullConstraints = [
surfaceView.topAnchor.constraint(equalTo: vc.layoutGuide.bottomAnchor,
constant: -fullInset),
]
NSLayoutConstraint.activate(fullConstraints)
}
}
func activateLayout(of state: FloatingPanelPosition?) {
func activateLayout(of state: FloatingPanelPosition) {
defer {
surfaceView.superview!.layoutIfNeeded()
}
var state = state
setBackdropAlpha(of: state)
NSLayoutConstraint.activate(fixedConstraints)
guard var state = state else {
NSLayoutConstraint.deactivate(fullConstraints + halfConstraints + tipConstraints)
NSLayoutConstraint.activate(offConstraints)
return
}
if layout.supportedPositions.contains(state) == false {
if supportedPositions.union([.hidden]).contains(state) == false {
state = layout.initialPosition
}
@@ -253,24 +348,36 @@ class FloatingPanelLayoutAdapter {
case .tip:
NSLayoutConstraint.deactivate(fullConstraints + halfConstraints + offConstraints)
NSLayoutConstraint.activate(tipConstraints)
case .hidden:
NSLayoutConstraint.deactivate(fullConstraints + halfConstraints + tipConstraints)
NSLayoutConstraint.activate(offConstraints)
}
}
private func checkConsistance(of layout: FloatingPanelLayout) {
// Verify layout configurations
assert(layout.supportedPositions.count > 1)
assert(layout.supportedPositions.contains(layout.initialPosition),
"Does not include an initial potision(\(layout.initialPosition)) in supportedPositions(\(layout.supportedPositions))")
layout.supportedPositions.forEach { (pos) in
assert(layout.insetFor(position: pos) != nil,
"Undefined an inset for a pos(\(pos))")
func setBackdropAlpha(of target: FloatingPanelPosition) {
if target == .hidden {
self.backdropView.alpha = 0.0
} else {
self.backdropView.alpha = layout.backdropAlphaFor(position: target)
}
}
func checkLayoutConsistance() {
// Verify layout configurations
assert(supportedPositions.count > 0)
assert(supportedPositions.contains(layout.initialPosition),
"Does not include an initial potision(\(layout.initialPosition)) in supportedPositions(\(supportedPositions))")
if layout is FloatingPanelIntrinsicLayout {
assert(layout.insetFor(position: .full) == nil, "Return `nil` for full position on FloatingPanelIntrinsicLayout")
}
if halfInset > 0 {
assert(halfInset > tipInset, "Invalid half and tip insets")
}
if fullInset > 0 {
assert(middleY > topY, "Invalid insets")
assert(bottomY > topY, "Invalid insets")
assert(middleY > topY, "Invalid insets { topY: \(topY), middleY: \(middleY) }")
assert(bottomY > topY, "Invalid insets { topY: \(topY), bottomY: \(bottomY) }")
}
}
}
@@ -10,7 +10,10 @@ class FloatingPanelSurfaceContentView: UIView {}
/// A view that presents a surface interface in a floating panel.
public class FloatingPanelSurfaceView: UIView {
/// A GrabberHandleView object displayed at the top of the surface view
/// A GrabberHandleView object displayed at the top of the surface view.
///
/// To use a custom grabber handle, hide this and then add the custom one
/// to the surface view at appropirate coordinates.
public var grabberHandle: GrabberHandleView!
/// The height of the grabber bar area
@@ -21,15 +24,12 @@ public class FloatingPanelSurfaceView: UIView {
/// A UIView object that can have the surface view added to it.
public var contentView: UIView!
private var color: UIColor? = .white { didSet { setNeedsDisplay() } }
var bottomOverflow: CGFloat = 0.0 { didSet { setNeedsDisplay() }}
private var color: UIColor? = .white { didSet { setNeedsLayout() } }
private var bottomOverflow: CGFloat = 0.0 // Must not call setNeedsLayout()
public override var backgroundColor: UIColor? {
get { return color }
set {
color = newValue
setNeedsDisplay()
}
set { color = newValue }
}
/// The radius to use when drawing top rounded corners.
@@ -59,7 +59,7 @@ public class FloatingPanelSurfaceView: UIView {
/// The color of the surface border.
public var borderWidth: CGFloat = 0.0 { didSet { setNeedsLayout() } }
private var shadowLayer: CAShapeLayer! { didSet { setNeedsLayout() } }
private var backgroundLayer: CAShapeLayer! { didSet { setNeedsLayout() } }
private struct Default {
public static let grabberTopPadding: CGFloat = 6.0
@@ -79,10 +79,14 @@ public class FloatingPanelSurfaceView: UIView {
super.backgroundColor = .clear
self.clipsToBounds = false
let backgroundLayer = CAShapeLayer()
layer.insertSublayer(backgroundLayer, at: 0)
self.backgroundLayer = backgroundLayer
let contentView = FloatingPanelSurfaceContentView()
addSubview(contentView)
self.contentView = contentView as UIView
// contentView.backgroundColor = .lightGray
contentView.backgroundColor = color
contentView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
contentView.topAnchor.constraint(equalTo: topAnchor, constant: 0.0),
@@ -102,17 +106,39 @@ public class FloatingPanelSurfaceView: UIView {
grabberHandle.heightAnchor.constraint(equalToConstant: grabberHandle.frame.height),
grabberHandle.centerXAnchor.constraint(equalTo: centerXAnchor),
])
let shadowLayer = CAShapeLayer()
layer.insertSublayer(shadowLayer, at: 0)
self.shadowLayer = shadowLayer
}
public override func layoutSubviews() {
super.layoutSubviews()
updateShadowLayer()
updateLayers()
updateContentViewMask()
contentView.layer.borderColor = borderColor?.cgColor
contentView.layer.borderWidth = borderWidth
contentView.backgroundColor = color
}
private func updateLayers() {
log.debug("SurfaceView bounds", bounds)
var rect = bounds
rect.size.height += bottomOverflow // Expand the height for overflow buffer
let path = UIBezierPath(roundedRect: rect,
byRoundingCorners: [.topLeft, .topRight],
cornerRadii: CGSize(width: cornerRadius, height: cornerRadius))
backgroundLayer.path = path.cgPath
backgroundLayer.fillColor = color?.cgColor
if shadowHidden == false {
layer.shadowColor = shadowColor.cgColor
layer.shadowOffset = shadowOffset
layer.shadowOpacity = shadowOpacity
layer.shadowRadius = shadowRadius
}
}
private func updateContentViewMask() {
if #available(iOS 11, *) {
// Don't use `contentView.clipToBounds` because it prevents content view from expanding the height of a subview of it
// for the bottom overflow like Auto Layout settings of UIVisualEffectView in Main.storyborad of Example/Maps.
@@ -129,26 +155,24 @@ public class FloatingPanelSurfaceView: UIView {
// Don't use `contentView.layer.mask` because of a UIVisualEffectView issue in iOS 10, https://forums.developer.apple.com/thread/50854
// Instead, a user can mask the content view manually in an application.
}
contentView.layer.borderColor = borderColor?.cgColor
contentView.layer.borderWidth = borderWidth
}
private func updateShadowLayer() {
log.debug("SurfaceView bounds", bounds)
var rect = bounds
rect.size.height += bottomOverflow // Expand the height for overflow buffer
let path = UIBezierPath(roundedRect: rect,
byRoundingCorners: [.topLeft, .topRight],
cornerRadii: CGSize(width: cornerRadius, height: cornerRadius))
shadowLayer.path = path.cgPath
shadowLayer.fillColor = color?.cgColor
if shadowHidden == false {
shadowLayer.shadowPath = shadowLayer.path
shadowLayer.shadowColor = shadowColor.cgColor
shadowLayer.shadowOffset = shadowOffset
shadowLayer.shadowOpacity = shadowOpacity
shadowLayer.shadowRadius = shadowRadius
}
func set(bottomOverflow: CGFloat) {
self.bottomOverflow = bottomOverflow
updateLayers()
updateContentViewMask()
}
func add(childView: UIView) {
contentView.addSubview(childView)
childView.frame = contentView.bounds
childView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
childView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 0.0),
childView.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: 0.0),
childView.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: 0.0),
childView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: 0.0),
])
}
}
@@ -0,0 +1,99 @@
//
// Created by Shin Yamamoto on 2018/11/21.
// Copyright © 2018 Shin Yamamoto. All rights reserved.
//
import UIKit
class FloatingPanelModalTransition: NSObject, UIViewControllerTransitioningDelegate {
func animationController(forPresented presented: UIViewController,
presenting: UIViewController,
source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return FloatingPanelModalPresentTransition()
}
func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return FloatingPanelModalDismissTransition()
}
func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController) -> UIPresentationController? {
return FloatingPanelPresentationController(presentedViewController: presented, presenting: presenting)
}
}
class FloatingPanelPresentationController: UIPresentationController {
override func presentationTransitionDidEnd(_ completed: Bool) {
// For non-animated presentation
if let fpc = presentedViewController as? FloatingPanelController, fpc.position == .hidden {
fpc.show(animated: false, completion: nil)
}
}
override func dismissalTransitionDidEnd(_ completed: Bool) {
// For non-animated dismissal
if let fpc = presentedViewController as? FloatingPanelController, fpc.position != .hidden {
fpc.hide(animated: false, completion: nil)
}
}
override func containerViewWillLayoutSubviews() {
guard
let containerView = self.containerView,
let fpc = presentedViewController as? FloatingPanelController,
let fpView = fpc.view
else { fatalError() }
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleBackdrop(tapGesture:)))
fpc.backdropView.addGestureRecognizer(tapGesture)
containerView.addSubview(fpView)
fpView.frame = containerView.bounds //MUST
}
@objc func handleBackdrop(tapGesture: UITapGestureRecognizer) {
presentedViewController.dismiss(animated: true, completion: nil)
}
}
class FloatingPanelModalPresentTransition: NSObject, UIViewControllerAnimatedTransitioning {
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
guard
let fpc = transitionContext?.viewController(forKey: .to) as? FloatingPanelController
else { fatalError()}
let animator = fpc.behavior.addAnimator(fpc, to: fpc.layout.initialPosition)
return TimeInterval(animator.duration)
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
guard
let fpc = transitionContext.viewController(forKey: .to) as? FloatingPanelController
else { fatalError() }
fpc.show(animated: true) {
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
}
}
}
class FloatingPanelModalDismissTransition: NSObject, UIViewControllerAnimatedTransitioning {
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
guard
let fpc = transitionContext?.viewController(forKey: .from) as? FloatingPanelController
else { fatalError()}
let animator = fpc.behavior.removeAnimator(fpc, from: fpc.position)
return TimeInterval(animator.duration)
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
guard
let fpc = transitionContext.viewController(forKey: .from) as? FloatingPanelController
else { fatalError() }
fpc.hide(animated: true) {
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
}
}
}
+30
View File
@@ -0,0 +1,30 @@
//
// Created by Shin Yamamoto on 2018/11/21.
// Copyright © 2018 Shin Yamamoto. All rights reserved.
//
import UIKit
class FloatingPanelPassThroughView: UIView {
public override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
let view = super.hitTest(point, with: event)
switch view {
case is FloatingPanelPassThroughView:
return nil
default:
return view
}
}
}
class FloatingPanelSurfaceWrapperView: UIView {
public override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
let view = super.hitTest(point, with: event)
switch view {
case is FloatingPanelSurfaceWrapperView:
return nil
default:
return view
}
}
}
@@ -24,8 +24,14 @@ public class GrabberHandleView: UIView {
self.backgroundColor = Default.barColor
render()
}
private func render() {
self.layer.masksToBounds = true
self.layer.cornerRadius = frame.size.height * 0.5
}
public override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
let view = super.hitTest(point, with: event)
return view == self ? nil : view
}
}
+13
View File
@@ -50,6 +50,8 @@ protocol SideLayoutGuideProvider {
extension UIView: SideLayoutGuideProvider {}
extension UILayoutGuide: SideLayoutGuideProvider {}
// The reason why UIView has no extensions of safe area insets and top/bottom guides
// is for iOS10 compat.
extension UIView {
var sideLayoutGuide: SideLayoutGuideProvider {
if #available(iOS 11.0, *) {
@@ -60,6 +62,17 @@ extension UIView {
}
}
extension UIView {
func disableAutoLayout() {
let frame = self.frame
translatesAutoresizingMaskIntoConstraints = true
self.frame = frame
}
func enableAutoLayout() {
translatesAutoresizingMaskIntoConstraints = false
}
}
extension UIGestureRecognizer.State: CustomDebugStringConvertible {
public var debugDescription: String {
switch self {
+179 -50
View File
@@ -23,16 +23,23 @@ The new interface displays the related contents and utilities in parallel as a u
- [CocoaPods](#cocoapods)
- [Carthage](#carthage)
- [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)
- [Usage](#usage)
- [Customize the layout of a floating panel with `FloatingPanelLayout` protocol](#customize-the-layout-of-a-floating-panel-with--floatingpanellayout-protocol)
- [Change the initial position, supported positions and height](#change-the-initial-position-supported-positions-and-height)
- [Show/Hide a floating panel in a view with your view hierarchy](#showhide-a-floating-panel-in-a-view-with-your-view-hierarchy)
- [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)
- [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)
- [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)
- [Make your contents correspond with a floating panel behavior](#make-your-contents-correspond-with-a-floating-panel-behavior)
- [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)
- [FloatingPanelSurfaceView's issue on iOS 10](#floatingpanelsurfaceviews-issue-on-ios-10)
- [Author](#author)
- [License](#license)
@@ -45,10 +52,11 @@ The new interface displays the related contents and utilities in parallel as a u
- [x] Fluid animation and gesture handling
- [x] Scroll view tracking
- [x] Common UI elements: Grabber handle, Backdrop and Surface rounding corners
- [x] 2 or 3 anchor positions(full, half, tip)
- [x] 1~3 anchor positions(full, half, tip)
- [x] Layout customization for all trait environments(i.e. Landscape orientation support)
- [x] Behavior customization
- [x] Free from common issues of Auto Layout and gesture handling
- [x] Modal presentation
Examples are here.
@@ -81,6 +89,8 @@ github "scenee/FloatingPanel"
## Getting Started
### Add a floating panel as a child view controller
```swift
import UIKit
import FloatingPanel
@@ -96,29 +106,75 @@ class ViewController: UIViewController, FloatingPanelControllerDelegate {
// Assign self as the delegate of the controller.
fpc.delegate = self // Optional
// Add a content view controller.
// Set a content view controller.
let contentVC = ContentViewController()
fpc.show(contentVC, sender: nil)
fpc.set(contentViewController: contentVC)
// Track a scroll view(or the siblings) in the content view controller.
fpc.track(scrollView: contentVC.tableView)
// Add the views managed by the `FloatingPanelController` object to self.view.
// Add and show the views managed by the `FloatingPanelController` object to self.view.
fpc.addPanel(toParent: self)
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
// Remove the views managed by the `FloatingPanelController` object from self.view.
fpc.removePanelFromParent()
}
...
}
```
### Present a floating panel as a modality
```swift
let fpc = FloatingPanelController()
let contentVC = ...
fpc.set(contentViewController: contentVC)
fpc.isRemovalInteractionEnabled = true // Optional: Let it removable by a swipe-down
self.present(fpc, animated: true, completion: nil)
```
You can show a floating panel over UINavigationController from the containnee view controllers as a modality of `.overCurrentContext` style.
NOTE: FloatingPanelController has the custom presentation controller. If you would like to customize the presentation/dismissal, please see [FloatingPanelTransitioning](https://github.com/SCENEE/FloatingPanel/blob/feat-modality/Framework/Sources/FloatingPanelTransitioning.swift).
## Usage
### Customize the layout of a floating panel with `FloatingPanelLayout` protocol
### Show/Hide a floating panel in a view with your view hierarchy
```swift
// Add the controller and the managed views to a view controller.
// From the second time, just call `show(animated:completion)`.
view.addSubview(fpc.view)
fpc.view.frame = view.bounds // MUST
parent.addChild(fpc)
// Show a floating panel to the initial position defined in your `FloatingPanelLayout` object.
fpc.show(animated: true) {
// Only for the first time
self.didMove(toParent: self)
}
...
// Hide it
fpc.hide(animated: true) {
// Remove it if needed
self.willMove(toParent: nil)
self.view.removeFromSuperview()
self.removeFromParent()
}
```
NOTE: `FloatingPanelController` wraps `show`/`hide` with `addPanel`/`removePanelFromParent` for easy-to-use. But `show`/`hide` are more convenience for your app.
### Customize the layout with `FloatingPanelLayout` protocol
#### Change the initial position and height
@@ -128,7 +184,6 @@ class ViewController: UIViewController, FloatingPanelControllerDelegate {
func floatingPanel(_ vc: FloatingPanelController, layoutFor newCollection: UITraitCollection) -> FloatingPanelLayout? {
return MyFloatingPanelLayout()
}
...
}
class MyFloatingPanelLayout: FloatingPanelLayout {
@@ -141,6 +196,7 @@ class MyFloatingPanelLayout: FloatingPanelLayout {
case .full: return 16.0 // A top inset from safe area
case .half: return 216.0 // A bottom inset from the safe area
case .tip: return 44.0 // A bottom inset from the safe area
default: return nil // Or `case .hidden: return nil`
}
}
}
@@ -154,7 +210,6 @@ class ViewController: UIViewController, FloatingPanelControllerDelegate {
func floatingPanel(_ vc: FloatingPanelController, layoutFor newCollection: UITraitCollection) -> FloatingPanelLayout? {
return (newCollection.verticalSizeClass == .compact) ? FloatingPanelLandscapeLayout() : nil // Returning nil indicates to use the default layout
}
...
}
class FloatingPanelLandscapeLayout: FloatingPanelLayout {
@@ -182,6 +237,33 @@ class FloatingPanelLandscapeLayout: FloatingPanelLayout {
}
```
#### Use Intrinsic height layout
1. Lay out your content View with the intrinsic height size. For example, see "Detail View Controller scene"/"Intrinsic View Controller scene" of [Main.storyboard](https://github.com/SCENEE/FloatingPanel/blob/master/Examples/Samples/Sources/Base.lproj/Main.storyboard). The 'Stack View.bottom' constraint determines the intrinsic height.
2. Create a layout that adopts and conforms to `FloatingPanelIntrinsicLayout` and use it.
```swift
class ViewController: UIViewController, FloatingPanelControllerDelegate {
...
func floatingPanel(_ vc: FloatingPanelController, layoutFor newCollection: UITraitCollection) -> FloatingPanelLayout? {
return RemovablePanelLayout()
}
}
class RemovablePanelLayout: FloatingPanelIntrinsicLayout {
var supportedPositions: Set<FloatingPanelPosition> {
return [.full, .half]
}
func insetFor(position: FloatingPanelPosition) -> CGFloat? {
switch position {
case .half: return 130.0
default: return nil // Must return nil for .full
}
}
...
}
```
### Customize the behavior with `FloatingPanelBehavior` protocol
#### Modify your floating panel's interaction
@@ -192,51 +274,67 @@ class ViewController: UIViewController, FloatingPanelControllerDelegate {
func floatingPanel(_ vc: FloatingPanelController, behaviorFor newCollection: UITraitCollection) -> FloatingPanelBehavior? {
return FloatingPanelStocksBehavior()
}
...
}
...
class FloatingPanelStocksBehavior: FloatingPanelBehavior {
var velocityThreshold: CGFloat {
return 15.0
}
...
func interactionAnimator(_ fpc: FloatingPanelController, to targetPosition: FloatingPanelPosition, with velocity: CGVector) -> UIViewPropertyAnimator {
let damping = self.damping(with: velocity)
let springTiming = UISpringTimingParameters(dampingRatio: damping, initialVelocity: velocity)
return UIViewPropertyAnimator(duration: 0.5, timingParameters: springTiming)
}
}
```
### Use a custom grabber handle
```swift
let myGrabberHandleView = MyGrabberHandleView()
fpc.surfaceView.grabberHandle.isHidden = true
fpc.surfaceView.addSubview(myGrabberHandleView)
```
### Add tap gestures to the surface or backdrop views
```swift
override func viewDidLoad() {
...
surfaceTapGesture = UITapGestureRecognizer(target: self, action: #selector(handleSurface(tapGesture:)))
fpc.surfaceView.addGestureRecognizer(surfaceTapGesture)
backdropTapGesture = UITapGestureRecognizer(target: self, action: #selector(handleBackdrop(tapGesture:)))
fpc.backdropView.addGestureRecognizer(backdropTapGesture)
surfaceTapGesture.isEnabled = (fpc.position == .tip)
}
// Enable `surfaceTapGesture` only at `tip` position
func floatingPanelDidChangePosition(_ vc: FloatingPanelController) {
surfaceTapGesture.isEnabled = (vc.position == .tip)
}
```
### Create an additional floating panel for a detail
```swift
class ViewController: UIViewController, FloatingPanelControllerDelegate {
var searchPanelVC: FloatingPanelController!
var detailPanelVC: FloatingPanelController!
override func viewDidLoad() {
// Setup Search panel
self.searchPanelVC = FloatingPanelController()
override func viewDidLoad() {
// Setup Search panel
self.searchPanelVC = FloatingPanelController()
let searchVC = SearchViewController()
self.searchPanelVC.set(contentViewController: searchVC)
self.searchPanelVC.track(scrollView: contentVC.tableView)
let searchVC = SearchViewController()
self.searchPanelVC.show(searchVC, sender: nil)
self.searchPanelVC.track(scrollView: contentVC.tableView)
self.searchPanelVC.addPanel(toParent: self)
self.searchPanelVC.addPanel(toParent: self)
// Setup Detail panel
self.detailPanelVC = FloatingPanelController()
// Setup Detail panel
self.detailPanelVC = FloatingPanelController()
let contentVC = ContentViewController()
self.detailPanelVC.set(contentViewController: contentVC)
self.detailPanelVC.track(scrollView: contentVC.scrollView)
let contentVC = ContentViewController()
self.detailPanelVC.show(contentVC, sender: nil)
self.detailPanelVC.track(scrollView: contentVC.scrollView)
self.detailPanelVC.addPanel(toParent: self)
}
...
self.detailPanelVC.addPanel(toParent: self)
}
```
@@ -245,18 +343,18 @@ class ViewController: UIViewController, FloatingPanelControllerDelegate {
In the following example, I move a floating panel to full or half position while opening or closing a search bar like Apple Maps.
```swift
func searchBarCancelButtonClicked(_ searchBar: UISearchBar) {
...
fpc.move(to: .half, animated: true)
}
func searchBarCancelButtonClicked(_ searchBar: UISearchBar) {
...
fpc.move(to: .half, animated: true)
}
func searchBarTextDidBeginEditing(_ searchBar: UISearchBar) {
...
fpc.move(to: .full, animated: true)
}
func searchBarTextDidBeginEditing(_ searchBar: UISearchBar) {
...
fpc.move(to: .full, animated: true)
}
```
### Make your contents correspond with a floating panel behavior
### Work your contents together with a floating panel behavior
```swift
class ViewController: UIViewController, FloatingPanelControllerDelegate {
@@ -273,15 +371,46 @@ class ViewController: UIViewController, FloatingPanelControllerDelegate {
searchVC.hideHeader()
}
}
...
}
```
## Notes
### FloatingPanelSurfaceView's issue on iOS 10
### 'Show' or 'Show Detail' Segues from `FloatingPanelController`'s content view controller
* 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.
'Show' or 'Show Detail' segues from a content view controller will be managed by a view controller(hereinafter called 'master VC') adding a floating panel. Because a floating panel is just a subview of the master VC(except for modality).
`FloatingPanelController` has no way to manage a stack of view controllers like `UINavigationController`. If so, it would be so complicated and the interface will become `UINavigationController`. This component should not have the responsibility to manage the stack.
By the way, a content view controller can present a view controller modally with `present(_:animated:completion:)` or 'Present Modally' segue.
However, sometimes you want to show a destination view controller of 'Show' or 'Show Detail' segue with another floating panel. It's possible to override `show(_:sender)` of the master VC!
Here is an example.
```swift
class ViewController: UIViewController {
var fpc: FloatingPanelController!
var secondFpc: FloatingPanelController!
...
override func show(_ vc: UIViewController, sender: Any?) {
secondFpc = FloatingPanelController()
secondFpc.set(contentViewController: vc)
secondFpc.addPanel(toParent: self)
}
}
```
A `FloatingPanelController` object proxies an action for `show(_:sender)` to the master VC. That's why the master VC can handle a destination view controller of a 'Show' or 'Show Detail' segue and you can hook `show(_:sender)` to show a secondary floating panel set the destination view controller to the content.
It's a great way to decouple between a floating panel and the content VC.
### 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.
So you need to draw top rounding corners of your content. Here is an example in Examples/Maps.
```swift
override func viewDidLayoutSubviews() {
@@ -292,11 +421,11 @@ override func viewDidLayoutSubviews() {
}
}
```
* If you sets clear color to `FloatingPanelSurfaceView.backgroundColor`, please note the bottom overflow of your content on bouncing at full position. To prevent it, you need to expand your content. For example, See Example/Maps's Auto Layout settings of UIVisualEffectView in Main.storyborad.
* If you sets clear color to `FloatingPanelSurfaceView.backgroundColor`, please note the bottom overflow of your content on bouncing at full position. To prevent it, you need to expand your content. For example, See Example/Maps App's Auto Layout settings of `UIVisualEffectView` in Main.storyboard.
## Author
Shin Yamamoto <shin@scenee.com>
Shin Yamamoto <shin@scenee.com> | [@scenee](https://twitter.com/scenee)
## License