Compare commits

..

4 Commits

Author SHA1 Message Date
Shin Yamamoto 837edc6abf Update sample 2019-06-01 23:36:20 +09:00
Shin Yamamoto 20fda3888f GrabberHandleView supports UIAppearance 2019-06-01 23:36:20 +09:00
Shin Yamamoto 4728c6848b FloatingPanelBackdropView supports UIAppearance 2019-06-01 23:36:20 +09:00
Shin Yamamoto 70c945c9f5 FloatingPanelSurfaceView supports UIAppearance 2019-06-01 23:36:20 +09:00
30 changed files with 762 additions and 2184 deletions
+19 -28
View File
@@ -22,40 +22,22 @@ jobs:
osx_image: xcode10
name: "Swift 4.2"
- script: xcodebuild -scheme FloatingPanel SWIFT_VERSION=5.0 clean build
osx_image: xcode10.3
osx_image: xcode10.2
name: "Swift 5.0"
- script: xcodebuild -scheme FloatingPanel SWIFT_VERSION=5.1 SUPPORTS_MACCATALYST=NO clean build
# SUPPORTS_MACCATALYST=NO because Xcode 11 runs on macOS 10.14 in Travis CI
osx_image: xcode11
name: "Swift 5.1"
- stage: "Tests"
osx_image: xcode10.3
osx_image: xcode10.2
script: xcodebuild clean test -scheme FloatingPanel -workspace FloatingPanel.xcworkspace -destination 'platform=iOS Simulator,OS=10.3.1,name=iPhone SE'
name: "iPhone SE (iOS 10.3)"
- script: xcodebuild clean test -scheme FloatingPanel -workspace FloatingPanel.xcworkspace -destination 'platform=iOS Simulator,OS=11.4,name=iPhone 7'
osx_image: xcode10.3
osx_image: xcode10.2
name: "iPhone 7 (iOS 11.4)"
- script: xcodebuild clean test -scheme FloatingPanel -workspace FloatingPanel.xcworkspace -destination 'platform=iOS Simulator,OS=12.2,name=iPhone X'
osx_image: xcode10.3
osx_image: xcode10.2
name: "iPhone X (iOS 12.2)"
- script: xcodebuild clean test -scheme FloatingPanel -workspace FloatingPanel.xcworkspace -destination 'platform=iOS Simulator,OS=13.0,name=iPhone 11'
osx_image: xcode11
name: "iPhone X (iOS 13.0)"
- stage: Build examples
osx_image: xcode11
script: xcodebuild -scheme Maps -sdk iphonesimulator clean build
name: "Maps"
- script: xcodebuild -scheme Stocks -sdk iphonesimulator clean build
osx_image: xcode11
name: "Stocks"
- script: xcodebuild -scheme Samples -sdk iphonesimulator clean build
osx_image: xcode11
name: "Samples"
- stage: Carthage
osx_image: xcode11
osx_image: xcode10.2
before_install:
- brew update
- brew outdated carthage || brew upgrade carthage
@@ -63,9 +45,18 @@ jobs:
- carthage build --no-skip-current
- stage: CocoaPods
osx_image: xcode11
before_install:
- gem install cocoapods
osx_image: xcode10.2
script:
- pod spec lint --allow-warnings
- pod lib lint --allow-warnings
- pod spec lint
- pod lib lint
- stage: Build examples
osx_image: xcode10.2
script: xcodebuild -scheme Maps -sdk iphonesimulator clean build
name: "Maps"
- script: xcodebuild -scheme Stocks -sdk iphonesimulator clean build
osx_image: xcode10.2
name: "Stocks"
- script: xcodebuild -scheme Samples -sdk iphonesimulator clean build
osx_image: xcode10.2
name: "Samples"
+6 -6
View File
@@ -7,13 +7,13 @@
objects = {
/* Begin PBXBuildFile section */
549D23D2233C77D5008EF4D7 /* FloatingPanel.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 549D23D1233C77D5008EF4D7 /* FloatingPanel.framework */; };
549D23D3233C77D5008EF4D7 /* FloatingPanel.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 549D23D1233C77D5008EF4D7 /* FloatingPanel.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
54B5112A216C3D840033A6F3 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54B51129216C3D840033A6F3 /* AppDelegate.swift */; };
54B5112C216C3D840033A6F3 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54B5112B216C3D840033A6F3 /* ViewController.swift */; };
54B5112F216C3D840033A6F3 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 54B5112D216C3D840033A6F3 /* Main.storyboard */; };
54B51131216C3D860033A6F3 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 54B51130216C3D860033A6F3 /* Assets.xcassets */; };
54B51134216C3D860033A6F3 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 54B51132216C3D860033A6F3 /* LaunchScreen.storyboard */; };
54B5113F216C407F0033A6F3 /* FloatingPanel.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 54B5113E216C407F0033A6F3 /* FloatingPanel.framework */; };
54B51140216C407F0033A6F3 /* FloatingPanel.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 54B5113E216C407F0033A6F3 /* FloatingPanel.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
/* End PBXBuildFile section */
/* Begin PBXCopyFilesBuildPhase section */
@@ -23,7 +23,7 @@
dstPath = "";
dstSubfolderSpec = 10;
files = (
549D23D3233C77D5008EF4D7 /* FloatingPanel.framework in Embed Frameworks */,
54B51140216C407F0033A6F3 /* FloatingPanel.framework in Embed Frameworks */,
);
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
@@ -31,7 +31,6 @@
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
549D23D1233C77D5008EF4D7 /* FloatingPanel.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = FloatingPanel.framework; sourceTree = BUILT_PRODUCTS_DIR; };
54B51126216C3D840033A6F3 /* Maps.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Maps.app; sourceTree = BUILT_PRODUCTS_DIR; };
54B51129216C3D840033A6F3 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
54B5112B216C3D840033A6F3 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = "<group>"; };
@@ -39,6 +38,7 @@
54B51130216C3D860033A6F3 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
54B51133216C3D860033A6F3 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
54B51135216C3D860033A6F3 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
54B5113E216C407F0033A6F3 /* FloatingPanel.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = FloatingPanel.framework; sourceTree = BUILT_PRODUCTS_DIR; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@@ -46,7 +46,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
549D23D2233C77D5008EF4D7 /* FloatingPanel.framework in Frameworks */,
54B5113F216C407F0033A6F3 /* FloatingPanel.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -56,7 +56,7 @@
54B5111D216C3D840033A6F3 = {
isa = PBXGroup;
children = (
549D23D1233C77D5008EF4D7 /* FloatingPanel.framework */,
54B5113E216C407F0033A6F3 /* FloatingPanel.framework */,
54B51128216C3D840033A6F3 /* Maps */,
54B51127216C3D840033A6F3 /* Products */,
);
+3 -13
View File
@@ -148,9 +148,11 @@ class SearchPanelViewController: UIViewController, UITableViewDataSource, UITabl
tableView.dataSource = self
tableView.delegate = self
searchBar.placeholder = "Search for a place or address"
searchBar.setSearchText(fontSize: 15.0)
let textField = searchBar.value(forKey: "_searchField") as! UITextField
textField.font = UIFont(name: textField.font!.fontName, size: 15.0)
hideHeader()
}
override func viewDidLayoutSubviews() {
@@ -272,15 +274,3 @@ class SearchHeaderView: UIView {
self.clipsToBounds = true
}
}
extension UISearchBar {
func setSearchText(fontSize: CGFloat) {
#if swift(>=5.1) // Xcode 11 or later
let font = searchTextField.font
searchTextField.font = font?.withSize(fontSize)
#else
let textField = value(forKey: "_searchField") as! UITextField
textField.font = textField.font?.withSize(fontSize)
#endif
}
}
@@ -14,9 +14,9 @@
545DB9F821511E6400CA77B8 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 545DB9F621511E6400CA77B8 /* LaunchScreen.storyboard */; };
545DBA0321511E6400CA77B8 /* SampleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 545DBA0221511E6400CA77B8 /* SampleTests.swift */; };
545DBA0E21511E6400CA77B8 /* SampleUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 545DBA0D21511E6400CA77B8 /* SampleUITests.swift */; };
549D23CB233C7779008EF4D7 /* FloatingPanel.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 549D23CA233C7779008EF4D7 /* FloatingPanel.framework */; };
549D23CC233C7779008EF4D7 /* FloatingPanel.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 549D23CA233C7779008EF4D7 /* FloatingPanel.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
54B51116216AFE5F0033A6F3 /* UIExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54B51115216AFE5F0033A6F3 /* UIExtensions.swift */; };
54B5113C216C40670033A6F3 /* FloatingPanel.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 54B5113B216C40670033A6F3 /* FloatingPanel.framework */; };
54B5113D216C40670033A6F3 /* FloatingPanel.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 54B5113B216C40670033A6F3 /* FloatingPanel.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
54CDC5D8215BBE23007D205C /* UIComponents.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CDC5D7215BBE23007D205C /* UIComponents.swift */; };
/* End PBXBuildFile section */
@@ -38,13 +38,13 @@
/* End PBXContainerItemProxy section */
/* Begin PBXCopyFilesBuildPhase section */
549D23CD233C7779008EF4D7 /* Embed Frameworks */ = {
54B5111C216C3B300033A6F3 /* Embed Frameworks */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 10;
files = (
549D23CC233C7779008EF4D7 /* FloatingPanel.framework in Embed Frameworks */,
54B5113D216C40670033A6F3 /* FloatingPanel.framework in Embed Frameworks */,
);
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
@@ -65,8 +65,8 @@
545DBA0921511E6400CA77B8 /* SamplesUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SamplesUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
545DBA0D21511E6400CA77B8 /* SampleUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SampleUITests.swift; sourceTree = "<group>"; };
545DBA0F21511E6400CA77B8 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
549D23CA233C7779008EF4D7 /* FloatingPanel.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = FloatingPanel.framework; sourceTree = BUILT_PRODUCTS_DIR; };
54B51115216AFE5F0033A6F3 /* UIExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIExtensions.swift; sourceTree = "<group>"; };
54B5113B216C40670033A6F3 /* FloatingPanel.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = FloatingPanel.framework; sourceTree = BUILT_PRODUCTS_DIR; };
54CDC5D7215BBE23007D205C /* UIComponents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIComponents.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
@@ -75,7 +75,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
549D23CB233C7779008EF4D7 /* FloatingPanel.framework in Frameworks */,
54B5113C216C40670033A6F3 /* FloatingPanel.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -99,11 +99,12 @@
545DB9E121511E6300CA77B8 = {
isa = PBXGroup;
children = (
549D23CA233C7779008EF4D7 /* FloatingPanel.framework */,
54B5113B216C40670033A6F3 /* FloatingPanel.framework */,
545DB9EC21511E6300CA77B8 /* Sources */,
545DBA0121511E6400CA77B8 /* Tests */,
545DBA0C21511E6400CA77B8 /* UITests */,
545DB9EB21511E6300CA77B8 /* Products */,
545DBA1B2151CC1000CA77B8 /* Frameworks */,
);
sourceTree = "<group>";
};
@@ -150,6 +151,13 @@
path = UITests;
sourceTree = "<group>";
};
545DBA1B2151CC1000CA77B8 /* Frameworks */ = {
isa = PBXGroup;
children = (
);
name = Frameworks;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
@@ -161,7 +169,7 @@
545DB9E621511E6300CA77B8 /* Sources */,
545DB9E721511E6300CA77B8 /* Frameworks */,
545DB9E821511E6300CA77B8 /* Resources */,
549D23CD233C7779008EF4D7 /* Embed Frameworks */,
54B5111C216C3B300033A6F3 /* Embed Frameworks */,
);
buildRules = (
);
@@ -4,8 +4,18 @@
//
import UIKit
import FloatingPanel
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
FloatingPanelSurfaceView.appearance().shadowHidden = false
FloatingPanelSurfaceView.appearance().cornerRadius = 6.0
// FloatingPanelSurfaceView.appearance().backgroundColor = .lightGray
// FloatingPanelBackdropView.appearance().backgroundColor = .red
// GrabberHandleView.appearance().barColor = .red
return true
}
}
@@ -1,11 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="14490.70" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="RoN-h0-uBD">
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="14460.31" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="RoN-h0-uBD">
<device id="retina5_9" orientation="portrait">
<adaptation id="fullscreen"/>
</device>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14490.49"/>
<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>
@@ -35,18 +35,18 @@
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<tableView clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="plain" separatorStyle="default" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="28" sectionFooterHeight="28" translatesAutoresizingMaskIntoConstraints="NO" id="7IS-PU-x0P">
<rect key="frame" x="0.0" y="0.0" width="375" height="812"/>
<rect key="frame" x="0.0" y="0.0" width="375" height="778"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<prototypes>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" reuseIdentifier="Cell" textLabel="M0G-C8-hAO" style="IBUITableViewCellStyleDefault" id="ySY-oA-g81">
<rect key="frame" x="0.0" y="28" width="375" height="43.666667938232422"/>
<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.666667938232422"/>
<rect key="frame" x="0.0" y="0.0" width="375" height="43.666666666666664"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Title" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="M0G-C8-hAO">
<rect key="frame" x="15" y="0.0" width="345" height="43.666667938232422"/>
<rect key="frame" x="15" y="0.0" width="345" height="43.666666666666664"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/>
@@ -62,7 +62,7 @@
<constraints>
<constraint firstItem="7IS-PU-x0P" firstAttribute="top" secondItem="Smh-Bd-AAc" secondAttribute="top" id="6yd-jv-ey3"/>
<constraint firstItem="7IS-PU-x0P" firstAttribute="leading" secondItem="39L-Nq-qfp" secondAttribute="leading" id="Z6Y-Dc-cei"/>
<constraint firstItem="7IS-PU-x0P" firstAttribute="bottom" secondItem="Smh-Bd-AAc" secondAttribute="bottom" id="fNW-DP-lhV"/>
<constraint firstItem="7IS-PU-x0P" firstAttribute="bottom" secondItem="39L-Nq-qfp" secondAttribute="bottom" id="fNW-DP-lhV"/>
<constraint firstItem="7IS-PU-x0P" firstAttribute="trailing" secondItem="39L-Nq-qfp" secondAttribute="trailing" id="vfY-Rc-FOI"/>
</constraints>
<viewLayoutGuide key="safeArea" id="39L-Nq-qfp"/>
@@ -87,35 +87,35 @@
<objects>
<viewController storyboardIdentifier="SettingsViewController" id="C1X-9Z-TyQ" customClass="SettingsViewController" customModule="Samples" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" id="af9-Zr-Ppc">
<rect key="frame" x="0.0" y="0.0" width="375" height="197.33333333333334"/>
<rect key="frame" x="0.0" y="0.0" width="375" height="197.33000000000001"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" distribution="fillProportionally" alignment="center" spacing="16" translatesAutoresizingMaskIntoConstraints="NO" id="n93-ZL-fmC">
<rect key="frame" x="32" y="16" width="311" height="181.33333333333334"/>
<rect key="frame" x="32" y="16" width="311" height="147.33333333333334"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Version: 1.0" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="WmC-Tq-NDN">
<rect key="frame" x="118.33333333333334" y="0.0" width="74.333333333333343" height="21"/>
<rect key="frame" x="118.33333333333334" y="0.0" width="74.333333333333343" height="17"/>
<fontDescription key="fontDescription" type="system" pointSize="14"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="UINavigationBar" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Ulg-gS-ah0">
<rect key="frame" x="90.666666666666686" y="37" width="130" height="25"/>
<rect key="frame" x="90.666666666666686" y="33" width="130" height="20.333333333333329"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleHeadline"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<stackView opaque="NO" contentMode="scaleToFill" alignment="center" spacing="126" translatesAutoresizingMaskIntoConstraints="NO" id="uEf-g4-CeU">
<rect key="frame" x="23.333333333333343" y="78" width="264.66666666666663" height="38"/>
<rect key="frame" x="23.333333333333343" y="69.333333333333329" width="264.33333333333326" height="31"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Large Titles" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="ogl-S5-4tJ">
<rect key="frame" x="0.0" y="8.9999999999999982" width="89.666666666666671" height="20.333333333333329"/>
<rect key="frame" x="0.0" y="5.3333333333333428" width="89.333333333333329" height="20.333333333333332"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<switch opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" contentHorizontalAlignment="center" contentVerticalAlignment="center" translatesAutoresizingMaskIntoConstraints="NO" id="js8-Qv-lUC">
<rect key="frame" x="215.66666666666666" y="3.6666666666666714" width="50.999999999999972" height="31"/>
<rect key="frame" x="215.33333333333334" y="0.0" width="51.000000000000028" height="31"/>
<connections>
<action selector="toggleLargeTitle:" destination="C1X-9Z-TyQ" eventType="valueChanged" id="FJS-Ty-mCY"/>
</connections>
@@ -123,16 +123,16 @@
</subviews>
</stackView>
<stackView opaque="NO" contentMode="scaleToFill" spacing="126" translatesAutoresizingMaskIntoConstraints="NO" id="ZtZ-Dz-4cC">
<rect key="frame" x="23.333333333333343" y="132" width="264.66666666666663" height="49.333333333333343"/>
<rect key="frame" x="23.333333333333343" y="116.33333333333334" width="264.66666666666663" height="31"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Translucent" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Z5i-rm-QgL">
<rect key="frame" x="0.0" y="0.0" width="89.666666666666671" height="49.333333333333336"/>
<rect key="frame" x="0.0" y="0.0" width="89.666666666666671" height="31"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<switch opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" contentHorizontalAlignment="center" contentVerticalAlignment="center" translatesAutoresizingMaskIntoConstraints="NO" id="s6b-j9-8Kw">
<rect key="frame" x="215.66666666666666" y="0.0" width="50.999999999999972" height="49.333333333333336"/>
<rect key="frame" x="215.66666666666666" y="0.0" width="50.999999999999972" height="31"/>
<connections>
<action selector="toggleTranslucent:" destination="C1X-9Z-TyQ" eventType="valueChanged" id="nL4-3L-9hh"/>
</connections>
@@ -309,22 +309,22 @@
<objects>
<viewController storyboardIdentifier="ModalViewController" id="bYI-y3-Rzb" customClass="ModalViewController" customModule="Samples" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="qwo-GK-p1U">
<rect key="frame" x="0.0" y="0.0" width="375" height="724"/>
<rect key="frame" x="0.0" y="0.0" width="375" height="778"/>
<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="724" width="375" height="0.0"/>
<rect key="frame" x="0.0" y="744" width="375" height="34"/>
<color key="backgroundColor" white="0.66666666666666663" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</view>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="sbF-Az-7sy">
<rect key="frame" x="20" y="0.0" width="39" height="30"/>
<rect key="frame" x="20" y="44" width="39" height="30"/>
<state key="normal" title="Close"/>
<connections>
<action selector="closeWithSender:" destination="bYI-y3-Rzb" eventType="touchUpInside" id="MSC-ch-YJK"/>
</connections>
</button>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" alignment="top" spacing="44" translatesAutoresizingMaskIntoConstraints="NO" id="9p4-06-y2T">
<rect key="frame" x="134.66666666666666" y="88" width="106" height="326"/>
<rect key="frame" x="139.66666666666666" y="132" width="96" height="252"/>
<subviews>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="i9x-x5-n1q">
<rect key="frame" x="0.0" y="0.0" width="80" height="30"/>
@@ -347,15 +347,8 @@
<action selector="moveToTipWithSender:" destination="bYI-y3-Rzb" eventType="touchUpInside" id="BmL-91-9ai"/>
</connections>
</button>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="swr-XM-GzZ">
<rect key="frame" x="0.0" y="222" width="106" height="30"/>
<state key="normal" title="Move to hidden"/>
<connections>
<action selector="moveToHiddenWithSender:" destination="bYI-y3-Rzb" eventType="touchUpInside" id="jfJ-0f-fdk"/>
</connections>
</button>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="szf-HE-QTk">
<rect key="frame" x="0.0" y="296" width="96" height="30"/>
<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"/>
@@ -524,7 +517,7 @@
</constraints>
</view>
<view alpha="0.5" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="Kva-Z7-0qY" customClass="OnSafeAreaView" customModule="Samples" customModuleProvider="target">
<rect key="frame" x="0.0" y="44" width="375" height="734"/>
<rect key="frame" x="0.0" y="44" width="375" height="700"/>
<color key="backgroundColor" red="0.0078431372550000003" green="0.72156862749999995" blue="0.45882352939999999" alpha="1" colorSpace="calibratedRGB"/>
</view>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="noi-1a-5bZ" customClass="CloseButton" customModule="Samples" customModuleProvider="target">
@@ -537,23 +530,6 @@
<action selector="closeWithSender:" destination="YC8-ae-15L" eventType="touchUpInside" id="Z2v-19-S5k"/>
</connections>
</button>
<stackView opaque="NO" contentMode="scaleToFill" distribution="fillProportionally" alignment="center" spacing="8" translatesAutoresizingMaskIntoConstraints="NO" id="qux-uG-4o2">
<rect key="frame" x="8" y="52" width="148.33333333333334" height="31"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="fitToBounds" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="7lq-d3-PKi">
<rect key="frame" x="0.0" y="5.3333333333333357" width="91.333333333333329" height="20.333333333333332"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<switch opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" contentHorizontalAlignment="center" contentVerticalAlignment="center" on="YES" translatesAutoresizingMaskIntoConstraints="NO" id="0MA-lV-KjS">
<rect key="frame" x="99.333333333333329" y="0.0" width="50.999999999999986" height="31"/>
<connections>
<action selector="modeChanged:" destination="YC8-ae-15L" eventType="valueChanged" id="IQ8-u2-Rib"/>
</connections>
</switch>
</subviews>
</stackView>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="22" translatesAutoresizingMaskIntoConstraints="NO" id="tP3-oJ-4EB">
<rect key="frame" x="130.66666666666666" y="132" width="114" height="134"/>
<subviews>
@@ -594,11 +570,9 @@
<constraint firstItem="tP3-oJ-4EB" firstAttribute="top" secondItem="aOK-7l-cA6" secondAttribute="top" constant="88" id="Zhb-Ss-epe"/>
<constraint firstItem="Kva-Z7-0qY" firstAttribute="trailing" secondItem="aOK-7l-cA6" secondAttribute="trailing" id="kkp-Yo-FQW"/>
<constraint firstItem="aOK-7l-cA6" firstAttribute="trailing" secondItem="noi-1a-5bZ" secondAttribute="trailing" constant="12" id="lv9-Nf-HNB"/>
<constraint firstItem="qux-uG-4o2" firstAttribute="top" secondItem="aOK-7l-cA6" secondAttribute="top" constant="8" id="naa-cf-ZIc"/>
<constraint firstItem="Kva-Z7-0qY" firstAttribute="leading" secondItem="aOK-7l-cA6" secondAttribute="leading" id="oVC-i1-TwS"/>
<constraint firstItem="aOK-7l-cA6" firstAttribute="bottom" secondItem="Kva-Z7-0qY" secondAttribute="bottom" id="rW2-mF-5DR"/>
<constraint firstItem="8yw-OC-Ubk" firstAttribute="top" relation="greaterThanOrEqual" secondItem="tP3-oJ-4EB" secondAttribute="bottom" constant="88" id="vKQ-h9-uKt"/>
<constraint firstItem="qux-uG-4o2" firstAttribute="leading" secondItem="g7l-kO-y7q" secondAttribute="leading" constant="8" id="zXb-R9-bMO"/>
</constraints>
<viewLayoutGuide key="safeArea" id="aOK-7l-cA6"/>
<connections>
@@ -610,8 +584,6 @@
<size key="freeformSize" width="375" height="778"/>
<connections>
<outlet property="closeButton" destination="noi-1a-5bZ" id="eWQ-ha-8y7"/>
<outlet property="intrinsicHeightConstraint" destination="vKQ-h9-uKt" id="QpA-WD-b17"/>
<outlet property="modeChangeView" destination="qux-uG-4o2" id="1Nq-fE-dXw"/>
<segue destination="bYI-y3-Rzb" kind="show" identifier="ShowSegue" id="r1P-2i-NDe"/>
</connections>
</viewController>
@@ -632,7 +604,7 @@
</connections>
</pongPressGestureRecognizer>
</objects>
<point key="canvasLocation" x="653.60000000000002" y="733.74384236453204"/>
<point key="canvasLocation" x="655" y="734"/>
</scene>
<!--Debug Text View Controller-->
<scene sceneID="Bkq-O7-q4A">
+31 -123
View File
@@ -17,15 +17,13 @@ class SampleListViewController: UIViewController {
case trackingTextView
case showDetail
case showModal
case showPanelModal
case showFloatingPanelModal
case showTabBar
case showPageView
case showPageContentView
case showNestedScrollView
case showRemovablePanel
case showIntrinsicView
case showContentInset
case showContainerMargins
var name: String {
switch self {
@@ -33,15 +31,13 @@ class SampleListViewController: UIViewController {
case .trackingTextView: return "Scroll tracking(TextView)"
case .showDetail: return "Show Detail Panel"
case .showModal: return "Show Modal"
case .showPanelModal: return "Show Panel Modal"
case .showFloatingPanelModal: return "Show Floating Panel Modal"
case .showTabBar: return "Show Tab Bar"
case .showPageView: return "Show Page View"
case .showPageContentView: return "Show Page Content View"
case .showNestedScrollView: return "Show Nested ScrollView"
case .showRemovablePanel: return "Show Removable Panel"
case .showIntrinsicView: return "Show Intrinsic View"
case .showContentInset: return "Show with ContentInset"
case .showContainerMargins: return "Show with ContainerMargins"
}
}
@@ -51,15 +47,13 @@ class SampleListViewController: UIViewController {
case .trackingTextView: return "ConsoleViewController"
case .showDetail: return "DetailViewController"
case .showModal: return "ModalViewController"
case .showPanelModal: return nil
case .showFloatingPanelModal: return nil
case .showTabBar: return "TabBarViewController"
case .showPageView: return nil
case .showPageContentView: return nil
case .showNestedScrollView: return "NestedScrollViewController"
case .showRemovablePanel: return "DetailViewController"
case .showIntrinsicView: return "IntrinsicViewController"
case .showContentInset: return nil
case .showContainerMargins: return nil
}
}
}
@@ -73,7 +67,18 @@ class SampleListViewController: UIViewController {
var mainPanelObserves: [NSKeyValueObservation] = []
var settingsObserves: [NSKeyValueObservation] = []
var pages: [UIViewController] = []
lazy var pages: [UIViewController] = {
let page1 = FloatingPanelController(delegate: self)
page1.view.backgroundColor = .blue
page1.show()
let page2 = FloatingPanelController(delegate: self)
page2.view.backgroundColor = .red
page2.show()
let page3 = FloatingPanelController(delegate: self)
page3.view.backgroundColor = .green
page3.show()
return [page1, page2, page3]
}()
override func viewDidLoad() {
super.viewDidLoad()
@@ -114,16 +119,10 @@ class SampleListViewController: UIViewController {
func addMainPanel(with contentVC: UIViewController) {
mainPanelObserves.removeAll()
let oldMainPanelVC = mainPanelVC
// Initialize FloatingPanelController
mainPanelVC = FloatingPanelController()
mainPanelVC.delegate = self
// Initialize FloatingPanelController and add the view
mainPanelVC.surfaceView.cornerRadius = 6.0
mainPanelVC.surfaceView.shadowHidden = false
// Set a content view controller
mainPanelVC.set(contentViewController: contentVC)
@@ -134,10 +133,6 @@ class SampleListViewController: UIViewController {
tapGesture.cancelsTouchesInView = false
tapGesture.numberOfTapsRequired = 2
mainPanelVC.surfaceView.addGestureRecognizer(tapGesture)
case .showPageContentView:
if let page = (mainPanelVC.contentViewController as? UIPageViewController)?.viewControllers?.first {
mainPanelVC.track(scrollView: (page as! DebugTableViewController).tableView)
}
case .showRemovablePanel, .showIntrinsicView:
mainPanelVC.isRemovalInteractionEnabled = true
@@ -165,13 +160,7 @@ class SampleListViewController: UIViewController {
}
// Add FloatingPanel to self.view
if let oldMainPanelVC = oldMainPanelVC {
oldMainPanelVC.removePanelFromParent(animated: true, completion: {
self.mainPanelVC.addPanel(toParent: self, belowView: nil, animated: true)
})
} else {
mainPanelVC.addPanel(toParent: self, belowView: nil, animated: true)
}
mainPanelVC.addPanel(toParent: self, belowView: nil, animated: true)
}
@objc
@@ -258,8 +247,6 @@ extension SampleListViewController: UITableViewDelegate {
}()
self.currentMenu = menu
detailPanelVC?.removePanelFromParent(animated: true, completion: nil)
detailPanelVC = nil
switch menu {
case .showDetail:
@@ -267,7 +254,6 @@ extension SampleListViewController: UITableViewDelegate {
// Initialize FloatingPanelController
detailPanelVC = FloatingPanelController()
detailPanelVC.delegate = self
// Initialize FloatingPanelController and add the view
detailPanelVC.surfaceView.cornerRadius = 6.0
@@ -276,24 +262,13 @@ extension SampleListViewController: UITableViewDelegate {
// Set a content view controller
detailPanelVC.set(contentViewController: contentVC)
detailPanelVC.contentMode = .fitToBounds
(contentVC as? DetailViewController)?.intrinsicHeightConstraint.priority = .defaultLow
// Add FloatingPanel to self.view
detailPanelVC.addPanel(toParent: self, belowView: nil, animated: true)
case .showModal, .showTabBar:
let modalVC = contentVC
modalVC.modalPresentationStyle = .fullScreen
present(modalVC, animated: true, completion: nil)
case .showPageView:
pages = [UIColor.blue, .red, .green].compactMap({ (color) -> UIViewController in
let page = FloatingPanelController(delegate: self)
page.view.backgroundColor = color
page.show()
return page
})
let pageVC = UIPageViewController(transitionStyle: .scroll, navigationOrientation: .horizontal, options: [:])
let closeButton = UIButton(type: .custom)
pageVC.view.addSubview(closeButton)
@@ -306,21 +281,11 @@ extension SampleListViewController: UITableViewDelegate {
])
pageVC.dataSource = self
pageVC.setViewControllers([pages[0]], direction: .forward, animated: false, completion: nil)
pageVC.modalPresentationStyle = .fullScreen
present(pageVC, animated: true, completion: nil)
case .showPageContentView:
pages = [DebugTableViewController(), DebugTableViewController(), DebugTableViewController()]
let pageVC = UIPageViewController(transitionStyle: .scroll, navigationOrientation: .horizontal, options: [:])
pageVC.dataSource = self
pageVC.delegate = self
pageVC.setViewControllers([pages[0]], direction: .forward, animated: false, completion: nil)
self.addMainPanel(with: pageVC)
case .showPanelModal:
case .showFloatingPanelModal:
let fpc = FloatingPanelController()
let contentVC = self.storyboard!.instantiateViewController(withIdentifier: "DetailViewController")
contentVC.loadViewIfNeeded()
(contentVC as? DetailViewController)?.modeChangeView.isHidden = true
fpc.set(contentViewController: contentVC)
fpc.delegate = self
@@ -330,35 +295,23 @@ extension SampleListViewController: UITableViewDelegate {
fpc.isRemovalInteractionEnabled = true
self.present(fpc, animated: true, completion: nil)
case .showContentInset:
let contentViewController = UIViewController()
contentViewController.view.backgroundColor = .green
let fpc = FloatingPanelController()
fpc.set(contentViewController: contentViewController)
fpc.surfaceView.contentInsets = .init(top: 20, left: 20, bottom: 20, right: 20)
fpc.delegate = self
fpc.isRemovalInteractionEnabled = true
self.present(fpc, animated: true, completion: nil)
case .showContainerMargins:
let fpc = FloatingPanelController()
fpc.surfaceView.cornerRadius = 38.5
fpc.surfaceView.backgroundColor = .red
fpc.surfaceView.containerMargins = .init(top: 24.0, left: 8.0, bottom: layoutInsets.bottom, right: 8.0)
#if swift(>=5.1) // Actually Xcode 11 or later
if #available(iOS 13.0, *) {
fpc.surfaceView.layer.cornerCurve = .continuous
}
#endif
fpc.surfaceView.contentInsets = .init(top: 20, left: 20, bottom: 0, right: 20)
fpc.delegate = self
fpc.isRemovalInteractionEnabled = true
self.present(fpc, animated: true, completion: nil)
default:
self.addMainPanel(with: contentVC)
detailPanelVC?.removePanelFromParent(animated: true, completion: nil)
mainPanelVC?.removePanelFromParent(animated: true) {
self.addMainPanel(with: contentVC)
}
}
}
@@ -378,15 +331,13 @@ extension SampleListViewController: FloatingPanelControllerDelegate {
return newCollection.verticalSizeClass == .compact ? RemovablePanelLandscapeLayout() : RemovablePanelLayout()
case .showIntrinsicView:
return IntrinsicPanelLayout()
case .showPanelModal:
case .showFloatingPanelModal:
if vc != mainPanelVC && vc != detailPanelVC {
return ModalPanelLayout()
}
fallthrough
case .showContentInset:
return NoInteractionBufferPanelLayout()
default:
return (newCollection.verticalSizeClass == .compact) ? nil : self
return (newCollection.verticalSizeClass == .compact) ? nil : self
}
}
@@ -448,40 +399,9 @@ extension SampleListViewController: UIPageViewControllerDataSource {
return pages[index - 1]
}
}
extension SampleListViewController: UIPageViewControllerDelegate {
// For showPageContent
func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) {
if completed, let page = pageViewController.viewControllers?.first {
(pageViewController.parent as! FloatingPanelController).track(scrollView: (page as! DebugTableViewController).tableView)
}
}
}
class IntrinsicPanelLayout: FloatingPanelIntrinsicLayout { }
class NoInteractionBufferPanelLayout: FloatingPanelLayout {
var initialPosition: FloatingPanelPosition {
return .full
}
func insetFor(position: FloatingPanelPosition) -> CGFloat? {
switch position {
case .full: return 0
case .half: return 216
case .tip: return 60
case .hidden: return nil
}
}
var topInteractionBuffer: CGFloat {
return 0.0
}
var bottomInteractionBuffer: CGFloat {
return 0.0
}
}
class RemovablePanelLayout: FloatingPanelIntrinsicLayout {
var supportedPositions: Set<FloatingPanelPosition> {
return [.full, .half]
@@ -753,10 +673,6 @@ class DebugTableViewController: InspectableViewController {
// Remove FloatingPanel from a view
(self.parent as! FloatingPanelController).removePanelFromParent(animated: true, completion: nil)
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
print("TableView --- ", scrollView.contentOffset, scrollView.contentInset)
}
}
extension DebugTableViewController: UITableViewDataSource {
@@ -799,8 +715,6 @@ extension DebugTableViewController: UITableViewDelegate {
}
class DetailViewController: InspectableViewController {
@IBOutlet weak var modeChangeView: UIStackView!
@IBOutlet weak var intrinsicHeightConstraint: NSLayoutConstraint!
@IBOutlet weak var closeButton: UIButton!
@IBAction func close(sender: UIButton) {
// (self.parent as? FloatingPanelController)?.removePanelFromParent(animated: true, completion: nil)
@@ -817,10 +731,6 @@ class DetailViewController: InspectableViewController {
break
}
}
@IBAction func modeChanged(_ sender: Any) {
guard let fpc = parent as? FloatingPanelController else { return }
fpc.contentMode = (fpc.contentMode == .static) ? .fitToBounds : .static
}
@IBAction func tapped(_ sender: Any) {
print("Detail panel is tapped!")
@@ -881,9 +791,7 @@ class ModalViewController: UIViewController, FloatingPanelControllerDelegate {
@IBAction func moveToTip(sender: UIButton) {
fpc.move(to: .tip, animated: true)
}
@IBAction func moveToHidden(sender: UIButton) {
fpc.move(to: .hidden, animated: true)
}
@IBAction func updateLayout(_ sender: Any) {
isNewlayout = !isNewlayout
UIView.animate(withDuration: 0.5) {
@@ -1021,7 +929,7 @@ extension TabBarContentViewController: UITextViewDelegate {
// Using KVO of `scrollView.contentOffset`). Because it can lead to an
// infinite loop if a user also resets a content offset as below and,
// in the situation, a user has to modify the library.
if fpc.position != .full, fpc.surfaceView.frame.minY > fpc.originYOfSurface(for: .full) {
if fpc.position != .full, fpc.surfaceView.frame.minY < fpc.originYOfSurface(for: .full) {
scrollView.contentOffset = .zero
}
}
@@ -1190,8 +1098,8 @@ class TwoTabBarPanelLayout: FloatingPanelLayout {
}
class TwoTabBarPanelBehavior: FloatingPanelBehavior {
func allowsRubberBanding(for edges: UIRectEdge) -> Bool {
return [UIRectEdge.top, UIRectEdge.bottom].contains(edges)
func allowsRubberBanding(for edge: UIRectEdge) -> Bool {
return (edge == .bottom || edge == .top)
}
}
@@ -7,13 +7,13 @@
objects = {
/* Begin PBXBuildFile section */
5433F24B21717EA300BDAA5D /* FloatingPanel.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5433F24A21717EA300BDAA5D /* FloatingPanel.framework */; };
5433F24C21717EA300BDAA5D /* FloatingPanel.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 5433F24A21717EA300BDAA5D /* FloatingPanel.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
548DF95421705BE00041922A /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 548DF95321705BE00041922A /* AppDelegate.swift */; };
548DF95621705BE00041922A /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 548DF95521705BE00041922A /* ViewController.swift */; };
548DF95921705BE00041922A /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 548DF95721705BE00041922A /* Main.storyboard */; };
548DF95B21705BE10041922A /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 548DF95A21705BE10041922A /* Assets.xcassets */; };
548DF95E21705BE10041922A /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 548DF95C21705BE10041922A /* LaunchScreen.storyboard */; };
549D23CF233C77CF008EF4D7 /* FloatingPanel.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 549D23CE233C77CF008EF4D7 /* FloatingPanel.framework */; };
549D23D0233C77CF008EF4D7 /* FloatingPanel.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 549D23CE233C77CF008EF4D7 /* FloatingPanel.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
/* End PBXBuildFile section */
/* Begin PBXCopyFilesBuildPhase section */
@@ -23,7 +23,7 @@
dstPath = "";
dstSubfolderSpec = 10;
files = (
549D23D0233C77CF008EF4D7 /* FloatingPanel.framework in Embed Frameworks */,
5433F24C21717EA300BDAA5D /* FloatingPanel.framework in Embed Frameworks */,
);
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
@@ -31,6 +31,7 @@
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
5433F24A21717EA300BDAA5D /* FloatingPanel.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = FloatingPanel.framework; sourceTree = BUILT_PRODUCTS_DIR; };
548DF95021705BE00041922A /* Stocks.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Stocks.app; sourceTree = BUILT_PRODUCTS_DIR; };
548DF95321705BE00041922A /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
548DF95521705BE00041922A /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = "<group>"; };
@@ -38,7 +39,6 @@
548DF95A21705BE10041922A /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
548DF95D21705BE10041922A /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
548DF95F21705BE10041922A /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
549D23CE233C77CF008EF4D7 /* FloatingPanel.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = FloatingPanel.framework; sourceTree = BUILT_PRODUCTS_DIR; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@@ -46,7 +46,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
549D23CF233C77CF008EF4D7 /* FloatingPanel.framework in Frameworks */,
5433F24B21717EA300BDAA5D /* FloatingPanel.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -56,7 +56,7 @@
548DF94721705BE00041922A = {
isa = PBXGroup;
children = (
549D23CE233C77CF008EF4D7 /* FloatingPanel.framework */,
5433F24A21717EA300BDAA5D /* FloatingPanel.framework */,
548DF95221705BE00041922A /* Stocks */,
548DF95121705BE00041922A /* Products */,
);
+2 -2
View File
@@ -1,7 +1,7 @@
Pod::Spec.new do |s|
s.name = "FloatingPanel"
s.version = "1.7.1"
s.version = "1.5.1"
s.summary = "FloatingPanel is a clean and easy-to-use UI component of a floating panel interface."
s.description = <<-DESC
FloatingPanel is a clean and easy-to-use UI component for a new interface introduced in Apple Maps, Shortcuts and Stocks app.
@@ -13,7 +13,7 @@ The new interface displays the related contents and utilities in parallel as a u
s.platform = :ios, "10.0"
s.source = { :git => "https://github.com/SCENEE/FloatingPanel.git", :tag => "v#{s.version}" }
s.source_files = "Framework/Sources/*.swift"
s.swift_versions = ["4.0", "4.2", "5.0"]
s.swift_version = "4.0"
s.framework = "UIKit"
@@ -7,7 +7,6 @@
objects = {
/* Begin PBXBuildFile section */
542753C622C49A6E00D17955 /* FloatingPanelLayoutTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 542753C522C49A6E00D17955 /* FloatingPanelLayoutTests.swift */; };
54352E9621A51A2500CBCA08 /* FloatingPanelTransitioning.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54352E9521A51A2500CBCA08 /* FloatingPanelTransitioning.swift */; };
54352E9821A521CA00CBCA08 /* FloatingPanelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54352E9721A521CA00CBCA08 /* FloatingPanelView.swift */; };
5450EEE421646DF500135936 /* FloatingPanelBehavior.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5450EEE321646DF500135936 /* FloatingPanelBehavior.swift */; };
@@ -17,16 +16,14 @@
545DB9DE215118C800CA77B8 /* UIExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 545DB9DD215118C800CA77B8 /* UIExtensions.swift */; };
545DB9E021511AC100CA77B8 /* FloatingPanelController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 545DB9DF21511AC100CA77B8 /* FloatingPanelController.swift */; };
545DBA2B2152383100CA77B8 /* GrabberHandleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 545DBA2A2152383100CA77B8 /* GrabberHandleView.swift */; };
546055BF2333C4740069F400 /* Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 542753C722C49A8F00D17955 /* Utils.swift */; };
549E944522CF295D0050AECF /* FloatingPanelPositionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 549E944422CF295D0050AECF /* FloatingPanelPositionTests.swift */; };
54A6B6B122968B530077F348 /* FloatingPanelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54A6B6B022968B530077F348 /* FloatingPanelTests.swift */; };
54A6B6B622968F710077F348 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 54A6B6B522968F710077F348 /* LaunchScreen.storyboard */; };
54A6B6B82296A8520077F348 /* FloatingPanelSurfaceViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54A6B6B72296A8520077F348 /* FloatingPanelSurfaceViewTests.swift */; };
54A6B6B82296A8520077F348 /* FloatingPanelViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54A6B6B72296A8520077F348 /* FloatingPanelViewTests.swift */; };
54ABD7AF216CCFF7002E6C13 /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54ABD7AE216CCFF7002E6C13 /* Logger.swift */; };
54CDC5D3215B6D5A007D205C /* FloatingPanelSurfaceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CDC5D2215B6D5A007D205C /* FloatingPanelSurfaceView.swift */; };
54CDC5D5215B6D8D007D205C /* FloatingPanelBackdropView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CDC5D4215B6D8D007D205C /* FloatingPanelBackdropView.swift */; };
54CFBFC3215CD045006B5735 /* FloatingPanelLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CFBFC2215CD045006B5735 /* FloatingPanelLayout.swift */; };
54CFBFC5215CD09C006B5735 /* FloatingPanelCore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CFBFC4215CD09C006B5735 /* FloatingPanelCore.swift */; };
54CFBFC5215CD09C006B5735 /* FloatingPanel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CFBFC4215CD09C006B5735 /* FloatingPanel.swift */; };
54E740CD218AFD67005C1A34 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54E740CC218AFD67005C1A34 /* AppDelegate.swift */; };
/* End PBXBuildFile section */
@@ -48,8 +45,6 @@
/* End PBXContainerItemProxy section */
/* Begin PBXFileReference section */
542753C522C49A6E00D17955 /* FloatingPanelLayoutTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FloatingPanelLayoutTests.swift; sourceTree = "<group>"; };
542753C722C49A8F00D17955 /* Utils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Utils.swift; sourceTree = "<group>"; };
54352E9521A51A2500CBCA08 /* FloatingPanelTransitioning.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FloatingPanelTransitioning.swift; sourceTree = "<group>"; };
54352E9721A521CA00CBCA08 /* FloatingPanelView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FloatingPanelView.swift; sourceTree = "<group>"; };
5450EEE321646DF500135936 /* FloatingPanelBehavior.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FloatingPanelBehavior.swift; sourceTree = "<group>"; };
@@ -62,15 +57,14 @@
545DB9DD215118C800CA77B8 /* UIExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIExtensions.swift; sourceTree = "<group>"; };
545DB9DF21511AC100CA77B8 /* FloatingPanelController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FloatingPanelController.swift; sourceTree = "<group>"; };
545DBA2A2152383100CA77B8 /* GrabberHandleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GrabberHandleView.swift; sourceTree = "<group>"; };
549E944422CF295D0050AECF /* FloatingPanelPositionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FloatingPanelPositionTests.swift; sourceTree = "<group>"; };
54A6B6B022968B530077F348 /* FloatingPanelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FloatingPanelTests.swift; sourceTree = "<group>"; };
54A6B6B522968F710077F348 /* LaunchScreen.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = LaunchScreen.storyboard; sourceTree = "<group>"; };
54A6B6B72296A8520077F348 /* FloatingPanelSurfaceViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FloatingPanelSurfaceViewTests.swift; sourceTree = "<group>"; };
54A6B6B72296A8520077F348 /* FloatingPanelViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FloatingPanelViewTests.swift; sourceTree = "<group>"; };
54ABD7AE216CCFF7002E6C13 /* Logger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Logger.swift; sourceTree = "<group>"; };
54CDC5D2215B6D5A007D205C /* FloatingPanelSurfaceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FloatingPanelSurfaceView.swift; sourceTree = "<group>"; };
54CDC5D4215B6D8D007D205C /* FloatingPanelBackdropView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FloatingPanelBackdropView.swift; sourceTree = "<group>"; };
54CFBFC2215CD045006B5735 /* FloatingPanelLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FloatingPanelLayout.swift; sourceTree = "<group>"; };
54CFBFC4215CD09C006B5735 /* FloatingPanelCore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FloatingPanelCore.swift; sourceTree = "<group>"; };
54CFBFC4215CD09C006B5735 /* FloatingPanel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FloatingPanel.swift; sourceTree = "<group>"; };
54E740CA218AFD67005C1A34 /* FloatingPanelTesting.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = FloatingPanelTesting.app; sourceTree = BUILT_PRODUCTS_DIR; };
54E740CC218AFD67005C1A34 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
54E740D8218AFD6A005C1A34 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
@@ -129,7 +123,7 @@
545DB9C42151169500CA77B8 /* FloatingPanel.h */,
545DB9DF21511AC100CA77B8 /* FloatingPanelController.swift */,
54352E9521A51A2500CBCA08 /* FloatingPanelTransitioning.swift */,
54CFBFC4215CD09C006B5735 /* FloatingPanelCore.swift */,
54CFBFC4215CD09C006B5735 /* FloatingPanel.swift */,
54CFBFC2215CD045006B5735 /* FloatingPanelLayout.swift */,
5450EEE321646DF500135936 /* FloatingPanelBehavior.swift */,
54352E9721A521CA00CBCA08 /* FloatingPanelView.swift */,
@@ -146,11 +140,8 @@
isa = PBXGroup;
children = (
54A6B6B022968B530077F348 /* FloatingPanelTests.swift */,
54A6B6B72296A8520077F348 /* FloatingPanelViewTests.swift */,
545DB9CF2151169500CA77B8 /* FloatingPanelControllerTests.swift */,
542753C522C49A6E00D17955 /* FloatingPanelLayoutTests.swift */,
54A6B6B72296A8520077F348 /* FloatingPanelSurfaceViewTests.swift */,
549E944422CF295D0050AECF /* FloatingPanelPositionTests.swift */,
542753C722C49A8F00D17955 /* Utils.swift */,
545DB9D12151169500CA77B8 /* Info.plist */,
);
path = Tests;
@@ -311,7 +302,7 @@
54CFBFC3215CD045006B5735 /* FloatingPanelLayout.swift in Sources */,
54CDC5D5215B6D8D007D205C /* FloatingPanelBackdropView.swift in Sources */,
54352E9821A521CA00CBCA08 /* FloatingPanelView.swift in Sources */,
54CFBFC5215CD09C006B5735 /* FloatingPanelCore.swift in Sources */,
54CFBFC5215CD09C006B5735 /* FloatingPanel.swift in Sources */,
54ABD7AF216CCFF7002E6C13 /* Logger.swift in Sources */,
545DB9E021511AC100CA77B8 /* FloatingPanelController.swift in Sources */,
5450EEE421646DF500135936 /* FloatingPanelBehavior.swift in Sources */,
@@ -327,10 +318,7 @@
files = (
54A6B6B122968B530077F348 /* FloatingPanelTests.swift in Sources */,
545DB9D02151169500CA77B8 /* FloatingPanelControllerTests.swift in Sources */,
549E944522CF295D0050AECF /* FloatingPanelPositionTests.swift in Sources */,
542753C622C49A6E00D17955 /* FloatingPanelLayoutTests.swift in Sources */,
54A6B6B82296A8520077F348 /* FloatingPanelSurfaceViewTests.swift in Sources */,
546055BF2333C4740069F400 /* Utils.swift in Sources */,
54A6B6B82296A8520077F348 /* FloatingPanelViewTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -484,7 +472,6 @@
isa = XCBuildConfiguration;
buildSettings = {
APPLICATION_EXTENSION_API_ONLY = YES;
BUILD_LIBRARY_FOR_DISTRIBUTION = YES;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "";
CODE_SIGN_STYLE = Automatic;
@@ -515,7 +502,6 @@
isa = XCBuildConfiguration;
buildSettings = {
APPLICATION_EXTENSION_API_ONLY = YES;
BUILD_LIBRARY_FOR_DISTRIBUTION = YES;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "";
CODE_SIGN_STYLE = Automatic;
@@ -704,7 +690,7 @@
PRODUCT_BUNDLE_IDENTIFIER = com.scenee.FloatingPanel;
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
SKIP_INSTALL = YES;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "TEST DEBUG __FP_LOG";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG __FP_LOG";
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 4.0;
@@ -29,9 +29,7 @@
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
<TestableReference
skipped = "NO"
parallelizable = "YES"
testExecutionOrdering = "random">
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "545DB9C92151169500CA77B8"
@@ -8,9 +8,9 @@ import UIKit.UIGestureRecognizerSubclass // For Xcode 9.4.1
///
/// FloatingPanel presentation model
///
class FloatingPanelCore: NSObject, UIGestureRecognizerDelegate {
class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate {
// MUST be a weak reference to prevent UI freeze on the presentation modally
weak var viewcontroller: FloatingPanelController?
weak var viewcontroller: FloatingPanelController!
let surfaceView: FloatingPanelSurfaceView
let backdropView: FloatingPanelBackdropView
@@ -19,17 +19,13 @@ class FloatingPanelCore: NSObject, UIGestureRecognizerDelegate {
weak var scrollView: UIScrollView? {
didSet {
oldValue?.panGestureRecognizer.removeTarget(self, action: nil)
scrollView?.panGestureRecognizer.addTarget(self, action: #selector(handle(panGesture:)))
guard let scrollView = scrollView else { return }
scrollView.panGestureRecognizer.addTarget(self, action: #selector(handle(panGesture:)))
}
}
private(set) var state: FloatingPanelPosition = .hidden {
didSet {
if let vc = viewcontroller {
vc.delegate?.floatingPanelDidChangePosition(vc)
}
}
didSet { viewcontroller.delegate?.floatingPanelDidChangePosition(viewcontroller) }
}
private var isBottomState: Bool {
@@ -40,7 +36,15 @@ class FloatingPanelCore: NSObject, UIGestureRecognizerDelegate {
let panGestureRecognizer: FloatingPanelPanGestureRecognizer
var isRemovalInteractionEnabled: Bool = false
fileprivate var animator: UIViewPropertyAnimator?
fileprivate var animator: UIViewPropertyAnimator? {
didSet {
// This intends to avoid `tableView(_:didSelectRowAt:)` not being
// called on first tap after the moving animation, but it doesn't
// seem to be enough. The same issue happens on Apple Maps so it
// might be an issue in `UITableView`.
scrollView?.isUserInteractionEnabled = (animator == nil)
}
}
private var initialFrame: CGRect = .zero
private var initialTranslationY: CGFloat = 0
@@ -51,20 +55,21 @@ class FloatingPanelCore: NSObject, UIGestureRecognizerDelegate {
// Scroll handling
private var initialScrollOffset: CGPoint = .zero
private var initialScrollFrame: CGRect = .zero
private var stopScrollDeceleration: Bool = false
private var scrollBouncable = false
private var scrollIndictorVisible = false
private var isScrollLocked: Bool = false
// MARK: - Interface
init(_ vc: FloatingPanelController, layout: FloatingPanelLayout, behavior: FloatingPanelBehavior) {
viewcontroller = vc
surfaceView = FloatingPanelSurfaceView()
surfaceView.backgroundColor = .white
backdropView = FloatingPanelBackdropView()
backdropView.backgroundColor = .black
backdropView.alpha = 0.0
self.layoutAdapter = FloatingPanelLayoutAdapter(surfaceView: surfaceView,
@@ -81,15 +86,10 @@ class FloatingPanelCore: NSObject, UIGestureRecognizerDelegate {
super.init()
panGestureRecognizer.floatingPanel = self
surfaceView.addGestureRecognizer(panGestureRecognizer)
panGestureRecognizer.addTarget(self, action: #selector(handle(panGesture:)))
panGestureRecognizer.delegate = self
// Set tap-to-dismiss in the backdrop view
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleBackdrop(tapGesture:)))
tapGesture.isEnabled = false
backdropView.dismissalTapGestureRecognizer = tapGesture
backdropView.addGestureRecognizer(tapGesture)
}
func move(to: FloatingPanelPosition, animated: Bool, completion: (() -> Void)? = nil) {
@@ -97,11 +97,6 @@ class FloatingPanelCore: NSObject, UIGestureRecognizerDelegate {
}
private func move(from: FloatingPanelPosition, to: FloatingPanelPosition, animated: Bool, completion: (() -> Void)? = nil) {
assert(layoutAdapter.isValid(to), "Can't move to '\(to)' position because it's not valid in the layout")
guard let vc = viewcontroller else {
completion?()
return
}
if state != layoutAdapter.topMostState {
lockScrollView()
}
@@ -111,11 +106,11 @@ class FloatingPanelCore: NSObject, UIGestureRecognizerDelegate {
let animator: UIViewPropertyAnimator
switch (from, to) {
case (.hidden, let to):
animator = behavior.addAnimator(vc, to: to)
animator = behavior.addAnimator(self.viewcontroller, to: to)
case (let from, .hidden):
animator = behavior.removeAnimator(vc, from: from)
animator = behavior.removeAnimator(self.viewcontroller, from: from)
case (let from, let to):
animator = behavior.moveAnimator(vc, from: from, to: to)
animator = behavior.moveAnimator(self.viewcontroller, from: from, to: to)
}
animator.addAnimations { [weak self] in
@@ -127,11 +122,7 @@ class FloatingPanelCore: NSObject, UIGestureRecognizerDelegate {
animator.addCompletion { [weak self] _ in
guard let `self` = self else { return }
self.animator = nil
if self.state == self.layoutAdapter.topMostState {
self.unlockScrollView()
} else {
self.lockScrollView()
}
self.unlockScrollView()
completion?()
}
self.animator = animator
@@ -139,11 +130,7 @@ class FloatingPanelCore: NSObject, UIGestureRecognizerDelegate {
} else {
self.state = to
self.updateLayout(to: to)
if self.state == self.layoutAdapter.topMostState {
self.unlockScrollView()
} else {
self.lockScrollView()
}
self.unlockScrollView()
completion?()
}
}
@@ -151,19 +138,14 @@ class FloatingPanelCore: NSObject, UIGestureRecognizerDelegate {
// MARK: - Layout update
private func updateLayout(to target: FloatingPanelPosition) {
self.layoutAdapter.activateFixedLayout()
self.layoutAdapter.activateInteractiveLayout(of: target)
self.layoutAdapter.activateLayout(of: target)
}
func getBackdropAlpha(at currentY: CGFloat, with translation: CGPoint) -> CGFloat {
let forwardY = (translation.y >= 0)
let segment = layoutAdapter.segument(at: currentY, forward: forwardY)
let lowerPos = segment.lower ?? layoutAdapter.topMostState
let upperPos = segment.upper ?? layoutAdapter.bottomMostState
let pre = forwardY ? lowerPos : upperPos
let next = forwardY ? upperPos : lowerPos
private func getBackdropAlpha(with translation: CGPoint) -> CGFloat {
let currentY = surfaceView.frame.minY
let next = directionalPosition(at: currentY, with: translation)
let pre = redirectionalPosition(at: currentY, with: translation)
let nextY = layoutAdapter.positionY(for: next)
let preY = layoutAdapter.positionY(for: pre)
@@ -185,8 +167,7 @@ class FloatingPanelCore: NSObject, UIGestureRecognizerDelegate {
/* log.debug("shouldRecognizeSimultaneouslyWith", otherGestureRecognizer) */
if let vc = viewcontroller,
vc.delegate?.floatingPanel(vc, shouldRecognizeSimultaneouslyWith: otherGestureRecognizer) ?? false {
if viewcontroller.delegate?.floatingPanel(viewcontroller, shouldRecognizeSimultaneouslyWith: otherGestureRecognizer) ?? false {
return true
}
@@ -202,10 +183,7 @@ class FloatingPanelCore: NSObject, UIGestureRecognizerDelegate {
default:
// Should recognize tap/long press gestures in parallel when the surface view is at an anchor position.
let surfaceFrame = surfaceView.layer.presentation()?.frame ?? surfaceView.frame
let surfaceY = surfaceFrame.minY
let adapterY = layoutAdapter.positionY(for: state)
return abs(surfaceY - adapterY) < (1.0 / surfaceView.traitCollection.displayScale)
return surfaceFrame.minY == layoutAdapter.positionY(for: state)
}
}
@@ -232,21 +210,15 @@ class FloatingPanelCore: NSObject, UIGestureRecognizerDelegate {
// the panel's pan gesture if not returns false
if let scrollGestureRecognizers = scrollView.gestureRecognizers,
scrollGestureRecognizers.contains(otherGestureRecognizer) {
switch otherGestureRecognizer {
case scrollView.panGestureRecognizer:
let offset = scrollView.contentOffset.y - scrollView.contentOffsetZero.y
return allowScrollPanGesture(at: CGPoint(x: 0.0, y: offset))
default:
return false
}
return false
}
}
if let vc = viewcontroller,
vc.delegate?.floatingPanel(vc, shouldRecognizeSimultaneouslyWith: otherGestureRecognizer) ?? false {
if viewcontroller.delegate?.floatingPanel(viewcontroller, shouldRecognizeSimultaneouslyWith: otherGestureRecognizer) ?? false {
return false
}
switch otherGestureRecognizer {
case is UIPanGestureRecognizer,
is UISwipeGestureRecognizer,
@@ -270,14 +242,6 @@ class FloatingPanelCore: NSObject, UIGestureRecognizerDelegate {
}
// MARK: - Gesture handling
@objc func handleBackdrop(tapGesture: UITapGestureRecognizer) {
viewcontroller?.dismiss(animated: true) { [weak self] in
guard let vc = self?.viewcontroller else { return }
vc.delegate?.floatingPanelDidEndRemove(vc)
}
}
@objc func handle(panGesture: UIPanGestureRecognizer) {
let velocity = panGesture.velocity(in: panGesture.view)
@@ -287,18 +251,14 @@ class FloatingPanelCore: NSObject, UIGestureRecognizerDelegate {
let location = panGesture.location(in: surfaceView)
let surfaceMinY = surfaceView.presentationFrame.minY
let adapterTopY = layoutAdapter.topY
let belowTop = surfaceMinY > (adapterTopY + (1.0 / surfaceView.traitCollection.displayScale))
let offset = scrollView.contentOffset.y - scrollView.contentOffsetZero.y
let belowTop = surfaceView.frame.minY > layoutAdapter.topY
log.debug("scroll gesture(\(state):\(panGesture.state)) --",
"belowTop = \(belowTop),",
"interactionInProgress = \(interactionInProgress),",
"scroll offset = \(offset),",
"scroll offset = \(scrollView.contentOffset.y),",
"location = \(location.y), velocity = \(velocity.y)")
if belowTop {
// Scroll offset pinning
if state == layoutAdapter.topMostState {
@@ -309,49 +269,33 @@ class FloatingPanelCore: NSObject, UIGestureRecognizerDelegate {
if grabberAreaFrame.contains(location) {
// Preserve the current content offset in moving from full.
scrollView.setContentOffset(initialScrollOffset, animated: false)
} else {
let offset = scrollView.contentOffset.y - scrollView.contentOffsetZero.y
if offset < 0 {
fitToBounds(scrollView: scrollView)
let translation = panGesture.translation(in: panGestureRecognizer.view!.superview)
startInteraction(with: translation, at: location)
}
}
}
} else {
scrollView.setContentOffset(initialScrollOffset, animated: false)
}
// Hide a scroll indicator at the non-top in dragging.
// Always hide a scroll indicator at the non-top.
if interactionInProgress {
lockScrollView()
} else {
if state == layoutAdapter.topMostState, self.animator == nil,
offset > 0, velocity.y < 0 {
unlockScrollView()
}
}
} else {
// Always show a scroll indicator at the top.
if interactionInProgress {
// Show a scroll indicator at the top in dragging.
if offset >= 0, velocity.y <= 0 {
unlockScrollView()
} else {
if state == layoutAdapter.topMostState {
// Adjust a small gap of the scroll offset just after swiping down starts in the grabber area.
if grabberAreaFrame.contains(location), grabberAreaFrame.contains(initialLocation) {
scrollView.setContentOffset(initialScrollOffset, animated: false)
}
}
}
unlockScrollView()
} else {
if state == layoutAdapter.topMostState {
// Hide a scroll indicator just before starting an interaction by swiping a panel down.
if velocity.y > 0, !allowScrollPanGesture(at: CGPoint(x: 0.0, y: offset)) {
lockScrollView()
}
// Show a scroll indicator when an animation is interrupted at the top and content is scrolled up
if velocity.y < 0, allowScrollPanGesture(at: CGPoint(x: 0.0, y: offset)) {
unlockScrollView()
}
// Adjust a small gap of the scroll offset just before swiping down starts in the grabber area,
if grabberAreaFrame.contains(location), grabberAreaFrame.contains(initialLocation) {
scrollView.setContentOffset(initialScrollOffset, animated: false)
}
let offset = scrollView.contentOffset.y - scrollView.contentOffsetZero.y
if state == layoutAdapter.topMostState, offset < 0, velocity.y > 0 {
fitToBounds(scrollView: scrollView)
let translation = panGesture.translation(in: panGestureRecognizer.view!.superview)
startInteraction(with: translation, at: location)
}
}
}
@@ -362,26 +306,23 @@ class FloatingPanelCore: NSObject, UIGestureRecognizerDelegate {
log.debug("panel gesture(\(state):\(panGesture.state)) --",
"translation = \(translation.y), location = \(location.y), velocity = \(velocity.y)")
if interactionInProgress == false, isDecelerating == false,
let vc = viewcontroller, vc.delegate?.floatingPanelShouldBeginDragging(vc) == false {
return
}
if let animator = self.animator {
guard surfaceView.presentationFrame.minY >= layoutAdapter.topMaxY else { return }
log.debug("panel animation(interruptible: \(animator.isInterruptible)) interrupted!!!")
log.debug("panel animation interrupted!!!")
if animator.isInterruptible {
animator.stopAnimation(false)
// A user can stop a panel at the nearest Y of a target position so this fine-tunes
// the a small gap between the presentation layer frame and model layer frame
// to unlock scroll view properly at finishAnimation(at:)
if abs(surfaceView.frame.minY - layoutAdapter.topY) <= 1.0 {
surfaceView.frame.origin.y = layoutAdapter.topY
}
animator.finishAnimation(at: .current)
} else {
self.animator = nil
}
self.animator = nil
// A user can stop a panel at the nearest Y of a target position
if abs(surfaceView.frame.minY - layoutAdapter.topY) < 1.0 {
surfaceView.frame.origin.y = layoutAdapter.topY
}
}
if interactionInProgress == false,
viewcontroller.delegate?.floatingPanelShouldBeginDragging(viewcontroller) == false {
return
}
if panGesture.state == .began {
@@ -400,15 +341,6 @@ class FloatingPanelCore: NSObject, UIGestureRecognizerDelegate {
}
panningChange(with: translation)
case .ended, .cancelled, .failed:
if interactionInProgress == false {
startInteraction(with: translation, at: location)
// Workaround: Prevent stopping the surface view b/w anchors if the pan gesture
// doesn't pass through .changed state after an interruptible animator is interrupted.
let dy = translation.y - .leastNonzeroMagnitude
layoutAdapter.updateInteractiveTopConstraint(diff: dy,
allowsTopBuffer: true,
with: behavior)
}
panningEnd(with: translation, velocity: velocity)
default:
break
@@ -476,36 +408,33 @@ class FloatingPanelCore: NSObject, UIGestureRecognizerDelegate {
// So here just preserve the current state if needed.
log.debug("panningBegan -- location = \(location.y)")
initialLocation = location
guard let scrollView = scrollView else { return }
if state == layoutAdapter.topMostState {
if grabberAreaFrame.contains(location) {
initialScrollOffset = scrollView.contentOffset
if let scrollView = scrollView {
initialScrollFrame = scrollView.frame
}
} else {
initialScrollOffset = scrollView.contentOffset
if let scrollView = scrollView {
initialScrollOffset = scrollView.contentOffset
}
}
}
private func panningChange(with translation: CGPoint) {
log.debug("panningChange -- translation = \(translation.y)")
let preY = surfaceView.frame.minY
let pre = surfaceView.frame.minY
let dy = translation.y - initialTranslationY
layoutAdapter.updateInteractiveTopConstraint(diff: dy,
allowsTopBuffer: allowsTopBuffer(for: dy),
with: behavior)
let currentY = surfaceView.frame.minY
backdropView.alpha = getBackdropAlpha(at: currentY, with: translation)
backdropView.alpha = getBackdropAlpha(with: translation)
preserveContentVCLayoutIfNeeded()
let didMove = (preY != currentY)
let didMove = (pre != surfaceView.frame.minY)
guard didMove else { return }
if let vc = viewcontroller {
vc.delegate?.floatingPanelDidMove(vc)
}
viewcontroller.delegate?.floatingPanelDidMove(viewcontroller)
}
private func allowsTopBuffer(for translationY: CGFloat) -> Bool {
@@ -520,27 +449,20 @@ class FloatingPanelCore: NSObject, UIGestureRecognizerDelegate {
}
private var disabledBottomAutoLayout = false
private var disabledAutoLayoutItems: Set<NSLayoutConstraint> = []
// Prevent stretching a view having a constraint to SafeArea.bottom in an overflow
// from the full position because SafeArea is global in a screen.
private func preserveContentVCLayoutIfNeeded() {
guard let vc = viewcontroller else { return }
guard vc.contentMode != .fitToBounds else { return }
// Must include topY
if (surfaceView.frame.minY <= layoutAdapter.topY) {
if !disabledBottomAutoLayout {
disabledAutoLayoutItems.removeAll()
vc.contentViewController?.view?.constraints.forEach({ (const) in
switch vc.contentViewController?.layoutGuide.bottomAnchor {
viewcontroller.contentViewController?.view?.constraints.forEach({ (const) in
switch viewcontroller.contentViewController?.layoutGuide.bottomAnchor {
case const.firstAnchor:
(const.secondItem as? UIView)?.disableAutoLayout()
const.isActive = false
disabledAutoLayoutItems.insert(const)
case const.secondAnchor:
(const.firstItem as? UIView)?.disableAutoLayout()
const.isActive = false
disabledAutoLayoutItems.insert(const)
default:
break
}
@@ -549,8 +471,8 @@ class FloatingPanelCore: NSObject, UIGestureRecognizerDelegate {
disabledBottomAutoLayout = true
} else {
if disabledBottomAutoLayout {
disabledAutoLayoutItems.forEach({ (const) in
switch vc.contentViewController?.layoutGuide.bottomAnchor {
viewcontroller.contentViewController?.view?.constraints.forEach({ (const) in
switch viewcontroller.contentViewController?.layoutGuide.bottomAnchor {
case const.firstAnchor:
(const.secondItem as? UIView)?.enableAutoLayout()
const.isActive = true
@@ -561,7 +483,6 @@ class FloatingPanelCore: NSObject, UIGestureRecognizerDelegate {
break
}
})
disabledAutoLayoutItems.removeAll()
}
disabledBottomAutoLayout = false
}
@@ -575,7 +496,7 @@ class FloatingPanelCore: NSObject, UIGestureRecognizerDelegate {
return
}
stopScrollDeceleration = surfaceView.frame.minY > (layoutAdapter.topY + (1.0 / surfaceView.traitCollection.displayScale)) // Projecting the dragging to the scroll dragging or not
stopScrollDeceleration = (surfaceView.frame.minY > layoutAdapter.topY) // Projecting the dragging to the scroll dragging or not
if stopScrollDeceleration {
DispatchQueue.main.async { [weak self] in
guard let `self` = self else { return }
@@ -583,37 +504,30 @@ class FloatingPanelCore: NSObject, UIGestureRecognizerDelegate {
}
}
let currentY = surfaceView.frame.minY
let targetPosition = self.targetPosition(from: currentY, with: velocity)
let targetPosition = self.targetPosition(with: velocity)
let distance = self.distance(to: targetPosition)
endInteraction(for: targetPosition)
if isRemovalInteractionEnabled, isBottomState {
let velocityVector = (distance != 0) ? CGVector(dx: 0, dy: min(velocity.y/distance, behavior.removalVelocity)) : .zero
// `velocityVector` will be replaced by just a velocity(not vector) when FloatingPanelRemovalInteraction will be added.
if shouldStartRemovalAnimation(with: velocityVector), let vc = viewcontroller {
vc.delegate?.floatingPanelDidEndDraggingToRemove(vc, withVelocity: velocity)
let animationVector = CGVector(dx: abs(velocityVector.dx), dy: abs(velocityVector.dy))
startRemovalAnimation(vc, with: animationVector) { [weak self] in
self?.finishRemovalAnimation()
let velocityVector = (distance != 0) ? CGVector(dx: 0,
dy: min(abs(velocity.y)/distance, behavior.removalVelocity)) : .zero
if shouldStartRemovalAnimation(with: 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, completion: { [weak self] in
guard let `self` = self else { return }
self.viewcontroller.delegate?.floatingPanelDidEndRemove(self.viewcontroller)
})
}
return
}
}
if let vc = viewcontroller {
vc.delegate?.floatingPanelDidEndDragging(vc, withVelocity: velocity, targetPosition: targetPosition)
}
if scrollView != nil, !stopScrollDeceleration,
surfaceView.frame.minY == layoutAdapter.topY,
targetPosition == layoutAdapter.topMostState {
self.state = targetPosition
self.updateLayout(to: targetPosition)
self.unlockScrollView()
return
}
viewcontroller.delegate?.floatingPanelDidEndDragging(viewcontroller, withVelocity: velocity, targetPosition: targetPosition)
// Workaround: Disable a tracking scroll to prevent bouncing a scroll content in a panel animating
let isScrollEnabled = scrollView?.isScrollEnabled
@@ -633,12 +547,12 @@ class FloatingPanelCore: NSObject, UIGestureRecognizerDelegate {
private func shouldStartRemovalAnimation(with velocityVector: CGVector) -> Bool {
let posY = layoutAdapter.positionY(for: state)
let currentY = surfaceView.frame.minY
let hiddenY = layoutAdapter.positionY(for: .hidden)
let bottomMaxY = layoutAdapter.bottomMaxY
let vth = behavior.removalVelocity
let pth = max(min(behavior.removalProgress, 1.0), 0.0)
let num = (currentY - posY)
let den = (hiddenY - posY)
let den = (bottomMaxY - posY)
guard num >= 0, den != 0, (num / den >= pth || velocityVector.dy == vth)
else { return false }
@@ -646,11 +560,10 @@ class FloatingPanelCore: NSObject, UIGestureRecognizerDelegate {
return true
}
private func startRemovalAnimation(_ vc: FloatingPanelController, with velocityVector: CGVector, completion: (() -> Void)?) {
let animator = behavior.removalInteractionAnimator(vc, with: velocityVector)
private func startRemovalAnimation(with velocityVector: CGVector, completion: (() -> Void)?) {
let animator = self.behavior.removalInteractionAnimator(self.viewcontroller, with: velocityVector)
animator.addAnimations { [weak self] in
self?.state = .hidden
self?.updateLayout(to: .hidden)
}
animator.addCompletion({ _ in
@@ -661,27 +574,17 @@ class FloatingPanelCore: NSObject, UIGestureRecognizerDelegate {
animator.startAnimation()
}
private func finishRemovalAnimation() {
viewcontroller?.dismiss(animated: false) { [weak self] in
guard let vc = self?.viewcontroller else { return }
vc.delegate?.floatingPanelDidEndRemove(vc)
}
}
private func startInteraction(with translation: CGPoint, at location: CGPoint) {
/* Don't lock a scroll view to show a scroll indicator after hitting the top */
log.debug("startInteraction -- translation = \(translation.y), location = \(location.y)")
guard interactionInProgress == false else { return }
var offset: CGPoint = .zero
initialFrame = surfaceView.frame
if state == layoutAdapter.topMostState, let scrollView = scrollView {
if grabberAreaFrame.contains(location) {
initialScrollOffset = scrollView.contentOffset
} else {
// Fit the surface bounds to a scroll offset content by startInteraction(at:offset:)
offset = CGPoint(x: -scrollView.contentOffset.x, y: -scrollView.contentOffset.y)
settle(scrollView: scrollView)
initialScrollOffset = scrollView.contentOffsetZero
}
log.debug("initial scroll offset --", initialScrollOffset)
@@ -689,15 +592,11 @@ class FloatingPanelCore: NSObject, UIGestureRecognizerDelegate {
initialTranslationY = translation.y
if let vc = viewcontroller {
vc.delegate?.floatingPanelWillBeginDragging(vc)
}
viewcontroller.delegate?.floatingPanelWillBeginDragging(viewcontroller)
layoutAdapter.startInteraction(at: state, offset: offset)
layoutAdapter.startInteraction(at: state)
interactionInProgress = true
lockScrollView()
}
private func endInteraction(for targetPosition: FloatingPanelPosition) {
@@ -710,7 +609,7 @@ class FloatingPanelCore: NSObject, UIGestureRecognizerDelegate {
interactionInProgress = false
// Prevent to keep a scroll view indicator visible at the half/tip position
if targetPosition != layoutAdapter.topMostState {
if state != layoutAdapter.topMostState {
lockScrollView()
}
@@ -725,36 +624,19 @@ class FloatingPanelCore: NSObject, UIGestureRecognizerDelegate {
private func startAnimation(to targetPosition: FloatingPanelPosition, at distance: CGFloat, with velocity: CGPoint) {
log.debug("startAnimation to \(targetPosition) -- distance = \(distance), velocity = \(velocity.y)")
guard let vc = viewcontroller else { return }
isDecelerating = true
viewcontroller.delegate?.floatingPanelWillBeginDecelerating(viewcontroller)
vc.delegate?.floatingPanelWillBeginDecelerating(vc)
let velocityVector = (distance != 0) ? CGVector(dx: 0, dy: abs(velocity.y)/distance) : .zero
let animator = behavior.interactionAnimator(vc, to: targetPosition, with: velocityVector)
let velocityVector = (distance != 0) ? CGVector(dx: 0, dy: min(abs(velocity.y)/distance, 30.0)) : .zero
let animator = behavior.interactionAnimator(self.viewcontroller, to: targetPosition, with: velocityVector)
animator.addAnimations { [weak self] in
guard let `self` = self, let vc = self.viewcontroller else { return }
guard let `self` = self else { return }
self.state = targetPosition
if animator.isInterruptible {
switch vc.contentMode {
case .fitToBounds:
UIView.performWithLinear(startTime: 0.0, relativeDuration: 0.75) {
self.layoutAdapter.activateFixedLayout()
self.surfaceView.superview!.layoutIfNeeded()
}
case .static:
self.layoutAdapter.activateFixedLayout()
}
} else {
self.layoutAdapter.activateFixedLayout()
}
self.layoutAdapter.activateInteractiveLayout(of: targetPosition)
self.updateLayout(to: targetPosition)
}
animator.addCompletion { [weak self] pos in
// Prevent calling `finishAnimation(at:)` by the old animator whose `isInterruptive` is false
// when a new animator has been started after the old one is interrupted.
guard let `self` = self, self.animator == animator else { return }
guard let `self` = self else { return }
self.finishAnimation(at: targetPosition)
}
self.animator = animator
@@ -767,26 +649,66 @@ class FloatingPanelCore: NSObject, UIGestureRecognizerDelegate {
self.isDecelerating = false
self.animator = nil
if let vc = viewcontroller {
vc.delegate?.floatingPanelDidEndDecelerating(vc)
}
self.viewcontroller.delegate?.floatingPanelDidEndDecelerating(self.viewcontroller)
if let scrollView = scrollView {
log.debug("finishAnimation -- scroll offset = \(scrollView.contentOffset)")
}
stopScrollDeceleration = false
log.debug("finishAnimation -- state = \(state) surface.minY = \(surfaceView.presentationFrame.minY) topY = \(layoutAdapter.topY)")
if state == layoutAdapter.topMostState, abs(surfaceView.presentationFrame.minY - layoutAdapter.topY) <= 1.0 {
// Don't unlock scroll view in animating view when presentation layer != model layer
if state == layoutAdapter.topMostState {
unlockScrollView()
}
}
private func distance(to targetPosition: FloatingPanelPosition) -> CGFloat {
let topY = layoutAdapter.topY
let middleY = layoutAdapter.middleY
let bottomY = layoutAdapter.bottomY
let currentY = surfaceView.frame.minY
let targetY = layoutAdapter.positionY(for: targetPosition)
return CGFloat(abs(currentY - targetY))
switch targetPosition {
case .full:
return CGFloat(abs(currentY - topY))
case .half:
return CGFloat(abs(currentY - middleY))
case .tip:
return CGFloat(abs(currentY - bottomY))
case .hidden:
fatalError("Now .hidden must not be used for a user interaction")
}
}
private func directionalPosition(at currentY: CGFloat, with translation: CGPoint) -> FloatingPanelPosition {
return getPosition(at: currentY, with: translation, directional: true)
}
private func redirectionalPosition(at currentY: CGFloat, with translation: CGPoint) -> FloatingPanelPosition {
return getPosition(at: currentY, with: translation, directional: false)
}
private func getPosition(at currentY: CGFloat, with translation: CGPoint, directional: Bool) -> FloatingPanelPosition {
let supportedPositions: Set = layoutAdapter.supportedPositions
if supportedPositions.count == 1 {
return state
}
let isForwardYAxis = (translation.y >= 0)
switch supportedPositions {
case [.full, .half]:
return (isForwardYAxis == directional) ? .half : .full
case [.half, .tip]:
return (isForwardYAxis == directional) ? .tip : .half
case [.full, .tip]:
return (isForwardYAxis == directional) ? .tip : .full
default:
let middleY = layoutAdapter.middleY
if currentY > middleY {
return (isForwardYAxis == directional) ? .tip : .half
} else {
return (isForwardYAxis == directional) ? .half : .full
}
}
}
// Distance travelled after decelerating to zero velocity at a constant rate.
@@ -795,57 +717,148 @@ class FloatingPanelCore: NSObject, UIGestureRecognizerDelegate {
return (initialVelocity / 1000.0) * decelerationRate / (1.0 - decelerationRate)
}
func targetPosition(from currentY: CGFloat, with velocity: CGPoint) -> (FloatingPanelPosition) {
guard let vc = viewcontroller else { return state }
private func targetPosition(with velocity: CGPoint) -> (FloatingPanelPosition) {
let currentY = surfaceView.frame.minY
let supportedPositions = layoutAdapter.supportedPositions
guard supportedPositions.count > 1 else {
if supportedPositions.count == 1 {
return state
}
let sortedPositions = Array(supportedPositions).sorted(by: { $0.rawValue < $1.rawValue })
switch supportedPositions {
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 [.full, .tip]:
return targetPosition(from: [.full, .tip], at: currentY, velocity: velocity)
default:
/*
[topY|full]---[th1]---[middleY|half]---[th2]---[bottomY|tip]
*/
let topY = layoutAdapter.topY
let middleY = layoutAdapter.middleY
let bottomY = layoutAdapter.bottomY
// Projection
let decelerationRate = behavior.momentumProjectionRate(vc)
let baseY = abs(layoutAdapter.positionY(for: layoutAdapter.bottomMostState) - layoutAdapter.positionY(for: layoutAdapter.topMostState))
let vecY = velocity.y / baseY
var pY = project(initialVelocity: vecY, decelerationRate: decelerationRate) * baseY + currentY
let nextState: FloatingPanelPosition
let forwardYDirection: Bool
let forwardY = velocity.y == 0 ? (currentY - layoutAdapter.positionY(for: state) > 0) : velocity.y > 0
/*
full <-> half <-> tip
*/
switch state {
case .full:
nextState = .half
forwardYDirection = true
case .half:
nextState = (currentY > middleY) ? .tip : .full
forwardYDirection = (currentY > middleY)
case .tip:
nextState = .half
forwardYDirection = false
case .hidden:
fatalError("Now .hidden must not be used for a user interaction")
}
let segment = layoutAdapter.segument(at: pY, forward: forwardY)
let redirectionalProgress = max(min(behavior.redirectionalProgress(viewcontroller, from: state, to: nextState), 1.0), 0.0)
var fromPos: FloatingPanelPosition
var toPos: FloatingPanelPosition
let th1: CGFloat
let th2: CGFloat
let (lowerPos, upperPos) = (segment.lower ?? sortedPositions.first!, segment.upper ?? sortedPositions.last!)
(fromPos, toPos) = forwardY ? (lowerPos, upperPos) : (upperPos, lowerPos)
if forwardYDirection {
th1 = topY + (middleY - topY) * redirectionalProgress
th2 = middleY + (bottomY - middleY) * redirectionalProgress
} else {
th1 = middleY - (middleY - topY) * redirectionalProgress
th2 = bottomY - (bottomY - middleY) * redirectionalProgress
}
if behavior.shouldProjectMomentum(vc, for: toPos) == false {
let segment = layoutAdapter.segument(at: currentY, forward: forwardY)
var (lowerPos, upperPos) = (segment.lower ?? sortedPositions.first!, segment.upper ?? sortedPositions.last!)
// Equate the segment out of {top,bottom} most state to the {top,bottom} most segment
if lowerPos == upperPos {
if forwardY {
upperPos = lowerPos.next(in: sortedPositions)
} else {
lowerPos = upperPos.pre(in: sortedPositions)
let decelerationRate = behavior.momentumProjectionRate(viewcontroller)
let baseY = abs(bottomY - topY)
let vecY = velocity.y / baseY
let pY = project(initialVelocity: vecY, decelerationRate: decelerationRate) * baseY + currentY
switch currentY {
case ..<th1:
switch pY {
case bottomY...:
return behavior.shouldProjectMomentum(viewcontroller, for: .tip) ? .tip : .half
case middleY...:
return .half
case topY...:
return .full
default:
return .full
}
case ...middleY:
switch pY {
case bottomY...:
return behavior.shouldProjectMomentum(viewcontroller, for: .tip) ? .tip : .half
case middleY...:
return .half
case topY...:
return .half
default:
return .full
}
case ..<th2:
switch pY {
case bottomY...:
return .tip
case middleY...:
return .half
case topY...:
return .half
default:
return behavior.shouldProjectMomentum(viewcontroller, for: .full) ? .full : .half
}
default:
switch pY {
case bottomY...:
return .tip
case middleY...:
return .tip
case topY...:
return .half
default:
return behavior.shouldProjectMomentum(viewcontroller, for: .full) ? .full : .half
}
}
(fromPos, toPos) = forwardY ? (lowerPos, upperPos) : (upperPos, lowerPos)
// Block a projection to a segment over the next from the current segment
// (= Trim pY with the current segment)
if forwardY {
pY = max(min(pY, layoutAdapter.positionY(for: toPos.next(in: sortedPositions))), layoutAdapter.positionY(for: fromPos))
}
}
private func targetPosition(from positions: [FloatingPanelPosition], at currentY: CGFloat, velocity: CGPoint) -> FloatingPanelPosition {
assert(positions.count == 2)
let top = positions[0]
let bottom = positions[1]
let topY = layoutAdapter.positionY(for: top)
let bottomY = layoutAdapter.positionY(for: bottom)
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
let decelerationRate = behavior.momentumProjectionRate(viewcontroller)
let pY = project(initialVelocity: velocity.y, decelerationRate: decelerationRate) + currentY
switch currentY {
case ..<th:
if pY >= bottomY {
return bottom
} else {
pY = max(min(pY, layoutAdapter.positionY(for: fromPos)), layoutAdapter.positionY(for: toPos.pre(in: sortedPositions)))
return top
}
default:
if pY <= topY {
return top
} else {
return bottom
}
}
// Redirection
let redirectionalProgress = max(min(behavior.redirectionalProgress(vc, from: fromPos, to: toPos), 1.0), 0.0)
let progress = abs(pY - layoutAdapter.positionY(for: fromPos)) / abs(layoutAdapter.positionY(for: fromPos) - layoutAdapter.positionY(for: toPos))
return progress > redirectionalProgress ? toPos : fromPos
}
// MARK: - ScrollView handling
@@ -853,11 +866,11 @@ class FloatingPanelCore: NSObject, UIGestureRecognizerDelegate {
private func lockScrollView() {
guard let scrollView = scrollView else { return }
if scrollView.isLocked {
if isScrollLocked {
log.debug("Already scroll locked.")
return
}
log.debug("lock scroll view")
isScrollLocked = true
scrollBouncable = scrollView.bounces
scrollIndictorVisible = scrollView.showsVerticalScrollIndicator
@@ -868,29 +881,47 @@ class FloatingPanelCore: NSObject, UIGestureRecognizerDelegate {
}
private func unlockScrollView() {
guard let scrollView = scrollView, scrollView.isLocked else { return }
log.debug("unlock scroll view")
guard let scrollView = scrollView, isScrollLocked else { return }
isScrollLocked = false
scrollView.isDirectionalLockEnabled = false
scrollView.bounces = scrollBouncable
scrollView.showsVerticalScrollIndicator = scrollIndictorVisible
}
private func fitToBounds(scrollView: UIScrollView) {
log.debug("fit scroll view to bounds -- scroll offset =", scrollView.contentOffset.y)
surfaceView.frame.origin.y = layoutAdapter.topY - scrollView.contentOffset.y
scrollView.transform = CGAffineTransform.identity.translatedBy(x: 0.0,
y: scrollView.contentOffset.y)
scrollView.scrollIndicatorInsets = UIEdgeInsets(top: -scrollView.contentOffset.y,
left: 0.0,
bottom: 0.0,
right: 0.0)
}
private func settle(scrollView: UIScrollView) {
log.debug("settle scroll view")
let frame = surfaceView.layer.presentation()?.frame ?? surfaceView.frame
surfaceView.transform = .identity
surfaceView.frame = frame
scrollView.transform = .identity
scrollView.frame = initialScrollFrame
scrollView.contentOffset = scrollView.contentOffsetZero
scrollView.scrollIndicatorInsets = .zero
}
private func stopScrollingWithDeceleration(at contentOffset: CGPoint) {
// Must use setContentOffset(_:animated) to force-stop deceleration
scrollView?.setContentOffset(contentOffset, animated: false)
}
private func allowScrollPanGesture(at contentOffset: CGPoint) -> Bool {
if state == layoutAdapter.topMostState {
return contentOffset.y <= -30.0 || contentOffset.y > 0
}
return false
}
}
class FloatingPanelPanGestureRecognizer: UIPanGestureRecognizer {
fileprivate weak var floatingPanel: FloatingPanelCore?
fileprivate weak var floatingPanel: FloatingPanel?
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesBegan(touches, with: event)
if floatingPanel?.animator != nil {
@@ -902,7 +933,7 @@ class FloatingPanelPanGestureRecognizer: UIPanGestureRecognizer {
return super.delegate
}
set {
guard newValue is FloatingPanelCore else {
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)
@@ -7,5 +7,25 @@ import UIKit
/// A view that presents a backdrop interface behind a floating panel.
public class FloatingPanelBackdropView: UIView {
public var dismissalTapGestureRecognizer: UITapGestureRecognizer!
override init(frame: CGRect) {
super.init(frame: frame)
setUp()
}
required public init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setUp()
}
private func setUp() {
layer.backgroundColor = UIColor.black.cgColor
}
@objc dynamic public override var backgroundColor: UIColor? {
get {
guard let color = layer.backgroundColor else { return nil }
return UIColor(cgColor: color)
}
set { layer.backgroundColor = newValue?.cgColor }
}
}
@@ -23,11 +23,6 @@ public protocol FloatingPanelBehavior {
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.
///
/// - Attention:
/// By default, it returns a non-interruptible animator to prevent a propagation of the animation to a content view.
/// However returning an interruptible animator is working well depending on a content view and it can be better
/// than using a non-interruptible one.
func interactionAnimator(_ fpc: FloatingPanelController, to targetPosition: FloatingPanelPosition, with velocity: CGVector) -> UIViewPropertyAnimator
/// Returns a UIViewPropertyAnimator object to add a floating panel to a position.
@@ -72,7 +67,14 @@ public protocol FloatingPanelBehavior {
public extension FloatingPanelBehavior {
func shouldProjectMomentum(_ fpc: FloatingPanelController, for proposedTargetPosition: FloatingPanelPosition) -> Bool {
return false
switch (fpc.position, proposedTargetPosition) {
case (.full, .tip):
return false
case (.tip, .full):
return false
default:
return true
}
}
func momentumProjectionRate(_ fpc: FloatingPanelController) -> CGFloat {
@@ -132,7 +134,7 @@ public class FloatingPanelDefaultBehavior: FloatingPanelBehavior {
public func interactionAnimator(_ fpc: FloatingPanelController, to targetPosition: FloatingPanelPosition, with velocity: CGVector) -> UIViewPropertyAnimator {
let timing = timeingCurve(with: velocity)
let animator = UIViewPropertyAnimator(duration: 0, timingParameters: timing)
animator.isInterruptible = false // Prevent a propagation of the animation(spring etc) to a content view
animator.isInterruptible = false
return animator
}
+35 -82
View File
@@ -12,9 +12,7 @@ public protocol FloatingPanelControllerDelegate: class {
// if it returns nil, FloatingPanelController uses the default behavior
func floatingPanel(_ vc: FloatingPanelController, behaviorFor newCollection: UITraitCollection) -> FloatingPanelBehavior?
/// Called when the floating panel has changed to a new position. Can be called inside an animation block, so any
/// view properties set inside this function will be automatically animated alongside the panel.
func floatingPanelDidChangePosition(_ vc: FloatingPanelController)
func floatingPanelDidChangePosition(_ vc: FloatingPanelController) // changed the settled position in the model layer
/// Asks the delegate if dragging should begin by the pan gesture recognizer.
func floatingPanelShouldBeginDragging(_ vc: FloatingPanelController) -> Bool
@@ -70,60 +68,18 @@ public enum FloatingPanelPosition: Int {
case half
case tip
case hidden
static var allCases: [FloatingPanelPosition] {
return [.full, .half, .tip, .hidden]
}
func next(in positions: [FloatingPanelPosition]) -> FloatingPanelPosition {
#if swift(>=4.2)
guard
let index = positions.firstIndex(of: self),
positions.indices.contains(index + 1)
else { return self }
#else
guard
let index = positions.index(of: self),
positions.indices.contains(index + 1)
else { return self }
#endif
return positions[index + 1]
}
func pre(in positions: [FloatingPanelPosition]) -> FloatingPanelPosition {
#if swift(>=4.2)
guard
let index = positions.firstIndex(of: self),
positions.indices.contains(index - 1)
else { return self }
#else
guard
let index = positions.index(of: self),
positions.indices.contains(index - 1)
else { return self }
#endif
return positions[index - 1]
}
}
///
/// A container view controller to display a floating panel to present contents in parallel as a user wants.
///
open class FloatingPanelController: UIViewController {
open class FloatingPanelController: UIViewController, UIScrollViewDelegate, UIGestureRecognizerDelegate {
/// Constants indicating how safe area insets are added to the adjusted content inset.
public enum ContentInsetAdjustmentBehavior: Int {
case always
case never
}
/// A flag used to determine how the controller object lays out the content view when the surface position changes.
public enum ContentMode: Int {
/// The option to fix the content to keep the height of the top most position.
case `static`
/// The option to scale the content to fit the bounds of the root view by changing the surface position.
case fitToBounds
}
/// The delegate of the floating panel controller object.
public weak var delegate: FloatingPanelControllerDelegate?{
didSet{
@@ -187,17 +143,9 @@ open class FloatingPanelController: UIViewController {
set { set(contentViewController: newValue) }
get { return _contentViewController }
}
public var contentMode: ContentMode = .static {
didSet {
guard position != .hidden else { return }
activateLayout()
}
}
private var _contentViewController: UIViewController?
private(set) var floatingPanel: FloatingPanelCore!
private var floatingPanel: FloatingPanel!
private var preSafeAreaInsets: UIEdgeInsets = .zero // Capture the latest one
private var safeAreaInsetsObservation: NSKeyValueObservation?
private let modalTransition = FloatingPanelModalTransition()
@@ -220,7 +168,7 @@ open class FloatingPanelController: UIViewController {
modalPresentationStyle = .custom
transitioningDelegate = modalTransition
floatingPanel = FloatingPanelCore(self,
floatingPanel = FloatingPanel(self,
layout: fetchLayout(for: self.traitCollection),
behavior: fetchBehavior(for: self.traitCollection))
}
@@ -229,7 +177,7 @@ open class FloatingPanelController: UIViewController {
floatingPanel.layoutAdapter.layout = fetchLayout(for: traitCollection)
floatingPanel.behavior = fetchBehavior(for: self.traitCollection)
}
// MARK:- Overrides
/// Creates the view that the controller manages.
@@ -248,19 +196,18 @@ open class FloatingPanelController: UIViewController {
self.view = view as UIView
}
open override func viewDidLayoutSubviews() {
open override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
if #available(iOS 11.0, *) {}
else {
// Because {top,bottom}LayoutGuide is managed as a view
if preSafeAreaInsets != layoutInsets,
floatingPanel.isDecelerating == false {
if preSafeAreaInsets != layoutInsets {
self.update(safeAreaInsets: layoutInsets)
}
}
}
open override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
open override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
if view.translatesAutoresizingMaskIntoConstraints {
@@ -269,9 +216,14 @@ open class FloatingPanelController: UIViewController {
}
}
open override func willTransition(to newCollection: UITraitCollection, with coordinator: UIViewControllerTransitionCoordinator) {
open override func willTransition(to newCollection: UITraitCollection, with coordinator: UIViewControllerTransitionCoordinator) {
super.willTransition(to: newCollection, with: coordinator)
self.prepare(for: newCollection)
// Change layout for a new trait collection
reloadLayout(for: newCollection)
setUpLayout()
floatingPanel.behavior = fetchBehavior(for: newCollection)
}
open override func viewWillDisappear(_ animated: Bool) {
@@ -279,15 +231,6 @@ open class FloatingPanelController: UIViewController {
safeAreaInsetsObservation = nil
}
// MARK:- Internals
func prepare(for newCollection: UITraitCollection) {
guard newCollection.shouldUpdateLayout(from: traitCollection) else { return }
// Change a layout & behavior for a new trait collection
reloadLayout(for: newCollection)
activateLayout()
floatingPanel.behavior = fetchBehavior(for: newCollection)
}
// MARK:- Privates
private func fetchLayout(for traitCollection: UITraitCollection) -> FloatingPanelLayout {
@@ -305,7 +248,8 @@ open class FloatingPanelController: UIViewController {
private func update(safeAreaInsets: UIEdgeInsets) {
guard
preSafeAreaInsets != safeAreaInsets
preSafeAreaInsets != safeAreaInsets,
self.floatingPanel.isDecelerating == false
else { return }
log.debug("Update safeAreaInsets", safeAreaInsets)
@@ -313,7 +257,7 @@ open class FloatingPanelController: UIViewController {
// Prevent an infinite loop on iOS 10: setUpLayout() -> viewDidLayoutSubviews() -> setUpLayout()
preSafeAreaInsets = safeAreaInsets
activateLayout()
setUpLayout()
switch contentInsetAdjustmentBehavior {
case .always:
@@ -326,6 +270,7 @@ open class FloatingPanelController: UIViewController {
private func reloadLayout(for traitCollection: UITraitCollection) {
floatingPanel.layoutAdapter.layout = fetchLayout(for: traitCollection)
floatingPanel.layoutAdapter.prepareLayout(in: self)
if let parent = self.parent {
if let layout = layout as? UIViewController, layout == parent {
@@ -337,9 +282,7 @@ open class FloatingPanelController: UIViewController {
}
}
private func activateLayout() {
floatingPanel.layoutAdapter.prepareLayout(in: self)
private func setUpLayout() {
// preserve the current content offset
let contentOffset = scrollView?.contentOffset
@@ -355,7 +298,7 @@ open class FloatingPanelController: UIViewController {
public func show(animated: Bool = false, completion: (() -> Void)? = nil) {
// Must apply the current layout here
reloadLayout(for: traitCollection)
activateLayout()
setUpLayout()
if #available(iOS 11.0, *) {
// Must track the safeAreaInsets of `self.view` to update the layout.
@@ -380,6 +323,7 @@ open class FloatingPanelController: UIViewController {
/// Hides the surface view to the hidden position
public func hide(animated: Bool = false, completion: (() -> Void)? = nil) {
safeAreaInsetsObservation = nil
move(to: .hidden,
animated: animated,
completion: completion)
@@ -425,9 +369,9 @@ open class FloatingPanelController: UIViewController {
show(animated: animated) { [weak self] in
guard let `self` = self else { return }
#if swift(>=4.2)
self.didMove(toParent: parent)
self.didMove(toParent: self)
#else
self.didMove(toParentViewController: parent)
self.didMove(toParentViewController: self)
#endif
}
}
@@ -569,12 +513,21 @@ open class FloatingPanelController: UIViewController {
/// animation block.
public func updateLayout() {
reloadLayout(for: traitCollection)
activateLayout()
setUpLayout()
}
/// Returns the y-coordinate of the point at the origin of the surface view.
public func originYOfSurface(for pos: FloatingPanelPosition) -> CGFloat {
return floatingPanel.layoutAdapter.positionY(for: pos)
switch pos {
case .full:
return floatingPanel.layoutAdapter.topY
case .half:
return floatingPanel.layoutAdapter.middleY
case .tip:
return floatingPanel.layoutAdapter.bottomY
case .hidden:
return floatingPanel.layoutAdapter.hiddenY
}
}
}
+106 -191
View File
@@ -11,24 +11,15 @@ import UIKit
/// It can't be used with FloatingPanelIntrinsicLayout.
public protocol FloatingPanelFullScreenLayout: FloatingPanelLayout { }
public extension FloatingPanelFullScreenLayout {
var positionReference: FloatingPanelLayoutReference {
return .fromSuperview
}
}
/// FloatingPanelIntrinsicLayout
///
/// Use the layout protocol if you want to layout a panel using the intrinsic height.
/// It can't be used with `FloatingPanelFullScreenLayout`.
/// It can't be used with FloatingPanelFullScreenLayout.
///
/// - Attention:
/// `insetFor(position:)` must return `nil` for the full position. Because
/// the inset is determined automatically by the intrinsic height.
/// You can customize insets only for the half, tip and hidden positions.
///
/// - Note:
/// By default, the `positionReference` is set to `.fromSafeArea`.
public protocol FloatingPanelIntrinsicLayout: FloatingPanelLayout { }
public extension FloatingPanelIntrinsicLayout {
@@ -43,15 +34,6 @@ public extension FloatingPanelIntrinsicLayout {
func insetFor(position: FloatingPanelPosition) -> CGFloat? {
return nil
}
var positionReference: FloatingPanelLayoutReference {
return .fromSafeArea
}
}
public enum FloatingPanelLayoutReference: Int {
case fromSafeArea = 0
case fromSuperview = 1
}
public protocol FloatingPanelLayout: class {
@@ -61,16 +43,14 @@ public protocol FloatingPanelLayout: class {
/// Returns a set of FloatingPanelPosition objects to tell the applicable
/// positions of the floating panel controller.
///
/// By default, it returns full, half and tip positions.
/// By default, it returns all position except for `hidden` position. Because
/// it's always supported by `FloatingPanelController` so you don't need to return it.
var supportedPositions: Set<FloatingPanelPosition> { get }
/// Return the interaction buffer to the top from the top position. Default is 6.0.
var topInteractionBuffer: CGFloat { get }
/// Return the interaction buffer to the bottom from the bottom position. Default is 6.0.
///
/// - Important:
/// The specified buffer is ignored when `FloatingPanelController.isRemovalInteractionEnabled` is set to true.
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).
@@ -92,9 +72,6 @@ public protocol FloatingPanelLayout: class {
///
/// Default is 0.3 at full position, otherwise 0.0.
func backdropAlphaFor(position: FloatingPanelPosition) -> CGFloat
var positionReference: FloatingPanelLayoutReference { get }
}
public extension FloatingPanelLayout {
@@ -115,10 +92,6 @@ public extension FloatingPanelLayout {
func backdropAlphaFor(position: FloatingPanelPosition) -> CGFloat {
return position == .full ? 0.3 : 0.0
}
var positionReference: FloatingPanelLayoutReference {
return .fromSafeArea
}
}
public class FloatingPanelDefaultLayout: FloatingPanelLayout {
@@ -157,13 +130,9 @@ public class FloatingPanelDefaultLandscapeLayout: FloatingPanelLayout {
}
}
struct LayoutSegment {
let lower: FloatingPanelPosition?
let upper: FloatingPanelPosition?
}
class FloatingPanelLayoutAdapter {
weak var vc: FloatingPanelController!
weak var vc: UIViewController!
private weak var surfaceView: FloatingPanelSurfaceView!
private weak var backdropView: FloatingPanelBackdropView!
@@ -185,8 +154,6 @@ class FloatingPanelLayoutAdapter {
private var tipConstraints: [NSLayoutConstraint] = []
private var offConstraints: [NSLayoutConstraint] = []
private var interactiveTopConstraint: NSLayoutConstraint?
private var bottomConstraint: NSLayoutConstraint?
private var heightConstraint: NSLayoutConstraint?
@@ -208,31 +175,70 @@ class FloatingPanelLayoutAdapter {
}
var supportedPositions: Set<FloatingPanelPosition> {
return layout.supportedPositions
var supportedPositions = layout.supportedPositions
supportedPositions.remove(.hidden)
return supportedPositions
}
var topMostState: FloatingPanelPosition {
return supportedPositions.sorted(by: { $0.rawValue < $1.rawValue }).first ?? .hidden
}
var bottomMostState: FloatingPanelPosition {
return supportedPositions.sorted(by: { $0.rawValue < $1.rawValue }).last ?? .hidden
if supportedPositions.contains(.full) {
return .full
}
if supportedPositions.contains(.half) {
return .half
}
return .tip
}
var topY: CGFloat {
return positionY(for: topMostState)
if supportedPositions.contains(.full) {
switch layout {
case is FloatingPanelIntrinsicLayout:
return surfaceView.superview!.bounds.height - surfaceView.bounds.height
case is FloatingPanelFullScreenLayout:
return fullInset
default:
return (safeAreaInsets.top + fullInset)
}
} else {
return middleY
}
}
var middleY: CGFloat {
if layout is FloatingPanelFullScreenLayout {
return surfaceView.superview!.bounds.height - halfInset
} else{
return surfaceView.superview!.bounds.height - (safeAreaInsets.bottom + halfInset)
}
}
var bottomY: CGFloat {
return positionY(for: bottomMostState)
if supportedPositions.contains(.tip) {
if layout is FloatingPanelFullScreenLayout {
return surfaceView.superview!.bounds.height - tipInset
} else{
return surfaceView.superview!.bounds.height - (safeAreaInsets.bottom + tipInset)
}
} else {
return middleY
}
}
var hiddenY: CGFloat {
return surfaceView.superview!.bounds.height
}
var topMaxY: CGFloat {
return topY - layout.topInteractionBuffer
return layout is FloatingPanelFullScreenLayout ? 0.0 : safeAreaInsets.top
}
var bottomMaxY: CGFloat {
return bottomY + layout.bottomInteractionBuffer
if layout is FloatingPanelFullScreenLayout{
return surfaceView.superview!.bounds.height - hiddenInset
} else {
return surfaceView.superview!.bounds.height - (safeAreaInsets.bottom + hiddenInset)
}
}
var adjustedContentInsets: UIEdgeInsets {
@@ -245,31 +251,13 @@ class FloatingPanelLayoutAdapter {
func positionY(for pos: FloatingPanelPosition) -> CGFloat {
switch pos {
case .full:
if layout is FloatingPanelIntrinsicLayout {
return surfaceView.superview!.bounds.height - surfaceView.bounds.height
}
switch layout.positionReference {
case .fromSafeArea:
return (safeAreaInsets.top + fullInset)
case .fromSuperview:
return fullInset
}
return topY
case .half:
switch layout.positionReference {
case .fromSafeArea:
return surfaceView.superview!.bounds.height - (safeAreaInsets.bottom + halfInset)
case .fromSuperview:
return surfaceView.superview!.bounds.height - halfInset
}
return middleY
case .tip:
switch layout.positionReference {
case .fromSafeArea:
return surfaceView.superview!.bounds.height - (safeAreaInsets.bottom + tipInset)
case .fromSuperview:
return surfaceView.superview!.bounds.height - tipInset
}
return bottomY
case .hidden:
return surfaceView.superview!.bounds.height - hiddenInset
return hiddenY
}
}
@@ -303,14 +291,10 @@ class FloatingPanelLayoutAdapter {
", content safe area(bottom) =", safeAreaBottom)
}
func prepareLayout(in vc: FloatingPanelController) {
func prepareLayout(in vc: UIViewController) {
self.vc = vc
NSLayoutConstraint.deactivate(fixedConstraints + fullConstraints + halfConstraints + tipConstraints + offConstraints)
NSLayoutConstraint.deactivate(constraint: self.heightConstraint)
self.heightConstraint = nil
NSLayoutConstraint.deactivate(constraint: self.bottomConstraint)
self.bottomConstraint = nil
surfaceView.translatesAutoresizingMaskIntoConstraints = false
backdropView.translatesAutoresizingMaskIntoConstraints = false
@@ -326,14 +310,9 @@ class FloatingPanelLayoutAdapter {
fixedConstraints = surfaceConstraints + backdropConstraints
if vc.contentMode == .fitToBounds {
bottomConstraint = surfaceView.bottomAnchor.constraint(equalTo: vc.view.bottomAnchor,
constant: 0.0)
}
// Flexible surface constraints for full, half, tip and off
let topAnchor: NSLayoutYAxisAnchor = {
if layout.positionReference == .fromSuperview {
if layout is FloatingPanelFullScreenLayout {
return vc.view.topAnchor
} else {
return vc.layoutGuide.topAnchor
@@ -352,7 +331,7 @@ class FloatingPanelLayoutAdapter {
}
let bottomAnchor: NSLayoutYAxisAnchor = {
if layout.positionReference == .fromSuperview {
if layout is FloatingPanelFullScreenLayout {
return vc.view.bottomAnchor
} else {
return vc.layoutGuide.bottomAnchor
@@ -374,20 +353,20 @@ class FloatingPanelLayoutAdapter {
]
}
func startInteraction(at state: FloatingPanelPosition, offset: CGPoint = .zero) {
guard self.interactiveTopConstraint == nil else { return }
func startInteraction(at state: FloatingPanelPosition) {
NSLayoutConstraint.deactivate(fullConstraints + halfConstraints + tipConstraints + offConstraints)
let interactiveTopConstraint: NSLayoutConstraint
switch layout.positionReference {
case .fromSafeArea:
initialConst = surfaceView.frame.minY - safeAreaInsets.top + offset.y
interactiveTopConstraint = surfaceView.topAnchor.constraint(equalTo: vc.layoutGuide.topAnchor,
constant: initialConst)
case .fromSuperview:
initialConst = surfaceView.frame.minY + offset.y
switch layout {
case is FloatingPanelIntrinsicLayout,
is FloatingPanelFullScreenLayout:
initialConst = surfaceView.frame.minY
interactiveTopConstraint = surfaceView.topAnchor.constraint(equalTo: vc.view.topAnchor,
constant: initialConst)
default:
initialConst = surfaceView.frame.minY - safeAreaInsets.top
interactiveTopConstraint = surfaceView.topAnchor.constraint(equalTo: vc.layoutGuide.topAnchor,
constant: initialConst)
}
NSLayoutConstraint.activate([interactiveTopConstraint])
self.interactiveTopConstraint = interactiveTopConstraint
@@ -396,75 +375,67 @@ class FloatingPanelLayoutAdapter {
func endInteraction(at state: FloatingPanelPosition) {
// Don't deactivate `interactiveTopConstraint` here because it leads to
// unsatisfiable constraints
if self.interactiveTopConstraint == nil {
// Actiavate `interactiveTopConstraint` for `fitToBounds` mode.
// It goes throught this path when the pan gesture state jumps
// from .begin to .end.
startInteraction(at: state)
}
}
// The method is separated from prepareLayout(to:) for the rotation support
// It must be called in FloatingPanelController.traitCollectionDidChange(_:)
func updateHeight() {
guard let vc = vc else { return }
NSLayoutConstraint.deactivate(constraint: heightConstraint)
heightConstraint = nil
if layout is FloatingPanelIntrinsicLayout {
updateIntrinsicHeight()
}
defer {
if layout is FloatingPanelIntrinsicLayout {
NSLayoutConstraint.deactivate(fullConstraints)
fullConstraints = [
surfaceView.topAnchor.constraint(equalTo: vc.layoutGuide.bottomAnchor,
constant: -fullInset),
]
}
if let const = self.heightConstraint {
NSLayoutConstraint.deactivate([const])
}
guard vc.contentMode != .fitToBounds else { return }
let heightConstraint: NSLayoutConstraint
switch layout {
case is FloatingPanelIntrinsicLayout:
updateIntrinsicHeight()
heightConstraint = surfaceView.heightAnchor.constraint(equalToConstant: intrinsicHeight + safeAreaInsets.bottom)
default:
let const = -(positionY(for: topMostState))
heightConstraint = surfaceView.heightAnchor.constraint(equalTo: vc.view.heightAnchor,
constant: const)
}
NSLayoutConstraint.activate(constraint: heightConstraint)
NSLayoutConstraint.activate([heightConstraint])
self.heightConstraint = heightConstraint
surfaceView.bottomOverflow = vc.view.bounds.height + layout.topInteractionBuffer
if layout is FloatingPanelIntrinsicLayout {
NSLayoutConstraint.deactivate(fullConstraints)
fullConstraints = [
surfaceView.topAnchor.constraint(equalTo: vc.layoutGuide.bottomAnchor,
constant: -fullInset),
]
}
}
func updateInteractiveTopConstraint(diff: CGFloat, allowsTopBuffer: Bool, with behavior: FloatingPanelBehavior) {
defer {
layoutSurfaceIfNeeded() // MUST be called to update `surfaceView.frame`
surfaceView.superview!.layoutIfNeeded() // MUST call here to update `surfaceView.frame`
}
let topMostConst: CGFloat = {
var ret: CGFloat = 0.0
switch layout.positionReference {
case .fromSafeArea:
ret = topY - safeAreaInsets.top
case .fromSuperview:
switch layout {
case is FloatingPanelIntrinsicLayout, is FloatingPanelFullScreenLayout:
ret = topY
default:
ret = topY - safeAreaInsets.top
}
return max(ret, 0.0) // The top boundary is equal to the related topAnchor.
}()
let bottomMostConst: CGFloat = {
var ret: CGFloat = 0.0
let _bottomY = vc.isRemovalInteractionEnabled ? positionY(for: .hidden) : bottomY
switch layout.positionReference {
case .fromSafeArea:
ret = _bottomY - safeAreaInsets.top
case .fromSuperview:
ret = _bottomY
switch layout {
case is FloatingPanelIntrinsicLayout, is FloatingPanelFullScreenLayout:
ret = bottomY
default:
ret = bottomY - safeAreaInsets.top
}
return min(ret, surfaceView.superview!.bounds.height)
return min(ret, bottomMaxY)
}()
let minConst = allowsTopBuffer ? topMostConst - layout.topInteractionBuffer : topMostConst
let maxConst = bottomMostConst + layout.bottomInteractionBuffer
@@ -494,29 +465,23 @@ class FloatingPanelLayoutAdapter {
return (1.0 - (1.0 / ((buffer * 0.55 / base) + 1.0))) * base
}
func activateFixedLayout() {
// Must deactivate `interactiveTopConstraint` here
NSLayoutConstraint.deactivate(constraint: self.interactiveTopConstraint)
self.interactiveTopConstraint = nil
NSLayoutConstraint.activate(fixedConstraints)
if vc.contentMode == .fitToBounds {
NSLayoutConstraint.activate(constraint: self.bottomConstraint)
}
}
func activateInteractiveLayout(of state: FloatingPanelPosition) {
func activateLayout(of state: FloatingPanelPosition) {
defer {
layoutSurfaceIfNeeded()
log.debug("activateLayout -- surface.presentation = \(self.surfaceView.presentationFrame) surface.frame = \(self.surfaceView.frame)")
surfaceView.superview!.layoutIfNeeded()
}
var state = state
setBackdropAlpha(of: state)
if isValid(state) == false {
// Must deactivate `interactiveTopConstraint` here
if let interactiveTopConstraint = interactiveTopConstraint {
NSLayoutConstraint.deactivate([interactiveTopConstraint])
self.interactiveTopConstraint = nil
}
NSLayoutConstraint.activate(fixedConstraints)
if supportedPositions.union([.hidden]).contains(state) == false {
state = layout.initialPosition
}
@@ -533,22 +498,6 @@ class FloatingPanelLayoutAdapter {
}
}
func activateLayout(of state: FloatingPanelPosition) {
activateFixedLayout()
activateInteractiveLayout(of: state)
}
func isValid(_ state: FloatingPanelPosition) -> Bool {
return supportedPositions.union([.hidden]).contains(state)
}
private func layoutSurfaceIfNeeded() {
#if !TEST
guard surfaceView.window != nil else { return }
#endif
surfaceView.superview?.layoutIfNeeded()
}
private func setBackdropAlpha(of target: FloatingPanelPosition) {
if target == .hidden {
self.backdropView.alpha = 0.0
@@ -560,7 +509,7 @@ class FloatingPanelLayoutAdapter {
private func checkLayoutConsistance() {
// Verify layout configurations
assert(supportedPositions.count > 0)
assert(supportedPositions.contains(layout.initialPosition),
assert(supportedPositions.union([.hidden]).contains(layout.initialPosition),
"Does not include an initial position (\(layout.initialPosition)) in supportedPositions (\(supportedPositions))")
if layout is FloatingPanelIntrinsicLayout {
@@ -579,38 +528,4 @@ class FloatingPanelLayoutAdapter {
assert(bottomY > topY, "Invalid insets { topY: \(topY), bottomY: \(bottomY) }")
}*/
}
func segument(at posY: CGFloat, forward: Bool) -> LayoutSegment {
/// ----------------------->Y
/// --> forward <-- backward
/// |-------|===o===|-------| |-------|-------|===o===|
/// |-------|-------x=======| |-------|=======x-------|
/// |-------|-------|===o===| |-------|===o===|-------|
/// pos: o/x, seguement: =
let sortedPositions = supportedPositions.sorted(by: { $0.rawValue < $1.rawValue })
let upperIndex: Int?
if forward {
#if swift(>=4.2)
upperIndex = sortedPositions.firstIndex(where: { posY < positionY(for: $0) })
#else
upperIndex = sortedPositions.index(where: { posY < positionY(for: $0) })
#endif
} else {
#if swift(>=4.2)
upperIndex = sortedPositions.firstIndex(where: { posY <= positionY(for: $0) })
#else
upperIndex = sortedPositions.index(where: { posY <= positionY(for: $0) })
#endif
}
switch upperIndex {
case 0:
return LayoutSegment(lower: nil, upper: sortedPositions.first)
case let upperIndex?:
return LayoutSegment(lower: sortedPositions[upperIndex - 1], upper: sortedPositions[upperIndex])
default:
return LayoutSegment(lower: sortedPositions[sortedPositions.endIndex - 1], upper: nil)
}
}
}
@@ -15,7 +15,7 @@ public class FloatingPanelSurfaceView: UIView {
public let grabberHandle: GrabberHandleView = GrabberHandleView()
/// Offset of the grabber handle from the top
public var grabberTopPadding: CGFloat = 6.0 { didSet {
@objc dynamic public var grabberTopPadding: CGFloat = 6.0 { didSet {
setNeedsUpdateConstraints()
} }
@@ -25,17 +25,19 @@ public class FloatingPanelSurfaceView: UIView {
}
/// Grabber view width and height
public var grabberHandleWidth: CGFloat = 36.0 { didSet {
@objc dynamic public var grabberHandleWidth: CGFloat = 36.0 { didSet {
setNeedsUpdateConstraints()
} }
public var grabberHandleHeight: CGFloat = 5.0 { didSet {
@objc dynamic public var grabberHandleHeight: CGFloat = 5.0 { didSet {
setNeedsUpdateConstraints()
} }
/// A root view of a content view controller
public weak var contentView: UIView!
/// The content insets specifying the insets around the content view.
///
/// - important: Currently the `bottom` inset is ignored.
public var contentInsets: UIEdgeInsets = .zero {
didSet {
// Needs update constraints
@@ -46,7 +48,7 @@ public class FloatingPanelSurfaceView: UIView {
private var color: UIColor? = .white { didSet { setNeedsLayout() } }
var bottomOverflow: CGFloat = 0.0 // Must not call setNeedsLayout()
public override var backgroundColor: UIColor? {
@objc dynamic public override var backgroundColor: UIColor? {
get { return color }
set { color = newValue }
}
@@ -55,34 +57,34 @@ public class FloatingPanelSurfaceView: UIView {
///
/// `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 {
@objc dynamic public var cornerRadius: CGFloat {
set { containerView.layer.cornerRadius = newValue; setNeedsLayout() }
get { return containerView.layer.cornerRadius }
}
/// A Boolean indicating whether the surface shadow is displayed.
public var shadowHidden: Bool = false { didSet { setNeedsLayout() } }
@objc dynamic public var shadowHidden: Bool = false { didSet { setNeedsLayout() } }
/// The color of the surface shadow.
public var shadowColor: UIColor = .black { didSet { setNeedsLayout() } }
@objc dynamic public var shadowColor: UIColor = .black { didSet { setNeedsLayout() } }
/// The offset (in points) of the surface shadow.
public var shadowOffset: CGSize = CGSize(width: 0.0, height: 1.0) { didSet { setNeedsLayout() } }
@objc dynamic public var shadowOffset: CGSize = CGSize(width: 0.0, height: 1.0) { didSet { setNeedsLayout() } }
/// The opacity of the surface shadow.
public var shadowOpacity: Float = 0.2 { didSet { setNeedsLayout() } }
@objc dynamic public var shadowOpacity: Float = 0.2 { didSet { setNeedsLayout() } }
/// The blur radius (in points) used to render the surface shadow.
public var shadowRadius: CGFloat = 3 { didSet { setNeedsLayout() } }
@objc dynamic public var shadowRadius: CGFloat = 3 { didSet { setNeedsLayout() } }
/// The width of the surface border.
public var borderColor: UIColor? { didSet { setNeedsLayout() } }
@objc dynamic public var borderColor: UIColor? { didSet { setNeedsLayout() } }
/// The color of the surface border.
public var borderWidth: CGFloat = 0.0 { didSet { setNeedsLayout() } }
@objc dynamic public var borderWidth: CGFloat = 0.0 { didSet { setNeedsLayout() } }
/// The margins to use when laying out the container view wrapping content.
public var containerMargins: UIEdgeInsets = .zero { didSet {
/// Offset of the container view from the top
@objc dynamic public var containerTopInset: CGFloat = 0.0 { didSet {
setNeedsUpdateConstraints()
} }
@@ -97,11 +99,9 @@ public class FloatingPanelSurfaceView: UIView {
@available(*, unavailable, renamed: "containerView")
public var backgroundView: UIView!
private lazy var containerViewTopConstraint = containerView.topAnchor.constraint(equalTo: topAnchor, constant: containerMargins.top)
private lazy var containerViewHeightConstraint = containerView.heightAnchor.constraint(equalTo: heightAnchor, multiplier: 1.0)
private lazy var containerViewLeftConstraint = containerView.leftAnchor.constraint(equalTo: leftAnchor, constant: 0.0)
private lazy var containerViewRightConstraint = containerView.rightAnchor.constraint(equalTo: rightAnchor, constant: 0.0)
private lazy var containerViewTopInsetConstraint: NSLayoutConstraint = containerView.topAnchor.constraint(equalTo: topAnchor, constant: containerTopInset)
private lazy var containerViewHeightConstraint: NSLayoutConstraint = containerView.heightAnchor.constraint(equalTo: heightAnchor, multiplier: 1.0)
/// The content view top constraint
private var contentViewTopConstraint: NSLayoutConstraint?
/// The content view left constraint
@@ -111,11 +111,9 @@ public class FloatingPanelSurfaceView: UIView {
/// The content height constraint
private var contentViewHeightConstraint: NSLayoutConstraint?
private lazy var grabberHandleWidthConstraint = grabberHandle.widthAnchor.constraint(equalToConstant: grabberHandleWidth)
private lazy var grabberHandleHeightConstraint = grabberHandle.heightAnchor.constraint(equalToConstant: grabberHandleHeight)
private lazy var grabberHandleTopConstraint = grabberHandle.topAnchor.constraint(equalTo: topAnchor, constant: grabberTopPadding)
public override class var requiresConstraintBasedLayout: Bool { return true }
private lazy var grabberHandleWidthConstraint: NSLayoutConstraint = grabberHandle.widthAnchor.constraint(equalToConstant: grabberHandleWidth)
private lazy var grabberHandleHeightConstraint: NSLayoutConstraint = grabberHandle.heightAnchor.constraint(equalToConstant: grabberHandleHeight)
private lazy var grabberHandleTopConstraint: NSLayoutConstraint = grabberHandle.topAnchor.constraint(equalTo: topAnchor, constant: grabberTopPadding)
override init(frame: CGRect) {
super.init(frame: frame)
@@ -134,9 +132,9 @@ public class FloatingPanelSurfaceView: UIView {
addSubview(containerView)
containerView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
containerViewTopConstraint,
containerViewLeftConstraint,
containerViewRightConstraint,
containerViewTopInsetConstraint,
containerView.leftAnchor.constraint(equalTo: leftAnchor, constant: 0.0),
containerView.rightAnchor.constraint(equalTo: rightAnchor, constant: 0.0),
containerViewHeightConstraint,
])
@@ -151,21 +149,18 @@ public class FloatingPanelSurfaceView: UIView {
}
public override func updateConstraints() {
containerViewTopConstraint.constant = containerMargins.top
containerViewLeftConstraint.constant = containerMargins.left
containerViewRightConstraint.constant = -containerMargins.right
containerViewHeightConstraint.constant = (containerMargins.bottom == 0) ? bottomOverflow : -(containerMargins.top + containerMargins.bottom)
super.updateConstraints()
containerViewTopInsetConstraint.constant = containerTopInset
containerViewHeightConstraint.constant = bottomOverflow
contentViewTopConstraint?.constant = containerMargins.top + contentInsets.top
contentViewLeftConstraint?.constant = containerMargins.left + contentInsets.left
contentViewRightConstraint?.constant = containerMargins.right + contentInsets.right
contentViewHeightConstraint?.constant = -(containerMargins.top + containerMargins.bottom + contentInsets.top + contentInsets.bottom)
contentViewTopConstraint?.constant = contentInsets.top
contentViewLeftConstraint?.constant = contentInsets.left
contentViewRightConstraint?.constant = contentInsets.right
contentViewHeightConstraint?.constant = -containerTopInset
grabberHandleTopConstraint.constant = grabberTopPadding
grabberHandleWidthConstraint.constant = grabberHandleWidth
grabberHandleHeightConstraint.constant = grabberHandleHeight
super.updateConstraints()
}
public override func layoutSubviews() {
@@ -200,7 +195,6 @@ public class FloatingPanelSurfaceView: UIView {
return
}
containerView.layer.masksToBounds = true
guard containerMargins.bottom == 0 else { return }
if #available(iOS 11, *) {
// Don't use `contentView.clipToBounds` because it prevents content view from expanding the height of a subview of it
// for the bottom overflow like Auto Layout settings of UIVisualEffectView in Main.storyboard of Example/Maps.
@@ -223,11 +217,10 @@ public class FloatingPanelSurfaceView: UIView {
/* contentView.frame = bounds */ // MUST NOT: Because the top safe area inset of a content VC will be incorrect.
contentView.translatesAutoresizingMaskIntoConstraints = false
let topConstraint = contentView.topAnchor.constraint(equalTo: topAnchor, constant: containerMargins.top + contentInsets.top)
let leftConstraint = contentView.leftAnchor.constraint(equalTo: leftAnchor, constant: containerMargins.left + contentInsets.left)
let rightConstraint = rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: containerMargins.right + contentInsets.right)
let heightPadding = containerMargins.top + containerMargins.bottom + contentInsets.top + contentInsets.bottom
let heightConstraint = contentView.heightAnchor.constraint(equalTo: heightAnchor, constant: -heightPadding)
let topConstraint = contentView.topAnchor.constraint(equalTo: topAnchor, constant: contentInsets.top)
let leftConstraint = contentView.leftAnchor.constraint(equalTo: leftAnchor, constant: contentInsets.left)
let rightConstraint = rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: contentInsets.right)
let heightConstraint = contentView.heightAnchor.constraint(equalTo: heightAnchor, constant: -containerTopInset)
NSLayoutConstraint.activate([
topConstraint,
leftConstraint,
@@ -59,7 +59,9 @@ class FloatingPanelPresentationController: UIPresentationController {
// Forward touch events to the presenting view controller
(fpc.view as? FloatingPanelPassThroughView)?.eventForwardingView = presentingViewController.view
fpc.backdropView.dismissalTapGestureRecognizer.isEnabled = true
// Set tap-to-dismiss in the backdrop view
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleBackdrop(tapGesture:)))
fpc.backdropView.addGestureRecognizer(tapGesture)
}
@objc func handleBackdrop(tapGesture: UITapGestureRecognizer) {
+3 -1
View File
@@ -7,7 +7,9 @@ import UIKit
public class GrabberHandleView: UIView {
public var barColor = UIColor(displayP3Red: 0.76, green: 0.77, blue: 0.76, alpha: 1.0) { didSet { backgroundColor = barColor } }
@objc dynamic public var barColor = UIColor(displayP3Red: 0.76, green: 0.77, blue: 0.76, alpha: 1.0) {
didSet { backgroundColor = barColor }
}
required public init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
+1 -1
View File
@@ -15,7 +15,7 @@
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>1.7.1</string>
<string>1.5.1</string>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
</dict>
+1 -34
View File
@@ -21,7 +21,7 @@ class CustomLayoutGuide: LayoutGuideProvider {
}
extension UIViewController {
@objc var layoutInsets: UIEdgeInsets {
var layoutInsets: UIEdgeInsets {
if #available(iOS 11.0, *) {
return view.safeAreaInsets
} else {
@@ -60,10 +60,6 @@ extension UIView {
return self
}
}
var presentationFrame: CGRect {
return layer.presentation()?.frame ?? frame
}
}
extension UIView {
@@ -75,12 +71,6 @@ extension UIView {
func enableAutoLayout() {
translatesAutoresizingMaskIntoConstraints = false
}
static func performWithLinear(startTime: Double = 0.0, relativeDuration: Double = 1.0, _ animations: @escaping (() -> Void)) {
UIView.animateKeyframes(withDuration: 0.0, delay: 0.0, options: [.calculationModeCubic], animations: {
UIView.addKeyframe(withRelativeStartTime: startTime, relativeDuration: relativeDuration, animations: animations)
}, completion: nil)
}
}
#if __FP_LOG
@@ -117,9 +107,6 @@ extension UIScrollView {
var contentOffsetZero: CGPoint {
return CGPoint(x: 0.0, y: 0.0 - contentInset.top)
}
var isLocked: Bool {
return !showsVerticalScrollIndicator && !bounces && isDirectionalLockEnabled
}
}
extension UISpringTimingParameters {
@@ -137,23 +124,3 @@ extension CGPoint {
y: CGFloat.nan)
}
}
extension UITraitCollection {
func shouldUpdateLayout(from previous: UITraitCollection) -> Bool {
return previous.horizontalSizeClass != horizontalSizeClass
|| previous.verticalSizeClass != verticalSizeClass
|| previous.preferredContentSizeCategory != preferredContentSizeCategory
|| previous.layoutDirection != layoutDirection
}
}
extension NSLayoutConstraint {
static func activate(constraint: NSLayoutConstraint?) {
guard let constraint = constraint else { return }
self.activate([constraint])
}
static func deactivate(constraint: NSLayoutConstraint?) {
guard let constraint = constraint else { return }
self.deactivate([constraint])
}
}
@@ -7,7 +7,9 @@ import XCTest
@testable import FloatingPanel
class FloatingPanelControllerTests: XCTestCase {
override func setUp() {}
override func tearDown() {}
func test_warningRetainCycle() {
@@ -26,125 +28,24 @@ class FloatingPanelControllerTests: XCTestCase {
func test_addPanel() {
guard let rootVC = UIApplication.shared.keyWindow?.rootViewController else { fatalError() }
let fpc = FloatingPanelController()
fpc.addPanel(toParent: rootVC)
waitRunLoop(secs: 1.0)
XCTAssert(fpc.surfaceView.frame.minY == (fpc.view.bounds.height - fpc.layoutInsets.bottom) - fpc.layout.insetFor(position: .half)!)
fpc.move(to: .tip, animated: false)
XCTAssert(fpc.surfaceView.frame.minY == (fpc.view.bounds.height - fpc.layoutInsets.bottom) - fpc.layout.insetFor(position: .tip)!)
}
@available(iOS 12.0, *)
func test_updateLayout_willTransition() {
class MyDelegate: FloatingPanelControllerDelegate {
func floatingPanel(_ vc: FloatingPanelController, layoutFor newCollection: UITraitCollection) -> FloatingPanelLayout? {
if newCollection.userInterfaceStyle == .dark {
XCTFail()
}
return nil
}
}
let myDelegate = MyDelegate()
let fpc = FloatingPanelController(delegate: myDelegate)
let traitCollection = UITraitCollection(traitsFrom: [fpc.traitCollection,
UITraitCollection(userInterfaceStyle: .dark)])
XCTAssertEqual(traitCollection.userInterfaceStyle, .dark)
fpc.prepare(for: traitCollection)
}
func test_moveTo() {
let delegate = FloatingPanelTestDelegate()
let fpc = FloatingPanelController(delegate: delegate)
XCTAssertEqual(delegate.position, .hidden)
fpc.showForTest()
XCTAssertEqual(delegate.position, .half)
fpc.hide()
XCTAssertEqual(delegate.position, .hidden)
fpc.move(to: .full, animated: false)
XCTAssertEqual(fpc.position, .full)
XCTAssertEqual(delegate.position, .full)
XCTAssertEqual(fpc.surfaceView.frame.minY, fpc.originYOfSurface(for: .full))
fpc.move(to: .half, animated: false)
XCTAssertEqual(fpc.position, .half)
XCTAssertEqual(delegate.position, .half)
XCTAssertEqual(fpc.surfaceView.frame.minY, fpc.originYOfSurface(for: .half))
fpc.move(to: .tip, animated: false)
XCTAssertEqual(fpc.position, .tip)
XCTAssertEqual(delegate.position, .tip)
XCTAssertEqual(fpc.surfaceView.frame.minY, fpc.originYOfSurface(for: .tip))
fpc.move(to: .hidden, animated: false)
XCTAssertEqual(fpc.position, .hidden)
XCTAssertEqual(delegate.position, .hidden)
XCTAssertEqual(fpc.surfaceView.frame.minY, fpc.originYOfSurface(for: .hidden))
fpc.move(to: .full, animated: true)
XCTAssertEqual(fpc.position, .full)
XCTAssertEqual(delegate.position, .full)
XCTAssertEqual(fpc.surfaceView.frame.minY, fpc.originYOfSurface(for: .full))
fpc.move(to: .half, animated: true)
XCTAssertEqual(fpc.position, .half)
XCTAssertEqual(delegate.position, .half)
XCTAssertEqual(fpc.surfaceView.frame.minY, fpc.originYOfSurface(for: .half))
fpc.move(to: .tip, animated: true)
XCTAssertEqual(fpc.position, .tip)
XCTAssertEqual(delegate.position, .tip)
XCTAssertEqual(fpc.surfaceView.frame.minY, fpc.originYOfSurface(for: .tip))
fpc.move(to: .hidden, animated: true)
XCTAssertEqual(fpc.position, .hidden)
XCTAssertEqual(delegate.position, .hidden)
XCTAssertEqual(fpc.surfaceView.frame.minY, fpc.originYOfSurface(for: .hidden))
}
func test_originSurfaceY() {
let fpc = FloatingPanelController(delegate: nil)
fpc.loadViewIfNeeded()
fpc.view.frame = CGRect(x: 0, y: 0, width: 375, height: 667)
fpc.show(animated: false, completion: nil)
fpc.move(to: .full, animated: false)
XCTAssertEqual(fpc.surfaceView.frame.minY, fpc.originYOfSurface(for: .full))
fpc.move(to: .half, animated: false)
XCTAssertEqual(fpc.surfaceView.frame.minY, fpc.originYOfSurface(for: .half))
fpc.move(to: .tip, animated: false)
XCTAssertEqual(fpc.surfaceView.frame.minY, fpc.originYOfSurface(for: .tip))
fpc.move(to: .hidden, animated: false)
XCTAssertEqual(fpc.surfaceView.frame.minY, fpc.originYOfSurface(for: .hidden))
}
func test_contentMode() {
let fpc = FloatingPanelController(delegate: nil)
fpc.loadViewIfNeeded()
fpc.view.frame = CGRect(x: 0, y: 0, width: 375, height: 667)
fpc.show(animated: false, completion: nil)
fpc.contentMode = .static
fpc.move(to: .full, animated: false)
XCTAssertEqual(fpc.surfaceView.frame.height, fpc.view.bounds.height - fpc.originYOfSurface(for: .full))
fpc.move(to: .half, animated: false)
XCTAssertEqual(fpc.surfaceView.frame.height, fpc.view.bounds.height - fpc.originYOfSurface(for: .full))
fpc.move(to: .tip, animated: false)
XCTAssertEqual(fpc.surfaceView.frame.height, fpc.view.bounds.height - fpc.originYOfSurface(for: .full))
fpc.contentMode = .fitToBounds
fpc.move(to: .full, animated: false)
XCTAssertEqual(fpc.surfaceView.frame.height, fpc.view.bounds.height - fpc.originYOfSurface(for: .full))
fpc.move(to: .half, animated: false)
XCTAssertEqual(fpc.surfaceView.frame.height, fpc.view.bounds.height - fpc.originYOfSurface(for: .half))
fpc.move(to: .tip, animated: false)
XCTAssertEqual(fpc.surfaceView.frame.height, fpc.view.bounds.height - fpc.originYOfSurface(for: .tip))
waitRunLoop(secs: 1.0)
XCTAssert(fpc.surfaceView.frame.minY == (fpc.view.bounds.height - fpc.layoutInsets.bottom) - fpc.layout.insetFor(position: .tip)!)
}
}
private class MyZombieViewController: UIViewController, FloatingPanelLayout, FloatingPanelBehavior, FloatingPanelControllerDelegate {
func waitRunLoop(secs: TimeInterval = 0) {
RunLoop.main.run(until: Date(timeIntervalSinceNow: secs))
}
class MyZombieViewController: UIViewController, FloatingPanelLayout, FloatingPanelBehavior, FloatingPanelControllerDelegate {
var fpc: FloatingPanelController?
override func viewDidLoad() {
fpc = FloatingPanelController(delegate: self)
@@ -1,254 +0,0 @@
//
// Created by Shin Yamamoto on 2019/06/27.
// Copyright © 2019 scenee. All rights reserved.
//
import XCTest
@testable import FloatingPanel
class FloatingPanelLayoutTests: XCTestCase {
var fpc: FloatingPanelController!
override func setUp() {
fpc = FloatingPanelController(delegate: nil)
fpc.loadViewIfNeeded()
fpc.view.frame = CGRect(x: 0, y: 0, width: 375, height: 667)
}
override func tearDown() {}
func test_layoutAdapter_topAndBottomMostState() {
XCTAssertEqual(fpc.floatingPanel.layoutAdapter.topMostState, .full)
XCTAssertEqual(fpc.floatingPanel.layoutAdapter.bottomMostState, .tip)
class FloatingPanelLayoutWithHidden: FloatingPanelLayout {
func insetFor(position: FloatingPanelPosition) -> CGFloat? { return nil }
let initialPosition: FloatingPanelPosition = .hidden
let supportedPositions: Set<FloatingPanelPosition> = [.hidden, .half, .full]
}
class FloatingPanelLayout2Positions: FloatingPanelLayout {
func insetFor(position: FloatingPanelPosition) -> CGFloat? { return nil }
let initialPosition: FloatingPanelPosition = .tip
let supportedPositions: Set<FloatingPanelPosition> = [.tip, .half]
}
let delegate = FloatingPanelTestDelegate()
delegate.layout = FloatingPanelLayoutWithHidden()
fpc.delegate = delegate
XCTAssertEqual(fpc.floatingPanel.layoutAdapter.topMostState, .full)
XCTAssertEqual(fpc.floatingPanel.layoutAdapter.bottomMostState, .hidden)
delegate.layout = FloatingPanelLayout2Positions()
fpc.delegate = delegate
XCTAssertEqual(fpc.floatingPanel.layoutAdapter.topMostState, .half)
XCTAssertEqual(fpc.floatingPanel.layoutAdapter.bottomMostState, .tip)
}
func test_layoutSegment_3position() {
class FloatingPanelLayout3Positions: FloatingPanelTestLayout {
let initialPosition: FloatingPanelPosition = .half
let supportedPositions: Set<FloatingPanelPosition> = [.tip, .half, .full]
}
let delegate = FloatingPanelTestDelegate()
delegate.layout = FloatingPanelLayout3Positions()
fpc.delegate = delegate
let fullPos = fpc.originYOfSurface(for: .full)
let halfPos = fpc.originYOfSurface(for: .half)
let tipPos = fpc.originYOfSurface(for: .tip)
let minPos = CGFloat.leastNormalMagnitude
let maxPos = CGFloat.greatestFiniteMagnitude
assertLayoutSegment(fpc.floatingPanel, with: [
(#line, pos: minPos, forwardY: true, lower: nil, upper: .full),
(#line, pos: minPos, forwardY: false, lower: nil, upper: .full),
(#line, pos: fullPos, forwardY: true, lower: .full, upper: .half),
(#line, pos: fullPos, forwardY: false, lower: nil, upper: .full),
(#line, pos: halfPos, forwardY: true, lower: .half, upper: .tip),
(#line, pos: halfPos, forwardY: false, lower: .full, upper: .half),
(#line, pos: tipPos, forwardY: true, lower: .tip, upper: nil),
(#line, pos: tipPos, forwardY: false, lower: .half, upper: .tip),
(#line, pos: maxPos, forwardY: true, lower: .tip, upper: nil),
(#line, pos: maxPos, forwardY: false, lower: .tip, upper: nil),
])
}
func test_layoutSegment_2positions() {
class FloatingPanelLayout2Positions: FloatingPanelTestLayout {
let initialPosition: FloatingPanelPosition = .half
let supportedPositions: Set<FloatingPanelPosition> = [.half, .full]
}
let delegate = FloatingPanelTestDelegate()
delegate.layout = FloatingPanelLayout2Positions()
fpc.delegate = delegate
let fullPos = fpc.originYOfSurface(for: .full)
let halfPos = fpc.originYOfSurface(for: .half)
let minPos = CGFloat.leastNormalMagnitude
let maxPos = CGFloat.greatestFiniteMagnitude
assertLayoutSegment(fpc.floatingPanel, with: [
(#line, pos: minPos, forwardY: true, lower: nil, upper: .full),
(#line, pos: minPos, forwardY: false, lower: nil, upper: .full),
(#line, pos: fullPos, forwardY: true, lower: .full, upper: .half),
(#line, pos: fullPos, forwardY: false, lower: nil, upper: .full),
(#line, pos: halfPos, forwardY: true, lower: .half, upper: nil),
(#line, pos: halfPos, forwardY: false, lower: .full, upper: .half),
(#line, pos: maxPos, forwardY: true, lower: .half, upper: nil),
(#line, pos: maxPos, forwardY: false, lower: .half, upper: nil),
])
}
func test_layoutSegment_1positions() {
class FloatingPanelLayout1Positions: FloatingPanelTestLayout {
let initialPosition: FloatingPanelPosition = .full
let supportedPositions: Set<FloatingPanelPosition> = [.full]
}
let delegate = FloatingPanelTestDelegate()
delegate.layout = FloatingPanelLayout1Positions()
fpc.delegate = delegate
let fullPos = fpc.originYOfSurface(for: .full)
let minPos = CGFloat.leastNormalMagnitude
let maxPos = CGFloat.greatestFiniteMagnitude
assertLayoutSegment(fpc.floatingPanel, with: [
(#line, pos: minPos, forwardY: true, lower: nil, upper: .full),
(#line, pos: minPos, forwardY: false, lower: nil, upper: .full),
(#line, pos: fullPos, forwardY: true, lower: .full, upper: nil),
(#line, pos: fullPos, forwardY: false, lower: nil, upper: .full),
(#line, pos: maxPos, forwardY: true, lower: .full, upper: nil),
(#line, pos: maxPos, forwardY: false, lower: .full, upper: nil),
])
}
func test_updateInteractiveTopConstraint() {
fpc.showForTest()
fpc.move(to: .full, animated: false)
fpc.floatingPanel.layoutAdapter.startInteraction(at: fpc.position)
fpc.floatingPanel.layoutAdapter.startInteraction(at: fpc.position) // Should be ignore
let fullPos = fpc.originYOfSurface(for: .full)
let tipPos = fpc.originYOfSurface(for: .tip)
var pre: CGFloat
var next: CGFloat
pre = fpc.surfaceView.frame.minY
fpc.floatingPanel.layoutAdapter.updateInteractiveTopConstraint(diff: -100.0, allowsTopBuffer: false, with: fpc.behavior)
next = fpc.surfaceView.frame.minY
XCTAssertEqual(next, pre)
fpc.floatingPanel.layoutAdapter.updateInteractiveTopConstraint(diff: -100.0, allowsTopBuffer: true, with: fpc.behavior)
next = fpc.surfaceView.frame.minY
XCTAssertEqual(next, fullPos - fpc.layout.topInteractionBuffer)
fpc.floatingPanel.layoutAdapter.updateInteractiveTopConstraint(diff: 100.0, allowsTopBuffer: true, with: fpc.behavior)
next = fpc.surfaceView.frame.minY
XCTAssertEqual(next, fullPos + 100.0)
fpc.floatingPanel.layoutAdapter.updateInteractiveTopConstraint(diff: tipPos - fullPos, allowsTopBuffer: true, with: fpc.behavior)
next = fpc.surfaceView.frame.minY
XCTAssertEqual(next, tipPos)
fpc.floatingPanel.layoutAdapter.updateInteractiveTopConstraint(diff: tipPos - fullPos + 100.0, allowsTopBuffer: true, with: fpc.behavior)
next = fpc.surfaceView.frame.minY
XCTAssertEqual(next, tipPos + fpc.layout.bottomInteractionBuffer)
fpc.floatingPanel.layoutAdapter.endInteraction(at: fpc.position)
}
func test_updateInteractiveTopConstraintWithHidden() {
class FloatingPanelLayout2Positions: FloatingPanelTestLayout {
let initialPosition: FloatingPanelPosition = .hidden
let supportedPositions: Set<FloatingPanelPosition> = [.hidden, .full]
}
let delegate = FloatingPanelTestDelegate()
delegate.layout = FloatingPanelLayout2Positions()
fpc.delegate = delegate
fpc.showForTest()
fpc.move(to: .full, animated: false)
fpc.floatingPanel.layoutAdapter.startInteraction(at: fpc.position)
let fullPos = fpc.originYOfSurface(for: .full)
let hiddenPos = fpc.originYOfSurface(for: .hidden)
var pre: CGFloat
var next: CGFloat
pre = fpc.surfaceView.frame.minY
fpc.floatingPanel.layoutAdapter.updateInteractiveTopConstraint(diff: -100.0, allowsTopBuffer: false, with: fpc.behavior)
next = fpc.surfaceView.frame.minY
XCTAssertEqual(next, pre)
fpc.floatingPanel.layoutAdapter.updateInteractiveTopConstraint(diff: -100.0, allowsTopBuffer: true, with: fpc.behavior)
next = fpc.surfaceView.frame.minY
XCTAssertEqual(next, fullPos - fpc.layout.topInteractionBuffer)
fpc.floatingPanel.layoutAdapter.updateInteractiveTopConstraint(diff: hiddenPos - fullPos + 100.0, allowsTopBuffer: true, with: fpc.behavior)
next = fpc.surfaceView.frame.minY
XCTAssertEqual(next, hiddenPos + fpc.layout.bottomInteractionBuffer)
fpc.floatingPanel.layoutAdapter.endInteraction(at: fpc.position)
}
func test_positionReference() {
fpc = CustomSafeAreaFloatingPanelController()
fpc.loadViewIfNeeded()
fpc.view.frame = CGRect(x: 0, y: 0, width: 375, height: 667)
class MyFloatingPanelFullLayout: FloatingPanelTestLayout {
var initialPosition: FloatingPanelPosition = .half
var positionReference: FloatingPanelLayoutReference {
return .fromSuperview
}
}
class MyFloatingPanelSafeAreaLayout: FloatingPanelTestLayout {
var initialPosition: FloatingPanelPosition = .half
var positionReference: FloatingPanelLayoutReference {
return .fromSafeArea
}
}
let fullLayout = MyFloatingPanelFullLayout()
let delegate = FloatingPanelTestDelegate()
delegate.layout = fullLayout
fpc.delegate = delegate
fpc.showForTest()
XCTAssertEqual(fpc.layout.positionReference, .fromSuperview)
XCTAssertEqual(fpc.originYOfSurface(for: .full), fullLayout.insetFor(position: .full))
XCTAssertEqual(fpc.originYOfSurface(for: .half), fpc.view!.frame.height - fullLayout.insetFor(position: .half)!)
XCTAssertEqual(fpc.originYOfSurface(for: .tip), fpc.view!.frame.height - fullLayout.insetFor(position: .tip)!)
let safeAreaLayout = MyFloatingPanelSafeAreaLayout()
delegate.layout = safeAreaLayout
fpc.delegate = delegate
XCTAssertEqual(fpc.layout.positionReference, .fromSafeArea)
XCTAssertEqual(fpc.originYOfSurface(for: .full),
fullLayout.insetFor(position: .full)! + fpc.layoutInsets.top)
XCTAssertEqual(fpc.originYOfSurface(for: .half),
fpc.view!.frame.height - (fullLayout.insetFor(position: .half)! + fpc.layoutInsets.bottom))
XCTAssertEqual(fpc.originYOfSurface(for: .tip),
fpc.view!.frame.height - (fullLayout.insetFor(position: .tip)! + fpc.layoutInsets.bottom))
}
}
private typealias LayoutSegmentTestParameter = (UInt, pos: CGFloat, forwardY: Bool, lower: FloatingPanelPosition?, upper: FloatingPanelPosition?)
private func assertLayoutSegment(_ floatingPanel: FloatingPanelCore, with params: [LayoutSegmentTestParameter]) {
params.forEach { (line, pos, forwardY, lowr, upper) in
let segument = floatingPanel.layoutAdapter.segument(at: pos, forward: forwardY)
XCTAssertEqual(segument.lower, lowr, line: line)
XCTAssertEqual(segument.upper, upper, line: line)
}
}
private class CustomSafeAreaFloatingPanelController: FloatingPanelController { }
extension CustomSafeAreaFloatingPanelController {
override var layoutInsets: UIEdgeInsets {
return UIEdgeInsets(top: 64.0, left: 0.0, bottom: 0.0, right: 34.0)
}
}
@@ -1,27 +0,0 @@
//
// Created by Shin Yamamoto on 2019/07/05.
// Copyright © 2019 scenee. All rights reserved.
//
import XCTest
@testable import FloatingPanel
class FloatingPanelPositionTests: XCTestCase {
override func setUp() { }
override func tearDown() { }
func test_nextAndPre() {
var positions: [FloatingPanelPosition]
positions = [.full, .half, .tip, .hidden]
XCTAssertEqual(FloatingPanelPosition.full.next(in: positions), .half)
XCTAssertEqual(FloatingPanelPosition.full.pre(in: positions), .full)
XCTAssertEqual(FloatingPanelPosition.hidden.next(in: positions), .hidden)
XCTAssertEqual(FloatingPanelPosition.hidden.pre(in: positions), .tip)
positions = [.full, .hidden]
XCTAssertEqual(FloatingPanelPosition.full.next(in: positions), .hidden)
XCTAssertEqual(FloatingPanelPosition.full.pre(in: positions), .full)
XCTAssertEqual(FloatingPanelPosition.hidden.next(in: positions), .hidden)
XCTAssertEqual(FloatingPanelPosition.hidden.pre(in: positions), .full)
}
}
@@ -1,118 +0,0 @@
//
// Created by Shin Yamamoto on 2019/05/23.
// Copyright © 2019 Shin Yamamoto. All rights reserved.
//
import XCTest
@testable import FloatingPanel
class FloatingPanelSurfaceViewTests: XCTestCase {
override func setUp() {}
override func tearDown() {}
func test_surfaceView() {
let surface = FloatingPanelSurfaceView(frame: CGRect(x: 0.0, y: 0.0, width: 320.0, height: 480.0))
XCTAssertTrue(FloatingPanelSurfaceView.requiresConstraintBasedLayout)
XCTAssert(surface.contentView == nil)
surface.layoutIfNeeded()
XCTAssert(surface.grabberHandle.frame.minY == 6.0)
XCTAssert(surface.grabberHandle.frame.width == surface.grabberHandleWidth)
XCTAssert(surface.grabberHandle.frame.height == surface.grabberHandleHeight)
surface.backgroundColor = .red
surface.layoutIfNeeded()
XCTAssert(surface.backgroundColor == surface.containerView.backgroundColor)
}
func test_surfaceView_grabberHandle() {
let surface = FloatingPanelSurfaceView(frame: CGRect(x: 0.0, y: 0.0, width: 320.0, height: 480.0))
XCTAssert(surface.contentView == nil)
surface.layoutIfNeeded()
XCTAssert(surface.grabberHandle.frame.minY == 6.0)
XCTAssert(surface.grabberHandle.frame.width == surface.grabberHandleWidth)
XCTAssert(surface.grabberHandle.frame.height == surface.grabberHandleHeight)
surface.grabberHandleWidth = 44.0
surface.grabberHandleHeight = 12.0
surface.setNeedsLayout()
surface.layoutIfNeeded()
XCTAssert(surface.grabberHandle.frame.width == surface.grabberHandleWidth, "\(surface.grabberHandle.frame.width) == \(surface.grabberHandleWidth)")
XCTAssert(surface.grabberHandle.frame.height == surface.grabberHandleHeight, "\(surface.grabberHandle.frame.height) == \(surface.grabberHandleHeight)")
}
func test_surfaceView_containerMargins() {
let surface = FloatingPanelSurfaceView(frame: CGRect(x: 0.0, y: 0.0, width: 320.0, height: 480.0))
surface.layoutIfNeeded()
XCTAssertEqual(surface.containerView.frame, surface.bounds)
surface.containerMargins = UIEdgeInsets(top: 16.0, left: 16.0, bottom: 16.0, right: 16.0)
surface.setNeedsLayout()
surface.layoutIfNeeded()
XCTAssertEqual(surface.containerView.frame, surface.bounds.inset(by: surface.containerMargins))
}
func test_surfaceView_contentInsets() {
let surface = FloatingPanelSurfaceView(frame: CGRect(x: 0.0, y: 0.0, width: 320.0, height: 480.0))
let contentView = UIView()
surface.add(contentView: contentView)
surface.layoutIfNeeded()
XCTAssertEqual(surface.contentView.frame, surface.bounds)
surface.contentInsets = UIEdgeInsets(top: 16.0, left: 16.0, bottom: 16.0, right: 16.0)
surface.setNeedsLayout()
surface.layoutIfNeeded()
XCTAssertEqual(surface.contentView.frame, surface.bounds.inset(by: surface.contentInsets))
}
func test_surfaceView_containerMargins_and_contentInsets() {
let surface = FloatingPanelSurfaceView(frame: CGRect(x: 0.0, y: 0.0, width: 320.0, height: 480.0))
let contentView = UIView()
surface.add(contentView: contentView)
surface.layoutIfNeeded()
XCTAssertEqual(surface.contentView.frame, surface.bounds)
surface.containerMargins = UIEdgeInsets(top: 16.0, left: 16.0, bottom: 16.0, right: 16.0)
surface.contentInsets = UIEdgeInsets(top: 16.0, left: 16.0, bottom: 16.0, right: 16.0)
surface.setNeedsLayout()
surface.layoutIfNeeded()
XCTAssertEqual(surface.containerView.frame, surface.bounds.inset(by: surface.containerMargins))
XCTAssertEqual(surface.contentView.frame, surface.containerView.bounds.inset(by: surface.contentInsets))
}
func test_surfaceView_cornderRaduis() {
let surface = FloatingPanelSurfaceView(frame: CGRect(x: 0.0, y: 0.0, width: 320.0, height: 480.0))
XCTAssert(surface.cornerRadius == 0.0)
XCTAssert(surface.containerView.layer.masksToBounds == false)
surface.cornerRadius = 10.0
surface.layoutIfNeeded()
XCTAssert(surface.cornerRadius == 10.0)
XCTAssert(surface.containerView.layer.cornerRadius == 10.0)
XCTAssert(surface.containerView.layer.masksToBounds == true)
surface.containerView.layer.cornerRadius = 12.0
surface.layoutIfNeeded()
XCTAssert(surface.cornerRadius == 12.0)
XCTAssert(surface.containerView.layer.masksToBounds == true)
surface.cornerRadius = 0.0
surface.layoutIfNeeded()
XCTAssert(surface.cornerRadius == 0.0)
XCTAssert(surface.containerView.layer.cornerRadius == 0.0)
XCTAssert(surface.containerView.layer.masksToBounds == false)
surface.containerView.layer.cornerRadius = 12.0
surface.setNeedsLayout()
surface.layoutIfNeeded()
XCTAssert(surface.cornerRadius == 12.0)
XCTAssert(surface.containerView.layer.masksToBounds == true)
}
func test_surfaceView_border() {
let surface = FloatingPanelSurfaceView(frame: CGRect(x: 0.0, y: 0.0, width: 320.0, height: 480.0))
XCTAssert(surface.borderColor == nil)
XCTAssert(surface.borderWidth == 0.0)
surface.borderColor = .red
surface.borderWidth = 3.0
surface.layoutIfNeeded()
XCTAssert(surface.containerView.layer.borderColor == UIColor.red.cgColor)
XCTAssert(surface.containerView.layer.borderWidth == 3.0)
}
}
+2 -525
View File
@@ -4,534 +4,11 @@
//
import XCTest
@testable import FloatingPanel
class FloatingPanelTests: XCTestCase {
override func setUp() {}
override func tearDown() {}
func test_scrolllock() {
let fpc = FloatingPanelController()
let contentVC1 = UITableViewController(nibName: nil, bundle: nil)
XCTAssertEqual(contentVC1.tableView.showsVerticalScrollIndicator, true)
XCTAssertEqual(contentVC1.tableView.bounces, true)
fpc.set(contentViewController: contentVC1)
fpc.track(scrollView: contentVC1.tableView)
fpc.showForTest()
XCTAssertEqual(fpc.position, .half)
XCTAssertEqual(contentVC1.tableView.showsVerticalScrollIndicator, false)
XCTAssertEqual(contentVC1.tableView.bounces, false)
fpc.move(to: .full, animated: false)
XCTAssertEqual(contentVC1.tableView.showsVerticalScrollIndicator, true)
XCTAssertEqual(contentVC1.tableView.bounces, true)
fpc.move(to: .tip, animated: false)
XCTAssertEqual(contentVC1.tableView.showsVerticalScrollIndicator, false)
XCTAssertEqual(contentVC1.tableView.bounces, false)
let exp1 = expectation(description: "move to full with animation")
fpc.move(to: .full, animated: true) {
XCTAssertEqual(contentVC1.tableView.showsVerticalScrollIndicator, true)
XCTAssertEqual(contentVC1.tableView.bounces, true)
exp1.fulfill()
}
wait(for: [exp1], timeout: 1.0)
let exp2 = expectation(description: "move to tip with animation")
fpc.move(to: .tip, animated: false) {
XCTAssertEqual(contentVC1.tableView.showsVerticalScrollIndicator, false)
XCTAssertEqual(contentVC1.tableView.bounces, false)
exp2.fulfill()
}
wait(for: [exp2], timeout: 1.0)
// Reset the content vc
let contentVC2 = UITableViewController(nibName: nil, bundle: nil)
XCTAssertEqual(contentVC2.tableView.showsVerticalScrollIndicator, true)
XCTAssertEqual(contentVC2.tableView.bounces, true)
fpc.set(contentViewController: contentVC2)
fpc.track(scrollView: contentVC2.tableView)
fpc.show(animated: false, completion: nil)
XCTAssertEqual(fpc.position, .half)
XCTAssertEqual(contentVC2.tableView.showsVerticalScrollIndicator, false)
XCTAssertEqual(contentVC2.tableView.bounces, false)
}
func test_getBackdropAlpha_1positions() {
class FloatingPanelLayout1Positions: FloatingPanelTestLayout {
let initialPosition: FloatingPanelPosition = .full
let supportedPositions: Set<FloatingPanelPosition> = [.full]
}
let delegate = FloatingPanelTestDelegate()
delegate.layout = FloatingPanelLayout1Positions()
let fpc = FloatingPanelController(delegate: delegate)
fpc.showForTest()
let fullPos = fpc.originYOfSurface(for: .full)
XCTAssertEqual(fpc.floatingPanel.getBackdropAlpha(at: fullPos - 100.0, with: CGPoint(x: 0.0, y: -100.0)), 0.3)
XCTAssertEqual(fpc.floatingPanel.getBackdropAlpha(at: fullPos, with: CGPoint(x: 0.0, y: 0.0)), 0.3)
XCTAssertEqual(fpc.floatingPanel.getBackdropAlpha(at: fullPos + 100.0, with: CGPoint(x: 0.0, y: 100.0)), 0.3) // ok??
}
func test_getBackdropAlpha_2positions() {
class FloatingPanelLayout2Positions: FloatingPanelTestLayout {
let initialPosition: FloatingPanelPosition = .half
let supportedPositions: Set<FloatingPanelPosition> = [.half, .full]
}
let delegate = FloatingPanelTestDelegate()
delegate.layout = FloatingPanelLayout2Positions()
let fpc = FloatingPanelController(delegate: delegate)
fpc.showForTest()
let fullPos = fpc.originYOfSurface(for: .full)
let halfPos = fpc.originYOfSurface(for: .half)
let distance1 = abs(halfPos - fullPos)
fpc.move(to: .full, animated: false)
XCTAssertEqual(fpc.floatingPanel.getBackdropAlpha(at: fullPos, with: CGPoint(x: 0.0, y: 0.0)), 0.3)
XCTAssertEqual(fpc.floatingPanel.getBackdropAlpha(at: fullPos + distance1 * 0.5, with: CGPoint(x: 0.0, y: distance1 * 0.5)), 0.3 * 0.5)
XCTAssertEqual(fpc.floatingPanel.getBackdropAlpha(at: halfPos, with: CGPoint(x: 0.0, y: distance1)), 0.0)
fpc.move(to: .half, animated: false)
XCTAssertEqual(fpc.floatingPanel.getBackdropAlpha(at: halfPos, with: CGPoint(x: 0.0, y: 0.0)), 0.0)
XCTAssertEqual(fpc.floatingPanel.getBackdropAlpha(at: fullPos + distance1 * 0.5, with: CGPoint(x: 0.0, y: -0.5 * distance1)), 0.3 * 0.5)
XCTAssertEqual(fpc.floatingPanel.getBackdropAlpha(at: fullPos, with: CGPoint(x: 0.0, y: -1 * distance1)), 0.3)
}
func test_getBackdropAlpha_2positionsWithHidden() {
class FloatingPanelLayout2Positions: FloatingPanelTestLayout {
let initialPosition: FloatingPanelPosition = .hidden
let supportedPositions: Set<FloatingPanelPosition> = [.hidden, .full]
}
let delegate = FloatingPanelTestDelegate()
delegate.layout = FloatingPanelLayout2Positions()
let fpc = FloatingPanelController(delegate: delegate)
fpc.showForTest()
let fullPos = fpc.originYOfSurface(for: .full)
let hiddenPos = fpc.originYOfSurface(for: .hidden)
XCTAssertEqual(fpc.floatingPanel.getBackdropAlpha(at: fullPos - 100.0, with: CGPoint(x: 0.0, y: -100.0)), 0.3)
XCTAssertEqual(fpc.floatingPanel.getBackdropAlpha(at: fullPos, with: CGPoint(x: 0.0, y: 0.0)), 0.3)
XCTAssertEqual(fpc.floatingPanel.getBackdropAlpha(at: hiddenPos, with: CGPoint(x: 0.0, y: 100.0)), 0.0)
}
func test_getBackdropAlpha_3positions() {
let fpc = FloatingPanelController()
fpc.showForTest()
let fullPos = fpc.originYOfSurface(for: .full)
let halfPos = fpc.originYOfSurface(for: .half)
let tipPos = fpc.originYOfSurface(for: .tip)
let distance1 = abs(halfPos - fullPos)
let distance2 = abs(tipPos - halfPos)
fpc.move(to: .full, animated: false)
XCTAssertEqual(fpc.floatingPanel.getBackdropAlpha(at: fullPos, with: CGPoint(x: 0.0, y: 0.0)), 0.3)
XCTAssertEqual(fpc.floatingPanel.getBackdropAlpha(at: fullPos + distance1 * 0.5, with: CGPoint(x: 0.0, y: distance1 * 0.5)), 0.3 * 0.5)
XCTAssertEqual(fpc.floatingPanel.getBackdropAlpha(at: halfPos, with: CGPoint(x: 0.0, y: distance1)), 0.0)
fpc.move(to: .half, animated: false)
XCTAssertEqual(fpc.floatingPanel.getBackdropAlpha(at: halfPos, with: CGPoint(x: 0.0, y: 0.0)), 0.0)
XCTAssertEqual(fpc.floatingPanel.getBackdropAlpha(at: fullPos + distance1 * 0.5, with: CGPoint(x: 0.0, y: -0.5 * distance1)), 0.3 * 0.5)
XCTAssertEqual(fpc.floatingPanel.getBackdropAlpha(at: fullPos, with: CGPoint(x: 0.0, y: -1 * distance1)), 0.3)
fpc.move(to: .tip, animated: false)
XCTAssertEqual(fpc.floatingPanel.getBackdropAlpha(at: tipPos, with: CGPoint(x: 0.0, y: 0.0)), 0.0)
XCTAssertEqual(fpc.floatingPanel.getBackdropAlpha(at: halfPos + distance2 * 0.5, with: CGPoint(x: 0.0, y: -0.5 * distance2)), 0.0)
XCTAssertEqual(fpc.floatingPanel.getBackdropAlpha(at: halfPos, with: CGPoint(x: 0.0, y: -1 * distance2)), 0.0)
}
func test_targetPosition_1positions() {
class FloatingPanelLayout1Positions: FloatingPanelTestLayout {
let initialPosition: FloatingPanelPosition = .full
let supportedPositions: Set<FloatingPanelPosition> = [.full]
}
let delegate = FloatingPanelTestDelegate()
delegate.layout = FloatingPanelLayout1Positions()
let fpc = FloatingPanelController(delegate: delegate)
fpc.showForTest()
let fullPos = fpc.originYOfSurface(for: .full)
fpc.move(to: .full, animated: false)
assertTargetPosition(fpc.floatingPanel, with: [
(#line, fullPos - 10.0, CGPoint(x: 0.0, y: 1000.0), .full), // redirect
(#line, fullPos, CGPoint(x: 0.0, y: -1000.0), .full), // redirect
(#line, fullPos, CGPoint(x: 0.0, y: -100.0), .full), // redirect
(#line, fullPos, CGPoint(x: 0.0, y: 0.0), .full),
(#line, fullPos, CGPoint(x: 0.0, y: 100.0), .full), // redirect
(#line, fullPos, CGPoint(x: 0.0, y: 1000.0), .full), // redirect
(#line, fullPos + 10.0, CGPoint(x: 0.0, y: 100.0), .full), // redirect
])
}
func test_targetPosition_2positions() {
class FloatingPanelLayout2Positions: FloatingPanelTestLayout {
let initialPosition: FloatingPanelPosition = .half
let supportedPositions: Set<FloatingPanelPosition> = [.half, .full]
}
let delegate = FloatingPanelTestDelegate()
delegate.layout = FloatingPanelLayout2Positions()
let fpc = FloatingPanelController(delegate: delegate)
fpc.showForTest()
let fullPos = fpc.originYOfSurface(for: .full)
let halfPos = fpc.originYOfSurface(for: .half)
fpc.move(to: .full, animated: false)
assertTargetPosition(fpc.floatingPanel, with: [
(#line, fullPos - 10.0, CGPoint(x: 0.0, y: 1000.0), .half), // project to half
(#line, fullPos, CGPoint(x: 0.0, y: -1000.0), .full), // redirect
(#line, fullPos, CGPoint(x: 0.0, y: -100.0), .full),
(#line, fullPos, CGPoint(x: 0.0, y: 0.0), .full),
(#line, fullPos, CGPoint(x: 0.0, y: 100.0), .full),
(#line, fullPos, CGPoint(x: 0.0, y: 1000.0), .half), // project to half
(#line, fullPos + 10.0, CGPoint(x: 0.0, y: 100.0), .full), // redirect
(#line, halfPos - 10.0, CGPoint(x: 0.0, y: -100.0), .half), // redirect
(#line, halfPos, CGPoint(x: 0.0, y: -1000.0), .full), // project to full
(#line, halfPos, CGPoint(x: 0.0, y: -100.0), .half),
(#line, halfPos, CGPoint(x: 0.0, y: 0.0), .half),
(#line, halfPos, CGPoint(x: 0.0, y: 100.0), .half),
(#line, halfPos, CGPoint(x: 0.0, y: 1000.0), .half), // redirect
(#line, halfPos + 10.0, CGPoint(x: 0.0, y: -1000.0), .full), // project to full
])
fpc.move(to: .half, animated: false)
assertTargetPosition(fpc.floatingPanel, with: [
(#line, fullPos - 10.0, CGPoint(x: 0.0, y: 1000.0), .half), // project to half
(#line, fullPos, CGPoint(x: 0.0, y: -1000.0), .full), // redirect
(#line, fullPos, CGPoint(x: 0.0, y: -100.0), .full),
(#line, fullPos, CGPoint(x: 0.0, y: 0.0), .full),
(#line, fullPos, CGPoint(x: 0.0, y: 100.0), .full),
(#line, fullPos, CGPoint(x: 0.0, y: 1000.0), .half), // project to half
(#line, fullPos + 10.0, CGPoint(x: 0.0, y: 100.0), .full), // redirect
(#line, halfPos - 10.0, CGPoint(x: 0.0, y: -100.0), .half), // redirect
(#line, halfPos, CGPoint(x: 0.0, y: -1000.0), .full), // project to full
(#line, halfPos, CGPoint(x: 0.0, y: -100.0), .half),
(#line, halfPos, CGPoint(x: 0.0, y: 0.0), .half),
(#line, halfPos, CGPoint(x: 0.0, y: 100.0), .half),
(#line, halfPos, CGPoint(x: 0.0, y: 1000.0), .half), // redirect
(#line, halfPos + 10.0, CGPoint(x: 0.0, y: -1000.0), .full), // project to full
])
}
func test_targetPosition_2positionsWithHidden() {
class FloatingPanelLayout2Positions: FloatingPanelTestLayout {
let initialPosition: FloatingPanelPosition = .hidden
let supportedPositions: Set<FloatingPanelPosition> = [.hidden, .full]
}
let delegate = FloatingPanelTestDelegate()
delegate.layout = FloatingPanelLayout2Positions()
let fpc = FloatingPanelController(delegate: delegate)
fpc.showForTest()
let fullPos = fpc.originYOfSurface(for: .full)
let hiddenPos = fpc.originYOfSurface(for: .hidden)
fpc.move(to: .full, animated: false)
assertTargetPosition(fpc.floatingPanel, with: [
(#line, fullPos - 10.0, CGPoint(x: 0.0, y: 1000.0), .hidden), // project to hidden
(#line, fullPos, CGPoint(x: 0.0, y: -1000.0), .full), // redirect
(#line, fullPos, CGPoint(x: 0.0, y: -100.0), .full),
(#line, fullPos, CGPoint(x: 0.0, y: 0.0), .full),
(#line, fullPos, CGPoint(x: 0.0, y: 100.0), .full),
(#line, fullPos, CGPoint(x: 0.0, y: 1000.0), .hidden), // project to hidden
(#line, fullPos + 10.0, CGPoint(x: 0.0, y: 100.0), .full), // redirect
(#line, hiddenPos - 10.0, CGPoint(x: 0.0, y: -100.0), .hidden), // redirect
(#line, hiddenPos, CGPoint(x: 0.0, y: -1000.0), .full), // project to full
(#line, hiddenPos, CGPoint(x: 0.0, y: -100.0), .hidden),
(#line, hiddenPos, CGPoint(x: 0.0, y: 0.0), .hidden),
(#line, hiddenPos, CGPoint(x: 0.0, y: 100.0), .hidden),
(#line, hiddenPos, CGPoint(x: 0.0, y: 1000.0), .hidden), // redirect
(#line, hiddenPos + 10.0, CGPoint(x: 0.0, y: -1000.0), .full), // project to full
])
fpc.move(to: .hidden, animated: false)
assertTargetPosition(fpc.floatingPanel, with: [
(#line, fullPos - 10.0, CGPoint(x: 0.0, y: 1000.0), .hidden), // project to hidden
(#line, fullPos, CGPoint(x: 0.0, y: -1000.0), .full), // redirect
(#line, fullPos, CGPoint(x: 0.0, y: -100.0), .full),
(#line, fullPos, CGPoint(x: 0.0, y: 0.0), .full),
(#line, fullPos, CGPoint(x: 0.0, y: 100.0), .full),
(#line, fullPos, CGPoint(x: 0.0, y: 1000.0), .hidden), // project to hidden
(#line, fullPos + 10.0, CGPoint(x: 0.0, y: 100.0), .full), // redirect
(#line, hiddenPos - 10.0, CGPoint(x: 0.0, y: -100.0), .hidden), // redirect
(#line, hiddenPos, CGPoint(x: 0.0, y: -1000.0), .full), // project to full
(#line, hiddenPos, CGPoint(x: 0.0, y: -100.0), .hidden),
(#line, hiddenPos, CGPoint(x: 0.0, y: 0.0), .hidden),
(#line, hiddenPos, CGPoint(x: 0.0, y: 100.0), .hidden),
(#line, hiddenPos, CGPoint(x: 0.0, y: 1000.0), .hidden), // redirect
(#line, hiddenPos + 10.0, CGPoint(x: 0.0, y: -1000.0), .full), // project to full
])
}
func test_targetPosition_2positionsFromFull() {
let delegate = FloatingPanelTestDelegate()
delegate.layout = FloatingPanelLayout3Positions()
let fpc = FloatingPanelController(delegate: delegate)
fpc.showForTest()
let fullPos = fpc.originYOfSurface(for: .full)
let halfPos = fpc.originYOfSurface(for: .half)
let tipPos = fpc.originYOfSurface(for: .tip)
// From .full
fpc.move(to: .full, animated: false)
assertTargetPosition(fpc.floatingPanel, with: [
(#line, fullPos - 500.0, CGPoint(x: 0.0, y: -100.0), .full), // far from topMostState
(#line, fullPos - 500.0, CGPoint(x: 0.0, y: 0.0), .full), // far from topMostState
(#line, fullPos - 500.0, CGPoint(x: 0.0, y: 100.0), .full), // far from topMostState
(#line, fullPos - 10.0, CGPoint(x: 0.0, y: 3000.0), .half), // block projecting to tip at half
(#line, fullPos, CGPoint(x: 0.0, y: -100.0), .full),
(#line, fullPos, CGPoint(x: 0.0, y: 0.0), .full),
(#line, fullPos, CGPoint(x: 0.0, y: 500.0), .half), // project to half
(#line, fullPos, CGPoint(x: 0.0, y: 1000.0), .half), // block projecting to tip at half
(#line, fullPos, CGPoint(x: 0.0, y: 3000.0), .half), // block projecting to tip at half
(#line, fullPos + 10.0, CGPoint(x: 0.0, y: 100.0), .full), // redirect
(#line, halfPos - 10.0, CGPoint(x: 0.0, y: -100.0), .half), // redirect
(#line, halfPos, CGPoint(x: 0.0, y: -1000.0), .full), //project to full
(#line, halfPos, CGPoint(x: 0.0, y: -100.0), .half),
(#line, halfPos, CGPoint(x: 0.0, y: 0.0), .half),
(#line, halfPos, CGPoint(x: 0.0, y: 100.0), .half),
(#line, halfPos, CGPoint(x: 0.0, y: 1000.0), .tip), // project to tip
(#line, halfPos + 10.0, CGPoint(x: 0.0, y: 100.0), .half), // redirect
(#line, tipPos - 10.0, CGPoint(x: 0.0, y: -100.0), .tip), // redirect
(#line, tipPos, CGPoint(x: 0.0, y: -3000.0), .half), // block projecting to full at half
(#line, tipPos, CGPoint(x: 0.0, y: -1000.0), .half), // block projecting to full at half
(#line, tipPos, CGPoint(x: 0.0, y: -500.0), .half), // project to half
(#line, tipPos, CGPoint(x: 0.0, y: -100.0), .tip),
(#line, tipPos, CGPoint(x: 0.0, y: 0.0), .tip),
(#line, tipPos, CGPoint(x: 0.0, y: 100.0), .tip),
(#line, tipPos + 10.0, CGPoint(x: 0.0, y: -3000.0), .half), // block projecting to full at half
(#line, tipPos + 500.0, CGPoint(x: 0.0, y: -100.0), .tip), // far from bottomMostState
(#line, tipPos + 500.0, CGPoint(x: 0.0, y: 0.0), .tip), // far from bottomMostState
(#line, tipPos + 500.0, CGPoint(x: 0.0, y: 100.0), .tip), // far from bottomMostState
])
}
func test_targetPosition_3positionsFromHalf() {
let delegate = FloatingPanelTestDelegate()
delegate.layout = FloatingPanelLayout3Positions()
let fpc = FloatingPanelController(delegate: delegate)
fpc.showForTest()
let fullPos = fpc.originYOfSurface(for: .full)
let halfPos = fpc.originYOfSurface(for: .half)
let tipPos = fpc.originYOfSurface(for: .tip)
// From .half
fpc.move(to: .half, animated: false)
assertTargetPosition(fpc.floatingPanel, with: [
(#line, fullPos - 500.0, CGPoint(x: 0.0, y: -100.0), .full), // far from topMostState
(#line, fullPos - 500.0, CGPoint(x: 0.0, y: 0.0), .full), // far from topMostState
(#line, fullPos - 500.0, CGPoint(x: 0.0, y: 100.0), .full), // far from topMostState
(#line, fullPos, CGPoint(x: 0.0, y: -100.0), .full),
(#line, fullPos, CGPoint(x: 0.0, y: 0.0), .full),
(#line, fullPos, CGPoint(x: 0.0, y: 500.0), .half), // project to half
(#line, fullPos, CGPoint(x: 0.0, y: 1000.0), .half), // block projecting to tip at half
(#line, fullPos, CGPoint(x: 0.0, y: 3000.0), .half), // block projecting to tip at half
(#line, fullPos + 10.0, CGPoint(x: 0.0, y: 100.0), .full), // redirect
(#line, halfPos - 10.0, CGPoint(x: 0.0, y: -100.0), .half), // redirect
(#line, halfPos, CGPoint(x: 0.0, y: -1000.0), .full),// project to full
(#line, halfPos, CGPoint(x: 0.0, y: -100.0), .half),
(#line, halfPos, CGPoint(x: 0.0, y: 0.0), .half),
(#line, halfPos, CGPoint(x: 0.0, y: 100.0), .half),
(#line, halfPos, CGPoint(x: 0.0, y: 1000.0), .tip), // project to tip
(#line, halfPos + 10.0, CGPoint(x: 0.0, y: 100.0), .half), // redirect
(#line, tipPos - 10.0, CGPoint(x: 0.0, y: -100.0), .tip), // redirect
(#line, tipPos, CGPoint(x: 0.0, y: -3000.0), .half), // block projecting to full at half
(#line, tipPos, CGPoint(x: 0.0, y: -1000.0), .half), // block projecting to full at half
(#line, tipPos, CGPoint(x: 0.0, y: -500.0), .half), // project to half
(#line, tipPos, CGPoint(x: 0.0, y: -100.0), .tip),
(#line, tipPos, CGPoint(x: 0.0, y: 0.0), .tip),
(#line, tipPos, CGPoint(x: 0.0, y: 100.0), .tip),
(#line, tipPos + 500.0, CGPoint(x: 0.0, y: -100.0), .tip), // far from bottomMostState
(#line, tipPos + 500.0, CGPoint(x: 0.0, y: 0.0), .tip), // far from bottomMostState
(#line, tipPos + 500.0, CGPoint(x: 0.0, y: 100.0), .tip), // far from bottomMostState
])
}
func test_targetPosition_3positionsFromTip() {
let delegate = FloatingPanelTestDelegate()
delegate.layout = FloatingPanelLayout3Positions()
let fpc = FloatingPanelController(delegate: delegate)
fpc.showForTest()
let fullPos = fpc.originYOfSurface(for: .full)
let halfPos = fpc.originYOfSurface(for: .half)
let tipPos = fpc.originYOfSurface(for: .tip)
// From .tip
fpc.move(to: .tip, animated: false)
assertTargetPosition(fpc.floatingPanel, with: [
(#line, fullPos - 500.0, CGPoint(x: 0.0, y: -100.0), .full), // far from topMostState
(#line, fullPos - 500.0, CGPoint(x: 0.0, y: 0.0), .full), // far from topMostState
(#line, fullPos - 500.0, CGPoint(x: 0.0, y: 100.0), .full), // far from topMostState
(#line, fullPos, CGPoint(x: 0.0, y: -100.0), .full),
(#line, fullPos, CGPoint(x: 0.0, y: 0.0), .full),
(#line, fullPos, CGPoint(x: 0.0, y: 500.0), .half), // project to half
(#line, fullPos, CGPoint(x: 0.0, y: 1000.0), .half), // block projecting to tip at half
(#line, fullPos, CGPoint(x: 0.0, y: 3000.0), .half), // block projecting to tip at half
(#line, fullPos + 10.0, CGPoint(x: 0.0, y: 100.0), .full), // redirect
(#line, halfPos - 10.0, CGPoint(x: 0.0, y: -100.0), .half), // redirect
(#line, halfPos, CGPoint(x: 0.0, y: -3000.0), .full), // project to full
(#line, halfPos, CGPoint(x: 0.0, y: -100.0), .half),
(#line, halfPos, CGPoint(x: 0.0, y: 0.0), .half),
(#line, halfPos, CGPoint(x: 0.0, y: 100.0), .half),
(#line, halfPos, CGPoint(x: 0.0, y: 1000.0), .tip), // project to tip
(#line, halfPos + 10.0, CGPoint(x: 0.0, y: 100.0), .half), // redirect
(#line, tipPos - 10.0, CGPoint(x: 0.0, y: -100.0), .tip), // redirect
(#line, tipPos, CGPoint(x: 0.0, y: -3000.0), .half), // block projecting to full at half
(#line, tipPos, CGPoint(x: 0.0, y: -1000.0), .half), // block projecting to full at half
(#line, tipPos, CGPoint(x: 0.0, y: -500.0), .half), // project to half
(#line, tipPos, CGPoint(x: 0.0, y: -100.0), .tip),
(#line, tipPos, CGPoint(x: 0.0, y: 0.0), .tip),
(#line, tipPos, CGPoint(x: 0.0, y: 100.0), .tip),
(#line, tipPos + 500.0, CGPoint(x: 0.0, y: -100.0), .tip), // far from bottomMostState
(#line, tipPos + 500.0, CGPoint(x: 0.0, y: 0.0), .tip), // far from bottomMostState
(#line, tipPos + 500.0, CGPoint(x: 0.0, y: 100.0), .tip), // far from bottomMostState
])
}
func test_targetPosition_3positionsAllProjection() {
let delegate = FloatingPanelTestDelegate()
delegate.layout = FloatingPanelLayout3Positions()
delegate.behavior = FloatingPanelProjectionalBehavior()
let fpc = FloatingPanelController(delegate: delegate)
fpc.showForTest()
let fullPos = fpc.originYOfSurface(for: .full)
let halfPos = fpc.originYOfSurface(for: .half)
let tipPos = fpc.originYOfSurface(for: .tip)
// From .full
fpc.move(to: .full, animated: false)
assertTargetPosition(fpc.floatingPanel, with: [
(#line, fullPos - 10.0, CGPoint(x: 0.0, y: 3000.0), .tip),
(#line, fullPos, CGPoint(x: 0.0, y: 1000.0), .tip),
(#line, fullPos, CGPoint(x: 0.0, y: 3000.0), .tip),
(#line, halfPos, CGPoint(x: 0.0, y: 1000.0), .tip),
(#line, halfPos, CGPoint(x: 0.0, y: -1000.0), .full),
(#line, tipPos, CGPoint(x: 0.0, y: -3000.0), .full),
(#line, tipPos, CGPoint(x: 0.0, y: -1000.0), .full),
(#line, tipPos + 10.0, CGPoint(x: 0.0, y: -3000.0), .full),
])
// From .half
fpc.move(to: .tip, animated: false)
assertTargetPosition(fpc.floatingPanel, with: [
(#line, fullPos, CGPoint(x: 0.0, y: 1000.0), .tip),
(#line, fullPos, CGPoint(x: 0.0, y: 3000.0), .tip),
(#line, tipPos, CGPoint(x: 0.0, y: -3000.0), .full),
(#line, tipPos, CGPoint(x: 0.0, y: -1000.0), .full),
])
// From .tip
fpc.move(to: .tip, animated: false)
assertTargetPosition(fpc.floatingPanel, with: [
(#line, fullPos - 10.0, CGPoint(x: 0.0, y: 3000.0), .tip),
(#line, fullPos, CGPoint(x: 0.0, y: 1000.0), .tip),
(#line, fullPos, CGPoint(x: 0.0, y: 3000.0), .tip),
(#line, halfPos, CGPoint(x: 0.0, y: 1000.0), .tip),
(#line, halfPos, CGPoint(x: 0.0, y: -1000.0), .full),
(#line, tipPos, CGPoint(x: 0.0, y: -3000.0), .full),
(#line, tipPos, CGPoint(x: 0.0, y: -1000.0), .full),
(#line, tipPos + 10.0, CGPoint(x: 0.0, y: -3000.0), .full),
])
}
func test_targetPosition_3positionsWithHidden() {
class FloatingPanelLayout3PositionsWithHidden: FloatingPanelTestLayout {
let initialPosition: FloatingPanelPosition = .hidden
let supportedPositions: Set<FloatingPanelPosition> = [.hidden, .half, .full]
}
let delegate = FloatingPanelTestDelegate()
delegate.layout = FloatingPanelLayout3PositionsWithHidden()
let fpc = FloatingPanelController(delegate: delegate)
fpc.showForTest()
XCTAssertEqual(fpc.position, .hidden)
fpc.move(to: .full, animated: false)
assertTargetPosition(fpc.floatingPanel, with: [
(#line, fpc.surfaceView.frame.minY, CGPoint(x: 0.0, y: 1000.0), .half),
])
fpc.move(to: .half, animated: false)
assertTargetPosition(fpc.floatingPanel, with: [
(#line, fpc.surfaceView.frame.minY, CGPoint(x: 0.0, y: -100.0), .half),
(#line, fpc.surfaceView.frame.minY, CGPoint(x: 0.0, y: -1000.0), .full),
(#line, fpc.surfaceView.frame.minY, CGPoint(x: 0.0, y: 0.0), .half),
(#line, fpc.surfaceView.frame.minY, CGPoint(x: 0.0, y: 1000.0), .hidden),
])
}
func test_targetPosition_3positionsWithHiddenWithoutFull() {
class FloatingPanelLayout3Positions: FloatingPanelTestLayout {
let initialPosition: FloatingPanelPosition = .hidden
let supportedPositions: Set<FloatingPanelPosition> = [.hidden, .tip, .half]
}
let delegate = FloatingPanelTestDelegate()
delegate.layout = FloatingPanelLayout3Positions()
delegate.behavior = FloatingPanelProjectionalBehavior()
let fpc = FloatingPanelController(delegate: delegate)
fpc.showForTest()
XCTAssertEqual(fpc.position, .hidden)
let halfPos = fpc.originYOfSurface(for: .half)
let tipPos = fpc.originYOfSurface(for: .tip)
//let hiddenPos = fpc.originYOfSurface(for: .hidden)
fpc.move(to: .half, animated: false)
assertTargetPosition(fpc.floatingPanel, with: [
(#line, halfPos, CGPoint(x: 0.0, y: -100.0), .half),
(#line, halfPos, CGPoint(x: 0.0, y: 0.0), .half),
(#line, halfPos, CGPoint(x: 0.0, y: 385.0), .tip), // projection
(#line, halfPos, CGPoint(x: 0.0, y: 1000.0), .hidden), // projection
(#line, halfPos + 10.0, CGPoint(x: 0.0, y: 100.0), .half), // redirection
(#line, tipPos - 10.0, CGPoint(x: 0.0, y: -100.0), .tip), // redirection
(#line, tipPos, CGPoint(x: 0.0, y: -3000.0), .half), //projection
(#line, tipPos, CGPoint(x: 0.0, y: -10.0), .tip),
(#line, tipPos, CGPoint(x: 0.0, y: 0.0), .tip),
(#line, tipPos, CGPoint(x: 0.0, y: 10.0), .tip),
(#line, tipPos, CGPoint(x: 0.0, y: 1000.0), .hidden), //projection
(#line, tipPos + 10.0, CGPoint(x: 0.0, y: 10.0), .tip), // redirection
(#line, tipPos - 10.0, CGPoint(x: 0.0, y: 10.0), .tip), // redirection
])
fpc.move(to: .tip, animated: false)
assertTargetPosition(fpc.floatingPanel, with: [
(#line, tipPos, CGPoint(x: 0.0, y: -100.0), .tip),
(#line, tipPos, CGPoint(x: 0.0, y: -1000.0), .half),
(#line, tipPos, CGPoint(x: 0.0, y: 0.0), .tip),
(#line, tipPos, CGPoint(x: 0.0, y: 1000.0), .hidden),
])
}
}
private class FloatingPanelLayout3Positions: FloatingPanelTestLayout {
let initialPosition: FloatingPanelPosition = .tip
let supportedPositions: Set<FloatingPanelPosition> = [.tip, .half, .full]
}
private typealias TestParameter = (UInt, CGFloat,CGPoint, FloatingPanelPosition)
private func assertTargetPosition(_ floatingPanel: FloatingPanelCore, with params: [TestParameter]) {
params.forEach { (line, pos, velocity, result) in
floatingPanel.surfaceView.frame.origin.y = pos
XCTAssertEqual(floatingPanel.targetPosition(from: pos, with: velocity), result, line: line)
}
}
private class FloatingPanelProjectionalBehavior: FloatingPanelBehavior {
func shouldProjectMomentum(_ fpc: FloatingPanelController, for proposedTargetPosition: FloatingPanelPosition) -> Bool {
return true
}
}
@@ -0,0 +1,67 @@
//
// Created by Shin Yamamoto on 2019/05/23.
// Copyright © 2019 Shin Yamamoto. All rights reserved.
//
import XCTest
@testable import FloatingPanel
class FloatingPanelViewTests: XCTestCase {
override func setUp() {}
override func tearDown() {}
func test_surfaceView() {
let surface = FloatingPanelSurfaceView(frame: CGRect(x: 0.0, y: 0.0, width: 320.0, height: 480.0))
XCTAssert(surface.contentView == nil)
surface.layoutIfNeeded()
XCTAssert(surface.grabberHandle.frame.minY == 6.0)
XCTAssert(surface.grabberHandle.frame.width == surface.grabberHandleWidth)
XCTAssert(surface.grabberHandle.frame.height == surface.grabberHandleHeight)
surface.backgroundColor = .red
surface.layoutIfNeeded()
XCTAssert(surface.backgroundColor == surface.containerView.backgroundColor)
}
func test_surfaceView_cornderRaduis() {
let surface = FloatingPanelSurfaceView(frame: CGRect(x: 0.0, y: 0.0, width: 320.0, height: 480.0))
XCTAssert(surface.cornerRadius == 0.0)
XCTAssert(surface.containerView.layer.masksToBounds == false)
surface.cornerRadius = 10.0
surface.layoutIfNeeded()
XCTAssert(surface.cornerRadius == 10.0)
XCTAssert(surface.containerView.layer.cornerRadius == 10.0)
XCTAssert(surface.containerView.layer.masksToBounds == true)
surface.containerView.layer.cornerRadius = 12.0
surface.layoutIfNeeded()
XCTAssert(surface.cornerRadius == 12.0)
XCTAssert(surface.containerView.layer.masksToBounds == true)
surface.cornerRadius = 0.0
surface.layoutIfNeeded()
XCTAssert(surface.cornerRadius == 0.0)
XCTAssert(surface.containerView.layer.cornerRadius == 0.0)
XCTAssert(surface.containerView.layer.masksToBounds == false)
surface.containerView.layer.cornerRadius = 12.0
surface.setNeedsLayout()
surface.layoutIfNeeded()
XCTAssert(surface.cornerRadius == 12.0)
XCTAssert(surface.containerView.layer.masksToBounds == true)
}
func test_surfaceView_border() {
let surface = FloatingPanelSurfaceView(frame: CGRect(x: 0.0, y: 0.0, width: 320.0, height: 480.0))
XCTAssert(surface.borderColor == nil)
XCTAssert(surface.borderWidth == 0.0)
surface.borderColor = .red
surface.borderWidth = 3.0
surface.layoutIfNeeded()
XCTAssert(surface.containerView.layer.borderColor == UIColor.red.cgColor)
XCTAssert(surface.containerView.layer.borderWidth == 3.0)
}
}
-46
View File
@@ -1,46 +0,0 @@
//
// Created by Shin Yamamoto on 2019/06/27.
// Copyright © 2019 scenee. All rights reserved.
//
import Foundation
@testable import FloatingPanel
func waitRunLoop(secs: TimeInterval = 0) {
RunLoop.main.run(until: Date(timeIntervalSinceNow: secs))
}
extension FloatingPanelController {
func showForTest() {
loadViewIfNeeded()
view.frame = CGRect(x: 0, y: 0, width: 375, height: 667)
show(animated: false, completion: nil)
}
}
class FloatingPanelTestDelegate: FloatingPanelControllerDelegate {
var layout: FloatingPanelLayout?
var behavior: FloatingPanelBehavior?
var position: FloatingPanelPosition = .hidden
func floatingPanel(_ vc: FloatingPanelController, layoutFor newCollection: UITraitCollection) -> FloatingPanelLayout? {
return layout
}
func floatingPanel(_ vc: FloatingPanelController, behaviorFor newCollection: UITraitCollection) -> FloatingPanelBehavior? {
return behavior
}
func floatingPanelDidChangePosition(_ vc: FloatingPanelController) {
position = vc.position
}
}
protocol FloatingPanelTestLayout: FloatingPanelFullScreenLayout {}
extension FloatingPanelTestLayout {
func insetFor(position: FloatingPanelPosition) -> CGFloat? {
switch position {
case .full: return 20.0
case .half: return 250.0
case .tip: return 60.0
default: return nil
}
}
}
-27
View File
@@ -1,27 +0,0 @@
// swift-tools-version:5.0
// The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription
let package = Package(
name: "FloatingPanel",
platforms: [
.iOS(.v10)
],
products: [
// Products define the executables and libraries produced by a package, and make them visible to other packages.
.library(
name: "FloatingPanel",
targets: ["FloatingPanel"]),
],
dependencies: [
// Dependencies declare other packages that this package depends on.
// .package(url: /* package url */, from: "1.0.0"),
],
targets: [
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
// Targets can depend on other targets in this package, and on products in packages which this package depends on.
.target(name: "FloatingPanel", path: "Framework/Sources"),
],
swiftLanguageVersions: [.version("5")]
)
+29 -172
View File
@@ -5,9 +5,9 @@
[![Swift 4.1](https://img.shields.io/badge/Swift-4.1-orange.svg?style=flat)](https://swift.org/)
[![Swift 4.2](https://img.shields.io/badge/Swift-4.2-orange.svg?style=flat)](https://swift.org/)
[![Swift 5.0](https://img.shields.io/badge/Swift-5.0-orange.svg?style=flat)](https://swift.org/)
[![Swift 5.1](https://img.shields.io/badge/Swift-5.1-orange.svg?style=flat)](https://swift.org/)
# FloatingPanel
# FloatingPanel
FloatingPanel is a simple and easy-to-use UI component for a new interface introduced in Apple Maps, Shortcuts and Stocks app.
The new interface displays the related contents and utilities in parallel as a user wants.
@@ -24,37 +24,25 @@ The new interface displays the related contents and utilities in parallel as a u
- [Installation](#installation)
- [CocoaPods](#cocoapods)
- [Carthage](#carthage)
- [Swift Package Manager with Xcode 11](#swift-package-manager-with-xcode-11)
- [Getting Started](#getting-started)
- [Add a floating panel as a child view controller](#add-a-floating-panel-as-a-child-view-controller)
- [Present a floating panel as a modality](#present-a-floating-panel-as-a-modality)
- [View hierarchy](#view-hierarchy)
- [Usage](#usage)
- [Show/Hide a floating panel in a view with your view hierarchy](#showhide-a-floating-panel-in-a-view-with-your-view-hierarchy)
- [Scale the content view when the surface position changes](#scale-the-content-view-when-the-surface-position-changes)
- [Customize the layout with `FloatingPanelLayout` protocol](#customize-the-layout-with-floatingpanellayout-protocol)
- [Change the initial position and height](#change-the-initial-position-and-height)
- [Support your landscape layout](#support-your-landscape-layout)
- [Use Intrinsic height layout](#use-intrinsic-height-layout)
- [Specify position insets from the frame of `FloatingPanelContrller.view`, not the SafeArea](#specify-position-insets-from-the-frame-of-floatingpanelcontrllerview-not-the-safearea)
- [Customize the behavior with `FloatingPanelBehavior` protocol](#customize-the-behavior-with-floatingpanelbehavior-protocol)
- [Modify your floating panel's interaction](#modify-your-floating-panels-interaction)
- [Activate the rubber-band effect on the top/bottom edges](#activate-the-rubber-band-effect-on-the-topbottom-edges)
- [Manage the projection of a pan gesture momentum](#manage-the-projection-of-a-pan-gesture-momentum)
- [Customize the surface design](#customize-the-surface-design)
- [Use a custom grabber handle](#use-a-custom-grabber-handle)
- [Customize layout of the grabber handle](#customize-layout-of-the-grabber-handle)
- [Customize content padding from surface edges](#customize-content-padding-from-surface-edges)
- [Customize margins of the surface edges](#customize-margins-of-the-surface-edges)
- [Customize gestures](#customize-gestures)
- [Suppress the panel interaction](#suppress-the-panel-interaction)
- [Add tap gestures to the surface or backdrop views](#add-tap-gestures-to-the-surface-or-backdrop-views)
- [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)
- [UISearchController issue](#uisearchcontroller-issue)
- [FloatingPanelSurfaceView's issue on iOS 10](#floatingpanelsurfaceviews-issue-on-ios-10)
- [Author](#author)
- [License](#license)
@@ -95,7 +83,7 @@ it, simply add the following line to your Podfile:
pod 'FloatingPanel'
```
✏️FloatingPanel v1.7.0 or later requires CocoaPods v1.7.0+ for `swift_versions` support.
✏️ To suppress "Swift Conversion" warnings in Xcode, please set a Swift version to `SWIFT_VERSION` for the project in your Podfile. It will be resolved in CocoaPods v1.7.0.
### Carthage
@@ -105,10 +93,6 @@ For [Carthage](https://github.com/Carthage/Carthage), add the following to your
github "scenee/FloatingPanel"
```
### Swift Package Manager with Xcode 11
Follow [this doc](https://developer.apple.com/documentation/swift_packages/adding_package_dependencies_to_your_app).
## Getting Started
### Add a floating panel as a child view controller
@@ -181,72 +165,41 @@ FloatingPanelController.view (FloatingPanelPassThroughView)
### Show/Hide a floating panel in a view with your view hierarchy
If you need more control over showing and hiding the floating panel, you can forgo the `addPanel` and `removePanelFromParent` methods. These methods are a convenience wrapper for **FloatingPanel**'s `show` and `hide` methods along with some required setup.
There are two ways to work with the `FloatingPanelController`:
1. Add it to the hierarchy once and then call `show` and `hide` methods to make it appear/disappear.
2. Add it to the hierarchy when needed and remove afterwards.
The following example shows how to add the controller to your `UIViewController` and how to remove it. Make sure that you never add the same `FloatingPanelController` to the hierarchy before removing it.
**NOTE**: `self.` prefix is not required, nor recommended. It's used here to make it clearer where do the functions used come from. `self` is an instance of a custom UIViewController in your code.
```swift
// Add the floating panel view to the controller's view on top of other views.
self.view.addSubview(fpc.view)
// REQUIRED. It makes the floating panel view have the same size as the controller's view.
fpc.view.frame = self.view.bounds
// 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
// In addition, Auto Layout constraints are highly recommended.
// Constraint the fpc.view to all four edges of your controller's view.
// It makes the layout more robust on trait collection change.
fpc.view.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
fpc.view.topAnchor.constraint(equalTo: self.view.topAnchor, constant: 0.0),
fpc.view.leftAnchor.constraint(equalTo: self.view.leftAnchor, constant: 0.0),
fpc.view.rightAnchor.constraint(equalTo: self.view.rightAnchor, constant: 0.0),
fpc.view.bottomAnchor.constraint(equalTo: self.view.bottomAnchor, constant: 0.0),
])
// Because it makes the layout more robust on trait collection change.
//
// fpc.view.translatesAutoresizingMaskIntoConstraints = false
// NSLayoutConstraint.activate([...])
//
// Add the floating panel controller to the controller hierarchy.
self.addChild(fpc)
parent.addChild(fpc)
// Show the floating panel at the initial position defined in your `FloatingPanelLayout` object.
// Show a floating panel to the initial position defined in your `FloatingPanelLayout` object.
fpc.show(animated: true) {
// Inform the floating panel controller that the transition to the controller hierarchy has completed.
fpc.didMove(toParent: self)
// Only for the first time
self.didMove(toParent: self)
}
```
After you add the `FloatingPanelController` as seen above, you can call `fpc.show(animated: true) { }` to show the panel and `fpc.hide(animated: true) { }` to hide it.
...
To remove the `FloatingPanelController` from the hierarchy, follow the example below.
```swift
// Inform the panel controller that it will be removed from the hierarchy.
fpc.willMove(toParent: nil)
// Hide the floating panel.
// Hide it
fpc.hide(animated: true) {
// Remove the floating panel view from your controller's view.
fpc.view.removeFromSuperview()
// Remove the floating panel controller from the controller hierarchy.
fpc.removeFromParent()
// Remove it if needed
self.willMove(toParent: nil)
self.view.removeFromSuperview()
self.removeFromParent()
}
```
### Scale the content view when the surface position changes
Specify the `contentMode` to `.fitToBounds` if the surface height fits the bounds of `FloatingPanelController.view` when the surface position changes
```swift
fpc.contentMode = .fitToBounds
```
Otherwise, `FloatingPanelController` fixes the content by the height of the top most position.
✏️ In `.fitToBounds` mode, the surface height changes as following a user interaction so that you have a responsibility to configure Auto Layout constrains not to break the layout of a content view by the elastic surface height.
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
@@ -338,27 +291,6 @@ class RemovablePanelLayout: FloatingPanelIntrinsicLayout {
}
```
#### Specify position insets from the frame of `FloatingPanelContrller.view`, not the SafeArea
There are 2 ways. One is returning `.fromSuperview` for `FloatingPanelLayout.positionReference` in your layout.
```swift
class MyFullScreenLayout: FloatingPanelLayout {
...
var positionReference: FloatingPanelLayoutReference {
return .fromSuperview
}
}
```
Another is using `FloatingPanelFullScreenLayout` protocol.
```swift
class MyFullScreenLayout: FloatingPanelFullScreenLayout {
...
}
```
### Customize the behavior with `FloatingPanelBehavior` protocol
#### Modify your floating panel's interaction
@@ -381,33 +313,7 @@ class FloatingPanelStocksBehavior: FloatingPanelBehavior {
}
```
#### Activate the rubber-band effect on the top/bottom edges
```swift
class FloatingPanelBehavior: FloatingPanelBehavior {
...
func allowsRubberBanding(for edge: UIRectEdge) -> Bool {
return true
}
}
```
#### Manage the projection of a pan gesture momentum
This allows full projectional panel behavior. For example, a user can swipe up a panel from tip to full nearby the tip position.
```swift
class FloatingPanelBehavior: FloatingPanelBehavior {
...
func shouldProjectMomentum(_ fpc: FloatingPanelController, for proposedTargetPosition: FloatingPanelPosition) -> Bool {
return true
}
}
```
### Customize the surface design
#### Use a custom grabber handle
### Use a custom grabber handle
```swift
let myGrabberHandleView = MyGrabberHandleView()
@@ -415,50 +321,7 @@ fpc.surfaceView.grabberHandle.isHidden = true
fpc.surfaceView.addSubview(myGrabberHandleView)
```
#### Customize layout of the grabber handle
```swift
fpc.surfaceView.grabberTopPadding = 10.0
fpc.surfaceView.grabberHandleWidth = 44.0
fpc.surfaceView.grabberHandleHeight = 12.0
```
#### Customize content padding from surface edges
```swift
fpc.surfaceView.contentInsets = .init(top: 20, left: 20, bottom: 20, right: 20)
```
#### Customize margins of the surface edges
```swift
fpc.surfaceView.containerMargins = .init(top: 20.0, left: 16.0, bottom: 16.0, right: 16.0)
```
The feature can be used for these 2 kind panels
* Facebook/Slack-like panel whose surface top edge is separated from the grabber handle.
* iOS native panel to display AirPods information, for example.
### Customize gestures
#### Suppress the panel interaction
You can disable the pan gesture recognizer directly
```swift
fpc.panGestureRecognizer.isEnable = false
```
Or use this `FloatingPanelControllerDelegate` method.
```swift
func floatingPanelShouldBeginDragging(_ vc: FloatingPanelController) -> Bool {
return aCondition ? false : true
}
```
#### Add tap gestures to the surface or backdrop views
### Add tap gestures to the surface or backdrop views
```swift
override func viewDidLoad() {
@@ -572,12 +435,6 @@ A `FloatingPanelController` object proxies an action for `show(_:sender)` to the
It's a great way to decouple between a floating panel and the content VC.
### UISearchController issue
`UISearchController` isn't able to be used with `FloatingPanelController` by the system design.
Because `UISearchController` automatically presents itself modally when a user interacts with the search bar, and then it swaps the superview of the search bar to the view managed by itself while it displays. As a result, `FloatingPanelController` can't control the search bar when it's active, as you can see from [the screen shot](https://github.com/SCENEE/FloatingPanel/issues/248#issuecomment-521263831).
### FloatingPanelSurfaceView's issue on iOS 10
* On iOS 10, `FloatingPanelSurfaceView.cornerRadius` isn't not automatically masked with the top rounded corners because of `UIVisualEffectView` issue. See https://forums.developer.apple.com/thread/50854.