Compare commits

..

4 Commits

Author SHA1 Message Date
Shin Yamamoto 1e6cb7b1ad Release v1.2.3 2018-12-07 16:50:08 +09:00
Shin Yamamoto 8ba4ce36a1 Merge pull request #75 from SCENEE/fix-animation-wobbling
The default interaction animator should be uninterruptible
2018-12-05 16:01:56 +09:00
Shin Yamamoto cf60b09225 Fix invalid backdrop alpha
The bug was found when I commented out `animator.startAnimation()`.
2018-12-05 14:08:31 +09:00
Shin Yamamoto 427ec45d42 Let the default interaction animator be uninterruptible
Because an interruptible animator causes a wobbling at the animation start.
when a user flick a panel quickly to move to full position nearby the position.
2018-12-05 13:56:43 +09:00
15 changed files with 303 additions and 776 deletions
-1
View File
@@ -2,7 +2,6 @@ language: swift
branches:
only:
- master
- next
cache:
directories:
- build
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="14460.31" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="RoN-h0-uBD">
<device id="retina5_9" orientation="portrait">
<device id="retina4_7" orientation="portrait">
<adaptation id="fullscreen"/>
</device>
<dependencies>
@@ -15,7 +15,7 @@
<objects>
<navigationController id="RoN-h0-uBD" sceneMemberID="viewController">
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" id="hNW-5m-Omi">
<rect key="frame" x="0.0" y="44" width="375" height="44"/>
<rect key="frame" x="0.0" y="20" width="375" height="44"/>
<autoresizingMask key="autoresizingMask"/>
</navigationBar>
<connections>
@@ -31,22 +31,22 @@
<objects>
<viewController id="jF4-A0-Eq6" customClass="SampleListViewController" customModule="Samples" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="Smh-Bd-AAc">
<rect key="frame" x="0.0" y="0.0" width="375" height="812"/>
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<tableView clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="plain" separatorStyle="default" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="28" sectionFooterHeight="28" translatesAutoresizingMaskIntoConstraints="NO" id="7IS-PU-x0P">
<rect key="frame" x="0.0" y="0.0" width="375" height="778"/>
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<prototypes>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" reuseIdentifier="Cell" textLabel="M0G-C8-hAO" style="IBUITableViewCellStyleDefault" id="ySY-oA-g81">
<rect key="frame" x="0.0" y="28" width="375" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="ySY-oA-g81" id="sXB-nH-2g2">
<rect key="frame" x="0.0" y="0.0" width="375" height="43.666666666666664"/>
<rect key="frame" x="0.0" y="0.0" width="375" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Title" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="M0G-C8-hAO">
<rect key="frame" x="15" y="0.0" width="345" height="43.666666666666664"/>
<rect key="frame" x="15" y="0.0" width="345" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/>
@@ -81,17 +81,17 @@
<objects>
<viewController id="RpE-lI-27a" customClass="TabBarContentViewController" customModule="Samples" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="JER-jz-KSq">
<rect key="frame" x="0.0" y="0.0" width="375" height="812"/>
<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.66666666666666" y="395.66666666666669" width="48" height="21"/>
<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="44" width="39" height="30"/>
<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"/>
@@ -119,17 +119,17 @@
<objects>
<viewController id="lto-Zc-Vtp" customClass="TabBarContentViewController" customModule="Samples" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="ji9-Ez-N7i">
<rect key="frame" x="0.0" y="0.0" width="375" height="812"/>
<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.66666666666666" y="395.66666666666669" width="46" height="21"/>
<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="44" width="39" height="30"/>
<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"/>
@@ -152,35 +152,6 @@
</objects>
<point key="canvasLocation" x="-380" y="1576"/>
</scene>
<!--Intrinsic View Controller-->
<scene sceneID="wtJ-qZ-aCl">
<objects>
<viewController storyboardIdentifier="IntrinsicViewController" title="Intrinsic View Controller" id="aK0-kv-mTu" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="eLM-xc-d9e">
<rect key="frame" x="0.0" y="0.0" width="375" height="812"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="1000" verticalHuggingPriority="1000" horizontalCompressionResistancePriority="1000" verticalCompressionResistancePriority="1000" text="Change this text" textAlignment="center" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="ge4-RW-Gmz">
<rect key="frame" x="24" y="68" width="327" height="20.333333333333329"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
</subviews>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstItem="vtu-Jb-oOn" firstAttribute="trailing" secondItem="ge4-RW-Gmz" secondAttribute="trailing" constant="24" id="V59-MD-Lcg"/>
<constraint firstItem="ge4-RW-Gmz" firstAttribute="leading" secondItem="eLM-xc-d9e" secondAttribute="leading" constant="24" id="hAO-P0-7Kw"/>
<constraint firstItem="ge4-RW-Gmz" firstAttribute="top" secondItem="vtu-Jb-oOn" secondAttribute="top" constant="24" id="j0s-fd-MYj"/>
<constraint firstAttribute="bottom" relation="greaterThanOrEqual" secondItem="ge4-RW-Gmz" secondAttribute="bottom" constant="24" id="tEn-PO-nVD"/>
</constraints>
<viewLayoutGuide key="safeArea" id="vtu-Jb-oOn"/>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="DfE-fL-zy5" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="-940" y="805"/>
</scene>
<!--Tab Bar View Controller-->
<scene sceneID="nQ5-PV-qFw">
<objects>
@@ -204,22 +175,22 @@
<objects>
<viewController storyboardIdentifier="ModalViewController" id="bYI-y3-Rzb" customClass="ModalViewController" customModule="Samples" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="qwo-GK-p1U">
<rect key="frame" x="0.0" y="0.0" width="375" height="812"/>
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="vut-mK-Y4t" customClass="SafeAreaView" customModule="Samples" customModuleProvider="target">
<rect key="frame" x="0.0" y="778" width="375" height="34"/>
<rect key="frame" x="0.0" y="667" width="375" height="0.0"/>
<color key="backgroundColor" white="0.66666666666666663" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</view>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="sbF-Az-7sy">
<rect key="frame" x="20" y="44" width="39" height="30"/>
<rect key="frame" x="20" y="20" width="39" height="30"/>
<state key="normal" title="Close"/>
<connections>
<action selector="closeWithSender:" destination="bYI-y3-Rzb" eventType="touchUpInside" id="MSC-ch-YJK"/>
</connections>
</button>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" alignment="top" spacing="44" translatesAutoresizingMaskIntoConstraints="NO" id="9p4-06-y2T">
<rect key="frame" x="139.66666666666666" y="132" width="96" height="252"/>
<rect key="frame" x="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"/>
@@ -420,14 +391,14 @@
</constraints>
</view>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="Kva-Z7-0qY" customClass="OnSafeAreaView" customModule="Samples" customModuleProvider="target">
<rect key="frame" x="0.0" y="700" width="375" height="44"/>
<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.66666666666666" y="132" width="114" height="82"/>
<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"/>
@@ -447,7 +418,7 @@
</subviews>
</stackView>
</subviews>
<color key="backgroundColor" red="1" green="0.83234566450000003" blue="0.47320586440000001" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<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"/>
@@ -460,7 +431,6 @@
<constraint firstItem="tAi-nk-rDB" firstAttribute="trailing" secondItem="noi-1a-5bZ" secondAttribute="trailing" constant="12" id="lv9-Nf-HNB"/>
<constraint firstItem="Kva-Z7-0qY" firstAttribute="leading" secondItem="tAi-nk-rDB" secondAttribute="leading" id="oVC-i1-TwS"/>
<constraint firstItem="tAi-nk-rDB" firstAttribute="bottom" secondItem="Kva-Z7-0qY" secondAttribute="bottom" id="rW2-mF-5DR"/>
<constraint firstItem="8yw-OC-Ubk" firstAttribute="top" relation="greaterThanOrEqual" secondItem="tP3-oJ-4EB" secondAttribute="bottom" constant="8" symbolic="YES" id="vKQ-h9-uKt"/>
</constraints>
<viewLayoutGuide key="safeArea" id="tAi-nk-rDB"/>
<connections>
+33 -80
View File
@@ -17,11 +17,9 @@ class SampleListViewController: UIViewController, UITableViewDataSource, UITable
case trackingTextView
case showDetail
case showModal
case showFloatingPanelModal
case showTabBar
case showNestedScrollView
case showRemovablePanel
case showIntrinsicView
var name: String {
switch self {
@@ -29,11 +27,9 @@ class SampleListViewController: UIViewController, UITableViewDataSource, UITable
case .trackingTextView: return "Scroll tracking(TextView)"
case .showDetail: return "Show Detail Panel"
case .showModal: return "Show Modal"
case .showFloatingPanelModal: return "Show Floating Panel Modal"
case .showTabBar: return "Show Tab Bar"
case .showNestedScrollView: return "Show Nested ScrollView"
case .showRemovablePanel: return "Show Removable Panel"
case .showIntrinsicView: return "Show Intrinsic View"
}
}
@@ -43,11 +39,9 @@ class SampleListViewController: UIViewController, UITableViewDataSource, UITable
case .trackingTextView: return "ConsoleViewController"
case .showDetail: return "DetailViewController"
case .showModal: return "ModalViewController"
case .showFloatingPanelModal: return nil
case .showTabBar: return "TabBarViewController"
case .showNestedScrollView: return "NestedScrollViewController"
case .showRemovablePanel: return "DetailViewController"
case .showIntrinsicView: return "IntrinsicViewController"
}
}
}
@@ -74,6 +68,7 @@ class SampleListViewController: UIViewController, UITableViewDataSource, UITable
// Initialize FloatingPanelController
mainPanelVC = FloatingPanelController()
mainPanelVC.delegate = self
mainPanelVC.isRemovalInteractionEnabled = (currentMenu == .showRemovablePanel)
// Initialize FloatingPanelController and add the view
mainPanelVC.surfaceView.cornerRadius = 6.0
@@ -82,17 +77,6 @@ class SampleListViewController: UIViewController, UITableViewDataSource, UITable
// Set a content view controller
mainPanelVC.set(contentViewController: contentVC)
// Enable tap-to-hide and removal interaction
switch currentMenu {
case .showRemovablePanel, .showIntrinsicView:
mainPanelVC.isRemovalInteractionEnabled = true
let backdropTapGesture = UITapGestureRecognizer(target: self, action: #selector(handleBackdrop(tapGesture:)))
mainPanelVC.backdropView.addGestureRecognizer(backdropTapGesture)
default:
break
}
// Track a scroll view
switch contentVC {
case let consoleVC as DebugTextViewController:
@@ -105,7 +89,6 @@ class SampleListViewController: UIViewController, UITableViewDataSource, UITable
default:
break
}
// Add FloatingPanel to self.view
mainPanelVC.addPanel(toParent: self, belowView: nil, animated: true)
}
@@ -114,10 +97,6 @@ class SampleListViewController: UIViewController, UITableViewDataSource, UITable
detailPanelVC.removePanelFromParent(animated: true, completion: nil)
}
@objc func handleBackdrop(tapGesture: UITapGestureRecognizer) {
mainPanelVC.hide(animated: true, completion: nil)
}
// MARK:- TableViewDatasource
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
@@ -145,8 +124,8 @@ class SampleListViewController: UIViewController, UITableViewDataSource, UITable
switch menu {
case .showDetail:
detailPanelVC?.removePanelFromParent(animated: false)
detailPanelVC?.removeFromParent()
// Initialize FloatingPanelController
detailPanelVC = FloatingPanelController()
@@ -162,18 +141,6 @@ class SampleListViewController: UIViewController, UITableViewDataSource, UITable
case .showModal, .showTabBar:
let modalVC = contentVC
present(modalVC, animated: true, completion: nil)
case .showFloatingPanelModal:
let fpc = FloatingPanelController()
let contentVC = self.storyboard!.instantiateViewController(withIdentifier: "DetailViewController")
fpc.set(contentViewController: contentVC)
fpc.delegate = self
fpc.surfaceView.cornerRadius = 38.5
fpc.surfaceView.shadowHidden = false
fpc.isRemovalInteractionEnabled = true
self.present(fpc, animated: true, completion: nil)
default:
detailPanelVC?.removePanelFromParent(animated: true, completion: nil)
mainPanelVC?.removePanelFromParent(animated: true) {
@@ -183,18 +150,10 @@ class SampleListViewController: UIViewController, UITableViewDataSource, UITable
}
func floatingPanel(_ vc: FloatingPanelController, layoutFor newCollection: UITraitCollection) -> FloatingPanelLayout? {
switch currentMenu {
case .showRemovablePanel:
if currentMenu == .showRemovablePanel {
return newCollection.verticalSizeClass == .compact ? RemovablePanelLandscapeLayout() : RemovablePanelLayout()
case .showIntrinsicView:
return IntrinsicPanelLayout()
case .showFloatingPanelModal:
if vc != mainPanelVC && vc != detailPanelVC {
return ModalPanelLayout()
}
fallthrough
default:
return (newCollection.verticalSizeClass == .compact) ? nil : self
} else {
return self
}
}
@@ -207,44 +166,24 @@ class SampleListViewController: UIViewController, UITableViewDataSource, UITable
case .full: return UIScreen.main.bounds.height == 667.0 ? 18.0 : 16.0
case .half: return 262.0
case .tip: return 69.0
case .hidden: return nil
}
}
}
class IntrinsicPanelLayout: FloatingPanelIntrinsicLayout { }
class RemovablePanelLayout: FloatingPanelIntrinsicLayout {
class RemovablePanelLayout: FloatingPanelLayout {
var initialPosition: FloatingPanelPosition {
return .half
}
var supportedPositions: Set<FloatingPanelPosition> {
return [.full, .half]
}
var topInteractionBuffer: CGFloat {
return 200.0
}
var bottomInteractionBuffer: CGFloat {
return 261.0 - 22.0
}
func insetFor(position: FloatingPanelPosition) -> CGFloat? {
switch position {
case .half: return 130.0
default: return nil
}
}
func backdropAlphaFor(position: FloatingPanelPosition) -> CGFloat {
return 0.3
}
}
class RemovablePanelLandscapeLayout: FloatingPanelIntrinsicLayout {
var supportedPositions: Set<FloatingPanelPosition> {
return [.full, .half]
}
var bottomInteractionBuffer: CGFloat {
return 261.0 - 22.0
}
func insetFor(position: FloatingPanelPosition) -> CGFloat? {
switch position {
case .full: return 16.0
case .half: return 261.0
default: return nil
}
@@ -254,9 +193,22 @@ class RemovablePanelLandscapeLayout: FloatingPanelIntrinsicLayout {
}
}
class ModalPanelLayout: FloatingPanelIntrinsicLayout {
var topInteractionBuffer: CGFloat {
return 100.0
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
@@ -297,8 +249,9 @@ class DebugTextViewController: UIViewController, UITextViewDelegate {
}
@IBAction func close(sender: UIButton) {
// (self.parent as? FloatingPanelController)?.removePanelFromParent(animated: true, completion: nil)
dismiss(animated: true, completion: nil)
// Now impossible
// dismiss(animated: true, completion: nil)
(self.parent as? FloatingPanelController)?.removePanelFromParent(animated: true, completion: nil)
}
}
@@ -470,8 +423,9 @@ class DebugTableViewController: UIViewController, UITableViewDataSource, UITable
class DetailViewController: UIViewController {
@IBOutlet weak var closeButton: UIButton!
@IBAction func close(sender: UIButton) {
// (self.parent as? FloatingPanelController)?.removePanelFromParent(animated: true, completion: nil)
dismiss(animated: true, completion: nil)
// Now impossible
// dismiss(animated: true, completion: nil)
(self.parent as? FloatingPanelController)?.removePanelFromParent(animated: true, completion: nil)
}
@IBAction func buttonPressed(_ sender: UIButton) {
@@ -567,7 +521,6 @@ class ModalSecondLayout: FloatingPanelLayout {
case .full: return 18.0
case .half: return 262.0
case .tip: return 44.0
case .hidden: return nil
}
}
}
@@ -114,7 +114,6 @@ class FloatingPanelStocksLayout: FloatingPanelLayout {
case .full: return 56.0
case .half: return 262.0
case .tip: return 85.0 + 44.0 // Visible + ToolView
default: return nil
}
}
+1 -1
View File
@@ -1,7 +1,7 @@
Pod::Spec.new do |s|
s.name = "FloatingPanel"
s.version = "1.3.0"
s.version = "1.2.3"
s.summary = "FloatingPanel is a clean and easy-to-use UI component of a floating panel interface."
s.description = <<-DESC
FloatingPanel is a clean and easy-to-use UI component for a new interface introduced in Apple Maps, Shortcuts and Stocks app.
@@ -7,8 +7,6 @@
objects = {
/* Begin PBXBuildFile section */
54352E9821A521CA00CBCA08 /* FloatingPanelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54352E9721A521CA00CBCA08 /* FloatingPanelView.swift */; };
54352E9621A51A2500CBCA08 /* FloatingPanelTransitioning.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54352E9521A51A2500CBCA08 /* FloatingPanelTransitioning.swift */; };
5450EEE421646DF500135936 /* FloatingPanelBehavior.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5450EEE321646DF500135936 /* FloatingPanelBehavior.swift */; };
545DB9CB2151169500CA77B8 /* FloatingPanel.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 545DB9C12151169500CA77B8 /* FloatingPanel.framework */; };
545DB9D02151169500CA77B8 /* ViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 545DB9CF2151169500CA77B8 /* ViewTests.swift */; };
@@ -34,8 +32,6 @@
/* End PBXContainerItemProxy section */
/* Begin PBXFileReference section */
54352E9721A521CA00CBCA08 /* FloatingPanelView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FloatingPanelView.swift; sourceTree = "<group>"; };
54352E9521A51A2500CBCA08 /* FloatingPanelTransitioning.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FloatingPanelTransitioning.swift; sourceTree = "<group>"; };
5450EEE321646DF500135936 /* FloatingPanelBehavior.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FloatingPanelBehavior.swift; sourceTree = "<group>"; };
545DB9C12151169500CA77B8 /* FloatingPanel.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = FloatingPanel.framework; sourceTree = BUILT_PRODUCTS_DIR; };
545DB9C42151169500CA77B8 /* FloatingPanelController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FloatingPanelController.h; sourceTree = "<group>"; };
@@ -96,11 +92,9 @@
545DB9C52151169500CA77B8 /* Info.plist */,
545DB9C42151169500CA77B8 /* FloatingPanelController.h */,
545DB9DF21511AC100CA77B8 /* FloatingPanelController.swift */,
54352E9521A51A2500CBCA08 /* FloatingPanelTransitioning.swift */,
54CFBFC4215CD09C006B5735 /* FloatingPanel.swift */,
54CFBFC2215CD045006B5735 /* FloatingPanelLayout.swift */,
5450EEE321646DF500135936 /* FloatingPanelBehavior.swift */,
54352E9721A521CA00CBCA08 /* FloatingPanelView.swift */,
54CDC5D2215B6D5A007D205C /* FloatingPanelSurfaceView.swift */,
54CDC5D4215B6D8D007D205C /* FloatingPanelBackdropView.swift */,
545DBA2A2152383100CA77B8 /* GrabberHandleView.swift */,
@@ -231,13 +225,11 @@
54CDC5D3215B6D5A007D205C /* FloatingPanelSurfaceView.swift in Sources */,
54CFBFC3215CD045006B5735 /* FloatingPanelLayout.swift in Sources */,
54CDC5D5215B6D8D007D205C /* FloatingPanelBackdropView.swift in Sources */,
54352E9821A521CA00CBCA08 /* FloatingPanelView.swift in Sources */,
54CFBFC5215CD09C006B5735 /* FloatingPanel.swift in Sources */,
54ABD7AF216CCFF7002E6C13 /* Logger.swift in Sources */,
545DB9E021511AC100CA77B8 /* FloatingPanelController.swift in Sources */,
5450EEE421646DF500135936 /* FloatingPanelBehavior.swift in Sources */,
545DBA2B2152383100CA77B8 /* GrabberHandleView.swift in Sources */,
54352E9621A51A2500CBCA08 /* FloatingPanelTransitioning.swift in Sources */,
545DB9DE215118C800CA77B8 /* UIExtensions.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
+51 -103
View File
@@ -33,12 +33,12 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
unowned let viewcontroller: FloatingPanelController
private(set) var state: FloatingPanelPosition = .hidden {
private(set) var state: FloatingPanelPosition = .tip {
didSet { viewcontroller.delegate?.floatingPanelDidChangePosition(viewcontroller) }
}
private var isBottomState: Bool {
let remains = layoutAdapter.supportedPositions.filter { $0.rawValue > state.rawValue }
let remains = layoutAdapter.layout.supportedPositions.filter { $0.rawValue > state.rawValue }
return remains.count == 0
}
@@ -60,10 +60,7 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
init(_ vc: FloatingPanelController, layout: FloatingPanelLayout, behavior: FloatingPanelBehavior) {
viewcontroller = vc
surfaceView = FloatingPanelSurfaceView()
surfaceView.backgroundColor = .white
surfaceView = vc.view as! FloatingPanelSurfaceView
backdropView = FloatingPanelBackdropView()
backdropView.backgroundColor = .black
backdropView.alpha = 0.0
@@ -73,6 +70,8 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
layout: layout)
self.behavior = behavior
state = layoutAdapter.layout.initialPosition
panGesture = FloatingPanelPanGestureRecognizer()
if #available(iOS 11.0, *) {
@@ -89,33 +88,26 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
func setUpViews(in vc: UIViewController) {
unowned let view = vc.view!
// FloatingPanelSurfaceWrapperView is needed to update the surface's height
// without animation and prevent the backdrop's cut-off on orientation change.
let surfaceWrapperView = FloatingPanelSurfaceWrapperView()
surfaceWrapperView.frame = view.bounds
surfaceWrapperView.backgroundColor = .clear
view.addSubview(surfaceWrapperView)
surfaceWrapperView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
surfaceWrapperView.topAnchor.constraint(equalTo: view.topAnchor, constant: 0.0),
surfaceWrapperView.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 0.0),
surfaceWrapperView.rightAnchor.constraint(equalTo: view.rightAnchor, constant: 0.0),
surfaceWrapperView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: 0.0),
])
surfaceWrapperView.addSubview(surfaceView)
view.insertSubview(backdropView, belowSubview: surfaceWrapperView)
view.insertSubview(backdropView, belowSubview: surfaceView)
backdropView.frame = view.bounds
layoutAdapter.prepareLayout(toParent: vc)
}
func move(to: FloatingPanelPosition, animated: Bool, completion: (() -> Void)? = nil) {
move(from: state, to: to, animated: animated, completion: completion)
}
private func move(from: FloatingPanelPosition, to: FloatingPanelPosition, animated: Bool, completion: (() -> Void)? = nil) {
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()
}
@@ -123,19 +115,23 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
if animated {
let animator: UIViewPropertyAnimator
switch (from, to) {
case (.hidden, let to):
case (nil, let to?):
animator = behavior.addAnimator(self.viewcontroller, to: to)
case (let from, .hidden):
animator = behavior.removeAnimator(self.viewcontroller, from: from)
case (let from, let 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?()
@@ -143,14 +139,16 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
animator.startAnimation()
} else {
self.updateLayout(to: to)
self.state = to
if let to = to {
self.state = to
}
completion?()
}
}
// MARK: - Layout update
private func updateLayout(to target: FloatingPanelPosition) {
private func updateLayout(to target: FloatingPanelPosition?) {
self.layoutAdapter.activateLayout(of: target)
}
@@ -293,8 +291,6 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
}
// Fix the scroll offset in moving the panel from half and tip.
scrollView.contentOffset.y = initialScrollOffset.y
case .hidden:
fatalError("A floating panel hidden must not be used by a user")
}
// Always hide a scroll indicator at the non-top.
@@ -317,7 +313,7 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
return
}
if let animator = self.animator {
if let animator = self.animator, animator.isInterruptible {
animator.stopAnimation(true)
self.animator = nil
}
@@ -415,17 +411,7 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
endInteraction(for: targetPosition)
if isRemovalInteractionEnabled, isBottomState {
let velocityVector = (distance != 0) ? CGVector(dx: 0,
dy: max(min(velocity.y/distance, behavior.removalVelocity), 0.0)) : .zero
if shouldStartRemovalAnimation(with: translation, velocityVector: velocityVector) {
viewcontroller.delegate?.floatingPanelDidEndDraggingToRemove(viewcontroller, withVelocity: velocity)
self.startRemovalAnimation(with: velocityVector) { [weak self] in
guard let self = self else { return }
self.viewcontroller.dismiss(animated: false)
self.viewcontroller.delegate?.floatingPanelDidEndRemove(self.viewcontroller)
}
if startRemovalAnimation(with: translation, velocity: velocity, distance: distance) {
return
}
}
@@ -436,32 +422,30 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
startAnimation(to: targetPosition, at: distance, with: velocity)
}
private func shouldStartRemovalAnimation(with translation: CGPoint, velocityVector: CGVector) -> Bool {
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
let num = (currentY - posY)
let den = (safeAreaBottomY - posY)
guard (safeAreaBottomY - posY) != 0 else { return false }
guard (currentY - posY) / (safeAreaBottomY - posY) >= pth || velocityVector.dy == vth else { return false }
guard num >= 0, den != 0, (num / den >= pth || velocityVector.dy == vth)
else { return false }
return true
}
private func startRemovalAnimation(with velocityVector: CGVector, completion: (() -> Void)?) {
viewcontroller.delegate?.floatingPanelDidEndDraggingToRemove(viewcontroller, withVelocity: velocity)
let animator = self.behavior.removalInteractionAnimator(self.viewcontroller, with: velocityVector)
animator.addAnimations { [weak self] in
self?.updateLayout(to: .hidden)
guard let self = self else { return }
self.updateLayout(to: nil)
}
animator.addCompletion({ _ in
completion?()
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) {
@@ -475,19 +459,6 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
viewcontroller.delegate?.floatingPanelWillBeginDragging(viewcontroller)
if layoutAdapter.layout is FloatingPanelIntrinsicLayout {
viewcontroller.contentViewController?.view?.constraints.forEach({ (const) in
switch viewcontroller.contentViewController?.layoutGuide.bottomAnchor {
case const.firstAnchor:
(const.secondItem as? UIView)?.disableAutoLayout()
case const.secondAnchor:
(const.firstItem as? UIView)?.disableAutoLayout()
default:
break
}
})
}
interactionInProgress = true
}
@@ -499,19 +470,6 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
if targetPosition != .full {
lockScrollView()
}
if layoutAdapter.layout is FloatingPanelIntrinsicLayout {
viewcontroller.contentViewController?.view?.constraints.forEach({ (const) in
switch viewcontroller.contentViewController?.layoutGuide.bottomAnchor {
case const.firstAnchor:
(const.secondItem as? UIView)?.enableAutoLayout()
case const.secondAnchor:
(const.firstItem as? UIView)?.enableAutoLayout()
default:
break
}
})
}
}
private func getCurrentY(from rect: CGRect, with translation: CGPoint) -> CGFloat {
@@ -529,12 +487,11 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
return max(topY, min(bottomY, y))
}
}
let topMax = layoutAdapter.topMaxY
let bottomMax = layoutAdapter.bottomMaxY
return max(max(topY - topBuffer, topMax), min(min(bottomY + bottomBuffer, bottomMax), y))
return max(topY - topBuffer, min(bottomY + bottomBuffer, y))
}
private func startAnimation(to targetPosition: FloatingPanelPosition, at distance: CGFloat, with velocity: CGPoint) {
log.debug("startAnimation", targetPosition, distance, velocity)
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)
@@ -578,7 +535,6 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
let middleY = layoutAdapter.middleY
let bottomY = layoutAdapter.bottomY
let currentY = getCurrentY(from: initialFrame, with: translation)
switch targetPosition {
case .full:
return CGFloat(fabs(Double(currentY - topY)))
@@ -586,15 +542,13 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
return CGFloat(fabs(Double(currentY - middleY)))
case .tip:
return CGFloat(fabs(Double(currentY - bottomY)))
case .hidden:
fatalError("A floating panel hidden must not be used by a user")
}
}
private func directionalPosition(with translation: CGPoint) -> FloatingPanelPosition {
let currentY = getCurrentY(from: initialFrame, with: translation)
let supportedPositions = layoutAdapter.supportedPositions
let supportedPositions: Set = layoutAdapter.layout.supportedPositions
if supportedPositions.count == 1 {
return state
@@ -614,14 +568,12 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
}
return currentY > middleY ? .tip : .half
case .half:
return translation.y >= 0 ? .tip : .full
return currentY > middleY ? .tip : .full
case .tip:
if translation.y >= 0 {
return .tip
}
return currentY > middleY ? .half : .full
case .hidden:
fatalError("A floating panel hidden must not be used by a user")
}
}
}
@@ -629,7 +581,7 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
private func redirectionalPosition(with translation: CGPoint) -> FloatingPanelPosition {
let currentY = getCurrentY(from: initialFrame, with: translation)
let supportedPositions = layoutAdapter.supportedPositions
let supportedPositions: Set = layoutAdapter.layout.supportedPositions
if supportedPositions.count == 1 {
return state
@@ -649,8 +601,6 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
return .half
case .tip:
return currentY > middleY ? .tip : .half
case .hidden:
fatalError("A floating panel hidden must not be used by a user")
}
}
}
@@ -664,7 +614,7 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
private func targetPosition(with translation: CGPoint, velocity: CGPoint) -> (FloatingPanelPosition) {
let currentY = getCurrentY(from: initialFrame, with: translation)
let supportedPositions = layoutAdapter.supportedPositions
let supportedPositions: Set = layoutAdapter.layout.supportedPositions
if supportedPositions.count == 1 {
return state
@@ -703,8 +653,6 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
case .tip:
target = .half
forwardYDirection = false
case .hidden:
fatalError("A floating panel hidden must not be used by a user")
}
let redirectionalProgress = max(min(behavior.redirectionalProgress(viewcontroller, from: state, to: target), 1.0), 0.0)
@@ -85,7 +85,9 @@ public extension FloatingPanelBehavior {
class FloatingPanelDefaultBehavior: FloatingPanelBehavior {
func interactionAnimator(_ fpc: FloatingPanelController, to targetPosition: FloatingPanelPosition, with velocity: CGVector) -> UIViewPropertyAnimator {
let timing = timeingCurve(with: velocity)
return UIViewPropertyAnimator(duration: 0, timingParameters: timing)
let animator = UIViewPropertyAnimator(duration: 0, timingParameters: timing)
animator.isInterruptible = false
return animator
}
private func timeingCurve(with velocity: CGVector) -> UITimingCurveProvider {
+61 -116
View File
@@ -51,13 +51,13 @@ public enum FloatingPanelPosition: Int, CaseIterable {
case full
case half
case tip
case hidden
}
///
/// A container view controller to display a floating panel to present contents in parallel as a user wants.
///
public class FloatingPanelController: UIViewController, UIScrollViewDelegate, UIGestureRecognizerDelegate {
/// Constants indicating how safe area insets are added to the adjusted content inset.
public enum ContentInsetAdjustmentBehavior: Int {
case always
@@ -69,7 +69,7 @@ public class FloatingPanelController: UIViewController, UIScrollViewDelegate, UI
/// Returns the surface view managed by the controller object. It's the same as `self.view`.
public var surfaceView: FloatingPanelSurfaceView! {
return floatingPanel.surfaceView
return view as? FloatingPanelSurfaceView
}
/// Returns the backdrop view managed by the controller object.
@@ -102,7 +102,7 @@ public class FloatingPanelController: UIViewController, UIScrollViewDelegate, UI
return floatingPanel.behavior
}
/// The content insets of the tracking scroll view derived from this safe area
/// 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
}
@@ -126,57 +126,35 @@ public class FloatingPanelController: UIViewController, UIScrollViewDelegate, UI
private var _contentViewController: UIViewController?
private var floatingPanel: FloatingPanel!
private var safeAreaInsetsObservation: NSKeyValueObservation?
private let modalTransition = FloatingPanelModalTransition()
private var layoutInsetsObservations: [NSKeyValueObservation] = []
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setUp()
}
/// Initialize a newly created floating panel controller.
public init() {
super.init(nibName: nil, bundle: nil)
setUp()
}
private func setUp() {
_ = FloatingPanelController.dismissSwizzling
modalPresentationStyle = .custom
transitioningDelegate = modalTransition
floatingPanel = FloatingPanel(self,
layout: fetchLayout(for: self.traitCollection),
behavior: fetchBehavior(for: self.traitCollection))
}
// MARK:- Overrides
/// 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.
override public func loadView() {
assert(self.storyboard == nil, "Storyboard isn't supported")
let view = FloatingPanelPassThroughView()
view.backgroundColor = .clear
let view = FloatingPanelSurfaceView()
view.backgroundColor = .white
self.view = view as UIView
}
public override func viewDidLoad() {
super.viewDidLoad()
floatingPanel.setUpViews(in: self)
}
public override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
view.frame.size = size
view.layoutIfNeeded()
floatingPanel.layoutAdapter.checkLayoutConsistance()
}
public override func willTransition(to newCollection: UITraitCollection, with coordinator: UIViewControllerTransitionCoordinator) {
super.willTransition(to: newCollection, with: coordinator)
@@ -190,36 +168,22 @@ public class FloatingPanelController: UIViewController, UIScrollViewDelegate, UI
super.traitCollectionDidChange(previousTraitCollection)
guard previousTraitCollection != traitCollection else { return }
self.update(safeAreaInsets: layoutInsets)
if let parent = parent {
self.update(safeAreaInsets: parent.layoutInsets)
}
}
public override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
// Must track safeAreaInsets/{top,bottom}LayoutGuide of the `self.view`
// to update floatingPanel.safeAreaInsets`. There are 2 reasons.
// 1. This or the parent VC doesn't call viewSafeAreaInsetsDidChange() on the bottom
// inset's update expectedly.
// 2. The safe area top inset can be variable on the large title navigation bar(iOS11+).
// That's why it needs the observation to keep `adjustedContentInsets` correct.
if #available(iOS 11.0, *) {
safeAreaInsetsObservation = self.observe(\.view.safeAreaInsets) { [weak self] (vc, chaneg) in
guard let self = self else { return }
self.update(safeAreaInsets: vc.layoutInsets)
}
} else {
// KVOs for topLayoutGuide & bottomLayoutGuide are not effective.
// Instead, safeAreaInsets is updated here
self.update(safeAreaInsets: layoutInsets)
// Need to update safeAreaInsets here to ensure that the `adjustedContentInsets` has a correct value.
// Because the parent VC does not call viewSafeAreaInsetsDidChange() expectedly and
// `view.safeAreaInsets` has a correct value of the bottom inset here.
if let parent = parent {
self.update(safeAreaInsets: parent.layoutInsets)
}
}
public override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
safeAreaInsetsObservation = nil
}
// MARK:- Privates
private func fetchLayout(for traitCollection: UITraitCollection) -> FloatingPanelLayout {
switch traitCollection.verticalSizeClass {
case .compact:
@@ -252,28 +216,15 @@ public class FloatingPanelController: UIViewController, UIScrollViewDelegate, UI
private func updateLayout(for traitCollection: UITraitCollection) {
floatingPanel.layoutAdapter.layout = fetchLayout(for: traitCollection)
floatingPanel.layoutAdapter.prepareLayout(in: self)
guard let parent = parent else { return }
floatingPanel.layoutAdapter.prepareLayout(toParent: parent)
floatingPanel.layoutAdapter.activateLayout(of: floatingPanel.state)
}
// MARK: - Container view controller interface
/// Shows the surface view at the initial position defined by the current layout
public func show(animated: Bool = false, completion: (() -> Void)? = nil) {
// Must apply the current layout here
updateLayout(for: traitCollection)
move(to: floatingPanel.layoutAdapter.layout.initialPosition,
animated: animated,
completion: completion)
}
/// Hides the surface view to the hidden position
public func hide(animated: Bool = false, completion: (() -> Void)? = nil) {
move(to: .hidden,
animated: animated,
completion: completion)
}
/// Adds the view managed by the controller as a child of the specified view controller.
/// - Parameters:
/// - parent: A parent view controller object that displays FloatingPanelController's view. A container view controller object isn't applicable.
@@ -290,17 +241,41 @@ public class FloatingPanelController: UIViewController, UIScrollViewDelegate, UI
precondition((parent is UITableViewController) == false, "UITableViewController should not be the parent because the view is a table view so that a floating panel doens't work well")
precondition((parent is UICollectionViewController) == false, "UICollectionViewController should not be the parent because the view is a collection view so that a floating panel doens't work well")
view.frame = parent.view.bounds
if let belowView = belowView {
parent.view.insertSubview(self.view, belowSubview: belowView)
} else {
parent.view.addSubview(self.view)
}
view.frame = parent.view.bounds // MUST
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)
show(animated: true) { [weak self] in
// 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.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)
}
@@ -316,10 +291,14 @@ public class FloatingPanelController: UIViewController, UIScrollViewDelegate, UI
return
}
hide(animated: animated) { [weak self] in
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?()
}
@@ -331,7 +310,6 @@ public class FloatingPanelController: UIViewController, UIScrollViewDelegate, UI
/// - animated: Pass true to animate the presentation; otherwise, pass false.
/// - completion: The block to execute after the view controller has finished moving. This block has no return value and takes no parameters. You may specify nil for this parameter.
public func move(to: FloatingPanelPosition, animated: Bool, completion: (() -> Void)? = nil) {
precondition(floatingPanel.layoutAdapter.vc != nil, "Use show(animated:completion)")
floatingPanel.move(to: to, animated: animated, completion: completion)
}
@@ -344,7 +322,7 @@ public class FloatingPanelController: UIViewController, UIScrollViewDelegate, UI
}
if let vc = contentViewController {
let surfaceView = floatingPanel.surfaceView
let surfaceView = self.view as! FloatingPanelSurfaceView
surfaceView.add(childView: vc.view)
addChild(vc)
vc.didMove(toParent: self)
@@ -352,7 +330,7 @@ public class FloatingPanelController: UIViewController, UIScrollViewDelegate, UI
_contentViewController = contentViewController
}
@available(*, unavailable, renamed: "set(contentViewController:)")
public override func show(_ vc: UIViewController, sender: Any?) {
if let target = self.parent?.targetViewController(forAction: #selector(UIViewController.show(_:sender:)), sender: sender) {
@@ -400,12 +378,11 @@ public class FloatingPanelController: UIViewController, UIScrollViewDelegate, UI
/// by the controller immediately.
///
/// This method updates the `FloatingPanelLayout` object from the delegate and
/// then it calls `layoutIfNeeded()` of the root view to force the view
/// 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)
floatingPanel.layoutAdapter.checkLayoutConsistance()
}
/// Returns the y-coordinate of the point at the origin of the surface view
@@ -417,38 +394,6 @@ public class FloatingPanelController: UIViewController, UIScrollViewDelegate, UI
return floatingPanel.layoutAdapter.middleY
case .tip:
return floatingPanel.layoutAdapter.bottomY
case .hidden:
return floatingPanel.layoutAdapter.hiddenY
}
}
}
extension FloatingPanelController {
private static let dismissSwizzling: Any? = {
let aClass: AnyClass! = UIViewController.self //object_getClass(vc)
if let imp = class_getMethodImplementation(aClass, #selector(dismiss(animated:completion:))),
let originalAltMethod = class_getInstanceMethod(aClass, #selector(fp_original_dismiss(animated:completion:))) {
method_setImplementation(originalAltMethod, imp)
}
let originalMethod = class_getInstanceMethod(aClass, #selector(dismiss(animated:completion:)))
let swizzledMethod = class_getInstanceMethod(aClass, #selector(fp_dismiss(animated:completion:)))
if let originalMethod = originalMethod, let swizzledMethod = swizzledMethod {
// switch implementation..
method_exchangeImplementations(originalMethod, swizzledMethod)
}
return nil
}()
}
public extension UIViewController {
@objc public func fp_original_dismiss(animated flag: Bool, completion: (() -> Void)? = nil) {
// Implementation will be replaced by IMP of self.dismiss(animated:completion:)
}
@objc public func fp_dismiss(animated flag: Bool, completion: (() -> Void)? = nil) {
if let fpc = parent as? FloatingPanelController, fpc.parent != nil {
fpc.removePanelFromParent(animated: flag, completion: completion)
} else {
self.fp_original_dismiss(animated: flag, completion: completion)
}
}
}
+65 -149
View File
@@ -5,37 +5,11 @@
import UIKit
/// FloatingPanelIntrinsicLayout
///
/// - Attention:
/// `insetFor(position:)` must return `nil` for full position because the inset is determined automatically.
/// You can customize insets only for half, tip and hidden positions
/// on FloatingPanelIntrinsicLayout.
public protocol FloatingPanelIntrinsicLayout: FloatingPanelLayout { }
public extension FloatingPanelIntrinsicLayout {
var initialPosition: FloatingPanelPosition {
return .full
}
var supportedPositions: Set<FloatingPanelPosition> {
return [.full]
}
func insetFor(position: FloatingPanelPosition) -> CGFloat? {
return nil
}
}
public protocol FloatingPanelLayout: class {
/// Returns the initial position of a floating panel.
var initialPosition: FloatingPanelPosition { get }
/// Returns a set of FloatingPanelPosition objects to tell the applicable
/// positions of the floating panel controller.
///
/// By default, it returns all position exepct for `hidden` position. Because
/// it's always supported by `FloatingPanelController` so you don't need to return it.
/// 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.
@@ -44,13 +18,10 @@ public protocol FloatingPanelLayout: class {
/// Return the interaction buffer to the bottom from the bottom position. Default is 6.0.
var bottomInteractionBuffer: CGFloat { get }
/// Returns a CGFloat value to determine a Y coordinate of a floating panel for each position(full, half, tip and hidden).
///
/// Its returning value indicates a different inset for each positiion.
/// For full position, a top inset from a safe area in `FloatingPanelController.view`.
/// For half or tip position, a bottom inset from the safe area.
/// For hidden position, a bottom inset from `FloatingPanelController.view`.
/// If a position isn't supported or the default value is used, return nil.
/// 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 X-axis and width layout constraints of the surface view of a floating panel.
@@ -70,7 +41,7 @@ public extension FloatingPanelLayout {
var bottomInteractionBuffer: CGFloat { return 6.0 }
var supportedPositions: Set<FloatingPanelPosition> {
return Set([.full, .half, .tip])
return Set(FloatingPanelPosition.allCases)
}
func prepareLayout(surfaceView: UIView, in view: UIView) -> [NSLayoutConstraint] {
@@ -86,8 +57,6 @@ public extension FloatingPanelLayout {
}
public class FloatingPanelDefaultLayout: FloatingPanelLayout {
public var contentViewController: UIViewController?
public var initialPosition: FloatingPanelPosition {
return .half
}
@@ -97,14 +66,11 @@ public class FloatingPanelDefaultLayout: FloatingPanelLayout {
case .full: return 18.0
case .half: return 262.0
case .tip: return 69.0
case .hidden: return nil
}
}
}
public class FloatingPanelDefaultLandscapeLayout: FloatingPanelLayout {
public var contentViewController: UIViewController?
public var initialPosition: FloatingPanelPosition {
return .tip
}
@@ -123,20 +89,24 @@ public class FloatingPanelDefaultLandscapeLayout: FloatingPanelLayout {
class FloatingPanelLayoutAdapter {
weak var vc: UIViewController!
private weak var parent: UIViewController!
private weak var surfaceView: FloatingPanelSurfaceView!
private weak var backdropView: FloatingPanelBackdropView!
var layout: FloatingPanelLayout
var safeAreaInsets: UIEdgeInsets = .zero {
var layout: FloatingPanelLayout {
didSet {
if oldValue != safeAreaInsets {
updateHeight()
}
checkLayoutConsistance()
}
}
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] = []
@@ -146,11 +116,7 @@ class FloatingPanelLayoutAdapter {
private var heightConstraints: [NSLayoutConstraint] = []
private var fullInset: CGFloat {
if layout is FloatingPanelIntrinsicLayout {
return intrinsicHeight
} else {
return layout.insetFor(position: .full) ?? 0.0
}
return layout.insetFor(position: .full) ?? 0.0
}
private var halfInset: CGFloat {
return layout.insetFor(position: .half) ?? 0.0
@@ -158,23 +124,10 @@ class FloatingPanelLayoutAdapter {
private var tipInset: CGFloat {
return layout.insetFor(position: .tip) ?? 0.0
}
private var hiddenInset: CGFloat {
return layout.insetFor(position: .hidden) ?? 0.0
}
var supportedPositions: Set<FloatingPanelPosition> {
var supportedPositions = layout.supportedPositions
supportedPositions.remove(.hidden)
return supportedPositions
}
var topY: CGFloat {
if supportedPositions.contains(.full) {
if layout is FloatingPanelIntrinsicLayout {
return surfaceView.superview!.bounds.height - (safeAreaInsets.bottom + fullInset)
} else {
return (safeAreaInsets.top + fullInset)
}
if layout.supportedPositions.contains(.full) {
return (safeAreaInsets.top + fullInset)
} else {
return middleY
}
@@ -185,24 +138,17 @@ class FloatingPanelLayoutAdapter {
}
var bottomY: CGFloat {
if supportedPositions.contains(.tip) {
if layout.supportedPositions.contains(.tip) {
return surfaceView.superview!.bounds.height - (safeAreaInsets.bottom + tipInset)
} else {
return middleY
}
}
var hiddenY: CGFloat {
return surfaceView.superview!.bounds.height
}
var safeAreaBottomY: CGFloat {
return surfaceView.superview!.bounds.height - (safeAreaInsets.bottom + hiddenInset)
return surfaceView.superview!.bounds.height - (safeAreaInsets.bottom)
}
var topMaxY: CGFloat { return -safeAreaInsets.top }
var bottomMaxY: CGFloat { return safeAreaBottomY }
var adjustedContentInsets: UIEdgeInsets {
return UIEdgeInsets(top: 0.0,
left: 0.0,
@@ -218,26 +164,17 @@ class FloatingPanelLayoutAdapter {
return middleY
case .tip:
return bottomY
case .hidden:
return hiddenY
}
}
var intrinsicHeight: CGFloat = 0.0
init(surfaceView: FloatingPanelSurfaceView, backdropView: FloatingPanelBackdropView, layout: FloatingPanelLayout) {
self.layout = layout
self.surfaceView = surfaceView
self.backdropView = backdropView
}
func updateIntrinsicHeight() {
let fittingSize = UIView.layoutFittingCompressedSize
intrinsicHeight = surfaceView.contentView.systemLayoutSizeFitting(fittingSize).height
}
func prepareLayout(in vc: UIViewController) {
self.vc = vc
func prepareLayout(toParent parent: UIViewController) {
self.parent = parent
surfaceView.translatesAutoresizingMaskIntoConstraints = false
backdropView.translatesAutoresizingMaskIntoConstraints = false
@@ -245,47 +182,40 @@ class FloatingPanelLayoutAdapter {
NSLayoutConstraint.deactivate(fixedConstraints + fullConstraints + halfConstraints + tipConstraints + offConstraints)
// Fixed constraints of surface and backdrop views
let surfaceConstraints = layout.prepareLayout(surfaceView: surfaceView, in: vc.view!)
let surfaceConstraints = layout.prepareLayout(surfaceView: surfaceView, in: parent.view!)
let backdropConstraints = [
backdropView.topAnchor.constraint(equalTo: vc.view.topAnchor, constant: 0.0),
backdropView.leftAnchor.constraint(equalTo: vc.view.leftAnchor,constant: 0.0),
backdropView.rightAnchor.constraint(equalTo: vc.view.rightAnchor, constant: 0.0),
backdropView.bottomAnchor.constraint(equalTo: vc.view.bottomAnchor, constant: 0.0),
backdropView.topAnchor.constraint(equalTo: parent.view.topAnchor,
constant: 0.0),
backdropView.leftAnchor.constraint(equalTo: parent.view.leftAnchor,
constant: 0.0),
backdropView.rightAnchor.constraint(equalTo: parent.view.rightAnchor,
constant: 0.0),
backdropView.bottomAnchor.constraint(equalTo: parent.view.bottomAnchor,
constant: 0.0),
]
fixedConstraints = surfaceConstraints + backdropConstraints
// Flexible surface constarints for full, half, tip and off
if layout is FloatingPanelIntrinsicLayout {
updateIntrinsicHeight()
fullConstraints = [
surfaceView.topAnchor.constraint(equalTo: vc.layoutGuide.bottomAnchor,
constant: -fullInset),
]
} else {
fullConstraints = [
surfaceView.topAnchor.constraint(equalTo: vc.layoutGuide.topAnchor,
constant: fullInset),
]
}
fullConstraints = [
surfaceView.topAnchor.constraint(equalTo: parent.layoutGuide.topAnchor,
constant: fullInset),
]
halfConstraints = [
surfaceView.topAnchor.constraint(equalTo: vc.layoutGuide.bottomAnchor,
surfaceView.topAnchor.constraint(equalTo: parent.layoutGuide.bottomAnchor,
constant: -halfInset),
]
tipConstraints = [
surfaceView.topAnchor.constraint(equalTo: vc.layoutGuide.bottomAnchor,
surfaceView.topAnchor.constraint(equalTo: parent.layoutGuide.bottomAnchor,
constant: -tipInset),
]
offConstraints = [
surfaceView.topAnchor.constraint(equalTo: vc.view.bottomAnchor,
constant: -hiddenInset),
surfaceView.topAnchor.constraint(equalTo: parent.view.bottomAnchor, constant: 0.0),
]
}
// The method is separated from prepareLayout(to:) for the rotation support
// It must be called in FloatingPanelController.traitCollectionDidChange(_:)
func updateHeight() {
guard let vc = vc else { return }
defer {
UIView.performWithoutAnimation {
surfaceView.superview!.layoutIfNeeded()
@@ -293,47 +223,33 @@ class FloatingPanelLayoutAdapter {
}
NSLayoutConstraint.deactivate(heightConstraints)
let height: CGFloat
if layout is FloatingPanelIntrinsicLayout {
updateIntrinsicHeight()
height = intrinsicHeight + safeAreaInsets.bottom
} else {
// Must use the`vc` height, not the screen height because safe area insets
// of `vc` are relative values. For example, a view controller in
// Navigation controller's safe area insets and frame can be changed whether
// the navigation bar is translucent or not.
height = vc.view.bounds.height - (safeAreaInsets.top + fullInset)
}
// 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 + layout.topInteractionBuffer)
if layout is FloatingPanelIntrinsicLayout {
NSLayoutConstraint.deactivate(fullConstraints)
fullConstraints = [
surfaceView.topAnchor.constraint(equalTo: vc.layoutGuide.bottomAnchor,
constant: -fullInset),
]
NSLayoutConstraint.activate(fullConstraints)
}
surfaceView.set(bottomOverflow: heightBuffer)
}
func activateLayout(of state: FloatingPanelPosition) {
func activateLayout(of state: FloatingPanelPosition?) {
defer {
surfaceView.superview!.layoutIfNeeded()
}
var state = state
setBackdropAlpha(of: state)
NSLayoutConstraint.activate(fixedConstraints)
if supportedPositions.union([.hidden]).contains(state) == false {
guard var state = state else {
NSLayoutConstraint.deactivate(fullConstraints + halfConstraints + tipConstraints)
NSLayoutConstraint.activate(offConstraints)
return
}
if layout.supportedPositions.contains(state) == false {
state = layout.initialPosition
}
@@ -348,36 +264,36 @@ class FloatingPanelLayoutAdapter {
case .tip:
NSLayoutConstraint.deactivate(fullConstraints + halfConstraints + offConstraints)
NSLayoutConstraint.activate(tipConstraints)
case .hidden:
NSLayoutConstraint.deactivate(fullConstraints + halfConstraints + tipConstraints)
NSLayoutConstraint.activate(offConstraints)
}
}
func setBackdropAlpha(of target: FloatingPanelPosition) {
if target == .hidden {
self.backdropView.alpha = 0.0
} else {
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))")
if layout is FloatingPanelIntrinsicLayout {
assert(layout.insetFor(position: .full) == nil, "Return `nil` for full position on FloatingPanelIntrinsicLayout")
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 { topY: \(topY), middleY: \(middleY) }")
assert(bottomY > topY, "Invalid insets { topY: \(topY), bottomY: \(bottomY) }")
assert(middleY > topY, "Invalid insets")
assert(bottomY > topY, "Invalid insets")
}
}
}
@@ -10,10 +10,7 @@ class FloatingPanelSurfaceContentView: UIView {}
/// A view that presents a surface interface in a floating panel.
public class FloatingPanelSurfaceView: UIView {
/// A GrabberHandleView object displayed at the top of the surface view.
///
/// To use a custom grabber handle, hide this and then add the custom one
/// to the surface view at appropirate coordinates.
/// A GrabberHandleView object displayed at the top of the surface view
public var grabberHandle: GrabberHandleView!
/// The height of the grabber bar area
@@ -1,99 +0,0 @@
//
// Created by Shin Yamamoto on 2018/11/21.
// Copyright © 2018 Shin Yamamoto. All rights reserved.
//
import UIKit
class FloatingPanelModalTransition: NSObject, UIViewControllerTransitioningDelegate {
func animationController(forPresented presented: UIViewController,
presenting: UIViewController,
source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return FloatingPanelModalPresentTransition()
}
func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return FloatingPanelModalDismissTransition()
}
func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController) -> UIPresentationController? {
return FloatingPanelPresentationController(presentedViewController: presented, presenting: presenting)
}
}
class FloatingPanelPresentationController: UIPresentationController {
override func presentationTransitionDidEnd(_ completed: Bool) {
// For non-animated presentation
if let fpc = presentedViewController as? FloatingPanelController, fpc.position == .hidden {
fpc.show(animated: false, completion: nil)
}
}
override func dismissalTransitionDidEnd(_ completed: Bool) {
// For non-animated dismissal
if let fpc = presentedViewController as? FloatingPanelController, fpc.position != .hidden {
fpc.hide(animated: false, completion: nil)
}
}
override func containerViewWillLayoutSubviews() {
guard
let containerView = self.containerView,
let fpc = presentedViewController as? FloatingPanelController,
let fpView = fpc.view
else { fatalError() }
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleBackdrop(tapGesture:)))
fpc.backdropView.addGestureRecognizer(tapGesture)
containerView.addSubview(fpView)
fpView.frame = containerView.bounds //MUST
}
@objc func handleBackdrop(tapGesture: UITapGestureRecognizer) {
presentedViewController.dismiss(animated: true, completion: nil)
}
}
class FloatingPanelModalPresentTransition: NSObject, UIViewControllerAnimatedTransitioning {
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
guard
let fpc = transitionContext?.viewController(forKey: .to) as? FloatingPanelController
else { fatalError()}
let animator = fpc.behavior.addAnimator(fpc, to: fpc.layout.initialPosition)
return TimeInterval(animator.duration)
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
guard
let fpc = transitionContext.viewController(forKey: .to) as? FloatingPanelController
else { fatalError() }
fpc.show(animated: true) {
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
}
}
}
class FloatingPanelModalDismissTransition: NSObject, UIViewControllerAnimatedTransitioning {
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
guard
let fpc = transitionContext?.viewController(forKey: .from) as? FloatingPanelController
else { fatalError()}
let animator = fpc.behavior.removeAnimator(fpc, from: fpc.position)
return TimeInterval(animator.duration)
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
guard
let fpc = transitionContext.viewController(forKey: .from) as? FloatingPanelController
else { fatalError() }
fpc.hide(animated: true) {
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
}
}
}
-30
View File
@@ -1,30 +0,0 @@
//
// Created by Shin Yamamoto on 2018/11/21.
// Copyright © 2018 Shin Yamamoto. All rights reserved.
//
import UIKit
class FloatingPanelPassThroughView: UIView {
public override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
let view = super.hitTest(point, with: event)
switch view {
case is FloatingPanelPassThroughView:
return nil
default:
return view
}
}
}
class FloatingPanelSurfaceWrapperView: UIView {
public override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
let view = super.hitTest(point, with: event)
switch view {
case is FloatingPanelSurfaceWrapperView:
return nil
default:
return view
}
}
}
-11
View File
@@ -62,17 +62,6 @@ extension UIView {
}
}
extension UIView {
func disableAutoLayout() {
let frame = self.frame
translatesAutoresizingMaskIntoConstraints = true
self.frame = frame
}
func enableAutoLayout() {
translatesAutoresizingMaskIntoConstraints = false
}
}
extension UIGestureRecognizer.State: CustomDebugStringConvertible {
public var debugDescription: String {
switch self {
+69 -123
View File
@@ -23,14 +23,10 @@ The new interface displays the related contents and utilities in parallel as a u
- [CocoaPods](#cocoapods)
- [Carthage](#carthage)
- [Getting Started](#getting-started)
- [Add a floating panel as a child view controller](#add-a-floating-panel-as-a-child-view-controller)
- [Present a floating panel as a modality](#present-a-floating-panel-as-a-modality)
- [Usage](#usage)
- [Show/Hide a floating panel in a view with your view hierarchy](#showhide-a-floating-panel-in-a-view-with-your-view-hierarchy)
- [Customize the layout with `FloatingPanelLayout` protocol](#customize-the-layout-with-floatingpanellayout-protocol)
- [Change the initial position and height](#change-the-initial-position-and-height)
- [Support your landscape layout](#support-your-landscape-layout)
- [Use Intrinsic height layout](#use-intrinsic-height-layout)
- [Customize the behavior with `FloatingPanelBehavior` protocol](#customize-the-behavior-with-floatingpanelbehavior-protocol)
- [Modify your floating panel's interaction](#modify-your-floating-panels-interaction)
- [Use a custom grabber handle](#use-a-custom-grabber-handle)
@@ -56,7 +52,6 @@ The new interface displays the related contents and utilities in parallel as a u
- [x] Layout customization for all trait environments(i.e. Landscape orientation support)
- [x] Behavior customization
- [x] Free from common issues of Auto Layout and gesture handling
- [x] Modal presentation
Examples are here.
@@ -89,8 +84,6 @@ github "scenee/FloatingPanel"
## Getting Started
### Add a floating panel as a child view controller
```swift
import UIKit
import FloatingPanel
@@ -119,61 +112,15 @@ class ViewController: UIViewController, FloatingPanelControllerDelegate {
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
// Remove the views managed by the `FloatingPanelController` object from self.view.
fpc.removePanelFromParent()
}
...
}
```
### Present a floating panel as a modality
```swift
let fpc = FloatingPanelController()
let contentVC = ...
fpc.set(contentViewController: contentVC)
fpc.isRemovalInteractionEnabled = true // Optional: Let it removable by a swipe-down
self.present(fpc, animated: true, completion: nil)
```
You can show a floating panel over UINavigationController from the containnee view controllers as a modality of `.overCurrentContext` style.
NOTE: FloatingPanelController has the custom presentation controller. If you would like to customize the presentation/dismissal, please see [FloatingPanelTransitioning](https://github.com/SCENEE/FloatingPanel/blob/feat-modality/Framework/Sources/FloatingPanelTransitioning.swift).
## Usage
### Show/Hide a floating panel in a view with your view hierarchy
```swift
// Add the controller and the managed views to a view controller.
// From the second time, just call `show(animated:completion)`.
view.addSubview(fpc.view)
fpc.view.frame = view.bounds // MUST
parent.addChild(fpc)
// Show a floating panel to the initial position defined in your `FloatingPanelLayout` object.
fpc.show(animated: true) {
// Only for the first time
self.didMove(toParent: self)
}
...
// Hide it
fpc.hide(animated: true) {
// Remove it if needed
self.willMove(toParent: nil)
self.view.removeFromSuperview()
self.removeFromParent()
}
```
NOTE: `FloatingPanelController` wraps `show`/`hide` with `addPanel`/`removePanelFromParent` for easy-to-use. But `show`/`hide` are more convenience for your app.
### Customize the layout with `FloatingPanelLayout` protocol
#### Change the initial position and height
@@ -184,6 +131,7 @@ class ViewController: UIViewController, FloatingPanelControllerDelegate {
func floatingPanel(_ vc: FloatingPanelController, layoutFor newCollection: UITraitCollection) -> FloatingPanelLayout? {
return MyFloatingPanelLayout()
}
...
}
class MyFloatingPanelLayout: FloatingPanelLayout {
@@ -196,7 +144,6 @@ class MyFloatingPanelLayout: FloatingPanelLayout {
case .full: return 16.0 // A top inset from safe area
case .half: return 216.0 // A bottom inset from the safe area
case .tip: return 44.0 // A bottom inset from the safe area
default: return nil // Or `case .hidden: return nil`
}
}
}
@@ -210,6 +157,7 @@ class ViewController: UIViewController, FloatingPanelControllerDelegate {
func floatingPanel(_ vc: FloatingPanelController, layoutFor newCollection: UITraitCollection) -> FloatingPanelLayout? {
return (newCollection.verticalSizeClass == .compact) ? FloatingPanelLandscapeLayout() : nil // Returning nil indicates to use the default layout
}
...
}
class FloatingPanelLandscapeLayout: FloatingPanelLayout {
@@ -237,33 +185,6 @@ class FloatingPanelLandscapeLayout: FloatingPanelLayout {
}
```
#### Use Intrinsic height layout
1. Lay out your content View with the intrinsic height size. For example, see "Detail View Controller scene"/"Intrinsic View Controller scene" of [Main.storyboard](https://github.com/SCENEE/FloatingPanel/blob/master/Examples/Samples/Sources/Base.lproj/Main.storyboard). The 'Stack View.bottom' constraint determines the intrinsic height.
2. Create a layout that adopts and conforms to `FloatingPanelIntrinsicLayout` and use it.
```swift
class ViewController: UIViewController, FloatingPanelControllerDelegate {
...
func floatingPanel(_ vc: FloatingPanelController, layoutFor newCollection: UITraitCollection) -> FloatingPanelLayout? {
return RemovablePanelLayout()
}
}
class RemovablePanelLayout: FloatingPanelIntrinsicLayout {
var supportedPositions: Set<FloatingPanelPosition> {
return [.full, .half]
}
func insetFor(position: FloatingPanelPosition) -> CGFloat? {
switch position {
case .half: return 130.0
default: return nil // Must return nil for .full
}
}
...
}
```
### Customize the behavior with `FloatingPanelBehavior` protocol
#### Modify your floating panel's interaction
@@ -274,67 +195,90 @@ class ViewController: UIViewController, FloatingPanelControllerDelegate {
func floatingPanel(_ vc: FloatingPanelController, behaviorFor newCollection: UITraitCollection) -> FloatingPanelBehavior? {
return FloatingPanelStocksBehavior()
}
...
}
...
class FloatingPanelStocksBehavior: FloatingPanelBehavior {
...
var velocityThreshold: CGFloat {
return 15.0
}
func interactionAnimator(_ fpc: FloatingPanelController, to targetPosition: FloatingPanelPosition, with velocity: CGVector) -> UIViewPropertyAnimator {
let damping = self.damping(with: velocity)
let springTiming = UISpringTimingParameters(dampingRatio: damping, initialVelocity: velocity)
return UIViewPropertyAnimator(duration: 0.5, timingParameters: springTiming)
}
...
}
```
### Use a custom grabber handle
```swift
let myGrabberHandleView = MyGrabberHandleView()
fpc.surfaceView.grabberHandle.isHidden = true
fpc.surfaceView.addSubview(myGrabberHandleView)
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
override func viewDidLoad() {
class ViewController: UIViewController, FloatingPanelControllerDelegate {
...
surfaceTapGesture = UITapGestureRecognizer(target: self, action: #selector(handleSurface(tapGesture:)))
fpc.surfaceView.addGestureRecognizer(surfaceTapGesture)
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)
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)
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
override func viewDidLoad() {
// Setup Search panel
self.searchPanelVC = FloatingPanelController()
class ViewController: UIViewController, FloatingPanelControllerDelegate {
var searchPanelVC: FloatingPanelController!
var detailPanelVC: FloatingPanelController!
let searchVC = SearchViewController()
self.searchPanelVC.set(contentViewController: searchVC)
self.searchPanelVC.track(scrollView: contentVC.tableView)
override func viewDidLoad() {
// Setup Search panel
self.searchPanelVC = FloatingPanelController()
self.searchPanelVC.addPanel(toParent: self)
let searchVC = SearchViewController()
self.searchPanelVC.set(contentViewController: searchVC)
self.searchPanelVC.track(scrollView: contentVC.tableView)
// Setup Detail panel
self.detailPanelVC = FloatingPanelController()
self.searchPanelVC.addPanel(toParent: self)
let contentVC = ContentViewController()
self.detailPanelVC.set(contentViewController: contentVC)
self.detailPanelVC.track(scrollView: contentVC.scrollView)
// Setup Detail panel
self.detailPanelVC = FloatingPanelController()
self.detailPanelVC.addPanel(toParent: self)
let contentVC = ContentViewController()
self.detailPanelVC.set(contentViewController: contentVC)
self.detailPanelVC.track(scrollView: contentVC.scrollView)
self.detailPanelVC.addPanel(toParent: self)
}
...
}
```
@@ -343,15 +287,15 @@ override func viewDidLoad() {
In the following example, I move a floating panel to full or half position while opening or closing a search bar like Apple Maps.
```swift
func searchBarCancelButtonClicked(_ searchBar: UISearchBar) {
...
fpc.move(to: .half, animated: true)
}
func searchBarCancelButtonClicked(_ searchBar: UISearchBar) {
...
fpc.move(to: .half, animated: true)
}
func searchBarTextDidBeginEditing(_ searchBar: UISearchBar) {
...
fpc.move(to: .full, animated: true)
}
func searchBarTextDidBeginEditing(_ searchBar: UISearchBar) {
...
fpc.move(to: .full, animated: true)
}
```
### Work your contents together with a floating panel behavior
@@ -371,6 +315,7 @@ class ViewController: UIViewController, FloatingPanelControllerDelegate {
searchVC.hideHeader()
}
}
...
}
```
@@ -378,7 +323,7 @@ class ViewController: UIViewController, FloatingPanelControllerDelegate {
### '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(except for modality).
'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.
@@ -401,16 +346,17 @@ class ViewController: UIViewController {
secondFpc.addPanel(toParent: self)
}
...
}
```
A `FloatingPanelController` object proxies an action for `show(_:sender)` to the master VC. That's why the master VC can handle a destination view controller of a 'Show' or 'Show Detail' segue and you can hook `show(_:sender)` to show a secondary floating panel set the destination view controller to the content.
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.
* 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() {
@@ -421,11 +367,11 @@ override func viewDidLayoutSubviews() {
}
}
```
* If you sets clear color to `FloatingPanelSurfaceView.backgroundColor`, please note the bottom overflow of your content on bouncing at full position. To prevent it, you need to expand your content. For example, See Example/Maps App's Auto Layout settings of `UIVisualEffectView` in Main.storyboard.
* 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> | [@scenee](https://twitter.com/scenee)
Shin Yamamoto <shin@scenee.com>
## License