Compare commits

...

37 Commits

Author SHA1 Message Date
Shin Yamamoto 60d0b62675 Release v1.1.0 2018-10-28 09:00:50 +09:00
Shin Yamamoto 7a8eb1833f Merge pull request #14 from SCENEE/improve-public-interface
Improve public interface
2018-10-28 08:59:55 +09:00
Shin Yamamoto 6367b76b9d Fix failure requirements of the pan gesture 2018-10-28 08:36:31 +09:00
Shin Yamamoto 977b685071 Modify a custom landscape layout for Maps 2018-10-28 08:36:31 +09:00
Shin Yamamoto b97d418158 Change the default landscape layout 2018-10-28 08:36:31 +09:00
Shin Yamamoto 0e4cb372d5 Add a sample code to test FloatingPanelController.move(to:animated:) 2018-10-28 08:36:31 +09:00
Shin Yamamoto 2ec7576ae9 Improve FloatingPanelBehavior protocol
present/dismiss words should be used for modality.
add/remove words are appropriate for them.
2018-10-28 08:36:31 +09:00
Shin Yamamoto c4bf4c3067 Open the pan gesture recognizer of FloatingPanelController 2018-10-28 06:41:37 +09:00
Shin Yamamoto ea9bbcad27 Check consistance of FloatingPanelLayout 2018-10-28 06:41:37 +09:00
Shin Yamamoto 71be1f2ed5 Change the type of 'supportedPositions' from Array to Set 2018-10-28 06:41:37 +09:00
Shin Yamamoto 349bb91c6c Improve the default impls of FloatingPanelLayout methods 2018-10-28 06:41:37 +09:00
Shin Yamamoto 52da673358 Fix README 2018-10-28 06:41:37 +09:00
Shin Yamamoto ce0b9d1413 Merge pull request #16 from 0xflotus/patch-1
fixed some errors
2018-10-28 06:34:55 +09:00
0xflotus 8ef332f3e5 fixed some errors 2018-10-27 21:23:06 +02:00
Shin Yamamoto a4002f83c1 Merge pull request #13 from SCENEE/fix-gesture-handling
Fix untracked scroll view's freezing in a floating panel
2018-10-27 20:49:56 +09:00
Shin Yamamoto 64d756d8a9 Add a nested scroll view's sample 2018-10-27 16:39:22 +09:00
Shin Yamamoto 187fe47268 Fix untracked scroll view's freezing in a floating panel 2018-10-27 15:16:08 +09:00
Shin Yamamoto 060f3a0b1b Merge pull request #11 from futuretap/master
Fixed some typos and language in comments
2018-10-27 09:55:53 +09:00
Ortwin Gentz 4dcc5bc564 Fixed some typos and language in comments 2018-10-26 15:30:11 +02:00
Shin Yamamoto 52efac6643 Fix Usage contents in README 2018-10-26 18:39:41 +09:00
Shin Yamamoto 97c91fb7aa Merge pull request #10 from SCENEE/support-travis-ci
Support Travis CI
2018-10-26 15:22:59 +09:00
Shin Yamamoto efcc598550 Add 'Build Status' shield in README 2018-10-26 15:00:02 +09:00
Shin Yamamoto fd5fc1f485 Add travis yml 2018-10-26 15:00:02 +09:00
Shin Yamamoto f713d4057f Merge pull request #9 from SCENEE/fix-surface-view-height
Fix surface view height
2018-10-26 14:29:50 +09:00
Shin Yamamoto 4ebbea8e86 Fix FloatingPanelLayout.{topInteractionBuffer,bottomInteractionBuffer} 2018-10-26 14:27:40 +09:00
Shin Yamamoto 5515e6f788 Update README
- Add shields
- Add TOC
- Update Usage section
- Revise the contents
2018-10-26 14:27:40 +09:00
Shin Yamamoto 65a6315f1b Fix the table view height in Examples/Maps
The bottom of a scroll view tracked by a floating panel controller must align
the bottom of a screen when `FloatingPanelController.contentInsetAdjustmentBehavior`
is set to `always`.
2018-10-26 14:27:40 +09:00
Shin Yamamoto aafe32bb3d Fix a critical bug on 2(full and half) anchor positions 2018-10-26 14:27:40 +09:00
Shin Yamamoto 1c6c783dbe Add sample codes in Samples app to test a floating panel in TabBar 2018-10-26 14:27:40 +09:00
Shin Yamamoto 1e322f47d4 Update doc comment 2018-10-26 14:27:40 +09:00
Shin Yamamoto 37196abe77 Improve updating the shadow layer of the surface 2018-10-24 12:53:27 +09:00
Shin Yamamoto e476cf5ce4 Fix the initial height of DebugTableViewController 2018-10-24 12:53:27 +09:00
Shin Yamamoto dc4b1e7a90 Escape UIVisualEffectView problem on iOS10
The floating panel controller can't resolve this issue, but the
workaround is much easy in the content view controller.
So I stop auto-rounding corners in content view on iOS 10.
2018-10-24 12:53:27 +09:00
Shin Yamamoto 95d188d5f1 Match the bottoms of the surface view and a device bottom 2018-10-24 08:16:42 +09:00
Shin Yamamoto 4dd60ca855 Update Samples App 2018-10-23 14:20:39 +09:00
Shin Yamamoto 5067917295 Fix README 2018-10-23 10:22:48 +09:00
Shin Yamamoto e620ef27ee Merge pull request #2 from SCENEE/enhance-scroll-view-tracking
Enhance scroll view handling
2018-10-23 09:58:30 +09:00
15 changed files with 908 additions and 197 deletions
+44
View File
@@ -0,0 +1,44 @@
language: swift
branches:
only:
- master
cache:
directories:
- build
- vendor
- /usr/local/Homebrew
- $HOME/Library/Caches/Homebrew
env:
global:
- LANG=en_US.UTF-8
- LC_ALL=en_US.UTF-8
skip_cleanup: true
jobs:
include:
- stage: carthage
osx_image: xcode10
before_install:
- brew update
- brew outdated carthage || brew upgrade carthage
script:
- carthage build --no-skip-current
- stage: podspec
osx_image: xcode10
script:
- pod spec lint
- stage: check Maps example
osx_image: xcode10
script:
- xcodebuild -scheme Maps -sdk iphonesimulator clean build
- stage: check Stocks example
osx_image: xcode10
script:
- xcodebuild -scheme Stocks -sdk iphonesimulator clean build
- stage: check Samples example
osx_image: xcode10
script:
- xcodebuild -scheme Samples -sdk iphonesimulator clean build
@@ -60,9 +60,9 @@
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<visualEffectView opaque="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="Ye3-uU-bq3">
<rect key="frame" x="0.0" y="0.0" width="375" height="778"/>
<rect key="frame" x="0.0" y="0.0" width="375" height="900"/>
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" id="ED1-gT-FBj">
<rect key="frame" x="0.0" y="0.0" width="375" height="778"/>
<rect key="frame" x="0.0" y="0.0" width="375" height="900"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<searchBar contentMode="redraw" searchBarStyle="minimal" translatesAutoresizingMaskIntoConstraints="NO" id="Zcj-SE-gb8">
@@ -70,7 +70,7 @@
<textInputTraits key="textInputTraits"/>
</searchBar>
<tableView clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" showsHorizontalScrollIndicator="NO" showsVerticalScrollIndicator="NO" dataMode="prototypes" style="plain" separatorStyle="default" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="28" sectionFooterHeight="28" translatesAutoresizingMaskIntoConstraints="NO" id="D7r-re-InH">
<rect key="frame" x="0.0" y="66" width="375" height="712"/>
<rect key="frame" x="0.0" y="66" width="375" height="748"/>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<view key="tableHeaderView" contentMode="scaleToFill" id="u28-LY-hIh" customClass="SearchHeaderView" customModule="Maps" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="375" height="116"/>
@@ -227,7 +227,7 @@
<constraint firstAttribute="trailing" secondItem="D7r-re-InH" secondAttribute="trailing" id="BES-GA-Btp"/>
<constraint firstItem="D7r-re-InH" firstAttribute="leading" secondItem="ED1-gT-FBj" secondAttribute="leading" id="UTe-YL-17h"/>
<constraint firstItem="Zcj-SE-gb8" firstAttribute="top" secondItem="ED1-gT-FBj" secondAttribute="top" constant="6" id="apU-Pd-PEO"/>
<constraint firstAttribute="bottom" secondItem="D7r-re-InH" secondAttribute="bottom" id="vfS-Lx-TXz"/>
<constraint firstAttribute="bottom" secondItem="D7r-re-InH" secondAttribute="bottom" constant="86" id="vfS-Lx-TXz"/>
<constraint firstItem="D7r-re-InH" firstAttribute="top" secondItem="Zcj-SE-gb8" secondAttribute="bottom" constant="4" id="vro-cd-B9c"/>
<constraint firstItem="Zcj-SE-gb8" firstAttribute="leading" secondItem="ED1-gT-FBj" secondAttribute="leading" id="wMb-L2-Z0W"/>
</constraints>
@@ -238,7 +238,7 @@
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstItem="Ye3-uU-bq3" firstAttribute="leading" secondItem="G74-X7-Za8" secondAttribute="leading" id="Kr2-sU-ZWZ"/>
<constraint firstItem="Ye3-uU-bq3" firstAttribute="bottom" secondItem="G74-X7-Za8" secondAttribute="bottom" id="aWM-s3-3o4"/>
<constraint firstItem="Ye3-uU-bq3" firstAttribute="bottom" secondItem="Ncl-E9-yRn" secondAttribute="bottom" constant="88" id="aWM-s3-3o4"/>
<constraint firstItem="Ye3-uU-bq3" firstAttribute="trailing" secondItem="G74-X7-Za8" secondAttribute="trailing" id="fEL-8y-Acc"/>
<constraint firstItem="Ye3-uU-bq3" firstAttribute="top" secondItem="Ncl-E9-yRn" secondAttribute="top" id="w77-ba-FrJ"/>
</constraints>
@@ -247,6 +247,7 @@
<connections>
<outlet property="searchBar" destination="Zcj-SE-gb8" id="BH7-Gy-RG5"/>
<outlet property="tableView" destination="D7r-re-InH" id="nRN-fY-b8j"/>
<outlet property="visualEffectView" destination="Ye3-uU-bq3" id="rS6-Mq-OKs"/>
</connections>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="EqR-Hp-zhc" userLabel="First Responder" sceneMemberID="firstResponder"/>
+46 -3
View File
@@ -84,15 +84,16 @@ class ViewController: UIViewController, MKMapViewDelegate, UISearchBarDelegate,
// MARK: FloatingPanelControllerDelegate
func floatingPanel(_ vc: FloatingPanelController, layoutFor newCollection: UITraitCollection) -> FloatingPanelLayout? {
switch traitCollection.verticalSizeClass {
switch newCollection.verticalSizeClass {
case .compact:
fpc.surfaceView.borderWidth = 1.0 / traitCollection.displayScale
fpc.surfaceView.borderColor = UIColor.black.withAlphaComponent(0.2)
return SearchPanelLandscapeLayout()
default:
fpc.surfaceView.borderWidth = 0.0
fpc.surfaceView.borderColor = nil
return nil
}
return nil
}
func floatingPanelDidMove(_ vc: FloatingPanelController) {
@@ -133,7 +134,8 @@ class SearchPanelViewController: UIViewController, UITableViewDataSource, UITabl
@IBOutlet weak var tableView: UITableView!
@IBOutlet weak var searchBar: UISearchBar!
@IBOutlet weak var visualEffectView: UIVisualEffectView!
override func viewDidLoad() {
super.viewDidLoad()
tableView.dataSource = self
@@ -143,6 +145,15 @@ class SearchPanelViewController: UIViewController, UITableViewDataSource, UITabl
textField.font = UIFont(name: textField.font!.fontName, size: 15.0)
hideHeader()
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
if #available(iOS 10, *) {
visualEffectView.layer.cornerRadius = 9.0
visualEffectView.clipsToBounds = true
}
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
@@ -193,6 +204,38 @@ class SearchPanelViewController: UIViewController, UITableViewDataSource, UITabl
}
}
public class SearchPanelLandscapeLayout: FloatingPanelLayout {
public var initialPosition: FloatingPanelPosition {
return .tip
}
public var supportedPositions: Set<FloatingPanelPosition> {
return [.full, .tip]
}
public func insetFor(position: FloatingPanelPosition) -> CGFloat? {
switch position {
case .full: return 16.0
case .tip: return 69.0
default: return nil
}
}
public func prepareLayout(surfaceView: UIView, in view: UIView) -> [NSLayoutConstraint] {
if #available(iOS 11.0, *) {
return [
surfaceView.leftAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leftAnchor, constant: 8.0),
surfaceView.widthAnchor.constraint(equalToConstant: 291),
]
} else {
return [
surfaceView.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 8.0),
surfaceView.widthAnchor.constraint(equalToConstant: 291),
]
}
}
}
class SearchCell: UITableViewCell {
@IBOutlet weak var iconImageView: UIImageView!
@IBOutlet weak var titleLabel: UILabel!
@@ -70,13 +70,106 @@
<navigationItem key="navigationItem" title="Samples" id="wCF-su-7up"/>
<connections>
<outlet property="tableView" destination="7IS-PU-x0P" id="YFM-9W-eP4"/>
<segue destination="bYI-y3-Rzb" kind="presentation" identifier="GoToTextView" id="6Ym-J6-Q6X"/>
</connections>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="eP2-DG-flv" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="57" y="27"/>
</scene>
<!--Item 2-->
<scene sceneID="lRc-OZ-sL4">
<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"/>
<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"/>
<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"/>
<state key="normal" title="Close"/>
<connections>
<action selector="closeWithSender:" destination="RpE-lI-27a" eventType="touchUpInside" id="hj3-Xv-6Gq"/>
<action selector="closeWithSender:" destination="bYI-y3-Rzb" eventType="touchUpInside" id="rg4-OH-Ojn"/>
</connections>
</button>
</subviews>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstItem="IvG-yp-yzI" firstAttribute="top" secondItem="2Cd-km-qEk" secondAttribute="top" id="18k-sV-PgT"/>
<constraint firstItem="AiP-dx-mFn" firstAttribute="centerY" secondItem="JER-jz-KSq" secondAttribute="centerY" id="NUc-tM-0dN"/>
<constraint firstItem="AiP-dx-mFn" firstAttribute="centerX" secondItem="JER-jz-KSq" secondAttribute="centerX" id="hwP-mu-Vmz"/>
<constraint firstItem="IvG-yp-yzI" firstAttribute="leading" secondItem="2Cd-km-qEk" secondAttribute="leading" constant="20" id="pYt-jE-CTF"/>
</constraints>
<viewLayoutGuide key="safeArea" id="2Cd-km-qEk"/>
</view>
<tabBarItem key="tabBarItem" tag="1" title="Item 2" id="qb3-RB-B28"/>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="NhZ-u5-Beh" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="326" y="1575"/>
</scene>
<!--Item 1-->
<scene sceneID="m6X-j6-yBM">
<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"/>
<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"/>
<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"/>
<state key="normal" title="Close"/>
<connections>
<action selector="closeWithSender:" destination="bYI-y3-Rzb" eventType="touchUpInside" id="YL4-GP-ZEZ"/>
<action selector="closeWithSender:" destination="lto-Zc-Vtp" eventType="touchUpInside" id="llo-9x-fQv"/>
</connections>
</button>
</subviews>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstItem="eFN-tN-4Ct" firstAttribute="leading" secondItem="f88-U8-Vja" secondAttribute="leading" constant="20" id="5BT-yZ-EKe"/>
<constraint firstItem="uoW-c8-9wx" firstAttribute="centerY" secondItem="ji9-Ez-N7i" secondAttribute="centerY" id="Nyw-Wt-78z"/>
<constraint firstItem="eFN-tN-4Ct" firstAttribute="top" secondItem="f88-U8-Vja" secondAttribute="top" id="hUV-3a-XkY"/>
<constraint firstItem="uoW-c8-9wx" firstAttribute="centerX" secondItem="ji9-Ez-N7i" secondAttribute="centerX" id="wDv-OH-7PX"/>
</constraints>
<viewLayoutGuide key="safeArea" id="f88-U8-Vja"/>
</view>
<tabBarItem key="tabBarItem" title="Item 1" id="HEV-kf-jxH"/>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="bkL-bc-hZC" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="-380" y="1576"/>
</scene>
<!--Tab Bar View Controller-->
<scene sceneID="nQ5-PV-qFw">
<objects>
<tabBarController storyboardIdentifier="TabBarViewController" id="c7K-XJ-TlT" customClass="TabBarViewController" customModule="Samples" customModuleProvider="target" sceneMemberID="viewController">
<tabBar key="tabBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" id="BPL-Dp-5pJ">
<rect key="frame" x="0.0" y="0.0" width="375" height="49"/>
<autoresizingMask key="autoresizingMask"/>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</tabBar>
<connections>
<segue destination="lto-Zc-Vtp" kind="relationship" relationship="viewControllers" id="6hP-AH-YiH"/>
<segue destination="RpE-lI-27a" kind="relationship" relationship="viewControllers" id="g6X-Sq-uSW"/>
</connections>
</tabBarController>
<placeholder placeholderIdentifier="IBFirstResponder" id="Z9x-EI-p2b" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="-183" y="806"/>
</scene>
<!--Modal View Controller-->
<scene sceneID="C9P-Ns-Qrq">
<objects>
@@ -96,14 +189,42 @@
<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"/>
<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"/>
<state key="normal" title="Move to full"/>
<connections>
<action selector="moveToFullWithSender:" destination="bYI-y3-Rzb" eventType="touchUpInside" id="TDe-3J-gIR"/>
</connections>
</button>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="2u5-cH-RAN">
<rect key="frame" x="0.0" y="74" width="85" height="30"/>
<state key="normal" title="Move to half"/>
<connections>
<action selector="moveToHalfWithSender:" destination="bYI-y3-Rzb" eventType="touchUpInside" id="12s-o7-Et5"/>
</connections>
</button>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="M4A-iO-RIE">
<rect key="frame" x="0.0" y="148" width="77" height="30"/>
<state key="normal" title="Move to tip"/>
<connections>
<action selector="moveToTipWithSender:" destination="bYI-y3-Rzb" eventType="touchUpInside" id="BmL-91-9ai"/>
</connections>
</button>
</subviews>
</stackView>
</subviews>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstItem="sbF-Az-7sy" firstAttribute="top" secondItem="GBa-yx-8to" secondAttribute="top" id="3VR-hj-zeQ"/>
<constraint firstItem="9p4-06-y2T" firstAttribute="top" secondItem="GBa-yx-8to" secondAttribute="top" constant="88" id="41n-Fn-hi3"/>
<constraint firstAttribute="bottom" secondItem="vut-mK-Y4t" secondAttribute="bottom" id="6eq-Kt-heZ"/>
<constraint firstItem="sbF-Az-7sy" firstAttribute="leading" secondItem="GBa-yx-8to" secondAttribute="leading" constant="20" id="T2G-1L-PRs"/>
<constraint firstItem="vut-mK-Y4t" firstAttribute="leading" secondItem="qwo-GK-p1U" secondAttribute="leading" id="gVC-jv-VJX"/>
<constraint firstItem="vut-mK-Y4t" firstAttribute="trailing" secondItem="GBa-yx-8to" secondAttribute="trailing" id="jkq-p2-lUm"/>
<constraint firstItem="9p4-06-y2T" firstAttribute="centerX" secondItem="qwo-GK-p1U" secondAttribute="centerX" id="l8t-p3-ETf"/>
<constraint firstItem="vut-mK-Y4t" firstAttribute="top" secondItem="GBa-yx-8to" secondAttribute="bottom" id="rMy-JT-t4B"/>
</constraints>
<viewLayoutGuide key="safeArea" id="GBa-yx-8to"/>
@@ -114,7 +235,128 @@
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="fbi-LZ-M4Y" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="57" y="-758"/>
<point key="canvasLocation" x="561" y="806"/>
</scene>
<!--Nested Scroll View Controller-->
<scene sceneID="TfC-A3-4R0">
<objects>
<viewController storyboardIdentifier="NestedScrollViewController" id="LAe-jm-k6f" customClass="NestedScrollViewController" customModule="Samples" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="414-Wy-0t1">
<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="sBe-tN-uMi">
<rect key="frame" x="0.0" y="32" width="375" height="635"/>
<subviews>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" translatesAutoresizingMaskIntoConstraints="NO" id="lFR-Sp-Sj1">
<rect key="frame" x="0.0" y="0.0" width="375" height="968"/>
<subviews>
<scrollView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" alwaysBounceHorizontal="YES" pagingEnabled="YES" showsVerticalScrollIndicator="NO" bouncesZoom="NO" translatesAutoresizingMaskIntoConstraints="NO" id="xba-kG-VQ2">
<rect key="frame" x="0.0" y="0.0" width="375" height="242"/>
<subviews>
<stackView opaque="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="WRe-tD-KTb">
<rect key="frame" x="0.0" y="0.0" width="1125" height="242"/>
<subviews>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="WuE-iq-0GW">
<rect key="frame" x="0.0" y="0.0" width="375" height="242"/>
<color key="backgroundColor" red="1" green="0.57810515169999999" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</view>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="w5Y-44-79g">
<rect key="frame" x="375" y="0.0" width="375" height="242"/>
<color key="backgroundColor" red="0.0078431372550000003" green="0.72156862749999995" blue="0.45882352939999999" alpha="1" colorSpace="calibratedRGB"/>
</view>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="jQf-k3-eAa">
<rect key="frame" x="750" y="0.0" width="375" height="242"/>
<color key="backgroundColor" red="0.016804177310000001" green="0.19835099580000001" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</view>
</subviews>
<constraints>
<constraint firstItem="w5Y-44-79g" firstAttribute="width" secondItem="WuE-iq-0GW" secondAttribute="width" id="kHy-eU-guw"/>
<constraint firstAttribute="height" constant="242" id="qzg-fI-j20"/>
<constraint firstItem="jQf-k3-eAa" firstAttribute="width" secondItem="WuE-iq-0GW" secondAttribute="width" id="zDe-Uj-FO0"/>
</constraints>
</stackView>
</subviews>
<constraints>
<constraint firstItem="WRe-tD-KTb" firstAttribute="leading" secondItem="xba-kG-VQ2" secondAttribute="leading" id="7QG-dB-afb"/>
<constraint firstAttribute="height" constant="242" id="Efw-D6-ksg"/>
<constraint firstAttribute="trailing" secondItem="WRe-tD-KTb" secondAttribute="trailing" id="ReM-cV-k0J"/>
<constraint firstItem="WRe-tD-KTb" firstAttribute="top" secondItem="xba-kG-VQ2" secondAttribute="top" id="Xla-QL-qwm"/>
<constraint firstItem="WuE-iq-0GW" firstAttribute="width" secondItem="xba-kG-VQ2" secondAttribute="width" id="qm0-cd-P69"/>
<constraint firstAttribute="bottom" secondItem="WRe-tD-KTb" secondAttribute="bottom" id="uha-Eo-lsv"/>
</constraints>
</scrollView>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="j8d-Tc-XQn">
<rect key="frame" x="0.0" y="242" width="375" height="242"/>
<color key="backgroundColor" red="0.0078431372550000003" green="0.72156862749999995" blue="0.45882352939999999" alpha="1" colorSpace="calibratedRGB"/>
<constraints>
<constraint firstAttribute="height" constant="242" id="Kw8-aw-DIp"/>
</constraints>
</view>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="bxy-HF-a7J">
<rect key="frame" x="0.0" y="484" width="375" height="242"/>
<color key="backgroundColor" red="1" green="0.2527923882" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstAttribute="height" constant="242" id="AIb-xl-srX"/>
</constraints>
</view>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="ZzA-fs-Va5">
<rect key="frame" x="0.0" y="726" width="375" height="242"/>
<color key="backgroundColor" red="0.016804177310000001" green="0.19835099580000001" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstAttribute="height" constant="242" id="TC1-jO-Wcz"/>
</constraints>
</view>
</subviews>
</stackView>
</subviews>
<constraints>
<constraint firstItem="lFR-Sp-Sj1" firstAttribute="width" secondItem="sBe-tN-uMi" secondAttribute="width" id="AtD-2C-97K"/>
<constraint firstAttribute="trailing" secondItem="lFR-Sp-Sj1" secondAttribute="trailing" id="F7t-Kr-VGd"/>
<constraint firstItem="lFR-Sp-Sj1" firstAttribute="leading" secondItem="sBe-tN-uMi" secondAttribute="leading" id="LzI-O9-5i0"/>
<constraint firstItem="lFR-Sp-Sj1" firstAttribute="top" secondItem="sBe-tN-uMi" secondAttribute="top" id="VwX-Hz-e8V"/>
<constraint firstAttribute="bottom" secondItem="lFR-Sp-Sj1" secondAttribute="bottom" id="hJt-0Z-dF3"/>
</constraints>
</scrollView>
</subviews>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<gestureRecognizers/>
<constraints>
<constraint firstItem="sBe-tN-uMi" firstAttribute="leading" secondItem="414-Wy-0t1" secondAttribute="leading" id="8Qd-my-knA"/>
<constraint firstItem="sBe-tN-uMi" firstAttribute="top" secondItem="414-Wy-0t1" secondAttribute="top" constant="32" id="9Js-LU-lNr"/>
<constraint firstAttribute="bottom" secondItem="sBe-tN-uMi" secondAttribute="bottom" id="jzB-47-P7e"/>
<constraint firstAttribute="trailing" secondItem="sBe-tN-uMi" secondAttribute="trailing" id="nHG-wg-pLP"/>
</constraints>
<viewLayoutGuide key="safeArea" id="sL5-d5-za2"/>
<connections>
<outletCollection property="gestureRecognizers" destination="tOa-bf-zGz" appends="YES" id="zle-Sz-M3U"/>
<outletCollection property="gestureRecognizers" destination="SCk-hG-weZ" appends="YES" id="OcK-FK-Lac"/>
<outletCollection property="gestureRecognizers" destination="Fvp-Z6-eVc" appends="YES" id="Fds-J5-YCg"/>
</connections>
</view>
<size key="freeformSize" width="375" height="667"/>
<connections>
<outlet property="scrollView" destination="sBe-tN-uMi" id="h4S-Zl-cLO"/>
</connections>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="QSd-gF-l5h" userLabel="First Responder" sceneMemberID="firstResponder"/>
<pongPressGestureRecognizer allowableMovement="10" minimumPressDuration="0.5" id="tOa-bf-zGz">
<connections>
<action selector="longPressed:" destination="LAe-jm-k6f" id="sE8-3l-Aos"/>
</connections>
</pongPressGestureRecognizer>
<tapGestureRecognizer id="SCk-hG-weZ">
<connections>
<action selector="tapped:" destination="LAe-jm-k6f" id="0Cw-vR-zRP"/>
</connections>
</tapGestureRecognizer>
<swipeGestureRecognizer direction="right" id="Fvp-Z6-eVc">
<connections>
<action selector="swipped:" destination="LAe-jm-k6f" id="Hav-7p-Tg8"/>
</connections>
</swipeGestureRecognizer>
</objects>
<point key="canvasLocation" x="1239" y="806"/>
</scene>
<!--Detail View Controller-->
<scene sceneID="b6k-zi-3wn">
@@ -134,13 +376,39 @@
<action selector="closeWithSender:" destination="YC8-ae-15L" eventType="touchUpInside" id="Z2v-19-S5k"/>
</connections>
</button>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="8yw-OC-Ubk">
<rect key="frame" x="0.0" y="690" width="375" height="88"/>
<color key="backgroundColor" white="0.66666666666666663" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstAttribute="height" constant="88" id="jwV-YU-tXG"/>
</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"/>
<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>
</subviews>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<gestureRecognizers/>
<constraints>
<constraint firstItem="noi-1a-5bZ" firstAttribute="top" secondItem="g7l-kO-y7q" secondAttribute="top" constant="12" id="EQy-cr-F2Y"/>
<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="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"/>
</constraints>
<viewLayoutGuide key="safeArea" id="tAi-nk-rDB"/>
<connections>
<outletCollection property="gestureRecognizers" destination="6Ca-p8-7uF" appends="YES" id="xOy-f1-NZE"/>
<outletCollection property="gestureRecognizers" destination="SPY-Vr-XDT" appends="YES" id="vgS-Am-jhQ"/>
<outletCollection property="gestureRecognizers" destination="Jg4-it-qJ5" appends="YES" id="ONf-5y-phY"/>
</connections>
</view>
<size key="freeformSize" width="375" height="778"/>
<connections>
@@ -148,8 +416,23 @@
</connections>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="Wqk-xl-O3I" userLabel="First Responder" sceneMemberID="firstResponder"/>
<tapGestureRecognizer id="6Ca-p8-7uF">
<connections>
<action selector="tapped:" destination="YC8-ae-15L" id="KFd-eT-RLn"/>
</connections>
</tapGestureRecognizer>
<swipeGestureRecognizer direction="right" id="SPY-Vr-XDT">
<connections>
<action selector="swipped:" destination="YC8-ae-15L" id="OFa-4C-8rI"/>
</connections>
</swipeGestureRecognizer>
<pongPressGestureRecognizer allowableMovement="10" minimumPressDuration="0.5" id="Jg4-it-qJ5">
<connections>
<action selector="longPressed:" destination="YC8-ae-15L" id="1G4-cf-RDE"/>
</connections>
</pongPressGestureRecognizer>
</objects>
<point key="canvasLocation" x="836" y="493.5960591133005"/>
<point key="canvasLocation" x="1442" y="-23"/>
</scene>
<!--Debug Text View Controller-->
<scene sceneID="Bkq-O7-q4A">
@@ -221,7 +504,7 @@ Section 1.10.33 of "de Finibus Bonorum et Malorum", written by Cicero in 45 BC
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="x1h-y1-h8q" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="836" y="-446"/>
<point key="canvasLocation" x="729" y="-23"/>
</scene>
</scenes>
</document>
@@ -86,3 +86,18 @@ class SafeAreaView: UIView {
])
}
}
@IBDesignable
class OnSafeAreaView: UIView {
override func prepareForInterfaceBuilder() {
let label = UILabel()
label.text = "On Safe Area"
addSubview(label)
label.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
label.centerXAnchor.constraint(equalTo: self.centerXAnchor),
label.centerYAnchor.constraint(equalTo: self.centerYAnchor, constant: -4.0),
])
}
}
+146 -4
View File
@@ -17,6 +17,8 @@ class SampleListViewController: UIViewController, UITableViewDataSource, UITable
case trackingTextView
case showDetail
case showModal
case showTabBar
case showNestedScrollView
var name: String {
switch self {
@@ -24,6 +26,8 @@ class SampleListViewController: UIViewController, UITableViewDataSource, UITable
case .trackingTextView: return "Scroll tracking (UITextView)"
case .showDetail: return "Show Detail Panel"
case .showModal: return "Show Modal"
case .showTabBar: return "Show Tab Bar"
case .showNestedScrollView: return "Show Nested ScrollView"
}
}
@@ -33,6 +37,8 @@ class SampleListViewController: UIViewController, UITableViewDataSource, UITable
case .trackingTextView: return "ConsoleViewController"
case .showDetail: return "DetailViewController"
case .showModal: return "ModalViewController"
case .showTabBar: return "TabBarViewController"
case .showNestedScrollView: return "NestedScrollViewController"
}
}
}
@@ -67,9 +73,10 @@ class SampleListViewController: UIViewController, UITableViewDataSource, UITable
case let contentVC as DebugTableViewController:
mainPanelVC.track(scrollView: contentVC.tableView)
case let contentVC as NestedScrollViewController:
mainPanelVC.track(scrollView: contentVC.scrollView)
default:
fatalError()
break
}
// Add FloatingPanel to self.view
mainPanelVC.addPanel(toParent: self, belowView: nil, animated: true)
@@ -120,7 +127,7 @@ class SampleListViewController: UIViewController, UITableViewDataSource, UITable
// Add FloatingPanel to self.view
detailPanelVC.addPanel(toParent: self, belowView: nil, animated: true)
case .showModal:
case .showModal, .showTabBar:
let modalVC = contentVC
present(modalVC, animated: true, completion: nil)
default:
@@ -132,12 +139,27 @@ class SampleListViewController: UIViewController, UITableViewDataSource, UITable
}
}
class NestedScrollViewController: UIViewController {
@IBOutlet weak var scrollView: UIScrollView!
@IBAction func longPressed(_ sender: Any) {
print("LongPressed!")
}
@IBAction func swipped(_ sender: Any) {
print("Swipped!")
}
@IBAction func tapped(_ sender: Any) {
print("Tapped!")
}
}
class DebugTextViewController: UIViewController, UITextViewDelegate {
@IBOutlet weak var textView: UITextView!
override func viewDidLoad() {
super.viewDidLoad()
textView.delegate = self
if #available(iOS 11.0, *) {
textView.contentInsetAdjustmentBehavior = .never
}
@@ -189,7 +211,7 @@ class DebugTableViewController: UITableViewController {
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
print("Content View: viewDidAppear")
print("Content View: viewDidAppear", view.bounds)
}
override func viewWillDisappear(_ animated: Bool) {
@@ -238,6 +260,15 @@ class DetailViewController: UIViewController {
// dismiss(animated: true, completion: nil)
(self.parent as? FloatingPanelController)?.removePanelFromParent(animated: true, completion: nil)
}
@IBAction func tapped(_ sender: Any) {
print("Detail panel is tapped!")
}
@IBAction func swipped(_ sender: Any) {
print("Detail panel is swipped!")
}
@IBAction func longPressed(_ sender: Any) {
print("Detail panel is longPressed!")
}
}
class ModalViewController: UIViewController {
@@ -272,4 +303,115 @@ class ModalViewController: UIViewController {
@IBAction func close(sender: UIButton) {
dismiss(animated: true, completion: nil)
}
@IBAction func moveToFull(sender: UIButton) {
fpc.move(to: .full, animated: true)
}
@IBAction func moveToHalf(sender: UIButton) {
fpc.move(to: .half, animated: true)
}
@IBAction func moveToTip(sender: UIButton) {
fpc.move(to: .tip, animated: true)
}
}
class TabBarViewController: UITabBarController {}
class TabBarContentViewController: UIViewController, FloatingPanelControllerDelegate {
var fpc: FloatingPanelController!
var consoleVC: DebugTextViewController!
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
let consoleVC = storyboard?.instantiateViewController(withIdentifier: "ConsoleViewController") as! DebugTextViewController
fpc.show(consoleVC, sender: self)
self.consoleVC = consoleVC
fpc.track(scrollView: consoleVC.textView)
// Add FloatingPanel to self.view
fpc.addPanel(toParent: self)
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
// Remove FloatingPanel from a view
fpc.removePanelFromParent(animated: false)
}
func floatingPanel(_ vc: FloatingPanelController, layoutFor newCollection: UITraitCollection) -> FloatingPanelLayout? {
switch self.tabBarItem.tag {
case 0:
return OneTabBarPanelLayout()
case 1:
return TwoTabBarPanel2Layout()
default:
return nil
}
}
@IBAction func close(sender: UIButton) {
dismiss(animated: true, completion: nil)
}
}
extension FloatingPanelLayout {
func prepareLayout(surfaceView: UIView, in view: UIView) -> [NSLayoutConstraint] {
if #available(iOS 11.0, *) {
return [
surfaceView.leftAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leftAnchor, constant: 0.0),
surfaceView.rightAnchor.constraint(equalTo: view.safeAreaLayoutGuide.rightAnchor, constant: 0.0),
]
} else {
return [
surfaceView.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 0.0),
surfaceView.rightAnchor.constraint(equalTo: view.rightAnchor, constant: 0.0),
]
}
}
}
class OneTabBarPanelLayout: FloatingPanelLayout {
var initialPosition: FloatingPanelPosition {
return .tip
}
var supportedPositions: Set<FloatingPanelPosition> {
return [.full, .tip]
}
func insetFor(position: FloatingPanelPosition) -> CGFloat? {
switch position {
case .full: return 16.0
case .tip: return 22.0
default: return nil
}
}
}
class TwoTabBarPanel2Layout: FloatingPanelLayout {
var initialPosition: FloatingPanelPosition {
return .half
}
var supportedPositions: Set<FloatingPanelPosition> {
return [.full, .half]
}
var bottomInteractionBuffer: CGFloat {
return 261.0 - 22.0
}
func insetFor(position: FloatingPanelPosition) -> CGFloat? {
switch position {
case .full: return 16.0
case .half: return 261.0
default: return nil
}
}
}
+5 -13
View File
@@ -102,15 +102,14 @@ class NewsViewController: UIViewController {
// MARK: My custom layout
class FloatingPanelStocksLayout: FloatingPanelLayout {
public var supportedPositions: [FloatingPanelPosition] {
return [.full, .half, .tip]
}
public var initialPosition: FloatingPanelPosition {
var initialPosition: FloatingPanelPosition {
return .tip
}
public func insetFor(position: FloatingPanelPosition) -> CGFloat? {
var topInteractionBuffer: CGFloat { return 0.0 }
var bottomInteractionBuffer: CGFloat { return 0.0 }
func insetFor(position: FloatingPanelPosition) -> CGFloat? {
switch position {
case .full: return 56.0
case .half: return 262.0
@@ -118,13 +117,6 @@ class FloatingPanelStocksLayout: FloatingPanelLayout {
}
}
public func prepareLayout(surfaceView: UIView, in view: UIView) -> [NSLayoutConstraint] {
return [
surfaceView.leftAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leftAnchor, constant: 0.0),
surfaceView.rightAnchor.constraint(equalTo: view.safeAreaLayoutGuide.rightAnchor, constant: 0.0),
]
}
var backdropAlpha: CGFloat = 0.0
}
+1 -1
View File
@@ -1,7 +1,7 @@
Pod::Spec.new do |s|
s.name = "FloatingPanel"
s.version = "1.0.0"
s.version = "1.1.0"
s.summary = "FloatingPanel is a simple 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.
+77 -37
View File
@@ -35,8 +35,9 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
private(set) var state: FloatingPanelPosition = .tip
let panGesture: FloatingPanelPanGestureRecognizer
private var animator: UIViewPropertyAnimator?
private let panGesture: UIPanGestureRecognizer
private var initialFrame: CGRect = .zero
private var transOffsetY: CGFloat = 0
private var interactionInProgress: Bool = false
@@ -60,7 +61,7 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
layout: layout)
self.behavior = behavior
panGesture = UIPanGestureRecognizer()
panGesture = FloatingPanelPanGestureRecognizer()
if #available(iOS 11.0, *) {
panGesture.name = "FloatingPanelSurface"
@@ -83,17 +84,43 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
}
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) {
if to != .full {
lockScrollView()
}
if animated {
let animator = behavior.presentAnimator(self.viewcontroller, from: state, to: to)
let animator: UIViewPropertyAnimator
switch (from, to) {
case (nil, 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):
animator = behavior.removeAnimator(self.viewcontroller, from: from)
case (nil, nil):
fatalError()
}
animator.addAnimations { [weak self] in
guard let self = self else { return }
self.updateLayout(to: to)
self.state = to
if let to = to {
self.state = to
}
}
animator.addCompletion { _ in
completion?()
@@ -101,30 +128,9 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
animator.startAnimation()
} else {
self.updateLayout(to: to)
self.state = to
completion?()
}
}
func present(animated: Bool, completion: (() -> Void)? = nil) {
self.layoutAdapter.activateLayout(of: nil)
move(to: layoutAdapter.layout.initialPosition, animated: animated, completion: completion)
}
func dismiss(animated: Bool, completion: (() -> Void)? = nil) {
if animated {
let animator = behavior.dismissAnimator(self.viewcontroller, from: state)
animator.addAnimations { [weak self] in
guard let self = self else { return }
self.updateLayout(to: nil)
if let to = to {
self.state = to
}
animator.addCompletion { _ in
completion?()
}
animator.startAnimation()
} else {
self.updateLayout(to: nil)
completion?()
}
}
@@ -166,7 +172,7 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldBeRequiredToFailBy otherGestureRecognizer: UIGestureRecognizer) -> Bool {
guard gestureRecognizer == panGesture else { return false }
// Do not begin any gestures excluding scrollView?.panGestureRecognizer until the pan gesture fails
// Do not begin any gestures excluding the tracking scrollView's pan gesture until the pan gesture fails
if otherGestureRecognizer == scrollView?.panGestureRecognizer {
return false
} else {
@@ -174,6 +180,24 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
}
}
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.
switch otherGestureRecognizer {
case scrollView?.panGestureRecognizer:
return false
case is UIPanGestureRecognizer,
is UISwipeGestureRecognizer,
is UIRotationGestureRecognizer,
is UIScreenEdgePanGestureRecognizer,
is UIPinchGestureRecognizer:
return true
default:
return false
}
}
// MARK: - Gesture handling
@objc func handle(panGesture: UIPanGestureRecognizer) {
@@ -225,7 +249,7 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
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 don't nothing here.
// So I do nothing here.
log.debug("panningBegan \(initialFrame)")
}
@@ -288,9 +312,7 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
let y = rect.offsetBy(dx: 0.0, dy: dy).origin.y
let topY = layoutAdapter.topY
let topInset = layoutAdapter.topInset
let topBuffer = layoutAdapter.layout.topInteractionBuffer
let bottomY = layoutAdapter.bottomY
let bottomBuffer = layoutAdapter.layout.bottomInteractionBuffer
@@ -300,7 +322,7 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
return max(topY, min(bottomY, y))
}
}
return max(topY - topInset + topBuffer, min(bottomY + bottomBuffer, y))
return max(topY - topBuffer, min(bottomY + bottomBuffer, y))
}
private func startAnimation(to targetPosition: FloatingPanelPosition, at distance: CGFloat, with velocity: CGPoint) {
@@ -364,16 +386,16 @@ 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: Set = layoutAdapter.layout.supportedPositions
assert(supportedPositions.count > 1)
switch supportedPositions {
case Set([.full, .half]):
case [.full, .half]:
return targetPosition(from: [.full, .half], at: currentY, velocity: velocity)
case [.half, .tip]:
return targetPosition(from: [.half, .tip], at: currentY, velocity: velocity)
case Set([.half, .tip]):
return targetPosition(from: [.half, .tip], at: currentY, velocity: velocity)
case Set([.full, .tip]):
case [.full, .tip]:
return targetPosition(from: [.full, .tip], at: currentY, velocity: velocity)
default:
/*
@@ -485,3 +507,21 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
}
}
}
class FloatingPanelPanGestureRecognizer: UIPanGestureRecognizer {
override weak var delegate: UIGestureRecognizerDelegate? {
get {
return super.delegate
}
set {
guard newValue is FloatingPanel else {
let exception = NSException(name: .invalidArgumentException,
reason: "FloatingPanelController's built-in pan gesture recognizer must have its controller as its delegate.",
userInfo: nil)
exception.raise()
return
}
super.delegate = newValue
}
}
}
+24 -7
View File
@@ -6,21 +6,38 @@
import UIKit
public protocol FloatingPanelBehavior {
// Returns a UIViewPropertyAnimator object in interacting a floating panel by a user pan gesture
/// Returns a UIViewPropertyAnimator object for interacting with a floating panel by a user pan gesture
func interactionAnimator(_ fpc: FloatingPanelController, to targetPosition: FloatingPanelPosition, with velocity: CGVector) -> UIViewPropertyAnimator
// Returns a UIViewPropertyAnimator object to present a floating panel
func presentAnimator(_ fpc: FloatingPanelController, from: FloatingPanelPosition, to: FloatingPanelPosition) -> UIViewPropertyAnimator
// Returns a UIViewPropertyAnimator object to dismiss a floating panel
func dismissAnimator(_ fpc: FloatingPanelController, from: FloatingPanelPosition) -> UIViewPropertyAnimator
/// Returns a UIViewPropertyAnimator object to add a floating panel to a position.
///
/// Its animator instance will be used to animate the surface view in `FloatingPanelController.addPanel(toParent:belowView:animated:)`.
/// Default is an animator with ease-in-out curve and 0.25 sec duration.
func addAnimator(_ fpc: FloatingPanelController, to: FloatingPanelPosition) -> UIViewPropertyAnimator
/// Returns a UIViewPropertyAnimator object to remove a floating panel from a position.
///
/// Its animator instance will be used to animate the surface view in `FloatingPanelController.removePanelFromParent(animated:completion:)`.
/// Default is an animator with ease-in-out curve and 0.25 sec duration.
func removeAnimator(_ fpc: FloatingPanelController, from: FloatingPanelPosition) -> UIViewPropertyAnimator
/// Returns a UIViewPropertyAnimator object to move a floating panel from a position to a position.
///
/// 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
}
public extension FloatingPanelBehavior {
func presentAnimator(_ fpc: FloatingPanelController, from: FloatingPanelPosition, to: FloatingPanelPosition) -> UIViewPropertyAnimator {
func addAnimator(_ fpc: FloatingPanelController, to: FloatingPanelPosition) -> UIViewPropertyAnimator {
return UIViewPropertyAnimator(duration: 0.25, curve: .easeInOut)
}
func dismissAnimator(_ fpc: FloatingPanelController, from: FloatingPanelPosition) -> UIViewPropertyAnimator {
func removeAnimator(_ fpc: FloatingPanelController, from: FloatingPanelPosition) -> UIViewPropertyAnimator {
return UIViewPropertyAnimator(duration: 0.25, curve: .easeInOut)
}
func moveAnimator(_ fpc: FloatingPanelController, from: FloatingPanelPosition, to: FloatingPanelPosition) -> UIViewPropertyAnimator {
return UIViewPropertyAnimator(duration: 0.25, curve: .easeInOut)
}
}
+25 -19
View File
@@ -36,7 +36,7 @@ public extension FloatingPanelControllerDelegate {
func floatingPanelDidEndDecelerating(_ vc: FloatingPanelController) {}
}
public enum FloatingPanelPosition: Int {
public enum FloatingPanelPosition: Int, CaseIterable {
case full
case half
case tip
@@ -66,35 +66,48 @@ public class FloatingPanelController: UIViewController, UIScrollViewDelegate, UI
return floatingPanel.backdropView
}
/// Returns the scroll view that the conroller tracks.
/// Returns the scroll view that the controller tracks.
public weak var scrollView: UIScrollView? {
return floatingPanel.scrollView
}
// The underlying gesture recognizer for pan gestures
public var panGestureRecognizer: UIPanGestureRecognizer {
return floatingPanel.panGesture
}
/// The current position of the floating panel controller's contents.
public var position: FloatingPanelPosition {
return floatingPanel.state
}
/// The content insets of the tracking scroll view derived the safe area of the parent view
/// The content insets of the tracking scroll view derived from the safe area of the parent view
public var adjustedContentInsets: UIEdgeInsets {
return floatingPanel.layoutAdapter.adjustedContentInsets
}
/// The behavior for determining the adjusted content offsets.
///
/// This property specifies how the content area of the tracking scroll view are modified using `adjustedContentInsets`. The default value of this property is FloatingPanelController.ContentInsetAdjustmentBehavior.always.
/// 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
private var floatingPanel: FloatingPanel!
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
floatingPanel = FloatingPanel(self,
layout: fetchLayout(for: self.traitCollection),
behavior: fetchBehavior(for: self.traitCollection))
}
/// Initialize a newly created a floating panel controller.
/// Initialize a newly created floating panel controller.
public init() {
super.init(nibName: nil, bundle: nil)
floatingPanel = FloatingPanel(self,
layout: fetchLayout(for: self.traitCollection),
behavior: fetchBehavior(for: self.traitCollection))
}
/// Creates the view that the controller manages.
@@ -105,12 +118,6 @@ public class FloatingPanelController: UIViewController, UIScrollViewDelegate, UI
view.backgroundColor = .white
self.view = view as UIView
let layout = fetchLayout(for: self.traitCollection)
let behavior = fetchBehavior(for: self.traitCollection)
floatingPanel = FloatingPanel(self,
layout: layout,
behavior: behavior)
}
public override func willTransition(to newCollection: UITraitCollection, with coordinator: UIViewControllerTransitionCoordinator) {
@@ -133,14 +140,13 @@ public class FloatingPanelController: UIViewController, UIScrollViewDelegate, UI
if let parent = parent {
self.update(safeAreaInsets: parent.layoutInsets)
}
floatingPanel.layoutAdapter.updateHeight()
floatingPanel.backdropView.isHidden = (traitCollection.verticalSizeClass == .compact)
}
public override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
// I needs to update safeAreaInsets here to ensure that the `adjustedContentInsets` has a correct value.
// 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 {
@@ -174,10 +180,10 @@ public class FloatingPanelController: UIViewController, UIScrollViewDelegate, UI
// MARK: - Container view controller interface
/// Adds the view managed the controller as a child of the specified view controller.
/// 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 conatiner view controller object isn't applicable.
/// - belowView: Insert the surface view managed by the controller below the specified view. As default, the surface view will be added to the end of the parent list of subviews.
/// - parent: A parent view controller object that displays FloatingPanelController's view. A container view controller object isn't applicable.
/// - belowView: Insert the surface view managed by the controller below the specified view. By default, the surface view will be added to the end of the parent list of subviews.
/// - animated: Pass true to animate the presentation; otherwise, pass false.
public func addPanel(toParent parent: UIViewController, belowView: UIView? = nil, animated: Bool = false) {
guard self.parent == nil else {
@@ -197,7 +203,7 @@ public class FloatingPanelController: UIViewController, UIScrollViewDelegate, UI
parent.addChild(self)
// Must set a layout again here because `self.traitCollection` is applied correctly on it's added to a parent VC
// 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
@@ -230,7 +236,7 @@ public class FloatingPanelController: UIViewController, UIScrollViewDelegate, UI
/// - Parameters:
/// - to: Pass a FloatingPanelPosition value to move the surface view to the position.
/// - animated: Pass true to animate the presentation; otherwise, pass false.
/// - completion: The block to execute after the view controller is dismissed. This block has no return value and takes no parameters. You may specify nil for this parameter.
/// - 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) {
floatingPanel.move(to: to, animated: animated, completion: completion)
}
@@ -256,7 +262,7 @@ public class FloatingPanelController: UIViewController, UIScrollViewDelegate, UI
/// Tracks the specified scroll view to correspond with the scroll.
///
/// - Attention:
/// The specified scroll view must be already assigned the delegate property because the controller intemediates the several delegate methods.
/// The specified scroll view must be already assigned to the delegate property because the controller intermediates between the various delegate methods.
///
public func track(scrollView: UIScrollView) {
floatingPanel.scrollView = scrollView
+80 -47
View File
@@ -6,24 +6,28 @@
import UIKit
public protocol FloatingPanelLayout: class {
/// Returns the initial position of a floating panel
/// Returns the initial position of a floating panel.
var initialPosition: FloatingPanelPosition { get }
/// Returns an array of FloatingPanelPosition object to tell the applicable position the floating panel controller
var supportedPositions: [FloatingPanelPosition] { get }
/// Return the interaction buffer of full position. Default is 6.0.
/// Returns a set of FloatingPanelPosition objects to tell the applicable positions of the floating panel controller. Default is all of them.
var supportedPositions: Set<FloatingPanelPosition> { get }
/// Return the interaction buffer to the top from the top position. Default is 6.0.
var topInteractionBuffer: CGFloat { get }
/// Return the interaction buffer of full position. Default is 6.0.
/// Return the interaction buffer to the bottom from the bottom position. Default is 6.0.
var bottomInteractionBuffer: CGFloat { get }
/// Returns a CGFloat value for a floating panel position(full, half, tip).
/// A value for full position indicates an inset from the safe area top.
/// On the other hand, values fro half and tip positions indicate insets from the safe area bottom.
/// 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.
func insetFor(position: FloatingPanelPosition) -> CGFloat?
/// Returns layout constraints for a surface view of a floaitng panel.
/// The layout constraints must not include ones for topAnchor and bottomAnchor
/// because constarints for them will be added by the floating panel controller.
/// Returns X-axis and width layout constraints of the surface view of a floating panel.
/// You must not include any Y-axis and height layout constraints of the surface view
/// because their constraints will be configured by the floating panel controller.
/// 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.
@@ -34,13 +38,20 @@ 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)
}
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),
]
}
}
public class FloatingPanelDefaultLayout: FloatingPanelLayout {
public var supportedPositions: [FloatingPanelPosition] {
return [.full, .half, .tip]
}
public var initialPosition: FloatingPanelPosition {
return .half
}
@@ -52,20 +63,13 @@ public class FloatingPanelDefaultLayout: FloatingPanelLayout {
case .tip: return 69.0
}
}
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),
]
}
}
public class FloatingPanelDefaultLandscapeLayout: FloatingPanelLayout {
public var initialPosition: FloatingPanelPosition {
return .tip
}
public var supportedPositions: [FloatingPanelPosition] {
public var supportedPositions: Set<FloatingPanelPosition> {
return [.full, .tip]
}
@@ -79,9 +83,9 @@ public class FloatingPanelDefaultLandscapeLayout: FloatingPanelLayout {
public func prepareLayout(surfaceView: UIView, in view: UIView) -> [NSLayoutConstraint] {
return [
surfaceView.leftAnchor.constraint(equalTo: view.sideLayoutGuide.leftAnchor, constant: 8.0),
surfaceView.widthAnchor.constraint(equalToConstant: 291),
]
surfaceView.leftAnchor.constraint(equalTo: view.sideLayoutGuide.leftAnchor, constant: 0.0),
surfaceView.rightAnchor.constraint(equalTo: view.sideLayoutGuide.rightAnchor, constant: 0.0),
]
}
}
@@ -90,9 +94,15 @@ class FloatingPanelLayoutAdapter {
private weak var surfaceView: FloatingPanelSurfaceView!
private weak var backdropVIew: FloatingPanelBackdropView!
var layout: FloatingPanelLayout
var layout: FloatingPanelLayout {
didSet { checkConsistance(of: layout) }
}
var safeAreaInsets: UIEdgeInsets = .zero
var safeAreaInsets: UIEdgeInsets = .zero {
didSet {
updateHeight()
}
}
private var heightBuffer: CGFloat = 88.0 // For bounce
private var fixedConstraints: [NSLayoutConstraint] = []
@@ -102,18 +112,22 @@ class FloatingPanelLayoutAdapter {
private var offConstraints: [NSLayoutConstraint] = []
private var heightConstraints: NSLayoutConstraint? = nil
var topInset: CGFloat {
private var fullInset: CGFloat {
return layout.insetFor(position: .full) ?? 0.0
}
var halfInset: CGFloat {
private var halfInset: CGFloat {
return layout.insetFor(position: .half) ?? 0.0
}
var tipInset: CGFloat {
private var tipInset: CGFloat {
return layout.insetFor(position: .tip) ?? 0.0
}
var topY: CGFloat {
return (safeAreaInsets.top + topInset)
if layout.supportedPositions.contains(.full) {
return (safeAreaInsets.top + fullInset)
} else {
return middleY
}
}
var middleY: CGFloat {
@@ -121,13 +135,17 @@ class FloatingPanelLayoutAdapter {
}
var bottomY: CGFloat {
return surfaceView.superview!.bounds.height - (safeAreaInsets.bottom + tipInset)
if layout.supportedPositions.contains(.tip) {
return surfaceView.superview!.bounds.height - (safeAreaInsets.bottom + tipInset)
} else {
return middleY
}
}
var adjustedContentInsets: UIEdgeInsets {
return UIEdgeInsets(top: 0.0,
left: 0.0,
bottom: (safeAreaInsets.top + topInset) + (heightBuffer + safeAreaInsets.bottom),
bottom: safeAreaInsets.bottom,
right: 0.0)
}
@@ -146,13 +164,6 @@ class FloatingPanelLayoutAdapter {
self.layout = layout
self.surfaceView = surfaceView
self.backdropVIew = backdropView
// Verify layout configurations
assert(layout.supportedPositions.count > 1)
assert(layout.supportedPositions.contains(layout.initialPosition))
if halfInset > 0 {
assert(halfInset >= tipInset)
}
}
func prepareLayout(toParent parent: UIViewController) {
@@ -178,7 +189,7 @@ class FloatingPanelLayoutAdapter {
// Flexible surface constarints for full, half, tip and off
fullConstraints = [
surfaceView.topAnchor.constraint(equalTo: parent.layoutGuide.topAnchor,
constant: topInset),
constant: fullInset),
]
halfConstraints = [
surfaceView.topAnchor.constraint(equalTo: parent.layoutGuide.bottomAnchor,
@@ -202,12 +213,16 @@ class FloatingPanelLayoutAdapter {
}
}
if let heightConstraints = self.heightConstraints {
NSLayoutConstraint.deactivate([heightConstraints])
if let consts = self.heightConstraints {
NSLayoutConstraint.deactivate([consts])
}
let heightConstraints = surfaceView.heightAnchor.constraint(equalToConstant: UIScreen.main.bounds.height + heightBuffer)
NSLayoutConstraint.activate([heightConstraints])
self.heightConstraints = heightConstraints
let height = UIScreen.main.bounds.height - (safeAreaInsets.top + fullInset)
let consts = surfaceView.heightAnchor.constraint(equalToConstant: height)
NSLayoutConstraint.activate([consts])
heightConstraints = consts
surfaceView.bottomOverflow = heightBuffer
}
func activateLayout(of state: FloatingPanelPosition?) {
@@ -240,4 +255,22 @@ class FloatingPanelLayoutAdapter {
NSLayoutConstraint.activate(tipConstraints)
}
}
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))")
}
if halfInset > 0 {
assert(halfInset > tipInset, "Invalid half and tip insets")
}
if fullInset > 0 {
assert(middleY > topY, "Invalid insets")
assert(bottomY > topY, "Invalid insets")
}
}
}
@@ -22,6 +22,7 @@ public class FloatingPanelSurfaceView: UIView {
public var contentView: UIView!
private var color: UIColor? = .white { didSet { setNeedsDisplay() } }
var bottomOverflow: CGFloat = 0.0 { didSet { setNeedsDisplay() }}
public override var backgroundColor: UIColor? {
get { return color }
@@ -31,7 +32,10 @@ public class FloatingPanelSurfaceView: UIView {
}
}
/// The radius to use when drawing rounded corners
/// The radius to use when drawing top rounded corners.
///
/// `self.contentView` is masked with the top rounded corners automatically on iOS 11 and later.
/// On iOS 10, they are not automatically masked because of a UIVisualEffectView issue. See https://forums.developer.apple.com/thread/50854
public var cornerRadius: CGFloat = 0.0 { didSet { setNeedsLayout() } }
/// A Boolean indicating whether the surface shadow is displayed.
@@ -73,6 +77,7 @@ public class FloatingPanelSurfaceView: UIView {
private func render() {
super.backgroundColor = .clear
self.clipsToBounds = false
let contentView = FloatingPanelSurfaceContentView()
addSubview(contentView)
@@ -97,30 +102,43 @@ 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()
// Don't use `contentView.layer.mask` because of UIVisualEffectView issue on ios10, https://forums.developer.apple.com/thread/50854
contentView.layer.cornerRadius = cornerRadius
contentView.clipsToBounds = true
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.
// Because the bottom of contentView must be fit to the bottom of a screen to work the `safeLayoutGuide` of a content VC.
let maskLayer = CAShapeLayer()
var rect = bounds
rect.size.height += bottomOverflow
let path = UIBezierPath(roundedRect: rect,
byRoundingCorners: [.topLeft, .topRight],
cornerRadii: CGSize(width: cornerRadius, height: cornerRadius))
maskLayer.path = path.cgPath
contentView.layer.mask = maskLayer
} else {
// 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() {
if shadowLayer != nil {
shadowLayer.removeFromSuperlayer()
}
shadowLayer = makeShadowLayer()
layer.insertSublayer(shadowLayer, at: 0)
}
private func makeShadowLayer() -> CAShapeLayer {
log.debug("SurfaceView bounds", bounds)
let shadowLayer = CAShapeLayer()
let path = UIBezierPath(roundedRect: 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
@@ -132,6 +150,5 @@ public class FloatingPanelSurfaceView: UIView {
shadowLayer.shadowOpacity = shadowOpacity
shadowLayer.shadowRadius = shadowRadius
}
return shadowLayer
}
}
+1 -1
View File
@@ -15,7 +15,7 @@
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<string>1.1.0</string>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
</dict>
+119 -41
View File
@@ -1,5 +1,12 @@
[![Build Status](https://travis-ci.org/SCENEE/FloatingPanel.svg?branch=master)](https://travis-ci.org/SCENEE/FloatingPanel)
[![Version](https://img.shields.io/cocoapods/v/FloatingPanel.svg)](https://cocoapods.org/pods/FloatingPanel)
[![Carthage compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage)
[![Platform](https://img.shields.io/cocoapods/p/FloatingPanel.svg)](https://cocoapods.org/pods/FloatingPanel)
[![Swift 4.2](https://img.shields.io/badge/Swift-4.2-orange.svg?style=flat)](https://swift.org/)
# FloatingPanel
FloatingPanel is a simple and easy-to-use UI component for a new interface introduced in Apple Maps, Shortcuts and Stocks app.
The new interface displays the related contents and utilities in parallel as a user wants.
@@ -8,6 +15,30 @@ The new interface displays the related contents and utilities in parallel as a u
![Maps(Landscape)](https://github.com/SCENEE/FloatingPanel/blob/master/assets/maps-landscape.gif)
<!-- TOC -->
- [Features](#features)
- [Requirements](#requirements)
- [Installation](#installation)
- [CocoaPods](#cocoapods)
- [Carthage](#carthage)
- [Getting Started](#getting-started)
- [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)
- [Support your landscape layout](#support-your-landscape-layout)
- [Customize the behavior with `FloatingPanelBehavior` protocol](#customize-the-behavior-with-floatingpanelbehavior-protocol)
- [Modify your floating panel's interaction](#modify-your-floating-panels-interaction)
- [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)
- [Notes](#notes)
- [FloatingPanelSurfaceView's issue on iOS 10](#floatingpanelsurfaceviews-issue-on-ios-10)
- [Author](#author)
- [License](#license)
<!-- /TOC -->
## Features
- [x] Simple container view controller
@@ -87,50 +118,41 @@ class ViewController: UIViewController, FloatingPanelControllerDelegate {
## Usage
### Move a positon with an animation
### Customize the layout of a floating panel with `FloatingPanelLayout` protocol
Move a floating panel to the top and middle of a view while opening and closeing a search bar like Apple Maps.
```swift
func searchBarCancelButtonClicked(_ searchBar: UISearchBar) {
...
fpc.move(to: .half, animated: true)
}
func searchBarTextDidBeginEditing(_ searchBar: UISearchBar) {
...
fpc.move(to: .full, animated: true)
}
```
### Make your contents correspond with FloatingPanel behavior
```swift
class ViewController: UIViewController, FloatingPanelControllerDelegate {
...
func floatingPanelWillBeginDragging(_ vc: FloatingPanelController) {
if vc.position == .full {
searchVC.searchBar.showsCancelButton = false
searchVC.searchBar.resignFirstResponder()
}
}
func floatingPanelDidEndDragging(_ vc: FloatingPanelController, withVelocity velocity: CGPoint, targetPosition: FloatingPanelPosition) {
if targetPosition != .full {
searchVC.hideHeader()
}
}
...
}
```
### Support your landscape layout with a `FloatingPanelLayout` object
#### Change the initial position and height
```swift
class ViewController: UIViewController, FloatingPanelControllerDelegate {
...
func floatingPanel(_ vc: FloatingPanelController, layoutFor newCollection: UITraitCollection) -> FloatingPanelLayout? {
return (newCollection.verticalSizeClass == .compact) ? FloatingPanelLandscapeLayout() : nil
return MyFloatingPanelLayout()
}
...
}
class MyFloatingPanelLayout: FloatingPanelLayout {
public var initialPosition: FloatingPanelPosition {
return .tip
}
public func insetFor(position: FloatingPanelPosition) -> CGFloat? {
switch position {
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
}
}
}
```
#### Support your landscape layout
```swift
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
}
...
}
@@ -139,7 +161,7 @@ class FloatingPanelLandscapeLayout: FloatingPanelLayout {
public var initialPosition: FloatingPanelPosition {
return .tip
}
public var supportedPositions: [FloatingPanelPosition] {
public var supportedPositions: Set<FloatingPanelPosition> {
return [.full, .tip]
}
@@ -160,7 +182,9 @@ class FloatingPanelLandscapeLayout: FloatingPanelLayout {
}
```
### Modify your floating panel's interaction with a `FloatingPanelBehavior` object
### Customize the behavior with `FloatingPanelBehavior` protocol
#### Modify your floating panel's interaction
```swift
class ViewController: UIViewController, FloatingPanelControllerDelegate {
@@ -177,7 +201,7 @@ class FloatingPanelStocksBehavior: FloatingPanelBehavior {
return 15.0
}
func interactionAnimator(to targetPosition: FloatingPanelPosition, with velocity: CGVector) -> UIViewPropertyAnimator {
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)
@@ -216,6 +240,60 @@ class ViewController: UIViewController, FloatingPanelControllerDelegate {
}
```
### Move a position with an animation
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 searchBarTextDidBeginEditing(_ searchBar: UISearchBar) {
...
fpc.move(to: .full, animated: true)
}
```
### Make your contents correspond with a floating panel behavior
```swift
class ViewController: UIViewController, FloatingPanelControllerDelegate {
...
func floatingPanelWillBeginDragging(_ vc: FloatingPanelController) {
if vc.position == .full {
searchVC.searchBar.showsCancelButton = false
searchVC.searchBar.resignFirstResponder()
}
}
func floatingPanelDidEndDragging(_ vc: FloatingPanelController, withVelocity velocity: CGPoint, targetPosition: FloatingPanelPosition) {
if targetPosition != .full {
searchVC.hideHeader()
}
}
...
}
```
## Notes
### 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() {
super.viewDidLayoutSubviews()
if #available(iOS 10, *) {
visualEffectView.layer.cornerRadius = 9.0
visualEffectView.clipsToBounds = true
}
}
```
* 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.
## Author
Shin Yamamoto <shin@scenee.com>