Compare commits
119 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0bb8342873 | |||
| d4f2a88fdf | |||
| af45d39841 | |||
| 66f5b0b210 | |||
| 4a3b79f1b4 | |||
| 12a100def8 | |||
| 47971f607a | |||
| 03a4d342a3 | |||
| 4f5abfefec | |||
| e1ee3c06e8 | |||
| 17ba704472 | |||
| e5391fa1f4 | |||
| c0647017b5 | |||
| 3686bb4b44 | |||
| 76c8ca4b20 | |||
| c53e64027b | |||
| c15d236320 | |||
| 281504c9c6 | |||
| eaf0ebe62b | |||
| a4a08662be | |||
| e8a0ffeca5 | |||
| 1399cc6fbd | |||
| 8b44ad4b08 | |||
| e4a1a6e293 | |||
| ba00786b91 | |||
| 5e7529d1e6 | |||
| e75108113a | |||
| 6d51b0d420 | |||
| c3b199755e | |||
| d57d8e9da5 | |||
| d24e1c5355 | |||
| 95c94560be | |||
| 7b4ed52eb1 | |||
| cab8c15474 | |||
| 72f5d59a75 | |||
| 273adc8d1b | |||
| 5f0f28cb0e | |||
| e1c9fe120b | |||
| 630580beb6 | |||
| 68a2c43580 | |||
| 817fce6d10 | |||
| 6e85afaee6 | |||
| 98c5096f67 | |||
| 57c7ced59d | |||
| 73e6d38344 | |||
| bd128bf8b0 | |||
| 16e8808ce5 | |||
| f4088fcb6b | |||
| 63b8aa24e8 | |||
| 5744491606 | |||
| f5ecbef724 | |||
| f176a2c70e | |||
| 51c124d3e4 | |||
| 5f6c97336e | |||
| 894eb77d5d | |||
| d1b5a1f517 | |||
| 300d5f8d91 | |||
| bbc885f783 | |||
| 6badeeebe5 | |||
| e282806422 | |||
| 629807584b | |||
| 922c0e53d2 | |||
| ec9fcd473a | |||
| de9f415ded | |||
| fd5ca2c2fc | |||
| c0b9ddc4a3 | |||
| 43f33083f1 | |||
| 2b483e6adb | |||
| 0cf0f42ca4 | |||
| c9ccea3f84 | |||
| c2dee28132 | |||
| 4fd4709182 | |||
| 00ccc0eb6a | |||
| ed91f51482 | |||
| c2cea95aa5 | |||
| 274027cb64 | |||
| b4a26344d9 | |||
| 580c708788 | |||
| f4d6380094 | |||
| e44dc06a61 | |||
| b6184f5b41 | |||
| e6fc2f397e | |||
| 60d0b62675 | |||
| 7a8eb1833f | |||
| 6367b76b9d | |||
| 977b685071 | |||
| b97d418158 | |||
| 0e4cb372d5 | |||
| 2ec7576ae9 | |||
| c4bf4c3067 | |||
| ea9bbcad27 | |||
| 71be1f2ed5 | |||
| 349bb91c6c | |||
| 52da673358 | |||
| ce0b9d1413 | |||
| 8ef332f3e5 | |||
| a4002f83c1 | |||
| 64d756d8a9 | |||
| 187fe47268 | |||
| 060f3a0b1b | |||
| 4dcc5bc564 | |||
| 52efac6643 | |||
| 97c91fb7aa | |||
| efcc598550 | |||
| fd5fc1f485 | |||
| f713d4057f | |||
| 4ebbea8e86 | |||
| 5515e6f788 | |||
| 65a6315f1b | |||
| aafe32bb3d | |||
| 1c6c783dbe | |||
| 1e322f47d4 | |||
| 37196abe77 | |||
| e476cf5ce4 | |||
| dc4b1e7a90 | |||
| 95d188d5f1 | |||
| 4dd60ca855 | |||
| 5067917295 | |||
| e620ef27ee |
+44
@@ -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"/>
|
||||
|
||||
@@ -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()
|
||||
@@ -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,42 @@ 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),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
public func backdropAlphaFor(position: FloatingPanelPosition) -> CGFloat {
|
||||
return 0.0
|
||||
}
|
||||
}
|
||||
|
||||
class SearchCell: UITableViewCell {
|
||||
@IBOutlet weak var iconImageView: UIImageView!
|
||||
@IBOutlet weak var titleLabel: UILabel!
|
||||
|
||||
@@ -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">
|
||||
<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="retina4_7" 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>
|
||||
@@ -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,49 @@
|
||||
<action selector="closeWithSender:" destination="bYI-y3-Rzb" eventType="touchUpInside" id="MSC-ch-YJK"/>
|
||||
</connections>
|
||||
</button>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" alignment="top" spacing="44" translatesAutoresizingMaskIntoConstraints="NO" id="9p4-06-y2T">
|
||||
<rect key="frame" x="139.5" y="108" 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"/>
|
||||
<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>
|
||||
<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>
|
||||
<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 +242,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="1311" y="806"/>
|
||||
</scene>
|
||||
<!--Detail View Controller-->
|
||||
<scene sceneID="b6k-zi-3wn">
|
||||
@@ -134,22 +383,86 @@
|
||||
<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>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="22" translatesAutoresizingMaskIntoConstraints="NO" id="tP3-oJ-4EB">
|
||||
<rect key="frame" x="130.5" y="108" 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"/>
|
||||
<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"/>
|
||||
</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>
|
||||
<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"/>
|
||||
<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="1440.8" y="-23.388305847076463"/>
|
||||
</scene>
|
||||
<!--Debug Text View Controller-->
|
||||
<scene sceneID="Bkq-O7-q4A">
|
||||
@@ -221,7 +534,10 @@ 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>
|
||||
<inferredMetricsTieBreakers>
|
||||
<segue reference="3yq-HE-Tgn"/>
|
||||
</inferredMetricsTieBreakers>
|
||||
</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),
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,13 +17,19 @@ class SampleListViewController: UIViewController, UITableViewDataSource, UITable
|
||||
case trackingTextView
|
||||
case showDetail
|
||||
case showModal
|
||||
case showTabBar
|
||||
case showNestedScrollView
|
||||
case showRemovablePanel
|
||||
|
||||
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 .showTabBar: return "Show Tab Bar"
|
||||
case .showNestedScrollView: return "Show Nested ScrollView"
|
||||
case .showRemovablePanel: return "Show Removable Panel"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,12 +39,16 @@ 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"
|
||||
case .showRemovablePanel: return "DetailViewController"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var mainPanelVC: FloatingPanelController!
|
||||
var detailPanelVC: FloatingPanelController!
|
||||
var currentMenu: Menu = .trackingTableView
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
@@ -46,30 +56,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
|
||||
mainPanelVC.isRemovalInteractionEnabled = (currentMenu == .showRemovablePanel)
|
||||
|
||||
// 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)
|
||||
|
||||
// Track a scroll view
|
||||
switch contentVC {
|
||||
case let consoleVC as DebugTextViewController:
|
||||
mainPanelVC.track(scrollView: consoleVC.textView)
|
||||
|
||||
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)
|
||||
@@ -97,11 +115,13 @@ 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()
|
||||
@@ -113,14 +133,12 @@ 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:
|
||||
case .showModal, .showTabBar:
|
||||
let modalVC = contentVC
|
||||
present(modalVC, animated: true, completion: nil)
|
||||
default:
|
||||
@@ -130,6 +148,85 @@ class SampleListViewController: UIViewController, UITableViewDataSource, UITable
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func floatingPanel(_ vc: FloatingPanelController, layoutFor newCollection: UITraitCollection) -> FloatingPanelLayout? {
|
||||
if currentMenu == .showRemovablePanel {
|
||||
return newCollection.verticalSizeClass == .compact ? RemovablePanelLandscapeLayout() : RemovablePanelLayout()
|
||||
} else {
|
||||
return 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class RemovablePanelLayout: 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
|
||||
}
|
||||
}
|
||||
func backdropAlphaFor(position: FloatingPanelPosition) -> CGFloat {
|
||||
return 0.3
|
||||
}
|
||||
}
|
||||
|
||||
class RemovablePanelLandscapeLayout: FloatingPanelLayout {
|
||||
var initialPosition: FloatingPanelPosition {
|
||||
return .half
|
||||
}
|
||||
var supportedPositions: Set<FloatingPanelPosition> {
|
||||
return [.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 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 {
|
||||
@@ -138,6 +235,7 @@ class DebugTextViewController: UIViewController, UITextViewDelegate {
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
textView.delegate = self
|
||||
|
||||
if #available(iOS 11.0, *) {
|
||||
textView.contentInsetAdjustmentBehavior = .never
|
||||
}
|
||||
@@ -157,16 +255,98 @@ class DebugTextViewController: UIViewController, UITextViewDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
@@ -189,7 +369,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) {
|
||||
@@ -216,19 +396,28 @@ 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 {
|
||||
@@ -238,27 +427,54 @@ class DetailViewController: UIViewController {
|
||||
// dismiss(animated: true, completion: nil)
|
||||
(self.parent as? FloatingPanelController)?.removePanelFromParent(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!")
|
||||
}
|
||||
@IBAction func swipped(_ sender: Any) {
|
||||
print("Detail panel is swipped!")
|
||||
}
|
||||
@IBAction func longPressed(_ sender: Any) {
|
||||
print("Detail panel is longPressed!")
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
@@ -272,4 +488,140 @@ 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)
|
||||
}
|
||||
|
||||
@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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
// Set a content view controller and track the scroll view
|
||||
let consoleVC = storyboard?.instantiateViewController(withIdentifier: "ConsoleViewController") as! DebugTextViewController
|
||||
fpc.set(contentViewController: consoleVC)
|
||||
fpc.track(scrollView: consoleVC.textView)
|
||||
self.consoleVC = consoleVC
|
||||
|
||||
// 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
@@ -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,14 +117,9 @@ 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),
|
||||
]
|
||||
func backdropAlphaFor(position: FloatingPanelPosition) -> CGFloat {
|
||||
return 0.0
|
||||
}
|
||||
|
||||
var backdropAlpha: CGFloat = 0.0
|
||||
}
|
||||
|
||||
// MARK: My custom behavior
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
Pod::Spec.new do |s|
|
||||
|
||||
s.name = "FloatingPanel"
|
||||
s.version = "1.0.0"
|
||||
s.summary = "FloatingPanel is a simple and easy-to-use UI component of a floating panel interface"
|
||||
s.version = "1.2.2"
|
||||
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"
|
||||
|
||||
@@ -33,11 +33,21 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
|
||||
|
||||
unowned let viewcontroller: FloatingPanelController
|
||||
|
||||
private(set) var state: FloatingPanelPosition = .tip
|
||||
private(set) var state: FloatingPanelPosition = .tip {
|
||||
didSet { viewcontroller.delegate?.floatingPanelDidChangePosition(viewcontroller) }
|
||||
}
|
||||
|
||||
private var isBottomState: Bool {
|
||||
let remains = layoutAdapter.layout.supportedPositions.filter { $0.rawValue > state.rawValue }
|
||||
return remains.count == 0
|
||||
}
|
||||
|
||||
let panGesture: FloatingPanelPanGestureRecognizer
|
||||
var isRemovalInteractionEnabled: Bool = false
|
||||
|
||||
private var animator: UIViewPropertyAnimator?
|
||||
private let panGesture: UIPanGestureRecognizer
|
||||
private var initialFrame: CGRect = .zero
|
||||
private var initialScrollOffset: CGPoint = .zero
|
||||
private var transOffsetY: CGFloat = 0
|
||||
private var interactionInProgress: Bool = false
|
||||
|
||||
@@ -60,7 +70,9 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
|
||||
layout: layout)
|
||||
self.behavior = behavior
|
||||
|
||||
panGesture = UIPanGestureRecognizer()
|
||||
state = layoutAdapter.layout.initialPosition
|
||||
|
||||
panGesture = FloatingPanelPanGestureRecognizer()
|
||||
|
||||
if #available(iOS 11.0, *) {
|
||||
panGesture.name = "FloatingPanelSurface"
|
||||
@@ -73,7 +85,7 @@ 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)
|
||||
@@ -83,17 +95,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 +139,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?()
|
||||
}
|
||||
}
|
||||
@@ -133,23 +150,24 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
|
||||
|
||||
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
|
||||
@@ -158,59 +176,155 @@ 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 scrollView?.panGestureRecognizer 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 {
|
||||
}
|
||||
|
||||
// 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 }
|
||||
|
||||
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 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
|
||||
}
|
||||
|
||||
// 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)
|
||||
@@ -222,18 +336,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 don't 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)
|
||||
|
||||
@@ -251,36 +403,73 @@ 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 {
|
||||
if startRemovalAnimation(with: translation, velocity: velocity, distance: distance) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
viewcontroller.delegate?.floatingPanelDidEndDragging(viewcontroller, withVelocity: velocity, targetPosition: targetPosition)
|
||||
viewcontroller.delegate?.floatingPanelWillBeginDecelerating(viewcontroller)
|
||||
|
||||
startAnimation(to: targetPosition, at: distance, with: velocity)
|
||||
}
|
||||
|
||||
private func startRemovalAnimation(with translation: CGPoint, velocity: CGPoint, distance: CGFloat) -> 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 velocityVector = (distance != 0) ? CGVector(dx: 0, dy: max(min(velocity.y/distance, vth), 0.0)) : .zero
|
||||
|
||||
guard (safeAreaBottomY - posY) != 0 else { return false }
|
||||
guard (currentY - posY) / (safeAreaBottomY - posY) >= pth || velocityVector.dy == vth else { return false }
|
||||
|
||||
viewcontroller.delegate?.floatingPanelDidEndDraggingToRemove(viewcontroller, withVelocity: velocity)
|
||||
let animator = self.behavior.removalInteractionAnimator(self.viewcontroller, with: velocityVector)
|
||||
animator.addAnimations { [weak self] in
|
||||
guard let self = self else { return }
|
||||
self.updateLayout(to: nil)
|
||||
}
|
||||
animator.addCompletion({ [weak self] (_) in
|
||||
guard let self = self else { return }
|
||||
self.viewcontroller.removePanelFromParent(animated: false)
|
||||
self.viewcontroller.delegate?.floatingPanelDidEndRemove(self.viewcontroller)
|
||||
})
|
||||
animator.startAnimation()
|
||||
return true
|
||||
}
|
||||
|
||||
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()
|
||||
viewcontroller.delegate?.floatingPanelWillBeginDragging(viewcontroller)
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
private func getCurrentY(from rect: CGRect, with translation: CGPoint) -> CGFloat {
|
||||
@@ -288,9 +477,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,19 +487,18 @@ 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) {
|
||||
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)
|
||||
}
|
||||
@@ -336,8 +522,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 {
|
||||
@@ -355,6 +544,66 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
|
||||
}
|
||||
}
|
||||
|
||||
private func directionalPosition(with translation: CGPoint) -> FloatingPanelPosition {
|
||||
let currentY = getCurrentY(from: initialFrame, with: translation)
|
||||
|
||||
let supportedPositions: Set = layoutAdapter.layout.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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func redirectionalPosition(with translation: CGPoint) -> FloatingPanelPosition {
|
||||
let currentY = getCurrentY(from: initialFrame, with: translation)
|
||||
|
||||
let supportedPositions: Set = layoutAdapter.layout.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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Distance travelled after decelerating to zero velocity at a constant rate.
|
||||
// Refer to the slides p176 of [Designing Fluid Interfaces](https://developer.apple.com/videos/play/wwdc2018/803/)
|
||||
private func project(initialVelocity: CGFloat) -> CGFloat {
|
||||
@@ -364,27 +613,59 @@ 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)
|
||||
if supportedPositions.count == 1 {
|
||||
return state
|
||||
}
|
||||
|
||||
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:
|
||||
/*
|
||||
[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
|
||||
}
|
||||
|
||||
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:
|
||||
@@ -424,7 +705,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:
|
||||
@@ -444,17 +728,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
|
||||
@@ -476,6 +758,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
|
||||
@@ -485,3 +774,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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,32 +6,89 @@
|
||||
import UIKit
|
||||
|
||||
public protocol FloatingPanelBehavior {
|
||||
// Returns a UIViewPropertyAnimator object in interacting 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 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
|
||||
|
||||
/// 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 presentAnimator(_ fpc: FloatingPanelController, from: FloatingPanelPosition, to: FloatingPanelPosition) -> UIViewPropertyAnimator {
|
||||
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)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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,
|
||||
|
||||
@@ -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,14 +36,18 @@ 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 {
|
||||
public enum FloatingPanelPosition: Int, CaseIterable {
|
||||
case full
|
||||
case half
|
||||
case tip
|
||||
@@ -66,35 +77,72 @@ 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 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 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
|
||||
|
||||
/// 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 layoutInsetsObservations: [NSKeyValueObservation] = []
|
||||
|
||||
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,25 +153,15 @@ 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) {
|
||||
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?) {
|
||||
@@ -133,14 +171,12 @@ 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 {
|
||||
@@ -162,7 +198,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
|
||||
@@ -172,12 +214,21 @@ public class FloatingPanelController: UIViewController, UIScrollViewDelegate, UI
|
||||
}
|
||||
}
|
||||
|
||||
private func updateLayout(for traitCollection: UITraitCollection) {
|
||||
floatingPanel.layoutAdapter.layout = fetchLayout(for: traitCollection)
|
||||
|
||||
guard let parent = parent else { return }
|
||||
|
||||
floatingPanel.layoutAdapter.prepareLayout(toParent: parent)
|
||||
floatingPanel.layoutAdapter.activateLayout(of: floatingPanel.state)
|
||||
}
|
||||
|
||||
// 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 {
|
||||
@@ -185,8 +236,10 @@ 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 {
|
||||
@@ -195,11 +248,33 @@ public class FloatingPanelController: UIViewController, UIScrollViewDelegate, UI
|
||||
parent.view.addSubview(self.view)
|
||||
}
|
||||
|
||||
layoutInsetsObservations.removeAll()
|
||||
|
||||
// Must track safeAreaInsets/{top,bottom}LayoutGuide of the `parent.view`
|
||||
// to update floatingPanel.safeAreaInsets`. There are 2 reasons.
|
||||
// 1. 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.
|
||||
// That's why it needs the observation to keep `adjustedContentInsets` correct.
|
||||
if #available(iOS 11.0, *) {
|
||||
let observaion = parent.observe(\.view.safeAreaInsets) { [weak self] (vc, chaneg) in
|
||||
guard let self = self else { return }
|
||||
self.update(safeAreaInsets: vc.layoutInsets)
|
||||
}
|
||||
layoutInsetsObservations.append(observaion)
|
||||
} else {
|
||||
// KVOs for topLayoutGuide & bottomLayoutGuide are not effective.
|
||||
// Instead, safeAreaInsets will be updated in viewDidAppear()
|
||||
}
|
||||
|
||||
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.behavior = fetchBehavior(for: traitCollection)
|
||||
|
||||
floatingPanel.setUpViews(in: parent)
|
||||
|
||||
floatingPanel.present(animated: animated) { [weak self] in
|
||||
guard let self = self else { return }
|
||||
self.didMove(toParent: parent)
|
||||
@@ -216,11 +291,14 @@ public class FloatingPanelController: UIViewController, UIScrollViewDelegate, UI
|
||||
return
|
||||
}
|
||||
|
||||
layoutInsetsObservations.removeAll()
|
||||
|
||||
floatingPanel.dismiss(animated: animated) { [weak self] in
|
||||
guard let self = self else { return }
|
||||
|
||||
self.willMove(toParent: nil)
|
||||
self.view.removeFromSuperview()
|
||||
self.backdropView.removeFromSuperview()
|
||||
self.removeFromParent()
|
||||
completion?()
|
||||
}
|
||||
@@ -230,25 +308,41 @@ 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)
|
||||
}
|
||||
|
||||
/// 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 = self.view as! FloatingPanelSurfaceView
|
||||
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
|
||||
@@ -256,12 +350,14 @@ 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
|
||||
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, *) {
|
||||
@@ -276,7 +372,18 @@ 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 parent's 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)
|
||||
}
|
||||
|
||||
/// Returns the y-coordinate of the point at the origin of the surface view
|
||||
public func originYOfSurface(for pos: FloatingPanelPosition) -> CGFloat {
|
||||
|
||||
@@ -6,41 +6,57 @@
|
||||
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.
|
||||
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 }
|
||||
|
||||
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),
|
||||
]
|
||||
}
|
||||
|
||||
func backdropAlphaFor(position: FloatingPanelPosition) -> CGFloat {
|
||||
return position == .full ? 0.3 : 0.0
|
||||
}
|
||||
}
|
||||
|
||||
public class FloatingPanelDefaultLayout: FloatingPanelLayout {
|
||||
public var supportedPositions: [FloatingPanelPosition] {
|
||||
return [.full, .half, .tip]
|
||||
}
|
||||
|
||||
public var initialPosition: FloatingPanelPosition {
|
||||
return .half
|
||||
}
|
||||
@@ -52,20 +68,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]
|
||||
}
|
||||
|
||||
@@ -76,44 +85,52 @@ 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: 8.0),
|
||||
surfaceView.widthAnchor.constraint(equalToConstant: 291),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class FloatingPanelLayoutAdapter {
|
||||
private weak var parent: UIViewController!
|
||||
private weak var surfaceView: FloatingPanelSurfaceView!
|
||||
private weak var backdropVIew: FloatingPanelBackdropView!
|
||||
private weak var backdropView: FloatingPanelBackdropView!
|
||||
|
||||
var layout: FloatingPanelLayout
|
||||
var layout: FloatingPanelLayout {
|
||||
didSet {
|
||||
checkLayoutConsistance()
|
||||
}
|
||||
}
|
||||
|
||||
var safeAreaInsets: UIEdgeInsets = .zero
|
||||
var safeAreaInsets: UIEdgeInsets = .zero {
|
||||
didSet {
|
||||
updateHeight()
|
||||
checkLayoutConsistance()
|
||||
}
|
||||
}
|
||||
|
||||
private var parentHeight: CGFloat = 0.0
|
||||
private var heightBuffer: CGFloat = 88.0 // For bounce
|
||||
private var fixedConstraints: [NSLayoutConstraint] = []
|
||||
private var fullConstraints: [NSLayoutConstraint] = []
|
||||
private var halfConstraints: [NSLayoutConstraint] = []
|
||||
private var tipConstraints: [NSLayoutConstraint] = []
|
||||
private var offConstraints: [NSLayoutConstraint] = []
|
||||
private var heightConstraints: NSLayoutConstraint? = nil
|
||||
private var heightConstraints: [NSLayoutConstraint] = []
|
||||
|
||||
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 +138,21 @@ 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 safeAreaBottomY: CGFloat {
|
||||
return surfaceView.superview!.bounds.height - (safeAreaInsets.bottom)
|
||||
}
|
||||
|
||||
var adjustedContentInsets: UIEdgeInsets {
|
||||
return UIEdgeInsets(top: 0.0,
|
||||
left: 0.0,
|
||||
bottom: (safeAreaInsets.top + topInset) + (heightBuffer + safeAreaInsets.bottom),
|
||||
bottom: safeAreaInsets.bottom,
|
||||
right: 0.0)
|
||||
}
|
||||
|
||||
@@ -145,40 +170,35 @@ class FloatingPanelLayoutAdapter {
|
||||
init(surfaceView: FloatingPanelSurfaceView, backdropView: FloatingPanelBackdropView, layout: FloatingPanelLayout) {
|
||||
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)
|
||||
}
|
||||
self.backdropView = backdropView
|
||||
}
|
||||
|
||||
func prepareLayout(toParent parent: UIViewController) {
|
||||
self.parent = parent
|
||||
|
||||
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,
|
||||
let backdropConstraints = [
|
||||
backdropView.topAnchor.constraint(equalTo: parent.view.topAnchor,
|
||||
constant: 0.0),
|
||||
backdropVIew.leftAnchor.constraint(equalTo: parent.view.leftAnchor,
|
||||
backdropView.leftAnchor.constraint(equalTo: parent.view.leftAnchor,
|
||||
constant: 0.0),
|
||||
backdropVIew.rightAnchor.constraint(equalTo: parent.view.rightAnchor,
|
||||
backdropView.rightAnchor.constraint(equalTo: parent.view.rightAnchor,
|
||||
constant: 0.0),
|
||||
backdropVIew.bottomAnchor.constraint(equalTo: parent.view.bottomAnchor,
|
||||
backdropView.bottomAnchor.constraint(equalTo: parent.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: topInset),
|
||||
constant: fullInset),
|
||||
]
|
||||
halfConstraints = [
|
||||
surfaceView.topAnchor.constraint(equalTo: parent.layoutGuide.bottomAnchor,
|
||||
@@ -189,7 +209,7 @@ class FloatingPanelLayoutAdapter {
|
||||
constant: -tipInset),
|
||||
]
|
||||
offConstraints = [
|
||||
surfaceView.topAnchor.constraint(equalTo: parent.layoutGuide.bottomAnchor, constant: 0.0),
|
||||
surfaceView.topAnchor.constraint(equalTo: parent.view.bottomAnchor, constant: 0.0),
|
||||
]
|
||||
}
|
||||
|
||||
@@ -202,18 +222,24 @@ class FloatingPanelLayoutAdapter {
|
||||
}
|
||||
}
|
||||
|
||||
if let heightConstraints = self.heightConstraints {
|
||||
NSLayoutConstraint.deactivate([heightConstraints])
|
||||
}
|
||||
let heightConstraints = surfaceView.heightAnchor.constraint(equalToConstant: UIScreen.main.bounds.height + heightBuffer)
|
||||
NSLayoutConstraint.activate([heightConstraints])
|
||||
self.heightConstraints = heightConstraints
|
||||
NSLayoutConstraint.deactivate(heightConstraints)
|
||||
// Must use the parent height, not the screen height because safe area insets
|
||||
// of the parent 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.
|
||||
let height = self.parent.view.bounds.height - (safeAreaInsets.top + fullInset)
|
||||
heightConstraints = [
|
||||
surfaceView.heightAnchor.constraint(equalToConstant: height)
|
||||
]
|
||||
NSLayoutConstraint.activate(heightConstraints)
|
||||
surfaceView.set(bottomOverflow: heightBuffer)
|
||||
}
|
||||
|
||||
func activateLayout(of state: FloatingPanelPosition?) {
|
||||
defer {
|
||||
surfaceView.superview!.layoutIfNeeded()
|
||||
}
|
||||
setBackdropAlpha(of: state)
|
||||
|
||||
NSLayoutConstraint.activate(fixedConstraints)
|
||||
|
||||
@@ -240,4 +266,34 @@ class FloatingPanelLayoutAdapter {
|
||||
NSLayoutConstraint.activate(tipConstraints)
|
||||
}
|
||||
}
|
||||
|
||||
func setBackdropAlpha(of target: FloatingPanelPosition?) {
|
||||
if let target = target {
|
||||
self.backdropView.alpha = layout.backdropAlphaFor(position: target)
|
||||
} else {
|
||||
self.backdropView.alpha = 0.0
|
||||
}
|
||||
}
|
||||
|
||||
func checkLayoutConsistance() {
|
||||
// Verify layout configurations
|
||||
let supportedPositions = layout.supportedPositions
|
||||
|
||||
assert(supportedPositions.count > 0)
|
||||
assert(supportedPositions.contains(layout.initialPosition),
|
||||
"Does not include an initial potision(\(layout.initialPosition)) in supportedPositions(\(supportedPositions))")
|
||||
|
||||
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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,17 +21,18 @@ 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() } }
|
||||
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 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.
|
||||
@@ -55,7 +56,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
|
||||
@@ -73,11 +74,16 @@ public class FloatingPanelSurfaceView: UIView {
|
||||
|
||||
private func render() {
|
||||
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),
|
||||
@@ -101,37 +107,69 @@ public class FloatingPanelSurfaceView: UIView {
|
||||
|
||||
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
|
||||
|
||||
updateLayers()
|
||||
updateContentViewMask()
|
||||
|
||||
contentView.layer.borderColor = borderColor?.cgColor
|
||||
contentView.layer.borderWidth = borderWidth
|
||||
contentView.backgroundColor = color
|
||||
}
|
||||
|
||||
private func updateShadowLayer() {
|
||||
if shadowLayer != nil {
|
||||
shadowLayer.removeFromSuperlayer()
|
||||
}
|
||||
shadowLayer = makeShadowLayer()
|
||||
layer.insertSublayer(shadowLayer, at: 0)
|
||||
}
|
||||
|
||||
private func makeShadowLayer() -> CAShapeLayer {
|
||||
private func updateLayers() {
|
||||
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
|
||||
shadowLayer.fillColor = color?.cgColor
|
||||
backgroundLayer.path = path.cgPath
|
||||
backgroundLayer.fillColor = color?.cgColor
|
||||
|
||||
if shadowHidden == false {
|
||||
shadowLayer.shadowPath = shadowLayer.path
|
||||
shadowLayer.shadowColor = shadowColor.cgColor
|
||||
shadowLayer.shadowOffset = shadowOffset
|
||||
shadowLayer.shadowOpacity = shadowOpacity
|
||||
shadowLayer.shadowRadius = shadowRadius
|
||||
layer.shadowColor = shadowColor.cgColor
|
||||
layer.shadowOffset = shadowOffset
|
||||
layer.shadowOpacity = shadowOpacity
|
||||
layer.shadowRadius = shadowRadius
|
||||
}
|
||||
return shadowLayer
|
||||
}
|
||||
|
||||
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.
|
||||
// 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.
|
||||
}
|
||||
}
|
||||
|
||||
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),
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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, *) {
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
[](https://travis-ci.org/SCENEE/FloatingPanel)
|
||||
[](https://cocoapods.org/pods/FloatingPanel)
|
||||
[](https://github.com/Carthage/Carthage)
|
||||
[](https://cocoapods.org/pods/FloatingPanel)
|
||||
[](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,13 +15,40 @@ The new interface displays the related contents and utilities in parallel as a u
|
||||
|
||||

|
||||
|
||||
<!-- TOC -->
|
||||
|
||||
- [Features](#features)
|
||||
- [Requirements](#requirements)
|
||||
- [Installation](#installation)
|
||||
- [CocoaPods](#cocoapods)
|
||||
- [Carthage](#carthage)
|
||||
- [Getting Started](#getting-started)
|
||||
- [Usage](#usage)
|
||||
- [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)
|
||||
- [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)
|
||||
- [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)
|
||||
|
||||
<!-- /TOC -->
|
||||
|
||||
## Features
|
||||
|
||||
- [x] Simple container view controller
|
||||
- [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
|
||||
@@ -65,14 +99,14 @@ 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)
|
||||
}
|
||||
|
||||
@@ -87,50 +121,41 @@ class ViewController: UIViewController, FloatingPanelControllerDelegate {
|
||||
|
||||
## Usage
|
||||
|
||||
### Move a positon with an animation
|
||||
### Customize the layout 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 +164,7 @@ class FloatingPanelLandscapeLayout: FloatingPanelLayout {
|
||||
public var initialPosition: FloatingPanelPosition {
|
||||
return .tip
|
||||
}
|
||||
public var supportedPositions: [FloatingPanelPosition] {
|
||||
public var supportedPositions: Set<FloatingPanelPosition> {
|
||||
return [.full, .tip]
|
||||
}
|
||||
|
||||
@@ -160,7 +185,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 +204,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)
|
||||
@@ -186,6 +213,45 @@ class FloatingPanelStocksBehavior: FloatingPanelBehavior {
|
||||
}
|
||||
```
|
||||
|
||||
### Use a custom grabber handle
|
||||
|
||||
```swift
|
||||
class ViewController: UIViewController {
|
||||
...
|
||||
override func viewDidLoad() {
|
||||
...
|
||||
let myGrabberHandleView = MyGrabberHandleView()
|
||||
fpc.surfaceView.grabberHandle.isHidden = true
|
||||
fpc.surfaceView.addSubview(myGrabberHandleView)
|
||||
}
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
### Add tap gestures to the surface or backdrop views
|
||||
|
||||
```swift
|
||||
class ViewController: UIViewController, FloatingPanelControllerDelegate {
|
||||
...
|
||||
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
|
||||
@@ -198,7 +264,7 @@ class ViewController: UIViewController, FloatingPanelControllerDelegate {
|
||||
self.searchPanelVC = FloatingPanelController()
|
||||
|
||||
let searchVC = SearchViewController()
|
||||
self.searchPanelVC.show(searchVC, sender: nil)
|
||||
self.searchPanelVC.set(contentViewController: searchVC)
|
||||
self.searchPanelVC.track(scrollView: contentVC.tableView)
|
||||
|
||||
self.searchPanelVC.addPanel(toParent: self)
|
||||
@@ -207,7 +273,7 @@ class ViewController: UIViewController, FloatingPanelControllerDelegate {
|
||||
self.detailPanelVC = FloatingPanelController()
|
||||
|
||||
let contentVC = ContentViewController()
|
||||
self.detailPanelVC.show(contentVC, sender: nil)
|
||||
self.detailPanelVC.set(contentViewController: contentVC)
|
||||
self.detailPanelVC.track(scrollView: contentVC.scrollView)
|
||||
|
||||
self.detailPanelVC.addPanel(toParent: self)
|
||||
@@ -216,6 +282,93 @@ 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)
|
||||
}
|
||||
```
|
||||
|
||||
### Work your contents together 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
|
||||
|
||||
### 'Show' or 'Show Detail' Segues from `FloatingPanelController`'s content view controller
|
||||
|
||||
'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.
|
||||
|
||||
`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 secondally 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() {
|
||||
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>
|
||||
|
||||
Reference in New Issue
Block a user