Compare commits
60 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3a0831a334 | |||
| add2f9ba4f | |||
| ca6a8d5c16 | |||
| 84eba1aefa | |||
| 289ea4c971 | |||
| 821b03376c | |||
| 19fe1cffef | |||
| b8fcb50874 | |||
| 9c45c31190 | |||
| 895790f697 | |||
| 43c7e8c2a0 | |||
| 904a66115c | |||
| d0932e8e37 | |||
| 22009013eb | |||
| da4668aa2b | |||
| 00ce232420 | |||
| 5b176b5cef | |||
| 15709f62f8 | |||
| 9168236534 | |||
| 16e709e8ab | |||
| 94c39f0f34 | |||
| 16fea625be | |||
| 6c69694cfe | |||
| 561d783479 | |||
| 1bd2e60200 | |||
| ec0e1b2dad | |||
| 9958fc5017 | |||
| 11dfc0d2f3 | |||
| 34246d1f37 | |||
| 3d6c0220e0 | |||
| ecbd318186 | |||
| 34809bd8ea | |||
| aa5fbe9e94 | |||
| 9fbb7df15a | |||
| d55f9a0abf | |||
| a932f3b782 | |||
| 9ea95d69a1 | |||
| e783b92905 | |||
| d380fdeb13 | |||
| 987bf79121 | |||
| be0ebd0fae | |||
| a6538b7a2a | |||
| 40194d91c0 | |||
| c8b2b33de0 | |||
| 7114f545ff | |||
| b4423bcaa2 | |||
| b34fd41650 | |||
| 199a77182b | |||
| 0a0f00172d | |||
| 25ca9487fb | |||
| 231ee4c9af | |||
| c872218446 | |||
| 8e8c6527d4 | |||
| 3b4e237eba | |||
| 9b1cb68f0a | |||
| 4ba8acaf08 | |||
| 02d8d4516c | |||
| 94829d2749 | |||
| 1522a0990f | |||
| 2e3bbaeec2 |
@@ -0,0 +1 @@
|
||||
github: SCENEE
|
||||
@@ -0,0 +1,104 @@
|
||||
name: ci
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
branches:
|
||||
- '*'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: macOS-11
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- name: "Swift 5.4"
|
||||
run: xcodebuild -scheme FloatingPanel SWIFT_VERSION=5.4 clean build
|
||||
env:
|
||||
DEVELOPER_DIR: /Applications/Xcode_12.5.1.app/Contents/Developer
|
||||
- name: "Swift 5.5"
|
||||
run: xcodebuild -scheme FloatingPanel SWIFT_VERSION=5.5 clean build
|
||||
env:
|
||||
DEVELOPER_DIR: /Applications/Xcode_13.0.app/Contents/Developer
|
||||
|
||||
build_compat:
|
||||
runs-on: macOS-10.15
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- name: "Swift 5.1"
|
||||
run: xcodebuild -scheme FloatingPanel SWIFT_VERSION=5.1 clean build
|
||||
env:
|
||||
DEVELOPER_DIR: /Applications/Xcode_11.3.1.app/Contents/Developer
|
||||
- name: "Swift 5.2"
|
||||
run: xcodebuild -scheme FloatingPanel SWIFT_VERSION=5.2 clean build
|
||||
env:
|
||||
DEVELOPER_DIR: /Applications/Xcode_11.7.app/Contents/Developer
|
||||
- name: "Swift 5.3"
|
||||
run: xcodebuild -scheme FloatingPanel SWIFT_VERSION=5.3 clean build
|
||||
env:
|
||||
DEVELOPER_DIR: /Applications/Xcode_12.4.app/Contents/Developer
|
||||
|
||||
testing:
|
||||
runs-on: macOS-11
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- name: "Testing in iOS 14.5"
|
||||
run: xcodebuild clean test -scheme FloatingPanel -workspace FloatingPanel.xcworkspace -destination 'platform=iOS Simulator,OS=14.5,name=iPhone 12 Pro'
|
||||
env:
|
||||
DEVELOPER_DIR: /Applications/Xcode_12.5.1.app/Contents/Developer
|
||||
- name: "Testing in iOS 15.0"
|
||||
run: xcodebuild clean test -scheme FloatingPanel -workspace FloatingPanel.xcworkspace -destination 'platform=iOS Simulator,OS=15.0,name=iPhone 13 Pro'
|
||||
env:
|
||||
DEVELOPER_DIR: /Applications/Xcode_13.0.app/Contents/Developer
|
||||
|
||||
testing_compat:
|
||||
runs-on: macOS-10.15
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- name: "Testing in iOS 13.7"
|
||||
run: xcodebuild clean test -scheme FloatingPanel -workspace FloatingPanel.xcworkspace -destination 'platform=iOS Simulator,OS=13.7,name=iPhone 11 Pro'
|
||||
env:
|
||||
DEVELOPER_DIR: /Applications/Xcode_11.7.app/Contents/Developer
|
||||
- name: "Testing in iOS 14.4"
|
||||
run: xcodebuild clean test -scheme FloatingPanel -workspace FloatingPanel.xcworkspace -destination 'platform=iOS Simulator,OS=14.4,name=iPhone 12 Pro'
|
||||
env:
|
||||
DEVELOPER_DIR: /Applications/Xcode_12.4.app/Contents/Developer
|
||||
|
||||
example:
|
||||
runs-on: macOS-11
|
||||
env:
|
||||
DEVELOPER_DIR: /Applications/Xcode_13.0.app/Contents/Developer
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- name: "Build Maps"
|
||||
run: xcodebuild -workspace FloatingPanel.xcworkspace -scheme Maps -sdk iphonesimulator clean build
|
||||
- name: "Build Stocks"
|
||||
run: xcodebuild -workspace FloatingPanel.xcworkspace -scheme Stocks -sdk iphonesimulator clean build
|
||||
- name: "Build Samples"
|
||||
run: xcodebuild -workspace FloatingPanel.xcworkspace -scheme Samples -sdk iphonesimulator clean build
|
||||
|
||||
swiftpm:
|
||||
runs-on: macOS-11
|
||||
env:
|
||||
DEVELOPER_DIR: /Applications/Xcode_13.0.app/Contents/Developer
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- name: "Swift Package build"
|
||||
run: swift build -Xswiftc "-sdk" -Xswiftc "`xcrun --sdk iphonesimulator --show-sdk-path`" -Xswiftc "-target" -Xswiftc "x86_64-apple-ios14.3-simulator"
|
||||
|
||||
carthage:
|
||||
runs-on: macOS-11
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- name: "Carthage build"
|
||||
run: carthage build --use-xcframeworks --no-skip-current
|
||||
|
||||
cocoapods:
|
||||
runs-on: macOS-11
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- name: "CocoaPods: pod lib lint"
|
||||
run: pod lib lint --allow-warnings
|
||||
- name: "CocoaPods: pod spec lint"
|
||||
run: pod spec lint --allow-warnings
|
||||
+1
-1
@@ -1,3 +1,3 @@
|
||||
--header "// Copyright 2018-Present Shin Yamamoto. All rights reserved. MIT license."
|
||||
--disable andOperator,anyObjectProtocol,blankLinesAroundMark,blankLinesAtEndOfScope,blankLinesAtStartOfScope,blankLinesBetweenScopes,braces,consecutiveBlankLines,consecutiveSpaces,duplicateImports,elseOnSameLine,emptyBraces,hoistPatternLet,indent,isEmpty,leadingDelimiters,linebreakAtEndOfFile,linebreaks,numberFormatting,ranges,redundantBackticks,redundantBreak,redundantExtensionACL,redundantFileprivate,redundantGet,redundantInit,redundantLet,redundantLetError,redundantNilInit,redundantObjc,redundantParens,redundantPattern,redundantRawValues,redundantReturn,redundantSelf,redundantVoidReturnType,semicolons,sortedImports,spaceAroundBraces,spaceAroundBrackets,spaceAroundComments,spaceAroundGenerics,spaceAroundOperators,spaceAroundParens,spaceInsideBraces,spaceInsideBrackets,spaceInsideComments,spaceInsideGenerics,spaceInsideParens,specifiers,strongOutlets,strongifiedSelf,todos,trailingClosures,trailingCommas,trailingSpace,typeSugar,unusedArguments,void,wrapArguments,yodaConditions
|
||||
--disable andOperator,anyObjectProtocol,blankLinesAroundMark,blankLinesAtEndOfScope,blankLinesAtStartOfScope,blankLinesBetweenScopes,braces,consecutiveBlankLines,consecutiveSpaces,duplicateImports,elseOnSameLine,emptyBraces,hoistPatternLet,indent,isEmpty,leadingDelimiters,linebreakAtEndOfFile,linebreaks,numberFormatting,redundantBackticks,redundantBreak,redundantExtensionACL,redundantFileprivate,redundantGet,redundantInit,redundantLet,redundantLetError,redundantNilInit,redundantObjc,redundantParens,redundantPattern,redundantRawValues,redundantReturn,redundantSelf,redundantVoidReturnType,semicolons,sortedImports,spaceAroundBraces,spaceAroundBrackets,spaceAroundComments,spaceAroundGenerics,spaceAroundOperators,spaceAroundParens,spaceInsideBraces,spaceInsideBrackets,spaceInsideComments,spaceInsideGenerics,spaceInsideParens,specifiers,strongOutlets,strongifiedSelf,todos,trailingClosures,trailingCommas,trailingSpace,typeSugar,unusedArguments,void,wrapArguments,yodaConditions
|
||||
|
||||
|
||||
-74
@@ -1,74 +0,0 @@
|
||||
language: objective-c
|
||||
branches:
|
||||
only:
|
||||
- master
|
||||
env:
|
||||
global:
|
||||
- LANG=en_US.UTF-8
|
||||
- LC_ALL=en_US.UTF-8
|
||||
jobs:
|
||||
include:
|
||||
- stage: "Builds"
|
||||
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.3
|
||||
name: "Swift 5.1"
|
||||
- script: xcodebuild -scheme FloatingPanel SWIFT_VERSION=5.2 clean build
|
||||
osx_image: xcode11.6
|
||||
name: "Swift 5.2"
|
||||
- script: xcodebuild -scheme FloatingPanel SWIFT_VERSION=5.3 clean build
|
||||
osx_image: xcode12
|
||||
name: "Swift 5.3"
|
||||
|
||||
- stage: "Tests"
|
||||
script: xcodebuild clean test -scheme FloatingPanel -workspace FloatingPanel.xcworkspace -destination 'platform=iOS Simulator,OS=10.3.1,name=iPhone SE (1st generation)'
|
||||
osx_image: xcode11.6
|
||||
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: xcode11.6
|
||||
name: "iPhone 7 (iOS 11.4)"
|
||||
- script: xcodebuild clean test -scheme FloatingPanel -workspace FloatingPanel.xcworkspace -destination 'platform=iOS Simulator,OS=12.4,name=iPhone X'
|
||||
osx_image: xcode11.6
|
||||
name: "iPhone X (iOS 12.4)"
|
||||
- script: xcodebuild clean test -scheme FloatingPanel -workspace FloatingPanel.xcworkspace -destination 'platform=iOS Simulator,OS=13.6,name=iPhone 11'
|
||||
osx_image: xcode11.6
|
||||
name: "iPhone X (iOS 13.6)"
|
||||
|
||||
- stage: "Build examples"
|
||||
osx_image: xcode11.6
|
||||
script: xcodebuild -workspace FloatingPanel.xcworkspace -scheme Maps -sdk iphonesimulator clean build
|
||||
name: "Maps"
|
||||
- script: xcodebuild -workspace FloatingPanel.xcworkspace -scheme Stocks -sdk iphonesimulator clean build
|
||||
osx_image: xcode11.6
|
||||
name: "Stocks"
|
||||
- script: xcodebuild -workspace FloatingPanel.xcworkspace -scheme Samples -sdk iphonesimulator clean build
|
||||
osx_image: xcode11.6
|
||||
name: "Samples"
|
||||
- script: xcodebuild -workspace FloatingPanel.xcworkspace -scheme SamplesObjC -sdk iphonesimulator clean build
|
||||
osx_image: xcode11.6
|
||||
name: "SamplesObjC"
|
||||
|
||||
- stage: "Swift Package Manager"
|
||||
script: swift build -Xswiftc "-sdk" -Xswiftc "`xcrun --sdk iphoneos --show-sdk-path`" -Xswiftc "-target" -Xswiftc "arm64-apple-ios13.0"
|
||||
osx_image: xcode11.6
|
||||
name: "iPhone OS"
|
||||
- script: swift build -Xswiftc "-sdk" -Xswiftc "`xcrun --sdk iphonesimulator --show-sdk-path`" -Xswiftc "-target" -Xswiftc "x86_64-apple-ios13.0-simulator"
|
||||
osx_image: xcode11.6
|
||||
name: "iPhone Simulator"
|
||||
|
||||
- stage: "Carthage"
|
||||
# Carthage doesn't fix the issue with Xcode 12: https://github.com/Carthage/Carthage/releases/tag/0.36.0
|
||||
osx_image: xcode11.6
|
||||
before_install:
|
||||
- brew update
|
||||
- brew outdated carthage || brew upgrade carthage
|
||||
script:
|
||||
- carthage build --no-skip-current
|
||||
|
||||
- stage: "CocoaPods"
|
||||
osx_image: xcode11.6
|
||||
before_install:
|
||||
- gem install cocoapods
|
||||
script:
|
||||
- pod spec lint --allow-warnings
|
||||
- pod lib lint --allow-warnings
|
||||
@@ -194,8 +194,8 @@ class MyPanelIntrinsicLayout: FloatingPanelLayout {
|
||||
var initialState: FloatingPanelState { .full }
|
||||
var anchors: [FloatingPanelState : FloatingPanelLayoutAnchoring] {
|
||||
return [
|
||||
.full: FloatingPanelIntrinsicLayoutAnchor(absoluteOffset: 16.0, referenceGuide: .safeArea)
|
||||
.half: FloatingPanelIntrinsicLayoutAnchor(fractionalOffset: 0.5, referenceGuide: .safeArea)
|
||||
.full: FloatingPanelIntrinsicLayoutAnchor(absoluteOffset: 16.0, referenceGuide: .safeArea),
|
||||
.half: FloatingPanelIntrinsicLayoutAnchor(fractionalOffset: 0.5, referenceGuide: .safeArea),
|
||||
.tip: FloatingPanelLayoutAnchor(absoluteInset: 44.0, edge: .bottom, referenceGuide: .safeArea)
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="16096" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="BYZ-38-t0r">
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="17506" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="BYZ-38-t0r">
|
||||
<device id="retina5_9" orientation="portrait" appearance="light"/>
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="16087"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="17505"/>
|
||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
@@ -28,6 +28,7 @@
|
||||
<blurEffect style="prominent"/>
|
||||
</visualEffectView>
|
||||
</subviews>
|
||||
<viewLayoutGuide key="safeArea" id="6Tk-OE-BBY"/>
|
||||
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<constraints>
|
||||
<constraint firstItem="5Jw-n2-Cpw" firstAttribute="trailing" secondItem="8bC-Xf-vdC" secondAttribute="trailing" id="1Fg-Be-qfh"/>
|
||||
@@ -39,7 +40,6 @@
|
||||
<constraint firstItem="6Tk-OE-BBY" firstAttribute="top" secondItem="d9i-3g-8Ja" secondAttribute="bottom" id="Y1G-hr-aEX"/>
|
||||
<constraint firstItem="d9i-3g-8Ja" firstAttribute="leading" secondItem="6Tk-OE-BBY" secondAttribute="leading" id="diB-Ij-HN3"/>
|
||||
</constraints>
|
||||
<viewLayoutGuide key="safeArea" id="6Tk-OE-BBY"/>
|
||||
</view>
|
||||
<connections>
|
||||
<outlet property="mapView" destination="5Jw-n2-Cpw" id="WVC-rU-mLe"/>
|
||||
@@ -233,6 +233,7 @@
|
||||
<blurEffect style="prominent"/>
|
||||
</visualEffectView>
|
||||
</subviews>
|
||||
<viewLayoutGuide key="safeArea" id="G74-X7-Za8"/>
|
||||
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<constraints>
|
||||
<constraint firstItem="Ye3-uU-bq3" firstAttribute="leading" secondItem="G74-X7-Za8" secondAttribute="leading" id="Kr2-sU-ZWZ"/>
|
||||
@@ -240,7 +241,6 @@
|
||||
<constraint firstItem="Ye3-uU-bq3" firstAttribute="trailing" secondItem="G74-X7-Za8" secondAttribute="trailing" id="fEL-8y-Acc"/>
|
||||
<constraint firstItem="Ye3-uU-bq3" firstAttribute="top" secondItem="Ncl-E9-yRn" secondAttribute="top" id="w77-ba-FrJ"/>
|
||||
</constraints>
|
||||
<viewLayoutGuide key="safeArea" id="G74-X7-Za8"/>
|
||||
</view>
|
||||
<connections>
|
||||
<outlet property="searchBar" destination="Zcj-SE-gb8" id="BH7-Gy-RG5"/>
|
||||
@@ -262,7 +262,7 @@
|
||||
<subviews>
|
||||
<visualEffectView opaque="NO" contentMode="scaleToFill" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="c3d-2e-0b1">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="812"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" id="9fL-a5-0LS">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="812"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
@@ -282,8 +282,8 @@
|
||||
<blurEffect style="extraLight"/>
|
||||
</visualEffectView>
|
||||
</subviews>
|
||||
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<viewLayoutGuide key="safeArea" id="ctv-Dd-JUc"/>
|
||||
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
</view>
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="EDp-D2-xcT" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
|
||||
|
||||
@@ -21,6 +21,9 @@ extension ViewController: UITableViewDelegate {
|
||||
|
||||
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
tableView.deselectRow(at: indexPath, animated: true)
|
||||
|
||||
deactivate(searchBar: searchVC.searchBar)
|
||||
|
||||
// Show a detail panel
|
||||
switch indexPath.row {
|
||||
case 0:
|
||||
|
||||
@@ -83,14 +83,17 @@ class ViewController: UIViewController {
|
||||
func layoutPanelForPhone() {
|
||||
fpc.track(scrollView: searchVC.tableView) // Only track the tabvle view on iPhone
|
||||
fpc.addPanel(toParent: self, animated: true)
|
||||
fpc.setApearanceForPhone()
|
||||
detailFpc.setApearanceForPhone()
|
||||
fpc.setAppearanceForPhone()
|
||||
detailFpc.setAppearanceForPhone()
|
||||
}
|
||||
}
|
||||
|
||||
extension FloatingPanelController {
|
||||
func setApearanceForPhone() {
|
||||
func setAppearanceForPhone() {
|
||||
let appearance = SurfaceAppearance()
|
||||
if #available(iOS 13.0, *) {
|
||||
appearance.cornerCurve = .continuous
|
||||
}
|
||||
appearance.cornerRadius = 8.0
|
||||
appearance.backgroundColor = .clear
|
||||
surfaceView.appearance = appearance
|
||||
@@ -114,19 +117,27 @@ extension FloatingPanelController {
|
||||
// MARK: - UISearchBarDelegate
|
||||
|
||||
extension ViewController: UISearchBarDelegate {
|
||||
func searchBarCancelButtonClicked(_ searchBar: UISearchBar) {
|
||||
func activate(searchBar: UISearchBar) {
|
||||
searchBar.showsCancelButton = true
|
||||
searchVC.showHeader(animated: true)
|
||||
searchVC.tableView.alpha = 1.0
|
||||
detailVC.dismiss(animated: true, completion: nil)
|
||||
}
|
||||
func deactivate(searchBar: UISearchBar) {
|
||||
searchBar.resignFirstResponder()
|
||||
searchBar.showsCancelButton = false
|
||||
searchVC.hideHeader(animated: true)
|
||||
}
|
||||
|
||||
func searchBarCancelButtonClicked(_ searchBar: UISearchBar) {
|
||||
deactivate(searchBar: searchBar)
|
||||
UIView.animate(withDuration: 0.25) {
|
||||
self.fpc.move(to: .half, animated: false)
|
||||
}
|
||||
}
|
||||
|
||||
func searchBarTextDidBeginEditing(_ searchBar: UISearchBar) {
|
||||
searchBar.showsCancelButton = true
|
||||
searchVC.showHeader(animated: true)
|
||||
searchVC.tableView.alpha = 1.0
|
||||
activate(searchBar: searchBar)
|
||||
UIView.animate(withDuration: 0.25) { [weak self] in
|
||||
self?.fpc.move(to: .full, animated: false)
|
||||
}
|
||||
|
||||
@@ -7,17 +7,32 @@
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
5442E22425FC51AF00A26F43 /* ImageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5442E22325FC51AF00A26F43 /* ImageViewController.swift */; };
|
||||
5442E22825FC51E200A26F43 /* MultiPanelController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5442E22725FC51E200A26F43 /* MultiPanelController.swift */; };
|
||||
5442E22C25FC521F00A26F43 /* SettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5442E22B25FC521F00A26F43 /* SettingsViewController.swift */; };
|
||||
5442E23025FC525200A26F43 /* TabBarViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5442E22F25FC525200A26F43 /* TabBarViewController.swift */; };
|
||||
5442E23425FC528400A26F43 /* DetailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5442E23325FC528400A26F43 /* DetailViewController.swift */; };
|
||||
5442E23A25FC52CD00A26F43 /* ModalViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5442E23925FC52CD00A26F43 /* ModalViewController.swift */; };
|
||||
5442E24025FC533800A26F43 /* DebugTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5442E23F25FC533800A26F43 /* DebugTableViewController.swift */; };
|
||||
5442E24425FC538200A26F43 /* InspectorViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5442E24325FC538200A26F43 /* InspectorViewController.swift */; };
|
||||
5442E24A25FC53C100A26F43 /* DebugTextViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5442E24925FC53C100A26F43 /* DebugTextViewController.swift */; };
|
||||
5442E25225FC541700A26F43 /* NestedScrollViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5442E25125FC541700A26F43 /* NestedScrollViewController.swift */; };
|
||||
54496C59263A7E5A0031E0C8 /* UseCaseController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54496C58263A7E5A0031E0C8 /* UseCaseController.swift */; };
|
||||
545DB9EE21511E6300CA77B8 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 545DB9ED21511E6300CA77B8 /* AppDelegate.swift */; };
|
||||
545DB9F021511E6300CA77B8 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 545DB9EF21511E6300CA77B8 /* ViewController.swift */; };
|
||||
545DB9F021511E6300CA77B8 /* MainViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 545DB9EF21511E6300CA77B8 /* MainViewController.swift */; };
|
||||
545DB9F321511E6300CA77B8 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 545DB9F121511E6300CA77B8 /* Main.storyboard */; };
|
||||
545DB9F521511E6400CA77B8 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 545DB9F421511E6400CA77B8 /* Assets.xcassets */; };
|
||||
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 */; };
|
||||
546341A125C6415100CA0596 /* UseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 546341A025C6415100CA0596 /* UseCase.swift */; };
|
||||
546341AC25C6426500CA0596 /* CustomState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 546341AB25C6426500CA0596 /* CustomState.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 */; };
|
||||
54CDC5D8215BBE23007D205C /* UIComponents.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CDC5D7215BBE23007D205C /* UIComponents.swift */; };
|
||||
54B51116216AFE5F0033A6F3 /* Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54B51115216AFE5F0033A6F3 /* Extensions.swift */; };
|
||||
54CDC5D8215BBE23007D205C /* Components.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CDC5D7215BBE23007D205C /* Components.swift */; };
|
||||
54EAD35B263A75EB006A36EA /* Layouts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54EAD35A263A75EB006A36EA /* Layouts.swift */; };
|
||||
54EAD365263A765F006A36EA /* PagePanelController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54EAD364263A765F006A36EA /* PagePanelController.swift */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
@@ -52,9 +67,20 @@
|
||||
/* End PBXCopyFilesBuildPhase section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
5442E22325FC51AF00A26F43 /* ImageViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageViewController.swift; sourceTree = "<group>"; };
|
||||
5442E22725FC51E200A26F43 /* MultiPanelController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultiPanelController.swift; sourceTree = "<group>"; };
|
||||
5442E22B25FC521F00A26F43 /* SettingsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewController.swift; sourceTree = "<group>"; };
|
||||
5442E22F25FC525200A26F43 /* TabBarViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabBarViewController.swift; sourceTree = "<group>"; };
|
||||
5442E23325FC528400A26F43 /* DetailViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailViewController.swift; sourceTree = "<group>"; };
|
||||
5442E23925FC52CD00A26F43 /* ModalViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModalViewController.swift; sourceTree = "<group>"; };
|
||||
5442E23F25FC533800A26F43 /* DebugTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebugTableViewController.swift; sourceTree = "<group>"; };
|
||||
5442E24325FC538200A26F43 /* InspectorViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InspectorViewController.swift; sourceTree = "<group>"; };
|
||||
5442E24925FC53C100A26F43 /* DebugTextViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebugTextViewController.swift; sourceTree = "<group>"; };
|
||||
5442E25125FC541700A26F43 /* NestedScrollViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NestedScrollViewController.swift; sourceTree = "<group>"; };
|
||||
54496C58263A7E5A0031E0C8 /* UseCaseController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UseCaseController.swift; sourceTree = "<group>"; };
|
||||
545DB9EA21511E6300CA77B8 /* Samples.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Samples.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
545DB9ED21511E6300CA77B8 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||
545DB9EF21511E6300CA77B8 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = "<group>"; };
|
||||
545DB9EF21511E6300CA77B8 /* MainViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainViewController.swift; sourceTree = "<group>"; };
|
||||
545DB9F221511E6300CA77B8 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
|
||||
545DB9F421511E6400CA77B8 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||
545DB9F721511E6400CA77B8 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
|
||||
@@ -65,9 +91,13 @@
|
||||
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>"; };
|
||||
546341A025C6415100CA0596 /* UseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UseCase.swift; sourceTree = "<group>"; };
|
||||
546341AB25C6426500CA0596 /* CustomState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomState.swift; 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>"; };
|
||||
54CDC5D7215BBE23007D205C /* UIComponents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIComponents.swift; sourceTree = "<group>"; };
|
||||
54B51115216AFE5F0033A6F3 /* Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Extensions.swift; sourceTree = "<group>"; };
|
||||
54CDC5D7215BBE23007D205C /* Components.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Components.swift; sourceTree = "<group>"; };
|
||||
54EAD35A263A75EB006A36EA /* Layouts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Layouts.swift; sourceTree = "<group>"; };
|
||||
54EAD364263A765F006A36EA /* PagePanelController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PagePanelController.swift; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
@@ -96,6 +126,23 @@
|
||||
/* End PBXFrameworksBuildPhase section */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
5442E22225FC519700A26F43 /* ViewControllers */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
5442E23F25FC533800A26F43 /* DebugTableViewController.swift */,
|
||||
5442E24925FC53C100A26F43 /* DebugTextViewController.swift */,
|
||||
5442E23325FC528400A26F43 /* DetailViewController.swift */,
|
||||
5442E24325FC538200A26F43 /* InspectorViewController.swift */,
|
||||
5442E22325FC51AF00A26F43 /* ImageViewController.swift */,
|
||||
5442E25125FC541700A26F43 /* NestedScrollViewController.swift */,
|
||||
5442E23925FC52CD00A26F43 /* ModalViewController.swift */,
|
||||
5442E22725FC51E200A26F43 /* MultiPanelController.swift */,
|
||||
5442E22B25FC521F00A26F43 /* SettingsViewController.swift */,
|
||||
5442E22F25FC525200A26F43 /* TabBarViewController.swift */,
|
||||
);
|
||||
path = ViewControllers;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
545DB9E121511E6300CA77B8 = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@@ -124,9 +171,13 @@
|
||||
545DB9F621511E6400CA77B8 /* LaunchScreen.storyboard */,
|
||||
545DB9F121511E6300CA77B8 /* Main.storyboard */,
|
||||
545DB9ED21511E6300CA77B8 /* AppDelegate.swift */,
|
||||
545DB9EF21511E6300CA77B8 /* ViewController.swift */,
|
||||
54B51115216AFE5F0033A6F3 /* UIExtensions.swift */,
|
||||
54CDC5D7215BBE23007D205C /* UIComponents.swift */,
|
||||
545DB9EF21511E6300CA77B8 /* MainViewController.swift */,
|
||||
54496C58263A7E5A0031E0C8 /* UseCaseController.swift */,
|
||||
546341AA25C6421000CA0596 /* UseCases */,
|
||||
5442E22225FC519700A26F43 /* ViewControllers */,
|
||||
54EAD35A263A75EB006A36EA /* Layouts.swift */,
|
||||
54B51115216AFE5F0033A6F3 /* Extensions.swift */,
|
||||
54CDC5D7215BBE23007D205C /* Components.swift */,
|
||||
545DB9F921511E6400CA77B8 /* Info.plist */,
|
||||
);
|
||||
path = Sources;
|
||||
@@ -150,6 +201,16 @@
|
||||
path = UITests;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
546341AA25C6421000CA0596 /* UseCases */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
546341A025C6415100CA0596 /* UseCase.swift */,
|
||||
546341AB25C6426500CA0596 /* CustomState.swift */,
|
||||
54EAD364263A765F006A36EA /* PagePanelController.swift */,
|
||||
);
|
||||
path = UseCases;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXNativeTarget section */
|
||||
@@ -303,10 +364,25 @@
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
54CDC5D8215BBE23007D205C /* UIComponents.swift in Sources */,
|
||||
54B51116216AFE5F0033A6F3 /* UIExtensions.swift in Sources */,
|
||||
545DB9F021511E6300CA77B8 /* ViewController.swift in Sources */,
|
||||
5442E23425FC528400A26F43 /* DetailViewController.swift in Sources */,
|
||||
54496C59263A7E5A0031E0C8 /* UseCaseController.swift in Sources */,
|
||||
54CDC5D8215BBE23007D205C /* Components.swift in Sources */,
|
||||
54B51116216AFE5F0033A6F3 /* Extensions.swift in Sources */,
|
||||
5442E24A25FC53C100A26F43 /* DebugTextViewController.swift in Sources */,
|
||||
546341AC25C6426500CA0596 /* CustomState.swift in Sources */,
|
||||
5442E23A25FC52CD00A26F43 /* ModalViewController.swift in Sources */,
|
||||
5442E22425FC51AF00A26F43 /* ImageViewController.swift in Sources */,
|
||||
5442E24025FC533800A26F43 /* DebugTableViewController.swift in Sources */,
|
||||
5442E25225FC541700A26F43 /* NestedScrollViewController.swift in Sources */,
|
||||
5442E22825FC51E200A26F43 /* MultiPanelController.swift in Sources */,
|
||||
546341A125C6415100CA0596 /* UseCase.swift in Sources */,
|
||||
545DB9F021511E6300CA77B8 /* MainViewController.swift in Sources */,
|
||||
545DB9EE21511E6300CA77B8 /* AppDelegate.swift in Sources */,
|
||||
5442E23025FC525200A26F43 /* TabBarViewController.swift in Sources */,
|
||||
54EAD35B263A75EB006A36EA /* Layouts.swift in Sources */,
|
||||
54EAD365263A765F006A36EA /* PagePanelController.swift in Sources */,
|
||||
5442E24425FC538200A26F43 /* InspectorViewController.swift in Sources */,
|
||||
5442E22C25FC521F00A26F43 /* SettingsViewController.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "IMG_0003.jpg",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 202 KiB |
@@ -1,11 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="17156" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="RoN-h0-uBD">
|
||||
<device id="retina5_9" orientation="portrait" appearance="light"/>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="17701" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="RoN-h0-uBD">
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="17125"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="17703"/>
|
||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||
<capability name="Stack View standard spacing" minToolsVersion="9.0"/>
|
||||
<capability name="System colors in document resources" minToolsVersion="11.0"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
@@ -28,24 +27,24 @@
|
||||
<!--Samples-->
|
||||
<scene sceneID="35L-Gs-Vts">
|
||||
<objects>
|
||||
<viewController id="jF4-A0-Eq6" customClass="SampleListViewController" customModule="Samples" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<viewController id="jF4-A0-Eq6" customClass="MainViewController" customModule="Samples" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<view key="view" contentMode="scaleToFill" id="Smh-Bd-AAc">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="812"/>
|
||||
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
|
||||
<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="600" height="600"/>
|
||||
<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="600" height="43.5"/>
|
||||
<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="600" height="43.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Title" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="M0G-C8-hAO">
|
||||
<rect key="frame" x="16" y="0.0" width="343" height="43.666667938232422"/>
|
||||
<rect key="frame" x="16" y="0.0" width="568" height="43.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||
<nil key="textColor"/>
|
||||
@@ -86,23 +85,23 @@
|
||||
<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" spacing="16" translatesAutoresizingMaskIntoConstraints="NO" id="n93-ZL-fmC">
|
||||
<rect key="frame" x="32" y="16" width="311" height="149.33333333333334"/>
|
||||
<rect key="frame" x="32" y="16" width="311" height="149.5"/>
|
||||
<subviews>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" distribution="fillProportionally" alignment="center" spacing="8" translatesAutoresizingMaskIntoConstraints="NO" id="DCi-Iv-o6d">
|
||||
<rect key="frame" x="0.0" y="0.0" width="311" height="52.666666666666664"/>
|
||||
<rect key="frame" x="0.0" y="0.0" width="311" height="53"/>
|
||||
<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="17.333333333333332"/>
|
||||
<rect key="frame" x="118.5" y="0.0" width="74.5" height="17.5"/>
|
||||
<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="78.333333333333329" y="25.333333333333336" width="154.66666666666669" height="27.333333333333336"/>
|
||||
<rect key="frame" x="78.5" y="25.5" width="154.5" height="27.5"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleTitle2"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
@@ -110,19 +109,19 @@
|
||||
</subviews>
|
||||
</stackView>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" distribution="fillProportionally" spacing="16" translatesAutoresizingMaskIntoConstraints="NO" id="J8j-7w-yCZ">
|
||||
<rect key="frame" x="0.0" y="68.666666666666657" width="311" height="80.666666666666657"/>
|
||||
<rect key="frame" x="0.0" y="69" width="311" height="80.5"/>
|
||||
<subviews>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" alignment="center" spacingType="standard" translatesAutoresizingMaskIntoConstraints="NO" id="uEf-g4-CeU">
|
||||
<rect key="frame" x="0.0" y="0.0" width="311" height="32"/>
|
||||
<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="5.9999999999999982" width="254" height="20.333333333333329"/>
|
||||
<rect key="frame" x="0.0" y="5.5" width="254" height="20.5"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleHeadline"/>
|
||||
<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="262" y="0.66666666666665719" width="51" height="31"/>
|
||||
<rect key="frame" x="262" y="0.5" width="51" height="31"/>
|
||||
<connections>
|
||||
<action selector="toggleLargeTitle:" destination="C1X-9Z-TyQ" eventType="valueChanged" id="FJS-Ty-mCY"/>
|
||||
</connections>
|
||||
@@ -130,16 +129,16 @@
|
||||
</subviews>
|
||||
</stackView>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" alignment="center" spacingType="standard" translatesAutoresizingMaskIntoConstraints="NO" id="ZtZ-Dz-4cC">
|
||||
<rect key="frame" x="0.0" y="47.999999999999986" width="311" height="32.666666666666671"/>
|
||||
<rect key="frame" x="0.0" y="48" width="311" height="32.5"/>
|
||||
<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="6.333333333333341" width="254" height="20.333333333333329"/>
|
||||
<rect key="frame" x="0.0" y="6" width="254" height="20.5"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleHeadline"/>
|
||||
<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="262" y="1" width="51" height="31"/>
|
||||
<rect key="frame" x="262" y="0.5" width="51" height="31"/>
|
||||
<connections>
|
||||
<action selector="toggleTranslucent:" destination="C1X-9Z-TyQ" eventType="valueChanged" id="nL4-3L-9hh"/>
|
||||
</connections>
|
||||
@@ -176,11 +175,11 @@
|
||||
<objects>
|
||||
<viewController id="RpE-lI-27a" customClass="TabBarContentViewController" customModule="Samples" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<view key="view" contentMode="scaleToFill" id="JER-jz-KSq">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="812"/>
|
||||
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="IvG-yp-yzI">
|
||||
<rect key="frame" x="20" y="44" width="39" height="30"/>
|
||||
<rect key="frame" x="20" y="0.0" width="39" height="30"/>
|
||||
<state key="normal" title="Close"/>
|
||||
<connections>
|
||||
<action selector="closeWithSender:" destination="RpE-lI-27a" eventType="touchUpInside" id="hj3-Xv-6Gq"/>
|
||||
@@ -207,11 +206,11 @@
|
||||
<objects>
|
||||
<viewController id="pOk-Zm-vD9" customClass="TabBarContentViewController" customModule="Samples" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<view key="view" contentMode="scaleToFill" id="85d-ub-G8k">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="812"/>
|
||||
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="NbG-e8-HdI">
|
||||
<rect key="frame" x="20" y="44" width="39" height="30"/>
|
||||
<rect key="frame" x="20" y="0.0" width="39" height="30"/>
|
||||
<state key="normal" title="Close"/>
|
||||
<connections>
|
||||
<action selector="closeWithSender:" destination="pOk-Zm-vD9" eventType="touchUpInside" id="111-PD-Pop"/>
|
||||
@@ -238,11 +237,11 @@
|
||||
<objects>
|
||||
<viewController id="lto-Zc-Vtp" customClass="TabBarContentViewController" customModule="Samples" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<view key="view" contentMode="scaleToFill" id="ji9-Ez-N7i">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="812"/>
|
||||
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="eFN-tN-4Ct">
|
||||
<rect key="frame" x="20" y="44" width="39" height="30"/>
|
||||
<rect key="frame" x="20" y="0.0" width="39" height="30"/>
|
||||
<state key="normal" title="Close"/>
|
||||
<connections>
|
||||
<action selector="closeWithSender:" destination="bYI-y3-Rzb" eventType="touchUpInside" id="YL4-GP-ZEZ"/>
|
||||
@@ -273,7 +272,7 @@
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="1000" verticalHuggingPriority="1000" horizontalCompressionResistancePriority="1000" verticalCompressionResistancePriority="1000" text="Change this text" textAlignment="center" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="ge4-RW-Gmz">
|
||||
<rect key="frame" x="125.66666666666669" y="24" width="124" height="20.333333333333329"/>
|
||||
<rect key="frame" x="125.5" y="24" width="124" height="20.5"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
@@ -293,6 +292,71 @@
|
||||
</objects>
|
||||
<point key="canvasLocation" x="2753" y="734"/>
|
||||
</scene>
|
||||
<!--Image View Controller-->
|
||||
<scene sceneID="NAI-Rh-ZQ6">
|
||||
<objects>
|
||||
<viewController storyboardIdentifier="ImageViewController" id="VWY-cF-RoY" customClass="ImageViewController" customModule="Samples" sceneMemberID="viewController">
|
||||
<view key="view" contentMode="scaleToFill" id="dAf-gD-ghB">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="778"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" translatesAutoresizingMaskIntoConstraints="NO" id="Gs4-S6-Goh">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="778"/>
|
||||
<subviews>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="t7x-eG-MKh">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="49"/>
|
||||
<color key="backgroundColor" systemColor="systemOrangeColor"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="49" id="DmG-pt-gij"/>
|
||||
</constraints>
|
||||
</view>
|
||||
<scrollView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" alwaysBounceHorizontal="YES" showsHorizontalScrollIndicator="NO" showsVerticalScrollIndicator="NO" translatesAutoresizingMaskIntoConstraints="NO" id="kRA-qy-GpJ">
|
||||
<rect key="frame" x="0.0" y="49" width="375" height="680"/>
|
||||
<subviews>
|
||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFill" horizontalHuggingPriority="1000" verticalHuggingPriority="1000" image="IMG_0003" translatesAutoresizingMaskIntoConstraints="NO" id="rGf-jW-WNf">
|
||||
<rect key="frame" x="0.0" y="0.0" width="750" height="501"/>
|
||||
<color key="backgroundColor" systemColor="systemPurpleColor"/>
|
||||
</imageView>
|
||||
</subviews>
|
||||
<color key="backgroundColor" red="0.0078431372550000003" green="0.72156862749999995" blue="0.45882352939999999" alpha="1" colorSpace="calibratedRGB"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="trailing" secondItem="rGf-jW-WNf" secondAttribute="trailing" id="472-Be-cUl"/>
|
||||
<constraint firstAttribute="bottom" secondItem="rGf-jW-WNf" secondAttribute="bottom" id="ncs-tN-3Wx"/>
|
||||
<constraint firstItem="rGf-jW-WNf" firstAttribute="leading" secondItem="kRA-qy-GpJ" secondAttribute="leading" id="rlv-5Y-utR"/>
|
||||
<constraint firstItem="rGf-jW-WNf" firstAttribute="top" secondItem="kRA-qy-GpJ" secondAttribute="top" id="zum-Zl-Wzz"/>
|
||||
</constraints>
|
||||
</scrollView>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="SEa-7Y-wa9">
|
||||
<rect key="frame" x="0.0" y="729" width="375" height="49"/>
|
||||
<color key="backgroundColor" systemColor="systemTealColor"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="49" id="3mS-zi-8BP"/>
|
||||
</constraints>
|
||||
</view>
|
||||
</subviews>
|
||||
</stackView>
|
||||
</subviews>
|
||||
<viewLayoutGuide key="safeArea" id="hCg-v5-nJs"/>
|
||||
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="trailing" secondItem="Gs4-S6-Goh" secondAttribute="trailing" id="6z7-Md-pxr"/>
|
||||
<constraint firstAttribute="bottom" secondItem="Gs4-S6-Goh" secondAttribute="bottom" id="PcQ-bu-yT3"/>
|
||||
<constraint firstItem="Gs4-S6-Goh" firstAttribute="top" secondItem="dAf-gD-ghB" secondAttribute="top" id="zGx-Wd-hjz"/>
|
||||
<constraint firstItem="Gs4-S6-Goh" firstAttribute="leading" secondItem="dAf-gD-ghB" secondAttribute="leading" id="zxi-Ty-U7L"/>
|
||||
</constraints>
|
||||
</view>
|
||||
<size key="freeformSize" width="375" height="778"/>
|
||||
<connections>
|
||||
<outlet property="footerView" destination="SEa-7Y-wa9" id="Gzj-dP-YXl"/>
|
||||
<outlet property="headerView" destination="t7x-eG-MKh" id="njM-un-U8q"/>
|
||||
<outlet property="scrollView" destination="kRA-qy-GpJ" id="iWC-o4-APi"/>
|
||||
<outlet property="stackView" destination="Gs4-S6-Goh" id="f1D-bO-mjr"/>
|
||||
</connections>
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="pnR-69-Ek4" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="3388" y="734.48275862068965"/>
|
||||
</scene>
|
||||
<!--Tab Bar View Controller-->
|
||||
<scene sceneID="nQ5-PV-qFw">
|
||||
<objects>
|
||||
@@ -317,11 +381,11 @@
|
||||
<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="758" width="375" height="0.0"/>
|
||||
<color key="backgroundColor" white="0.66666666666666663" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
</view>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="sbF-Az-7sy">
|
||||
@@ -332,7 +396,7 @@
|
||||
</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="134.5" y="88" width="106" height="326"/>
|
||||
<subviews>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="i9x-x5-n1q">
|
||||
<rect key="frame" x="0.0" y="0.0" width="80" height="30"/>
|
||||
@@ -532,11 +596,11 @@
|
||||
</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="0.0" width="375" height="778"/>
|
||||
<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="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="noi-1a-5bZ" customClass="CloseButton" customModule="Samples" customModuleProvider="target">
|
||||
<rect key="frame" x="319" y="44" width="44" height="44"/>
|
||||
<rect key="frame" x="319" y="0.0" width="44" height="44"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="44" id="0jg-5D-A1F"/>
|
||||
<constraint firstAttribute="width" constant="44" id="1Cq-PA-wgW"/>
|
||||
@@ -546,10 +610,10 @@
|
||||
</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" height="31"/>
|
||||
<rect key="frame" x="8" y="8" width="148" 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" height="20.333333333333332"/>
|
||||
<rect key="frame" x="0.0" y="5.5" width="91" height="20.5"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
@@ -563,7 +627,7 @@
|
||||
</subviews>
|
||||
</stackView>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="22" translatesAutoresizingMaskIntoConstraints="NO" id="tP3-oJ-4EB">
|
||||
<rect key="frame" x="130.66666666666666" y="88" width="114" height="134"/>
|
||||
<rect key="frame" x="130.5" y="88" width="114" height="134"/>
|
||||
<subviews>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="c5r-jU-haj">
|
||||
<rect key="frame" x="0.0" y="0.0" width="114" height="30"/>
|
||||
@@ -717,12 +781,22 @@ Section 1.10.33 of "de Finibus Bonorum et Malorum", written by Cicero in 45 BC
|
||||
<point key="canvasLocation" x="-1" y="734"/>
|
||||
</scene>
|
||||
</scenes>
|
||||
<designables>
|
||||
<designable name="noi-1a-5bZ">
|
||||
<size key="intrinsicContentSize" width="30" height="30"/>
|
||||
</designable>
|
||||
</designables>
|
||||
<inferredMetricsTieBreakers>
|
||||
<segue reference="r1P-2i-NDe"/>
|
||||
</inferredMetricsTieBreakers>
|
||||
<resources>
|
||||
<image name="IMG_0003" width="750" height="501"/>
|
||||
<systemColor name="systemBackgroundColor">
|
||||
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
</systemColor>
|
||||
<systemColor name="systemOrangeColor">
|
||||
<color red="1" green="0.58431372549019611" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
</systemColor>
|
||||
<systemColor name="systemPurpleColor">
|
||||
<color red="0.68627450980392157" green="0.32156862745098042" blue="0.87058823529411766" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
</systemColor>
|
||||
<systemColor name="systemTealColor">
|
||||
<color red="0.35294117647058826" green="0.78431372549019607" blue="0.98039215686274506" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
</systemColor>
|
||||
</resources>
|
||||
</document>
|
||||
|
||||
+3
-3
@@ -3,7 +3,7 @@
|
||||
import UIKit
|
||||
|
||||
@IBDesignable
|
||||
class CloseButton: UIButton {
|
||||
final class CloseButton: UIButton {
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
super.init(coder: aDecoder)
|
||||
render()
|
||||
@@ -71,7 +71,7 @@ class CloseButton: UIButton {
|
||||
}
|
||||
|
||||
@IBDesignable
|
||||
class SafeAreaView: UIView {
|
||||
final class SafeAreaView: UIView {
|
||||
override func prepareForInterfaceBuilder() {
|
||||
let label = UILabel()
|
||||
label.text = "Safe Area"
|
||||
@@ -86,7 +86,7 @@ class SafeAreaView: UIView {
|
||||
|
||||
|
||||
@IBDesignable
|
||||
class OnSafeAreaView: UIView {
|
||||
final class OnSafeAreaView: UIView {
|
||||
override func prepareForInterfaceBuilder() {
|
||||
let label = UILabel()
|
||||
label.text = "On Safe Area"
|
||||
@@ -0,0 +1,90 @@
|
||||
// Copyright 2018 the FloatingPanel authors. All rights reserved. MIT license.
|
||||
|
||||
import UIKit
|
||||
import FloatingPanel
|
||||
|
||||
/**
|
||||
- Attention: `FloatingPanelLayout` must not be applied by the parent view
|
||||
controller of a panel. But here `MainViewController` adopts it
|
||||
purposely to check if the library prints an appropriate warning.
|
||||
*/
|
||||
extension MainViewController: FloatingPanelLayout {
|
||||
var position: FloatingPanelPosition { .bottom }
|
||||
var initialState: FloatingPanelState { .half }
|
||||
var anchors: [FloatingPanelState: FloatingPanelLayoutAnchoring] {
|
||||
return [
|
||||
.full: FloatingPanelLayoutAnchor(absoluteInset: UIScreen.main.bounds.height == 667.0 ? 18.0 : 16.0, edge: .top, referenceGuide: .safeArea),
|
||||
.half: FloatingPanelLayoutAnchor(absoluteInset: 262.0, edge: .bottom, referenceGuide: .safeArea),
|
||||
.tip: FloatingPanelLayoutAnchor(absoluteInset: 69.0, edge: .bottom, referenceGuide: .safeArea)
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
class TopPositionedPanelLayout: FloatingPanelLayout {
|
||||
let position: FloatingPanelPosition = .top
|
||||
let initialState: FloatingPanelState = .full
|
||||
|
||||
var anchors: [FloatingPanelState : FloatingPanelLayoutAnchoring] {
|
||||
return [
|
||||
.full: FloatingPanelLayoutAnchor(absoluteInset: 88.0, edge: .bottom, referenceGuide: .safeArea),
|
||||
.half: FloatingPanelLayoutAnchor(absoluteInset: 216.0, edge: .top, referenceGuide: .safeArea),
|
||||
.tip: FloatingPanelLayoutAnchor(absoluteInset: 44.0, edge: .top, referenceGuide: .safeArea)
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
class IntrinsicPanelLayout: FloatingPanelBottomLayout {
|
||||
override var initialState: FloatingPanelState { .full }
|
||||
override var anchors: [FloatingPanelState : FloatingPanelLayoutAnchoring] {
|
||||
return [
|
||||
.full: FloatingPanelIntrinsicLayoutAnchor(fractionalOffset: 0.0, referenceGuide: .safeArea)
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
class RemovablePanelLayout: FloatingPanelLayout {
|
||||
let position: FloatingPanelPosition = .bottom
|
||||
let initialState: FloatingPanelState = .half
|
||||
|
||||
var anchors: [FloatingPanelState : FloatingPanelLayoutAnchoring] {
|
||||
return [
|
||||
.full: FloatingPanelIntrinsicLayoutAnchor(fractionalOffset: 0.0, referenceGuide: .safeArea),
|
||||
.half: FloatingPanelLayoutAnchor(absoluteInset: 130.0, edge: .bottom, referenceGuide: .safeArea)
|
||||
]
|
||||
}
|
||||
|
||||
func backdropAlpha(for state: FloatingPanelState) -> CGFloat {
|
||||
return 0.3
|
||||
}
|
||||
}
|
||||
|
||||
class RemovablePanelLandscapeLayout: FloatingPanelLayout {
|
||||
let position: FloatingPanelPosition = .bottom
|
||||
let initialState: FloatingPanelState = .full
|
||||
|
||||
var anchors: [FloatingPanelState : FloatingPanelLayoutAnchoring] {
|
||||
return [
|
||||
.full: FloatingPanelIntrinsicLayoutAnchor(fractionalOffset: 0.0, referenceGuide: .safeArea),
|
||||
.half: FloatingPanelLayoutAnchor(absoluteInset: 216.0, edge: .bottom, referenceGuide: .safeArea)
|
||||
]
|
||||
}
|
||||
|
||||
func backdropAlpha(for state: FloatingPanelState) -> CGFloat {
|
||||
return 0.3
|
||||
}
|
||||
}
|
||||
|
||||
class ModalPanelLayout: FloatingPanelLayout {
|
||||
let position: FloatingPanelPosition = .bottom
|
||||
let initialState: FloatingPanelState = .full
|
||||
|
||||
var anchors: [FloatingPanelState : FloatingPanelLayoutAnchoring] {
|
||||
return [
|
||||
.full: FloatingPanelIntrinsicLayoutAnchor(absoluteOffset: 0.0, referenceGuide: .safeArea),
|
||||
]
|
||||
}
|
||||
|
||||
func backdropAlpha(for state: FloatingPanelState) -> CGFloat {
|
||||
return 0.3
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
// Copyright 2018 the FloatingPanel authors. All rights reserved. MIT license.
|
||||
|
||||
import UIKit
|
||||
import FloatingPanel
|
||||
|
||||
final class MainViewController: UIViewController {
|
||||
@IBOutlet weak var tableView: UITableView!
|
||||
private var observations: [NSKeyValueObservation] = []
|
||||
|
||||
private lazy var useCaseController = UseCaseController(mainVC: self)
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
tableView.dataSource = self
|
||||
tableView.delegate = self
|
||||
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "Cell")
|
||||
automaticallyAdjustsScrollViewInsets = false
|
||||
|
||||
let searchController = UISearchController(searchResultsController: nil)
|
||||
if #available(iOS 11.0, *) {
|
||||
navigationItem.searchController = searchController
|
||||
navigationItem.hidesSearchBarWhenScrolling = false
|
||||
navigationItem.largeTitleDisplayMode = .automatic
|
||||
} else {
|
||||
// Fallback on earlier versions
|
||||
}
|
||||
var insets = UIEdgeInsets.zero
|
||||
insets.bottom += 69.0
|
||||
tableView.contentInset = insets
|
||||
|
||||
// Show the initial panel
|
||||
useCaseController.set(useCase: .trackingTableView)
|
||||
}
|
||||
|
||||
override func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
if #available(iOS 11.0, *) {
|
||||
if let observation = navigationController?.navigationBar.observe(\.prefersLargeTitles, changeHandler: { (bar, _) in
|
||||
self.tableView.reloadData()
|
||||
}) {
|
||||
observations.append(observation)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override func viewWillDisappear(_ animated: Bool) {
|
||||
super.viewWillDisappear(animated)
|
||||
observations.removeAll()
|
||||
}
|
||||
|
||||
// MARK:- Actions
|
||||
@IBAction func showDebugMenu(_ sender: UIBarButtonItem) {
|
||||
useCaseController.setUpSettingsPanel(for: self)
|
||||
}
|
||||
}
|
||||
|
||||
extension MainViewController: UITableViewDataSource {
|
||||
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
if #available(iOS 11.0, *) {
|
||||
if navigationController?.navigationBar.prefersLargeTitles == true {
|
||||
return UseCase.allCases.count + 30
|
||||
} else {
|
||||
return UseCase.allCases.count
|
||||
}
|
||||
} else {
|
||||
return UseCase.allCases.count
|
||||
}
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
|
||||
if UseCase.allCases.count > indexPath.row {
|
||||
let menu = UseCase.allCases[indexPath.row]
|
||||
cell.textLabel?.text = menu.name
|
||||
} else {
|
||||
cell.textLabel?.text = "\(indexPath.row) row"
|
||||
}
|
||||
return cell
|
||||
}
|
||||
}
|
||||
|
||||
extension MainViewController: UITableViewDelegate {
|
||||
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
guard UseCase.allCases.count > indexPath.row else { return }
|
||||
|
||||
// Change panels
|
||||
useCaseController.set(useCase: UseCase.allCases[indexPath.row])
|
||||
}
|
||||
|
||||
@objc func dismissPresentedVC() {
|
||||
self.presentedViewController?.dismiss(animated: true, completion: nil)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,329 @@
|
||||
// Copyright 2018-Present Shin Yamamoto. All rights reserved. MIT license.
|
||||
|
||||
import UIKit
|
||||
import FloatingPanel
|
||||
|
||||
final class UseCaseController: NSObject {
|
||||
unowned let mainVC: MainViewController
|
||||
private(set) var useCase: UseCase = .trackingTableView
|
||||
|
||||
fileprivate var mainPanelVC: FloatingPanelController!
|
||||
private var detailPanelVC: FloatingPanelController!
|
||||
private var settingsPanelVC: FloatingPanelController!
|
||||
|
||||
private lazy var pagePanelController = PagePanelController()
|
||||
|
||||
private var mainPanelObserves: [NSKeyValueObservation] = []
|
||||
|
||||
init(mainVC: MainViewController) {
|
||||
self.mainVC = mainVC
|
||||
}
|
||||
|
||||
func set(useCase: UseCase) {
|
||||
self.useCase = useCase
|
||||
|
||||
let contentVC = useCase.makeContentViewController(with: mainVC.storyboard!)
|
||||
|
||||
detailPanelVC?.removePanelFromParent(animated: true, completion: nil)
|
||||
detailPanelVC = nil
|
||||
|
||||
switch useCase {
|
||||
case .showDetail:
|
||||
detailPanelVC?.removePanelFromParent(animated: false)
|
||||
|
||||
// Initialize FloatingPanelController
|
||||
detailPanelVC = FloatingPanelController()
|
||||
detailPanelVC.delegate = self
|
||||
|
||||
let appearance = SurfaceAppearance()
|
||||
appearance.cornerRadius = 6.0
|
||||
detailPanelVC.surfaceView.appearance = appearance
|
||||
|
||||
// Set a content view controller
|
||||
detailPanelVC.set(contentViewController: contentVC)
|
||||
|
||||
detailPanelVC.contentMode = .fitToBounds
|
||||
(contentVC as? DetailViewController)?.intrinsicHeightConstraint.isActive = false
|
||||
|
||||
// Add FloatingPanel to self.view
|
||||
detailPanelVC.addPanel(toParent: mainVC, animated: true)
|
||||
case .showModal, .showTabBar:
|
||||
let modalVC = contentVC
|
||||
modalVC.modalPresentationStyle = .fullScreen
|
||||
mainVC.present(modalVC, animated: true, completion: nil)
|
||||
|
||||
case .showPageView:
|
||||
let pageVC = pagePanelController.makePageViewController(for: mainVC)
|
||||
mainVC.present(pageVC, animated: true, completion: nil)
|
||||
|
||||
case .showPageContentView:
|
||||
let pageVC = pagePanelController.makePageViewControllerForContent()
|
||||
self.addMainPanel(with: pageVC)
|
||||
case .showPanelModal:
|
||||
let fpc = FloatingPanelController()
|
||||
let contentVC = mainVC.storyboard!.instantiateViewController(withIdentifier: "DetailViewController")
|
||||
contentVC.loadViewIfNeeded()
|
||||
(contentVC as? DetailViewController)?.modeChangeView.isHidden = true
|
||||
fpc.set(contentViewController: contentVC)
|
||||
fpc.delegate = self
|
||||
|
||||
let appearance = SurfaceAppearance()
|
||||
appearance.cornerRadius = 38.5
|
||||
fpc.surfaceView.appearance = appearance
|
||||
fpc.backdropView.dismissalTapGestureRecognizer.isEnabled = true
|
||||
|
||||
fpc.isRemovalInteractionEnabled = true
|
||||
|
||||
mainVC.present(fpc, animated: true, completion: nil)
|
||||
|
||||
case .showMultiPanelModal:
|
||||
let fpc = MultiPanelController()
|
||||
mainVC.present(fpc, animated: true, completion: nil)
|
||||
|
||||
case .showPanelInSheetModal:
|
||||
let fpc = FloatingPanelController()
|
||||
let contentVC = UIViewController()
|
||||
fpc.set(contentViewController: contentVC)
|
||||
fpc.delegate = self
|
||||
|
||||
let apprearance = SurfaceAppearance()
|
||||
apprearance.cornerRadius = 38.5
|
||||
apprearance.shadows = []
|
||||
fpc.surfaceView.appearance = apprearance
|
||||
fpc.isRemovalInteractionEnabled = true
|
||||
|
||||
let mvc = UIViewController()
|
||||
mvc.view.backgroundColor = UIColor(displayP3Red: 2/255, green: 184/255, blue: 117/255, alpha: 1.0)
|
||||
fpc.addPanel(toParent: mvc)
|
||||
mainVC.present(mvc, animated: true, completion: nil)
|
||||
case .showContentInset:
|
||||
let contentViewController = UIViewController()
|
||||
contentViewController.view.backgroundColor = .green
|
||||
|
||||
let fpc = FloatingPanelController()
|
||||
fpc.set(contentViewController: contentViewController)
|
||||
fpc.surfaceView.contentPadding = .init(top: 20, left: 20, bottom: 20, right: 20)
|
||||
|
||||
fpc.delegate = self
|
||||
fpc.isRemovalInteractionEnabled = true
|
||||
mainVC.present(fpc, animated: true, completion: nil)
|
||||
|
||||
case .showContainerMargins:
|
||||
let fpc = FloatingPanelController()
|
||||
|
||||
let appearance = SurfaceAppearance()
|
||||
appearance.cornerRadius = 38.5
|
||||
fpc.surfaceView.appearance = appearance
|
||||
|
||||
fpc.surfaceView.backgroundColor = .red
|
||||
fpc.surfaceView.containerMargins = .init(top: 24.0, left: 8.0, bottom: max(mainVC.layoutInsets.bottom, 8.0), right: 8.0)
|
||||
#if swift(>=5.1) // Actually Xcode 11 or later
|
||||
if #available(iOS 13.0, *) {
|
||||
fpc.surfaceView.layer.cornerCurve = .continuous
|
||||
}
|
||||
#endif
|
||||
|
||||
fpc.delegate = self
|
||||
fpc.isRemovalInteractionEnabled = true
|
||||
mainVC.present(fpc, animated: true, completion: nil)
|
||||
default:
|
||||
self.addMainPanel(with: contentVC)
|
||||
}
|
||||
}
|
||||
|
||||
private func addMainPanel(with contentVC: UIViewController) {
|
||||
mainPanelObserves.removeAll()
|
||||
|
||||
let oldMainPanelVC = mainPanelVC
|
||||
|
||||
mainPanelVC = FloatingPanelController()
|
||||
mainPanelVC.delegate = self
|
||||
mainPanelVC.contentInsetAdjustmentBehavior = .always
|
||||
|
||||
let appearance = SurfaceAppearance()
|
||||
appearance.cornerRadius = 6.0
|
||||
mainPanelVC.surfaceView.appearance = appearance
|
||||
|
||||
set(contentViewController: contentVC)
|
||||
|
||||
useCase.setUpInteraction(for: self)
|
||||
|
||||
// Add FloatingPanel to self.view
|
||||
if let oldMainPanelVC = oldMainPanelVC {
|
||||
oldMainPanelVC.removePanelFromParent(animated: true, completion: {
|
||||
self.mainPanelVC.addPanel(toParent: self.mainVC, animated: true)
|
||||
})
|
||||
} else {
|
||||
mainPanelVC.addPanel(toParent: mainVC, animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
private func set(contentViewController contentVC: UIViewController) {
|
||||
mainPanelVC.set(contentViewController: contentVC)
|
||||
// Track a scroll view
|
||||
switch contentVC {
|
||||
case let consoleVC as DebugTextViewController:
|
||||
mainPanelVC.track(scrollView: consoleVC.textView)
|
||||
|
||||
case let contentVC as DebugTableViewController:
|
||||
let ob = contentVC.tableView.observe(\.isEditing) { (tableView, _) in
|
||||
self.mainPanelVC.panGestureRecognizer.isEnabled = !tableView.isEditing
|
||||
}
|
||||
mainPanelObserves.append(ob)
|
||||
mainPanelVC.track(scrollView: contentVC.tableView)
|
||||
case let contentVC as NestedScrollViewController:
|
||||
mainPanelVC.track(scrollView: contentVC.scrollView)
|
||||
case let navVC as UINavigationController:
|
||||
if let rootVC = (navVC.topViewController as? MainViewController) {
|
||||
rootVC.loadViewIfNeeded()
|
||||
mainPanelVC.track(scrollView: rootVC.tableView)
|
||||
}
|
||||
case let contentVC as ImageViewController:
|
||||
if #available(iOS 11.0, *) {
|
||||
let mode: ImageViewController.Mode = (useCase == .showAdaptivePanelWithCustomGuide) ? .withHeaderFooter : .onlyImage
|
||||
let layoutGuide = contentVC.layoutGuideFor(mode: mode)
|
||||
mainPanelVC.layout = ImageViewController.PanelLayout(targetGuide: layoutGuide)
|
||||
} else {
|
||||
mainPanelVC.layout = ImageViewController.PanelLayout(targetGuide: nil)
|
||||
}
|
||||
mainPanelVC.delegate = nil
|
||||
mainPanelVC.isRemovalInteractionEnabled = true
|
||||
mainPanelVC.track(scrollView: contentVC.scrollView)
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
@objc
|
||||
fileprivate func handleSurface(tapGesture: UITapGestureRecognizer) {
|
||||
switch mainPanelVC.state {
|
||||
case .full:
|
||||
mainPanelVC.move(to: .half, animated: true)
|
||||
default:
|
||||
mainPanelVC.move(to: .full, animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
func setUpSettingsPanel(for mainVC: MainViewController) {
|
||||
guard settingsPanelVC == nil else { return }
|
||||
// Initialize FloatingPanelController
|
||||
settingsPanelVC = FloatingPanelController()
|
||||
|
||||
let appearance = SurfaceAppearance()
|
||||
appearance.cornerRadius = 6.0
|
||||
settingsPanelVC.surfaceView.appearance = appearance
|
||||
|
||||
settingsPanelVC.isRemovalInteractionEnabled = true
|
||||
settingsPanelVC.backdropView.dismissalTapGestureRecognizer.isEnabled = true
|
||||
settingsPanelVC.delegate = self
|
||||
|
||||
let contentVC = mainVC.storyboard?.instantiateViewController(withIdentifier: "SettingsViewController")
|
||||
|
||||
// Set a content view controller
|
||||
settingsPanelVC.set(contentViewController: contentVC)
|
||||
|
||||
// Add FloatingPanel to self.view
|
||||
settingsPanelVC.addPanel(toParent: mainVC, animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
extension UseCaseController: FloatingPanelControllerDelegate {
|
||||
func floatingPanel(_ vc: FloatingPanelController, contentOffsetForPinning trackingScrollView: UIScrollView) -> CGPoint {
|
||||
if useCase == .showNavigationController, #available(iOS 11.0, *) {
|
||||
// 148.0 is the SafeArea's top value for a navigation bar with a large title.
|
||||
return CGPoint(x: 0.0, y: 0.0 - trackingScrollView.contentInset.top - 148.0)
|
||||
}
|
||||
return CGPoint(x: 0.0, y: 0.0 - trackingScrollView.contentInset.top)
|
||||
}
|
||||
|
||||
func floatingPanel(_ vc: FloatingPanelController, layoutFor newCollection: UITraitCollection) -> FloatingPanelLayout {
|
||||
if vc == settingsPanelVC {
|
||||
return IntrinsicPanelLayout()
|
||||
}
|
||||
|
||||
switch useCase {
|
||||
case .showTopPositionedPanel:
|
||||
return TopPositionedPanelLayout()
|
||||
case .showRemovablePanel:
|
||||
return newCollection.verticalSizeClass == .compact ? RemovablePanelLandscapeLayout() : RemovablePanelLayout()
|
||||
case .showIntrinsicView:
|
||||
return IntrinsicPanelLayout()
|
||||
case .showPanelModal:
|
||||
if vc != mainPanelVC && vc != detailPanelVC {
|
||||
return ModalPanelLayout()
|
||||
}
|
||||
fallthrough
|
||||
case .showContentInset:
|
||||
return FloatingPanelBottomLayout()
|
||||
case .showCustomStatePanel:
|
||||
return FloatingPanelLayoutWithCustomState()
|
||||
default:
|
||||
return (newCollection.verticalSizeClass == .compact) ? FloatingPanelBottomLayout() : mainVC
|
||||
}
|
||||
}
|
||||
|
||||
func floatingPanelDidRemove(_ vc: FloatingPanelController) {
|
||||
switch vc {
|
||||
case settingsPanelVC:
|
||||
settingsPanelVC = nil
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension UseCaseController: UIGestureRecognizerDelegate {
|
||||
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
|
||||
switch useCase {
|
||||
case .showNestedScrollView:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRequireFailureOf otherGestureRecognizer: UIGestureRecognizer) -> Bool {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
extension UseCase {
|
||||
func makeContentViewController(with storyboard: UIStoryboard) -> UIViewController {
|
||||
guard let storyboardID = self.storyboardID else { return DebugTableViewController() }
|
||||
return storyboard.instantiateViewController(withIdentifier: storyboardID)
|
||||
}
|
||||
|
||||
func setUpInteraction(for useCaseController: UseCaseController) {
|
||||
let mainVC = useCaseController.mainVC
|
||||
let mainPanelVC = useCaseController.mainPanelVC!
|
||||
|
||||
// Enable tap-to-hide and removal interaction
|
||||
switch self {
|
||||
case .trackingTableView:
|
||||
let tapGesture = UITapGestureRecognizer(target: useCaseController, action: #selector(UseCaseController.handleSurface(tapGesture:)))
|
||||
tapGesture.cancelsTouchesInView = false
|
||||
tapGesture.numberOfTapsRequired = 2
|
||||
// Prevents a delay to response a tap in menus of DebugTableViewController.
|
||||
tapGesture.delaysTouchesEnded = false
|
||||
mainPanelVC.surfaceView.addGestureRecognizer(tapGesture)
|
||||
case .showNestedScrollView:
|
||||
mainPanelVC.panGestureRecognizer.delegateProxy = useCaseController
|
||||
case .showPageContentView:
|
||||
if let page = (mainPanelVC.contentViewController as? UIPageViewController)?.viewControllers?.first {
|
||||
mainPanelVC.track(scrollView: (page as! DebugTableViewController).tableView)
|
||||
}
|
||||
case .showRemovablePanel, .showIntrinsicView:
|
||||
mainPanelVC.isRemovalInteractionEnabled = true
|
||||
mainPanelVC.backdropView.dismissalTapGestureRecognizer.isEnabled = true
|
||||
case .showNavigationController:
|
||||
mainPanelVC.contentInsetAdjustmentBehavior = .never
|
||||
case .showTopPositionedPanel: // For debug
|
||||
let contentVC = UIViewController()
|
||||
contentVC.view.backgroundColor = .red
|
||||
mainPanelVC.set(contentViewController: contentVC)
|
||||
mainPanelVC.addPanel(toParent: mainVC, animated: true)
|
||||
return
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
// Copyright 2018-Present Shin Yamamoto. All rights reserved. MIT license.
|
||||
|
||||
import Foundation
|
||||
import FloatingPanel
|
||||
|
||||
extension FloatingPanelState {
|
||||
static let lastQuart: FloatingPanelState = FloatingPanelState(rawValue: "lastQuart", order: 750)
|
||||
static let firstQuart: FloatingPanelState = FloatingPanelState(rawValue: "firstQuart", order: 250)
|
||||
}
|
||||
|
||||
class FloatingPanelLayoutWithCustomState: FloatingPanelBottomLayout {
|
||||
override var anchors: [FloatingPanelState: FloatingPanelLayoutAnchoring] {
|
||||
return [
|
||||
.full: FloatingPanelLayoutAnchor(absoluteInset: 0.0, edge: .top, referenceGuide: .safeArea),
|
||||
.lastQuart: FloatingPanelLayoutAnchor(fractionalInset: 0.75, edge: .bottom, referenceGuide: .safeArea),
|
||||
.half: FloatingPanelLayoutAnchor(fractionalInset: 0.5, edge: .bottom, referenceGuide: .safeArea),
|
||||
.firstQuart: FloatingPanelLayoutAnchor(fractionalInset: 0.25, edge: .bottom, referenceGuide: .safeArea),
|
||||
.tip: FloatingPanelLayoutAnchor(absoluteInset: 20.0, edge: .bottom, referenceGuide: .safeArea),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
// Copyright 2018 the FloatingPanel authors. All rights reserved. MIT license.
|
||||
|
||||
import UIKit
|
||||
import FloatingPanel
|
||||
|
||||
final class PagePanelController: NSObject {
|
||||
lazy var pages = [UIColor.blue, .red, .green].compactMap({ (color) -> UIViewController in
|
||||
let page = FloatingPanelController(delegate: self)
|
||||
page.view.backgroundColor = color
|
||||
page.panGestureRecognizer.delegateProxy = self
|
||||
page.show()
|
||||
return page
|
||||
})
|
||||
|
||||
func makePageViewControllerForContent() -> UIPageViewController {
|
||||
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)
|
||||
return pageVC
|
||||
}
|
||||
|
||||
func makePageViewController(for vc: MainViewController) -> UIPageViewController {
|
||||
let pageVC = UIPageViewController(transitionStyle: .scroll, navigationOrientation: .horizontal, options: [:])
|
||||
let closeButton = UIButton(type: .custom)
|
||||
pageVC.view.addSubview(closeButton)
|
||||
closeButton.setTitle("Close", for: .normal)
|
||||
closeButton.translatesAutoresizingMaskIntoConstraints = false
|
||||
closeButton.addTarget(vc, action: #selector(MainViewController.dismissPresentedVC), for: .touchUpInside)
|
||||
NSLayoutConstraint.activate([
|
||||
closeButton.topAnchor.constraint(equalTo: pageVC.layoutGuide.topAnchor, constant: 16.0),
|
||||
closeButton.leftAnchor.constraint(equalTo: pageVC.view.leftAnchor, constant: 16.0),
|
||||
])
|
||||
pageVC.dataSource = self
|
||||
pageVC.setViewControllers([pages[0]], direction: .forward, animated: false, completion: nil)
|
||||
pageVC.modalPresentationStyle = .fullScreen
|
||||
return pageVC
|
||||
}
|
||||
}
|
||||
|
||||
extension PagePanelController: FloatingPanelControllerDelegate {
|
||||
func floatingPanel(_ vc: FloatingPanelController, layoutFor newCollection: UITraitCollection) -> FloatingPanelLayout {
|
||||
return FloatingPanelBottomLayout()
|
||||
}
|
||||
}
|
||||
|
||||
extension PagePanelController: UIGestureRecognizerDelegate {
|
||||
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
|
||||
return true
|
||||
}
|
||||
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRequireFailureOf otherGestureRecognizer: UIGestureRecognizer) -> Bool {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
extension PagePanelController: UIPageViewControllerDataSource {
|
||||
func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
|
||||
guard
|
||||
let index = pages.firstIndex(of: viewController),
|
||||
index + 1 < pages.count
|
||||
else { return nil }
|
||||
return pages[index + 1]
|
||||
}
|
||||
func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
|
||||
guard
|
||||
let index = pages.firstIndex(of: viewController),
|
||||
index - 1 >= 0
|
||||
else { return nil }
|
||||
return pages[index - 1]
|
||||
}
|
||||
}
|
||||
|
||||
extension PagePanelController: 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
// Copyright 2018-Present Shin Yamamoto. All rights reserved. MIT license.
|
||||
|
||||
import Foundation
|
||||
|
||||
enum UseCase: Int, CaseIterable {
|
||||
case trackingTableView
|
||||
case trackingTextView
|
||||
case showDetail
|
||||
case showModal
|
||||
case showPanelModal
|
||||
case showMultiPanelModal
|
||||
case showPanelInSheetModal
|
||||
case showTabBar
|
||||
case showPageView
|
||||
case showPageContentView
|
||||
case showNestedScrollView
|
||||
case showRemovablePanel
|
||||
case showIntrinsicView
|
||||
case showContentInset
|
||||
case showContainerMargins
|
||||
case showNavigationController
|
||||
case showTopPositionedPanel
|
||||
case showAdaptivePanel
|
||||
case showAdaptivePanelWithCustomGuide
|
||||
case showCustomStatePanel
|
||||
|
||||
var name: String {
|
||||
switch self {
|
||||
case .trackingTableView: return "Scroll tracking(TableView)"
|
||||
case .trackingTextView: return "Scroll tracking(TextView)"
|
||||
case .showDetail: return "Show Detail Panel"
|
||||
case .showModal: return "Show Modal"
|
||||
case .showPanelModal: return "Show Panel Modal"
|
||||
case .showMultiPanelModal: return "Show Multi Panel Modal"
|
||||
case .showPanelInSheetModal: return "Show Panel in Sheet 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"
|
||||
case .showNavigationController: return "Show Navigation Controller"
|
||||
case .showTopPositionedPanel: return "Show Top Positioned Panel"
|
||||
case .showAdaptivePanel: return "Show Adaptive Panel"
|
||||
case .showAdaptivePanelWithCustomGuide: return "Show Adaptive Panel (Custom Layout Guide)"
|
||||
case .showCustomStatePanel: return "Show Panel with Custom state"
|
||||
}
|
||||
}
|
||||
|
||||
var storyboardID: String? {
|
||||
switch self {
|
||||
case .trackingTableView: return nil
|
||||
case .trackingTextView: return "ConsoleViewController" // Storyboard only
|
||||
case .showDetail: return String(describing: DetailViewController.self)
|
||||
case .showModal: return String(describing: ModalViewController.self)
|
||||
case .showMultiPanelModal: return nil
|
||||
case .showPanelInSheetModal: return nil
|
||||
case .showPanelModal: return nil
|
||||
case .showTabBar: return String(describing: TabBarViewController.self)
|
||||
case .showPageView: return nil
|
||||
case .showPageContentView: return nil
|
||||
case .showNestedScrollView: return String(describing: NestedScrollViewController.self)
|
||||
case .showRemovablePanel: return String(describing: DetailViewController.self)
|
||||
case .showIntrinsicView: return "IntrinsicViewController" // Storyboard only
|
||||
case .showContentInset: return nil
|
||||
case .showContainerMargins: return nil
|
||||
case .showNavigationController: return "RootNavigationController" // Storyboard only
|
||||
case .showTopPositionedPanel: return nil
|
||||
case .showAdaptivePanel,
|
||||
.showAdaptivePanelWithCustomGuide:
|
||||
return String(describing: ImageViewController.self)
|
||||
case .showCustomStatePanel:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,272 @@
|
||||
// Copyright 2018 the FloatingPanel authors. All rights reserved. MIT license.
|
||||
|
||||
import UIKit
|
||||
import FloatingPanel
|
||||
|
||||
class DebugTableViewController: InspectableViewController {
|
||||
// MARK: - Views
|
||||
|
||||
lazy var tableView: UITableView = {
|
||||
let tableView = UITableView(frame: .zero, style: .plain)
|
||||
tableView.dataSource = self
|
||||
tableView.delegate = self
|
||||
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "Cell")
|
||||
return tableView
|
||||
}()
|
||||
lazy var buttonStackView: UIStackView = {
|
||||
let stackView = UIStackView()
|
||||
stackView.axis = .vertical
|
||||
stackView.distribution = .fillEqually
|
||||
stackView.alignment = .trailing
|
||||
stackView.spacing = 10.0
|
||||
return stackView
|
||||
}()
|
||||
private lazy var reorderButton: UIButton = {
|
||||
let button = UIButton()
|
||||
button.setTitle(Menu.reorder.rawValue, for: .normal)
|
||||
button.setTitleColor(view.tintColor, for: .normal)
|
||||
button.addTarget(self, action: #selector(reorderItems), for: .touchUpInside)
|
||||
return button
|
||||
}()
|
||||
private lazy var trackingSwitchWrapper: UIStackView = {
|
||||
let stackView = UIStackView()
|
||||
stackView.axis = .horizontal
|
||||
stackView.distribution = .fillProportionally
|
||||
stackView.alignment = .center
|
||||
stackView.spacing = 8.0
|
||||
stackView.addArrangedSubview(trackingLabel)
|
||||
stackView.addArrangedSubview(trackingSwitch)
|
||||
return stackView
|
||||
}()
|
||||
private lazy var trackingLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.text = "Tracking"
|
||||
label.font = UIFont.systemFont(ofSize: 17.0, weight: .regular)
|
||||
return label
|
||||
}()
|
||||
private lazy var trackingSwitch: UISwitch = {
|
||||
let trackingSwitch = UISwitch()
|
||||
trackingSwitch.isOn = true
|
||||
trackingSwitch.addTarget(self, action: #selector(turnTrackingOn), for: .touchUpInside)
|
||||
return trackingSwitch
|
||||
}()
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
private lazy var items: [String] = {
|
||||
let items = (0..<100).map { "Items \($0)" }
|
||||
return Command.replace(items: items)
|
||||
}()
|
||||
private var itemHeight: CGFloat = 66.0
|
||||
|
||||
enum Menu: String, CaseIterable {
|
||||
case turnOffTracking = "Tracking"
|
||||
case reorder = "Reorder"
|
||||
}
|
||||
|
||||
enum Command: Int, CaseIterable {
|
||||
case animateScroll
|
||||
case changeContentSize
|
||||
case moveToFull
|
||||
case moveToHalf
|
||||
var text: String {
|
||||
switch self {
|
||||
case .animateScroll: return "Scroll in the middle"
|
||||
case .changeContentSize: return "Change content size"
|
||||
case .moveToFull: return "Move to Full"
|
||||
case.moveToHalf: return "Move to Half"
|
||||
}
|
||||
}
|
||||
|
||||
static func replace(items: [String]) -> [String] {
|
||||
return items.enumerated().map { (index, text) -> String in
|
||||
if let action = Command(rawValue: index) {
|
||||
return "\(index). \(action.text)"
|
||||
}
|
||||
return text
|
||||
}
|
||||
}
|
||||
|
||||
func execute(for vc: DebugTableViewController) {
|
||||
switch self {
|
||||
case .animateScroll:
|
||||
vc.animateScroll()
|
||||
case .changeContentSize:
|
||||
vc.changeContentSize()
|
||||
case .moveToFull:
|
||||
vc.moveToFull()
|
||||
case .moveToHalf:
|
||||
vc.moveToHalf()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Lifecycle
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
layoutTableView()
|
||||
layoutMenuStackView()
|
||||
setUpMenu()
|
||||
}
|
||||
|
||||
private func layoutTableView() {
|
||||
view.addSubview(tableView)
|
||||
tableView.translatesAutoresizingMaskIntoConstraints = false
|
||||
NSLayoutConstraint.activate([
|
||||
tableView.topAnchor.constraint(equalTo: view.topAnchor),
|
||||
tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
||||
tableView.leftAnchor.constraint(equalTo: view.leftAnchor),
|
||||
tableView.rightAnchor.constraint(equalTo: view.rightAnchor)
|
||||
])
|
||||
}
|
||||
|
||||
private func layoutMenuStackView() {
|
||||
view.addSubview(buttonStackView)
|
||||
buttonStackView.translatesAutoresizingMaskIntoConstraints = false
|
||||
NSLayoutConstraint.activate([
|
||||
buttonStackView.topAnchor.constraint(equalTo: view.topAnchor, constant: 22.0),
|
||||
buttonStackView.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -22.0),
|
||||
])
|
||||
}
|
||||
|
||||
private func setUpMenu() {
|
||||
for menu in Menu.allCases {
|
||||
switch menu {
|
||||
case .reorder:
|
||||
buttonStackView.addArrangedSubview(reorderButton)
|
||||
case .turnOffTracking:
|
||||
buttonStackView.addArrangedSubview(trackingSwitchWrapper)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Menu
|
||||
@objc
|
||||
private func reorderItems() {
|
||||
if reorderButton.titleLabel?.text == Menu.reorder.rawValue {
|
||||
tableView.isEditing = true
|
||||
reorderButton.setTitle("Cancel", for: .normal)
|
||||
} else {
|
||||
tableView.isEditing = false
|
||||
reorderButton.setTitle(Menu.reorder.rawValue, for: .normal)
|
||||
}
|
||||
}
|
||||
|
||||
@objc
|
||||
private func turnTrackingOn(_ sender: UISwitch) {
|
||||
guard let fpc = self.parent as? FloatingPanelController else { return }
|
||||
if sender.isOn {
|
||||
fpc.track(scrollView: tableView)
|
||||
} else {
|
||||
fpc.untrack(scrollView: tableView)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Actions
|
||||
|
||||
private func execute(command: Command) {
|
||||
command.execute(for: self)
|
||||
}
|
||||
|
||||
@objc
|
||||
private func animateScroll() {
|
||||
tableView.scrollToRow(at: IndexPath(row: lround(Double(items.count) / 2.0),
|
||||
section: 0),
|
||||
at: .top, animated: true)
|
||||
}
|
||||
|
||||
@objc
|
||||
private func changeContentSize() {
|
||||
let actionSheet = UIAlertController(title: "Change content size", message: "", preferredStyle: .actionSheet)
|
||||
actionSheet.addAction(UIAlertAction(title: "Large", style: .default, handler: { (_) in
|
||||
self.itemHeight = 66.0
|
||||
self.changeItems(100)
|
||||
}))
|
||||
actionSheet.addAction(UIAlertAction(title: "Match", style: .default, handler: { (_) in
|
||||
switch self.tableView.bounds.height {
|
||||
case 585: // iPhone 6,7,8
|
||||
self.itemHeight = self.tableView.bounds.height / 13.0
|
||||
self.changeItems(13)
|
||||
case 656: // iPhone {6,7,8} Plus
|
||||
self.itemHeight = self.tableView.bounds.height / 16.0
|
||||
self.changeItems(16)
|
||||
default: // iPhone X family
|
||||
self.itemHeight = self.tableView.bounds.height / 12.0
|
||||
self.changeItems(12)
|
||||
}
|
||||
}))
|
||||
actionSheet.addAction(UIAlertAction(title: "Short", style: .default, handler: { (_) in
|
||||
self.itemHeight = 66.0
|
||||
self.changeItems(3)
|
||||
}))
|
||||
|
||||
self.present(actionSheet, animated: true, completion: nil)
|
||||
}
|
||||
|
||||
private func changeItems(_ count: Int) {
|
||||
items = Command.replace(items: (0..<count).map{ "\($0). No action" })
|
||||
tableView.reloadData()
|
||||
}
|
||||
|
||||
@objc
|
||||
private func moveToFull() {
|
||||
(self.parent as! FloatingPanelController).move(to: .full, animated: true)
|
||||
}
|
||||
|
||||
@objc
|
||||
private func moveToHalf() {
|
||||
(self.parent as! FloatingPanelController).move(to: .half, animated: true)
|
||||
}
|
||||
|
||||
@objc
|
||||
private func close(sender: UIButton) {
|
||||
// Remove FloatingPanel from a view
|
||||
(self.parent as! FloatingPanelController).removePanelFromParent(animated: true, completion: nil)
|
||||
}
|
||||
}
|
||||
|
||||
extension DebugTableViewController: UITableViewDataSource {
|
||||
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
return items.count
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
|
||||
return itemHeight
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
|
||||
cell.textLabel?.text = items[indexPath.row]
|
||||
return cell
|
||||
}
|
||||
}
|
||||
|
||||
extension DebugTableViewController: UITableViewDelegate {
|
||||
func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
||||
print("TableView --- ", scrollView.contentOffset, scrollView.contentInset)
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
print("DebugTableViewController -- select row \(indexPath.row)")
|
||||
guard let action = Command(rawValue: indexPath.row) else { return }
|
||||
execute(command: action)
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, editActionsForRowAt indexPath: IndexPath) -> [UITableViewRowAction]? {
|
||||
return [
|
||||
UITableViewRowAction(style: .destructive, title: "Delete", handler: { (action, path) in
|
||||
self.items.remove(at: path.row)
|
||||
tableView.deleteRows(at: [path], with: .automatic)
|
||||
}),
|
||||
]
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, canMoveRowAt indexPath: IndexPath) -> Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {
|
||||
items.insert(items.remove(at: sourceIndexPath.row), at: destinationIndexPath.row)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
// Copyright 2018 the FloatingPanel authors. All rights reserved. MIT license.
|
||||
|
||||
import UIKit
|
||||
|
||||
final class DebugTextViewController: UIViewController, UITextViewDelegate {
|
||||
@IBOutlet weak var textView: UITextView!
|
||||
@IBOutlet weak var textViewTopConstraint: NSLayoutConstraint!
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
textView.delegate = self
|
||||
print("viewDidLoad: TextView --- ", textView.contentOffset, textView.contentInset)
|
||||
|
||||
if #available(iOS 11.0, *) {
|
||||
textView.contentInsetAdjustmentBehavior = .never
|
||||
}
|
||||
}
|
||||
|
||||
override func viewWillLayoutSubviews() {
|
||||
print("viewWillLayoutSubviews: TextView --- ", textView.contentOffset, textView.contentInset, textView.frame)
|
||||
}
|
||||
|
||||
override func viewDidLayoutSubviews() {
|
||||
super.viewDidLayoutSubviews()
|
||||
print("viewDidLayoutSubviews: TextView --- ", textView.contentOffset, textView.contentInset, textView.frame)
|
||||
}
|
||||
|
||||
override func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
print("TextView --- ", textView.contentOffset, textView.contentInset, textView.frame)
|
||||
}
|
||||
|
||||
func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
||||
print("TextView --- ", scrollView.contentOffset, scrollView.contentInset)
|
||||
if #available(iOS 11.0, *) {
|
||||
print("TextView --- ", scrollView.adjustedContentInset)
|
||||
}
|
||||
}
|
||||
|
||||
@IBAction func close(sender: UIButton) {
|
||||
// (self.parent as? FloatingPanelController)?.removePanelFromParent(animated: true, completion: nil)
|
||||
dismiss(animated: true, completion: nil)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
// Copyright 2018-Present Shin Yamamoto. All rights reserved. MIT license.
|
||||
|
||||
import UIKit
|
||||
import FloatingPanel
|
||||
|
||||
final 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)
|
||||
dismiss(animated: true, completion: nil)
|
||||
}
|
||||
|
||||
@IBAction func buttonPressed(_ sender: UIButton) {
|
||||
switch sender.titleLabel?.text {
|
||||
case "Show":
|
||||
performSegue(withIdentifier: "ShowSegue", sender: self)
|
||||
case "Present Modally":
|
||||
performSegue(withIdentifier: "PresentModallySegue", sender: self)
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
@IBAction func 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!")
|
||||
}
|
||||
@IBAction func swipped(_ sender: Any) {
|
||||
print("Detail panel is swipped!")
|
||||
}
|
||||
@IBAction func longPressed(_ sender: Any) {
|
||||
print("Detail panel is longPressed!")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
// Copyright 2018-Present Shin Yamamoto. All rights reserved. MIT license.
|
||||
|
||||
import UIKit
|
||||
import FloatingPanel
|
||||
|
||||
final class ImageViewController: UIViewController {
|
||||
class PanelLayout: FloatingPanelLayout {
|
||||
weak var targetGuide: UILayoutGuide?
|
||||
init(targetGuide: UILayoutGuide?) {
|
||||
self.targetGuide = targetGuide
|
||||
}
|
||||
let position: FloatingPanelPosition = .bottom
|
||||
let initialState: FloatingPanelState = .full
|
||||
var anchors: [FloatingPanelState : FloatingPanelLayoutAnchoring] {
|
||||
if #available(iOS 11.0, *), let targetGuide = targetGuide {
|
||||
return [
|
||||
.full: FloatingPanelAdaptiveLayoutAnchor(absoluteOffset: 0,
|
||||
contentLayout: targetGuide,
|
||||
referenceGuide: .superview),
|
||||
.half: FloatingPanelAdaptiveLayoutAnchor(fractionalOffset: 0.5,
|
||||
contentLayout: targetGuide,
|
||||
referenceGuide: .superview)
|
||||
]
|
||||
} else {
|
||||
return [
|
||||
.full: FloatingPanelLayoutAnchor(absoluteInset: 500,
|
||||
edge: .bottom,
|
||||
referenceGuide: .superview)
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@IBOutlet weak var headerView: UIView!
|
||||
@IBOutlet weak var footerView: UIView!
|
||||
@IBOutlet weak var scrollView: UIScrollView!
|
||||
@IBOutlet weak var stackView: UIStackView!
|
||||
|
||||
enum Mode {
|
||||
case onlyImage
|
||||
case withHeaderFooter
|
||||
}
|
||||
|
||||
@available(iOS 11.0, *)
|
||||
func layoutGuideFor(mode: Mode) -> UILayoutGuide {
|
||||
switch mode {
|
||||
case .onlyImage:
|
||||
self.headerView.isHidden = true
|
||||
self.footerView.isHidden = true
|
||||
return scrollView.contentLayoutGuide
|
||||
case .withHeaderFooter:
|
||||
self.headerView.isHidden = false
|
||||
self.footerView.isHidden = false
|
||||
let guide = UILayoutGuide()
|
||||
view.addLayoutGuide(guide)
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
scrollView.heightAnchor.constraint(equalTo: scrollView.contentLayoutGuide.heightAnchor),
|
||||
|
||||
guide.topAnchor.constraint(equalTo: stackView.topAnchor),
|
||||
guide.leftAnchor.constraint(equalTo: stackView.leftAnchor),
|
||||
guide.bottomAnchor.constraint(equalTo: stackView.bottomAnchor),
|
||||
guide.rightAnchor.constraint(equalTo: stackView.rightAnchor),
|
||||
])
|
||||
return guide
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
// Copyright 2018 the FloatingPanel authors. All rights reserved. MIT license.
|
||||
|
||||
import UIKit
|
||||
|
||||
class InspectableViewController: UIViewController {
|
||||
override func viewWillLayoutSubviews() {
|
||||
super.viewWillLayoutSubviews()
|
||||
print(">>> Content View: viewWillLayoutSubviews", layoutInsets)
|
||||
}
|
||||
|
||||
override func viewDidLayoutSubviews() {
|
||||
super.viewDidLayoutSubviews()
|
||||
print(">>> Content View: viewDidLayoutSubviews", layoutInsets)
|
||||
}
|
||||
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
print(">>> Content View: viewWillAppear", layoutInsets)
|
||||
}
|
||||
|
||||
override func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
print(">>> Content View: viewDidAppear", view.bounds, layoutInsets)
|
||||
}
|
||||
|
||||
override func viewWillDisappear(_ animated: Bool) {
|
||||
super.viewWillDisappear(animated)
|
||||
print(">>> Content View: viewWillDisappear")
|
||||
}
|
||||
|
||||
override func viewDidDisappear(_ animated: Bool) {
|
||||
super.viewDidDisappear(animated)
|
||||
print(">>> Content View: viewDidDisappear")
|
||||
}
|
||||
|
||||
override func willMove(toParent parent: UIViewController?) {
|
||||
super.willMove(toParent: parent)
|
||||
print(">>> Content View: willMove(toParent: \(String(describing: parent))")
|
||||
}
|
||||
|
||||
override func didMove(toParent parent: UIViewController?) {
|
||||
super.didMove(toParent: parent)
|
||||
print(">>> Content View: didMove(toParent: \(String(describing: parent))")
|
||||
}
|
||||
public override func willTransition(to newCollection: UITraitCollection, with coordinator: UIViewControllerTransitionCoordinator) {
|
||||
print(">>> Content View: willTransition(to: \(newCollection), with: \(coordinator))", layoutInsets)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
// Copyright 2018 the FloatingPanel authors. All rights reserved. MIT license.
|
||||
|
||||
import UIKit
|
||||
import FloatingPanel
|
||||
|
||||
final class ModalViewController: UIViewController, FloatingPanelControllerDelegate {
|
||||
var fpc: FloatingPanelController!
|
||||
var consoleVC: DebugTextViewController!
|
||||
|
||||
@IBOutlet weak var safeAreaView: UIView!
|
||||
|
||||
var isNewlayout: Bool = false
|
||||
|
||||
override func viewDidLoad() {
|
||||
// Initialize FloatingPanelController
|
||||
fpc = FloatingPanelController()
|
||||
fpc.delegate = self
|
||||
|
||||
let appearance = SurfaceAppearance()
|
||||
appearance.cornerRadius = 6.0
|
||||
fpc.surfaceView.appearance = appearance
|
||||
|
||||
// Set a content view controller and track the scroll view
|
||||
let consoleVC = storyboard?.instantiateViewController(withIdentifier: "ConsoleViewController") as! DebugTextViewController
|
||||
fpc.set(contentViewController: consoleVC)
|
||||
fpc.track(scrollView: consoleVC.textView)
|
||||
|
||||
self.consoleVC = consoleVC
|
||||
|
||||
// Add FloatingPanel to self.view
|
||||
fpc.addPanel(toParent: self, at: view.subviews.firstIndex(of: safeAreaView) ?? -1)
|
||||
}
|
||||
|
||||
override func viewDidDisappear(_ animated: Bool) {
|
||||
super.viewDidDisappear(animated)
|
||||
// Remove FloatingPanel from a view
|
||||
fpc.removePanelFromParent(animated: false)
|
||||
}
|
||||
|
||||
@IBAction func close(sender: UIButton) {
|
||||
dismiss(animated: true, completion: nil)
|
||||
}
|
||||
|
||||
@IBAction func moveToFull(sender: UIButton) {
|
||||
fpc.move(to: .full, animated: true)
|
||||
}
|
||||
@IBAction func moveToHalf(sender: UIButton) {
|
||||
fpc.move(to: .half, animated: true)
|
||||
}
|
||||
@IBAction func moveToTip(sender: UIButton) {
|
||||
fpc.move(to: .tip, animated: true)
|
||||
}
|
||||
@IBAction func moveToHidden(sender: UIButton) {
|
||||
fpc.move(to: .hidden, animated: true)
|
||||
}
|
||||
@IBAction func updateLayout(_ sender: Any) {
|
||||
isNewlayout = !isNewlayout
|
||||
UIView.animate(withDuration: 0.5) {
|
||||
self.fpc.layout = (self.isNewlayout) ? ModalSecondLayout() : FloatingPanelBottomLayout()
|
||||
self.fpc.invalidateLayout()
|
||||
}
|
||||
}
|
||||
|
||||
func floatingPanel(_ vc: FloatingPanelController, layoutFor newCollection: UITraitCollection) -> FloatingPanelLayout {
|
||||
return (isNewlayout) ? ModalSecondLayout() : FloatingPanelBottomLayout()
|
||||
}
|
||||
|
||||
class ModalSecondLayout: FloatingPanelLayout {
|
||||
var position: FloatingPanelPosition = .bottom
|
||||
var initialState: FloatingPanelState { .half }
|
||||
var anchors: [FloatingPanelState : FloatingPanelLayoutAnchoring] {
|
||||
return [
|
||||
.full: FloatingPanelLayoutAnchor(absoluteInset: 16.0, edge: .top, referenceGuide: .safeArea),
|
||||
.half: FloatingPanelLayoutAnchor(absoluteInset: 262, edge: .top, referenceGuide: .safeArea),
|
||||
.tip: FloatingPanelLayoutAnchor(absoluteInset: 44.0, edge: .bottom, referenceGuide: .safeArea)
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
// Copyright 2018-Present Shin Yamamoto. All rights reserved. MIT license.
|
||||
|
||||
import UIKit
|
||||
import FloatingPanel
|
||||
import WebKit
|
||||
|
||||
final class MultiPanelController: FloatingPanelController, FloatingPanelControllerDelegate {
|
||||
|
||||
private final class FirstPanelContentViewController: UIViewController {
|
||||
|
||||
lazy var webView: WKWebView = WKWebView()
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
view.addSubview(webView)
|
||||
webView.frame = view.bounds
|
||||
webView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
||||
webView.load(URLRequest(url: URL(string: "https://www.apple.com")!))
|
||||
|
||||
let vc = MultiSecondPanelController()
|
||||
vc.setUpContent()
|
||||
vc.addPanel(toParent: self)
|
||||
}
|
||||
}
|
||||
|
||||
private final class MultiSecondPanelController: FloatingPanelController {
|
||||
|
||||
private final class SecondPanelContentViewController: DebugTableViewController {}
|
||||
|
||||
func setUpContent() {
|
||||
contentInsetAdjustmentBehavior = .never
|
||||
let vc = SecondPanelContentViewController()
|
||||
vc.loadViewIfNeeded()
|
||||
vc.title = "Second Panel"
|
||||
vc.buttonStackView.isHidden = true
|
||||
let navigationController = UINavigationController(rootViewController: vc)
|
||||
navigationController.navigationBar.barTintColor = .white
|
||||
navigationController.navigationBar.titleTextAttributes = [
|
||||
.foregroundColor: UIColor.black
|
||||
]
|
||||
set(contentViewController: navigationController)
|
||||
self.track(scrollView: vc.tableView)
|
||||
surfaceView.containerMargins = .init(top: 24.0, left: 0.0, bottom: layoutInsets.bottom, right: 0.0)
|
||||
}
|
||||
}
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
layout = FirstViewLayout()
|
||||
isRemovalInteractionEnabled = true
|
||||
|
||||
let vc = FirstPanelContentViewController()
|
||||
set(contentViewController: vc)
|
||||
track(scrollView: vc.webView.scrollView)
|
||||
}
|
||||
|
||||
private final class FirstViewLayout: FloatingPanelLayout {
|
||||
let position: FloatingPanelPosition = .bottom
|
||||
let initialState: FloatingPanelState = .full
|
||||
var anchors: [FloatingPanelState : FloatingPanelLayoutAnchoring] {
|
||||
return [
|
||||
.full: FloatingPanelLayoutAnchor(absoluteInset: 40.0, edge: .top, referenceGuide: .superview)
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
// Copyright 2018 the FloatingPanel authors. All rights reserved. MIT license.
|
||||
|
||||
import UIKit
|
||||
|
||||
final class NestedScrollViewController: UIViewController {
|
||||
@IBOutlet weak var scrollView: UIScrollView!
|
||||
@IBOutlet weak var nestedScrollView: UIScrollView!
|
||||
|
||||
@IBAction func longPressed(_ sender: Any) {
|
||||
print("LongPressed!")
|
||||
}
|
||||
@IBAction func swipped(_ sender: Any) {
|
||||
print("Swipped!")
|
||||
}
|
||||
@IBAction func tapped(_ sender: Any) {
|
||||
print("Tapped!")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
// Copyright 2018-Present Shin Yamamoto. All rights reserved. MIT license.
|
||||
|
||||
import UIKit
|
||||
import FloatingPanel
|
||||
|
||||
final class SettingsViewController: InspectableViewController {
|
||||
@IBOutlet weak var largeTitlesSwicth: UISwitch!
|
||||
@IBOutlet weak var translucentSwicth: UISwitch!
|
||||
@IBOutlet weak var versionLabel: UILabel!
|
||||
|
||||
override func viewDidLoad() {
|
||||
versionLabel.text = "Version: \(Bundle.main.infoDictionary?["CFBundleVersion"] ?? "--")"
|
||||
}
|
||||
|
||||
override func viewDidLayoutSubviews() {
|
||||
super.viewDidLayoutSubviews()
|
||||
if #available(iOS 11.0, *) {
|
||||
let prefersLargeTitles = navigationController!.navigationBar.prefersLargeTitles
|
||||
largeTitlesSwicth.setOn(prefersLargeTitles, animated: false)
|
||||
} else {
|
||||
largeTitlesSwicth.isEnabled = false
|
||||
}
|
||||
let isTranslucent = navigationController!.navigationBar.isTranslucent
|
||||
translucentSwicth.setOn(isTranslucent, animated: false)
|
||||
}
|
||||
|
||||
@IBAction func toggleLargeTitle(_ sender: UISwitch) {
|
||||
if #available(iOS 11.0, *) {
|
||||
navigationController?.navigationBar.prefersLargeTitles = sender.isOn
|
||||
}
|
||||
}
|
||||
@IBAction func toggleTranslucent(_ sender: UISwitch) {
|
||||
navigationController?.navigationBar.isTranslucent = sender.isOn
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,248 @@
|
||||
// Copyright 2018-Present Shin Yamamoto. All rights reserved. MIT license.
|
||||
|
||||
import UIKit
|
||||
import FloatingPanel
|
||||
|
||||
final class TabBarViewController: UITabBarController {}
|
||||
|
||||
final class TabBarContentViewController: UIViewController {
|
||||
enum Tab3Mode {
|
||||
case changeOffset
|
||||
case changeAutoLayout
|
||||
var label: String {
|
||||
switch self {
|
||||
case .changeAutoLayout: return "Use AutoLayout(OK)"
|
||||
case .changeOffset: return "Use ContentOffset(NG)"
|
||||
}
|
||||
}
|
||||
}
|
||||
lazy var fpc = FloatingPanelController()
|
||||
var consoleVC: DebugTextViewController!
|
||||
|
||||
var threeLayout: ThreeTabBarPanelLayout!
|
||||
var tab3Mode: Tab3Mode = .changeAutoLayout
|
||||
var switcherLabel: UILabel!
|
||||
|
||||
override func viewDidLoad() {
|
||||
fpc.delegate = self
|
||||
|
||||
let appearance = SurfaceAppearance()
|
||||
appearance.cornerRadius = 6.0
|
||||
fpc.surfaceView.appearance = appearance
|
||||
|
||||
// Set a content view controller and track the scroll view
|
||||
let consoleVC = storyboard?.instantiateViewController(withIdentifier: "ConsoleViewController") as! DebugTextViewController
|
||||
fpc.set(contentViewController: consoleVC)
|
||||
consoleVC.textView.delegate = self // MUST call it before fpc.track(scrollView:)
|
||||
fpc.track(scrollView: consoleVC.textView)
|
||||
self.consoleVC = consoleVC
|
||||
|
||||
// Add FloatingPanel to self.view
|
||||
fpc.addPanel(toParent: self)
|
||||
|
||||
|
||||
switch tabBarItem.tag {
|
||||
case 1:
|
||||
fpc.behavior = TwoTabBarPanelBehavior()
|
||||
case 2:
|
||||
let switcher = UISwitch()
|
||||
fpc.view.addSubview(switcher)
|
||||
switcher.translatesAutoresizingMaskIntoConstraints = false
|
||||
NSLayoutConstraint.activate([
|
||||
switcher.bottomAnchor.constraint(equalTo: fpc.surfaceView.topAnchor, constant: -16.0),
|
||||
switcher.rightAnchor.constraint(equalTo: fpc.surfaceView.rightAnchor, constant: -16.0),
|
||||
])
|
||||
switcher.isOn = true
|
||||
switcher.tintColor = .white
|
||||
switcher.backgroundColor = .white
|
||||
switcher.layer.cornerRadius = 16.0
|
||||
switcher.addTarget(self,
|
||||
action: #selector(changeTab3Mode(_:)),
|
||||
for: .valueChanged)
|
||||
let label = UILabel()
|
||||
label.text = tab3Mode.label
|
||||
fpc.view.addSubview(label)
|
||||
switcherLabel = label
|
||||
label.translatesAutoresizingMaskIntoConstraints = false
|
||||
NSLayoutConstraint.activate([
|
||||
label.centerYAnchor.constraint(equalTo: switcher.centerYAnchor, constant: 0.0),
|
||||
label.rightAnchor.constraint(equalTo: switcher.leftAnchor, constant: -16.0),
|
||||
])
|
||||
|
||||
// Turn off the mask instead of content inset change
|
||||
consoleVC.textView.clipsToBounds = false
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
override func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
fpc.invalidateLayout()
|
||||
}
|
||||
|
||||
// MARK: - Action
|
||||
|
||||
@IBAction func close(sender: UIButton) {
|
||||
dismiss(animated: true, completion: nil)
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
@objc
|
||||
private func changeTab3Mode(_ sender: UISwitch) {
|
||||
if sender.isOn {
|
||||
tab3Mode = .changeAutoLayout
|
||||
} else {
|
||||
tab3Mode = .changeOffset
|
||||
}
|
||||
switcherLabel.text = tab3Mode.label
|
||||
}
|
||||
}
|
||||
|
||||
extension TabBarContentViewController: UITextViewDelegate {
|
||||
func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
||||
guard self.tabBarItem.tag == 2 else { return }
|
||||
// Reset an invalid content offset by a user after updating the layout
|
||||
// of `consoleVC.textView`.
|
||||
// NOTE: FloatingPanel doesn't implicitly reset the offset(i.e.
|
||||
// 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.state != .full, fpc.surfaceLocation.y > fpc.surfaceLocation(for: .full).y {
|
||||
scrollView.contentOffset = .zero
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension TabBarContentViewController: FloatingPanelControllerDelegate {
|
||||
// MARK: - FloatingPanel
|
||||
|
||||
func floatingPanel(_ vc: FloatingPanelController, layoutFor newCollection: UITraitCollection) -> FloatingPanelLayout {
|
||||
switch self.tabBarItem.tag {
|
||||
case 0:
|
||||
return OneTabBarPanelLayout()
|
||||
case 1:
|
||||
return TwoTabBarPanelLayout()
|
||||
case 2:
|
||||
threeLayout = ThreeTabBarPanelLayout(parent: self)
|
||||
return threeLayout
|
||||
default:
|
||||
return FloatingPanelBottomLayout()
|
||||
}
|
||||
}
|
||||
|
||||
func floatingPanelDidMove(_ vc: FloatingPanelController) {
|
||||
guard self.tabBarItem.tag == 2 else { return }
|
||||
switch tab3Mode {
|
||||
case .changeAutoLayout:
|
||||
/* Good solution: Manipulate top constraint */
|
||||
assert(consoleVC.textViewTopConstraint != nil)
|
||||
let safeAreaTop = vc.layoutInsets.top
|
||||
if vc.surfaceLocation.y + threeLayout.topPadding < safeAreaTop {
|
||||
consoleVC.textViewTopConstraint?.constant = min(safeAreaTop - vc.surfaceLocation.y,
|
||||
safeAreaTop)
|
||||
} else {
|
||||
consoleVC.textViewTopConstraint?.constant = threeLayout.topPadding
|
||||
}
|
||||
case .changeOffset:
|
||||
/*
|
||||
Bad solution: Manipulate scroll content inset
|
||||
|
||||
FloatingPanelController keeps a content offset in moving a panel
|
||||
so that changing content inset or offset causes a buggy behavior.
|
||||
*/
|
||||
guard let scrollView = consoleVC.textView else { return }
|
||||
var insets = vc.adjustedContentInsets
|
||||
if vc.surfaceView.frame.minY < vc.layoutInsets.top {
|
||||
insets.top = vc.layoutInsets.top - vc.surfaceView.frame.minY
|
||||
} else {
|
||||
insets.top = 0.0
|
||||
}
|
||||
scrollView.contentInset = insets
|
||||
|
||||
if vc.surfaceView.frame.minY > 0 {
|
||||
scrollView.contentOffset = CGPoint(x: 0.0,
|
||||
y: 0.0 - scrollView.contentInset.top)
|
||||
}
|
||||
}
|
||||
|
||||
if vc.surfaceLocation.y > vc.surfaceLocation(for: .half).y {
|
||||
let progress = (vc.surfaceLocation.y - vc.surfaceLocation(for: .half).y)
|
||||
/ (vc.surfaceLocation(for: .tip).y - vc.surfaceLocation(for: .half).y)
|
||||
threeLayout.leftConstraint.constant = max(min(progress, 1.0), 0.0) * threeLayout.sideMargin
|
||||
threeLayout.rightConstraint.constant = -max(min(progress, 1.0), 0.0) * threeLayout.sideMargin
|
||||
} else {
|
||||
threeLayout.leftConstraint.constant = 0.0
|
||||
threeLayout.rightConstraint.constant = 0.0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class OneTabBarPanelLayout: FloatingPanelLayout {
|
||||
var initialState: FloatingPanelState { .tip }
|
||||
var position: FloatingPanelPosition { .bottom }
|
||||
var anchors: [FloatingPanelState : FloatingPanelLayoutAnchoring] {
|
||||
return [
|
||||
.full: FloatingPanelLayoutAnchor(absoluteInset: 16.0, edge: .top, referenceGuide: .safeArea),
|
||||
.tip: FloatingPanelLayoutAnchor(absoluteInset: 22.0, edge: .bottom, referenceGuide: .safeArea)
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
class TwoTabBarPanelLayout: FloatingPanelLayout {
|
||||
var initialState: FloatingPanelState { .half }
|
||||
var position: FloatingPanelPosition { .bottom }
|
||||
var anchors: [FloatingPanelState : FloatingPanelLayoutAnchoring] {
|
||||
return [
|
||||
.full: FloatingPanelLayoutAnchor(absoluteInset: 100.0, edge: .top, referenceGuide: .safeArea),
|
||||
.half: FloatingPanelLayoutAnchor(absoluteInset: 261.0, edge: .bottom, referenceGuide: .safeArea)
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
class TwoTabBarPanelBehavior: FloatingPanelBehavior {
|
||||
func allowsRubberBanding(for edges: UIRectEdge) -> Bool {
|
||||
return [UIRectEdge.top, UIRectEdge.bottom].contains(edges)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class ThreeTabBarPanelLayout: FloatingPanelLayout {
|
||||
weak var parentVC: UIViewController!
|
||||
|
||||
var leftConstraint: NSLayoutConstraint!
|
||||
var rightConstraint: NSLayoutConstraint!
|
||||
|
||||
let topPadding: CGFloat = 17.0
|
||||
let sideMargin: CGFloat = 16.0
|
||||
|
||||
init(parent: UIViewController) {
|
||||
parentVC = parent
|
||||
}
|
||||
|
||||
var initialState: FloatingPanelState { .half }
|
||||
var position: FloatingPanelPosition { .bottom }
|
||||
var anchors: [FloatingPanelState : FloatingPanelLayoutAnchoring] {
|
||||
return [
|
||||
.full: FloatingPanelLayoutAnchor(absoluteInset: 0.0, edge: .top, referenceGuide: .superview),
|
||||
.half: FloatingPanelLayoutAnchor(absoluteInset: 261.0 + parentVC.layoutInsets.bottom, edge: .bottom, referenceGuide: .superview),
|
||||
.tip: FloatingPanelLayoutAnchor(absoluteInset: 88.0 + parentVC.layoutInsets.bottom, edge: .bottom, referenceGuide: .superview),
|
||||
]
|
||||
}
|
||||
|
||||
func backdropAlpha(for state: FloatingPanelState) -> CGFloat {
|
||||
return 0.3
|
||||
}
|
||||
|
||||
func prepareLayout(surfaceView: UIView, in view: UIView) -> [NSLayoutConstraint] {
|
||||
if #available(iOS 11.0, *) {
|
||||
leftConstraint = surfaceView.leftAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leftAnchor, constant: 0.0)
|
||||
rightConstraint = surfaceView.rightAnchor.constraint(equalTo: view.safeAreaLayoutGuide.rightAnchor, constant: 0.0)
|
||||
} else {
|
||||
leftConstraint = surfaceView.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 0.0)
|
||||
rightConstraint = surfaceView.rightAnchor.constraint(equalTo: view.rightAnchor, constant: 0.0)
|
||||
}
|
||||
return [ leftConstraint, rightConstraint ]
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,22 @@
|
||||
#import "ViewController.h"
|
||||
@import FloatingPanel;
|
||||
|
||||
// Defining a custom FloatingPanelState
|
||||
@interface FloatingPanelState(Extended)
|
||||
+ (FloatingPanelState *)LastQuart;
|
||||
@end
|
||||
|
||||
@implementation FloatingPanelState(Extended)
|
||||
static FloatingPanelState *_lastQuart;
|
||||
+ (FloatingPanelState *)LastQuart {
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
_lastQuart = [[FloatingPanelState alloc] initWithRawValue:@"lastquart" order:750];
|
||||
});
|
||||
return _lastQuart;
|
||||
}
|
||||
@end
|
||||
|
||||
@interface ViewController()<FloatingPanelControllerDelegate>
|
||||
@end
|
||||
|
||||
@@ -20,7 +36,7 @@
|
||||
[fpc setBehavior:[MyFloatingPanelBehavior new]];
|
||||
[fpc setRemovalInteractionEnabled:NO];
|
||||
|
||||
[fpc addPanelToParent:self at:self.view.subviews.count animated:NO];
|
||||
[fpc addPanelToParent:self at:self.view.subviews.count animated:NO completion:nil];
|
||||
[fpc moveToState:FloatingPanelState.Tip animated:true completion:nil];
|
||||
|
||||
[self updateAppearance: fpc];
|
||||
@@ -59,12 +75,15 @@
|
||||
}
|
||||
- (NSDictionary<FloatingPanelState *, id<FloatingPanelLayoutAnchoring>> *)anchors {
|
||||
return @{
|
||||
FloatingPanelState.LastQuart: [[FloatingPanelLayoutAnchor alloc] initWithFractionalInset:0.25
|
||||
edge:FloatingPanelReferenceEdgeTop
|
||||
referenceGuide:FloatingPanelLayoutReferenceGuideSafeArea],
|
||||
FloatingPanelState.Half: [[FloatingPanelLayoutAnchor alloc] initWithFractionalInset:0.5
|
||||
edge:FloatingPanelReferenceEdgeTop
|
||||
referenceGuide:FloatingPanelLayoutReferenceGuideSafeArea],
|
||||
referenceGuide:FloatingPanelLayoutReferenceGuideSafeArea],
|
||||
FloatingPanelState.Tip: [[FloatingPanelLayoutAnchor alloc] initWithAbsoluteInset:44.0
|
||||
edge:FloatingPanelReferenceEdgeBottom
|
||||
referenceGuide:FloatingPanelLayoutReferenceGuideSafeArea],
|
||||
referenceGuide:FloatingPanelLayoutReferenceGuideSafeArea],
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
Pod::Spec.new do |s|
|
||||
|
||||
s.name = "FloatingPanel"
|
||||
s.version = "2.0.0"
|
||||
s.version = "2.4.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.
|
||||
|
||||
@@ -9,12 +9,11 @@
|
||||
/* Begin PBXBuildFile section */
|
||||
542753C622C49A6E00D17955 /* LayoutTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 542753C522C49A6E00D17955 /* LayoutTests.swift */; };
|
||||
54352E9621A51A2500CBCA08 /* Transitioning.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54352E9521A51A2500CBCA08 /* Transitioning.swift */; };
|
||||
54352E9821A521CA00CBCA08 /* PassThroughView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54352E9721A521CA00CBCA08 /* PassThroughView.swift */; };
|
||||
54352E9821A521CA00CBCA08 /* PassthroughView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54352E9721A521CA00CBCA08 /* PassthroughView.swift */; };
|
||||
5450EEE421646DF500135936 /* Behavior.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5450EEE321646DF500135936 /* Behavior.swift */; };
|
||||
545DB9CB2151169500CA77B8 /* FloatingPanel.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 545DB9C12151169500CA77B8 /* FloatingPanel.framework */; };
|
||||
545DB9D02151169500CA77B8 /* ControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 545DB9CF2151169500CA77B8 /* ControllerTests.swift */; };
|
||||
545DB9D22151169500CA77B8 /* FloatingPanel.h in Headers */ = {isa = PBXBuildFile; fileRef = 545DB9C42151169500CA77B8 /* FloatingPanel.h */; settings = {ATTRIBUTES = (Public, ); }; };
|
||||
545DB9DE215118C800CA77B8 /* UIExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 545DB9DD215118C800CA77B8 /* UIExtensions.swift */; };
|
||||
545DB9E021511AC100CA77B8 /* Controller.swift in Sources */ = {isa = PBXBuildFile; fileRef = 545DB9DF21511AC100CA77B8 /* Controller.swift */; };
|
||||
545DBA2B2152383100CA77B8 /* GrabberView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 545DBA2A2152383100CA77B8 /* GrabberView.swift */; };
|
||||
546055BF2333C4740069F400 /* TestSupports.swift in Sources */ = {isa = PBXBuildFile; fileRef = 542753C722C49A8F00D17955 /* TestSupports.swift */; };
|
||||
@@ -24,7 +23,7 @@
|
||||
5469F4B024B30E1500537F8A /* LayoutAnchoring.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5469F4AF24B30E1500537F8A /* LayoutAnchoring.swift */; };
|
||||
5469F4B224B30F1100537F8A /* Position.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5469F4B124B30F1100537F8A /* Position.swift */; };
|
||||
5469F4B424B30F3500537F8A /* LayoutReferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5469F4B324B30F3500537F8A /* LayoutReferences.swift */; };
|
||||
549C371F2361E15E007D8058 /* UtilTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 549C371E2361E15D007D8058 /* UtilTests.swift */; };
|
||||
549C371F2361E15E007D8058 /* ExtensionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 549C371E2361E15D007D8058 /* ExtensionTests.swift */; };
|
||||
549E944522CF295D0050AECF /* StateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 549E944422CF295D0050AECF /* StateTests.swift */; };
|
||||
54A6B6B122968B530077F348 /* CoreTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54A6B6B022968B530077F348 /* CoreTests.swift */; };
|
||||
54A6B6B82296A8520077F348 /* SurfaceViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54A6B6B72296A8520077F348 /* SurfaceViewTests.swift */; };
|
||||
@@ -33,6 +32,7 @@
|
||||
54CDC5D5215B6D8D007D205C /* BackdropView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CDC5D4215B6D8D007D205C /* BackdropView.swift */; };
|
||||
54CFBFC3215CD045006B5735 /* Layout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CFBFC2215CD045006B5735 /* Layout.swift */; };
|
||||
54CFBFC5215CD09C006B5735 /* Core.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CFBFC4215CD09C006B5735 /* Core.swift */; };
|
||||
54DBA3DC262E938500D75969 /* Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54DBA3DB262E938500D75969 /* Extensions.swift */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
@@ -56,7 +56,7 @@
|
||||
542753C522C49A6E00D17955 /* LayoutTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LayoutTests.swift; sourceTree = "<group>"; };
|
||||
542753C722C49A8F00D17955 /* TestSupports.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestSupports.swift; sourceTree = "<group>"; };
|
||||
54352E9521A51A2500CBCA08 /* Transitioning.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Transitioning.swift; sourceTree = "<group>"; };
|
||||
54352E9721A521CA00CBCA08 /* PassThroughView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PassThroughView.swift; sourceTree = "<group>"; };
|
||||
54352E9721A521CA00CBCA08 /* PassthroughView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PassthroughView.swift; sourceTree = "<group>"; };
|
||||
5450EEE321646DF500135936 /* Behavior.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Behavior.swift; sourceTree = "<group>"; };
|
||||
545DB9C12151169500CA77B8 /* FloatingPanel.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = FloatingPanel.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
545DB9C42151169500CA77B8 /* FloatingPanel.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FloatingPanel.h; sourceTree = "<group>"; };
|
||||
@@ -64,7 +64,6 @@
|
||||
545DB9CA2151169500CA77B8 /* FloatingPanelTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = FloatingPanelTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
545DB9CF2151169500CA77B8 /* ControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ControllerTests.swift; sourceTree = "<group>"; };
|
||||
545DB9D12151169500CA77B8 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
545DB9DD215118C800CA77B8 /* UIExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIExtensions.swift; sourceTree = "<group>"; };
|
||||
545DB9DF21511AC100CA77B8 /* Controller.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Controller.swift; sourceTree = "<group>"; };
|
||||
545DBA2A2152383100CA77B8 /* GrabberView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GrabberView.swift; sourceTree = "<group>"; };
|
||||
5469F49F24B003EF00537F8A /* LaunchScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = LaunchScreen.storyboard; sourceTree = "<group>"; };
|
||||
@@ -74,7 +73,7 @@
|
||||
5469F4AF24B30E1500537F8A /* LayoutAnchoring.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LayoutAnchoring.swift; sourceTree = "<group>"; };
|
||||
5469F4B124B30F1100537F8A /* Position.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Position.swift; sourceTree = "<group>"; };
|
||||
5469F4B324B30F3500537F8A /* LayoutReferences.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LayoutReferences.swift; sourceTree = "<group>"; };
|
||||
549C371E2361E15D007D8058 /* UtilTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UtilTests.swift; sourceTree = "<group>"; };
|
||||
549C371E2361E15D007D8058 /* ExtensionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExtensionTests.swift; sourceTree = "<group>"; };
|
||||
549E944422CF295D0050AECF /* StateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StateTests.swift; sourceTree = "<group>"; };
|
||||
54A6B6B022968B530077F348 /* CoreTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreTests.swift; sourceTree = "<group>"; };
|
||||
54A6B6B72296A8520077F348 /* SurfaceViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SurfaceViewTests.swift; sourceTree = "<group>"; };
|
||||
@@ -83,6 +82,7 @@
|
||||
54CDC5D4215B6D8D007D205C /* BackdropView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackdropView.swift; sourceTree = "<group>"; };
|
||||
54CFBFC2215CD045006B5735 /* Layout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Layout.swift; sourceTree = "<group>"; };
|
||||
54CFBFC4215CD09C006B5735 /* Core.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Core.swift; sourceTree = "<group>"; };
|
||||
54DBA3DB262E938500D75969 /* Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Extensions.swift; sourceTree = "<group>"; };
|
||||
54E740CA218AFD67005C1A34 /* FloatingPanelTesting.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = FloatingPanelTesting.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
@@ -142,12 +142,12 @@
|
||||
5469F4B324B30F3500537F8A /* LayoutReferences.swift */,
|
||||
5469F4AF24B30E1500537F8A /* LayoutAnchoring.swift */,
|
||||
5450EEE321646DF500135936 /* Behavior.swift */,
|
||||
54352E9721A521CA00CBCA08 /* PassThroughView.swift */,
|
||||
54352E9721A521CA00CBCA08 /* PassthroughView.swift */,
|
||||
54CDC5D2215B6D5A007D205C /* SurfaceView.swift */,
|
||||
54CDC5D4215B6D8D007D205C /* BackdropView.swift */,
|
||||
545DBA2A2152383100CA77B8 /* GrabberView.swift */,
|
||||
54352E9521A51A2500CBCA08 /* Transitioning.swift */,
|
||||
545DB9DD215118C800CA77B8 /* UIExtensions.swift */,
|
||||
54DBA3DB262E938500D75969 /* Extensions.swift */,
|
||||
54ABD7AE216CCFF7002E6C13 /* Logger.swift */,
|
||||
545DB9C42151169500CA77B8 /* FloatingPanel.h */,
|
||||
545DB9C52151169500CA77B8 /* Info.plist */,
|
||||
@@ -164,7 +164,7 @@
|
||||
542753C522C49A6E00D17955 /* LayoutTests.swift */,
|
||||
54A6B6B72296A8520077F348 /* SurfaceViewTests.swift */,
|
||||
549E944422CF295D0050AECF /* StateTests.swift */,
|
||||
549C371E2361E15D007D8058 /* UtilTests.swift */,
|
||||
549C371E2361E15D007D8058 /* ExtensionTests.swift */,
|
||||
542753C722C49A8F00D17955 /* TestSupports.swift */,
|
||||
545DB9D12151169500CA77B8 /* Info.plist */,
|
||||
);
|
||||
@@ -328,14 +328,14 @@
|
||||
54CFBFC3215CD045006B5735 /* Layout.swift in Sources */,
|
||||
5469F4B424B30F3500537F8A /* LayoutReferences.swift in Sources */,
|
||||
54CDC5D5215B6D8D007D205C /* BackdropView.swift in Sources */,
|
||||
54352E9821A521CA00CBCA08 /* PassThroughView.swift in Sources */,
|
||||
54352E9821A521CA00CBCA08 /* PassthroughView.swift in Sources */,
|
||||
54CFBFC5215CD09C006B5735 /* Core.swift in Sources */,
|
||||
54ABD7AF216CCFF7002E6C13 /* Logger.swift in Sources */,
|
||||
545DB9E021511AC100CA77B8 /* Controller.swift in Sources */,
|
||||
54DBA3DC262E938500D75969 /* Extensions.swift in Sources */,
|
||||
5450EEE421646DF500135936 /* Behavior.swift in Sources */,
|
||||
545DBA2B2152383100CA77B8 /* GrabberView.swift in Sources */,
|
||||
54352E9621A51A2500CBCA08 /* Transitioning.swift in Sources */,
|
||||
545DB9DE215118C800CA77B8 /* UIExtensions.swift in Sources */,
|
||||
5469F4AE24B30D7E00537F8A /* State.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
@@ -345,7 +345,7 @@
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
54A6B6B122968B530077F348 /* CoreTests.swift in Sources */,
|
||||
549C371F2361E15E007D8058 /* UtilTests.swift in Sources */,
|
||||
549C371F2361E15E007D8058 /* ExtensionTests.swift in Sources */,
|
||||
545DB9D02151169500CA77B8 /* ControllerTests.swift in Sources */,
|
||||
549E944522CF295D0050AECF /* StateTests.swift in Sources */,
|
||||
542753C622C49A6E00D17955 /* LayoutTests.swift in Sources */,
|
||||
|
||||
@@ -35,6 +35,8 @@ The new interface displays the related contents and utilities in parallel as a u
|
||||
- [Support your landscape layout](#support-your-landscape-layout)
|
||||
- [Use the intrinsic size of a content in your panel layout](#use-the-intrinsic-size-of-a-content-in-your-panel-layout)
|
||||
- [Specify an anchor for each state by an inset of the `FloatingPanelController.view` frame](#specify-an-anchor-for-each-state-by-an-inset-of-the-floatingpanelcontrollerview-frame)
|
||||
- [Change the backdrop alpha](#change-the-backdrop-alpha)
|
||||
- [Using custome panel states](#using-custome-panel-states)
|
||||
- [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 panel edges](#activate-the-rubber-band-effect-on-panel-edges)
|
||||
@@ -53,6 +55,7 @@ The new interface displays the related contents and utilities in parallel as a u
|
||||
- [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)
|
||||
- [Enabling the tap-to-dismiss action of the backdrop view](#enabling-the-tap-to-dismiss-action-of-the-backdrop-view)
|
||||
- [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)
|
||||
@@ -71,7 +74,7 @@ The new interface displays the related contents and utilities in parallel as a u
|
||||
- [x] Multi panel support
|
||||
- [x] Modal presentation
|
||||
- [x] 4 positioning support(top, left, bottom, right)
|
||||
- [x] 1~3 magnetic anchors(full, half, tip)
|
||||
- [x] 1 or more magnetic anchors(full, half, tip and more)
|
||||
- [x] Layout support for all trait environments(i.e. Landscape orientation)
|
||||
- [x] Common UI elements: surface, backdrop and grabber handle
|
||||
- [x] Free from common issues of Auto Layout and gesture handling
|
||||
@@ -163,7 +166,7 @@ self.present(fpc, animated: true, completion: nil)
|
||||
|
||||
You can show a floating panel over UINavigationController from the container view controllers as a modality of `.overCurrentContext` style.
|
||||
|
||||
:pencil2: FloatingPanelController has the custom presentation controller. If you would like to customize the presentation/dismissal, please see [FloatingPanelTransitioning](https://github.com/SCENEE/FloatingPanel/blob/master/Framework/Sources/FloatingPanelTransitioning.swift).
|
||||
:pencil2: FloatingPanelController has the custom presentation controller. If you would like to customize the presentation/dismissal, please see [Transitioning](https://github.com/SCENEE/FloatingPanel/blob/master/Sources/Transitioning.swift).
|
||||
|
||||
## View hierarchy
|
||||
|
||||
@@ -370,6 +373,46 @@ class MyFullScreenLayout: FloatingPanelLayout {
|
||||
|
||||
:pencil2: `FloatingPanelFullScreenLayout` is deprecated on v1.
|
||||
|
||||
#### Change the backdrop alpha
|
||||
|
||||
You can change the backdrop alpha by `FloatingPanelLayout.backdropAlpha(for:)` for each state(`.full`, `.half` and `.tip`).
|
||||
|
||||
For instance, if a panel seems like the backdrop view isn't there on `.half` state, it's time to implement the backdropAlpha API and return a value for the state as below.
|
||||
|
||||
```swift
|
||||
class MyPanelLayout: FloatingPanelLayout {
|
||||
func backdropAlpha(for state: FloatingPanelState) -> CGFloat {
|
||||
switch state {
|
||||
case .full, .half: return 0.3
|
||||
default: return 0.0
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Using custome panel states
|
||||
|
||||
You're able to define custom panel states and use them as the following example.
|
||||
|
||||
```swift
|
||||
extension FloatingPanelState {
|
||||
static let lastQuart: FloatingPanelState = FloatingPanelState(rawValue: "lastQuart", order: 750)
|
||||
static let firstQuart: FloatingPanelState = FloatingPanelState(rawValue: "firstQuart", order: 250)
|
||||
}
|
||||
|
||||
class FloatingPanelLayoutWithCustomState: FloatingPanelBottomLayout {
|
||||
override var anchors: [FloatingPanelState: FloatingPanelLayoutAnchoring] {
|
||||
return [
|
||||
.full: FloatingPanelLayoutAnchor(absoluteInset: 0.0, edge: .top, referenceGuide: .safeArea),
|
||||
.lastQuart: FloatingPanelLayoutAnchor(fractionalInset: 0.75, edge: .bottom, referenceGuide: .safeArea),
|
||||
.half: FloatingPanelLayoutAnchor(fractionalInset: 0.5, edge: .bottom, referenceGuide: .safeArea),
|
||||
.firstQuart: FloatingPanelLayoutAnchor(fractionalInset: 0.25, edge: .bottom, referenceGuide: .safeArea),
|
||||
.tip: FloatingPanelLayoutAnchor(absoluteInset: 20.0, edge: .bottom, referenceGuide: .safeArea),
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Customize the behavior with `FloatingPanelBehavior` protocol
|
||||
|
||||
#### Modify your floating panel's interaction
|
||||
@@ -522,8 +565,8 @@ override func viewDidLoad() {
|
||||
surfaceTapGesture.isEnabled = (fpc.position == .tip)
|
||||
}
|
||||
|
||||
// Enable `surfaceTapGesture` only at `tip` position
|
||||
func floatingPanelDidChangePosition(_ vc: FloatingPanelController) {
|
||||
// Enable `surfaceTapGesture` only at `tip` state
|
||||
func floatingPanelDidChangeState(_ vc: FloatingPanelController) {
|
||||
surfaceTapGesture.isEnabled = (vc.position == .tip)
|
||||
}
|
||||
```
|
||||
@@ -616,6 +659,14 @@ class ViewController: UIViewController, FloatingPanelControllerDelegate {
|
||||
}
|
||||
```
|
||||
|
||||
### Enabling the tap-to-dismiss action of the backdrop view
|
||||
|
||||
The tap-to-dismiss action is disabled by default. So it needs to be enabled as below.
|
||||
|
||||
```swift
|
||||
fpc.backdropView.dismissalTapGestureRecognizer.isEnabled = true
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
### 'Show' or 'Show Detail' Segues from `FloatingPanelController`'s content view controller
|
||||
|
||||
@@ -7,5 +7,5 @@ import UIKit
|
||||
public class BackdropView: UIView {
|
||||
|
||||
/// The gesture recognizer for tap gestures to dismiss a panel.
|
||||
public var dismissalTapGestureRecognizer: UITapGestureRecognizer!
|
||||
@objc public var dismissalTapGestureRecognizer: UITapGestureRecognizer!
|
||||
}
|
||||
|
||||
+20
-19
@@ -43,44 +43,41 @@ public protocol FloatingPanelBehavior {
|
||||
/// This method allows a panel to activate the rubber band effect to a given edge of the surface view. By default, the effect is disabled.
|
||||
@objc optional
|
||||
func allowsRubberBanding(for edge: UIRectEdge) -> Bool
|
||||
|
||||
/// Returns the velocity threshold for the default interactive removal gesture.
|
||||
///
|
||||
/// In case `floatingPanel:shouldRemoveAt:with` is implemented, this value will not be used. The default value of `FloatingPanelDefaultBehavior` is 5.5
|
||||
@objc optional
|
||||
var removalInteractionVelocityThreshold: CGFloat { get }
|
||||
}
|
||||
|
||||
/// The default behavior object for a panel
|
||||
///
|
||||
/// This behavior object is fine-tuned to behave as a search panel(card) in Apple Maps on iPhone portrait orientation.
|
||||
public class FloatingPanelDefaultBehavior: FloatingPanelBehavior {
|
||||
public var springDecelerationRate: CGFloat {
|
||||
open class FloatingPanelDefaultBehavior: FloatingPanelBehavior {
|
||||
public init() {}
|
||||
|
||||
open var springDecelerationRate: CGFloat {
|
||||
return UIScrollView.DecelerationRate.fast.rawValue + 0.001
|
||||
}
|
||||
|
||||
public var springResponseTime: CGFloat {
|
||||
open var springResponseTime: CGFloat {
|
||||
return 0.4
|
||||
}
|
||||
|
||||
public var momentumProjectionRate: CGFloat {
|
||||
open var momentumProjectionRate: CGFloat {
|
||||
return UIScrollView.DecelerationRate.normal.rawValue
|
||||
}
|
||||
|
||||
public func redirectionalProgress(_ fpc: FloatingPanelController, from: FloatingPanelState, to: FloatingPanelState) -> CGFloat {
|
||||
open func redirectionalProgress(_ fpc: FloatingPanelController, from: FloatingPanelState, to: FloatingPanelState) -> CGFloat {
|
||||
return 0.5
|
||||
}
|
||||
|
||||
func addPanelAnimator(_ fpc: FloatingPanelController, to: FloatingPanelState) -> UIViewPropertyAnimator {
|
||||
return UIViewPropertyAnimator(duration: 0.0,
|
||||
timingParameters: UISpringTimingParameters(decelerationRate: UIScrollView.DecelerationRate.fast.rawValue,
|
||||
frequencyResponse: 0.25))
|
||||
}
|
||||
|
||||
func removePanelAnimator(_ fpc: FloatingPanelController, from: FloatingPanelState, with velocity: CGVector) -> UIViewPropertyAnimator {
|
||||
return UIViewPropertyAnimator(duration: 0.0,
|
||||
timingParameters: UISpringTimingParameters(decelerationRate: UIScrollView.DecelerationRate.fast.rawValue,
|
||||
frequencyResponse: 0.25,
|
||||
initialVelocity: velocity))
|
||||
}
|
||||
|
||||
public func allowsRubberBanding(for edge: UIRectEdge) -> Bool {
|
||||
open func allowsRubberBanding(for edge: UIRectEdge) -> Bool {
|
||||
return false
|
||||
}
|
||||
|
||||
open var removalInteractionVelocityThreshold: CGFloat = 5.5
|
||||
}
|
||||
|
||||
class BehaviorAdapter {
|
||||
@@ -103,6 +100,10 @@ class BehaviorAdapter {
|
||||
var momentumProjectionRate: CGFloat {
|
||||
behavior.momentumProjectionRate ?? FloatingPanelDefaultBehavior().momentumProjectionRate
|
||||
}
|
||||
|
||||
var removalInteractionVelocityThreshold: CGFloat {
|
||||
behavior.removalInteractionVelocityThreshold ?? FloatingPanelDefaultBehavior().removalInteractionVelocityThreshold
|
||||
}
|
||||
|
||||
func redirectionalProgress(from: FloatingPanelState, to: FloatingPanelState) -> CGFloat {
|
||||
behavior.redirectionalProgress?(vc, from: from, to: to) ?? FloatingPanelDefaultBehavior().redirectionalProgress(vc,from: from, to: to)
|
||||
|
||||
+49
-27
@@ -5,7 +5,7 @@ import UIKit
|
||||
/// A set of methods implemented by the delegate of a panel controller allows the adopting delegate to respond to
|
||||
/// messages from the FloatingPanelController class and thus respond to, and in some affect, operations such as
|
||||
/// dragging, attracting a panel, layout of a panel and the content, and transition animations.
|
||||
@objc public protocol FloatingPanelControllerDelegate: class {
|
||||
@objc public protocol FloatingPanelControllerDelegate {
|
||||
/// Returns a FloatingPanelLayout object. If you use the default one, you can use a `FloatingPanelBottomLayout` object.
|
||||
@objc(floatingPanel:layoutForTraitCollection:) optional
|
||||
func floatingPanel(_ fpc: FloatingPanelController, layoutFor newCollection: UITraitCollection) -> FloatingPanelLayout
|
||||
@@ -26,10 +26,12 @@ import UIKit
|
||||
@objc(floatingPanel:animatorForDismissingWithVelocity:) optional
|
||||
func floatingPanel(_ fpc: FloatingPanelController, animatorForDismissingWith velocity: CGVector) -> UIViewPropertyAnimator
|
||||
|
||||
/// Called when a 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 a panel.
|
||||
/// Called when a panel has changed to a new state.
|
||||
///
|
||||
/// This can be called inside an animation block for presenting, dismissing a panel or moving a panel with your
|
||||
/// animation. So any view properties set inside this function will be automatically animated alongside a panel.
|
||||
@objc optional
|
||||
func floatingPanelDidChangePosition(_ fpc: FloatingPanelController)
|
||||
func floatingPanelDidChangeState(_ fpc: FloatingPanelController)
|
||||
|
||||
/// Asks the delegate if dragging should begin by the pan gesture recognizer.
|
||||
@objc optional
|
||||
@@ -274,7 +276,7 @@ open class FloatingPanelController: UIViewController {
|
||||
open override func loadView() {
|
||||
assert(self.storyboard == nil, "Storyboard isn't supported")
|
||||
|
||||
let view = PassThroughView()
|
||||
let view = PassthroughView()
|
||||
view.backgroundColor = .clear
|
||||
|
||||
backdropView.frame = view.bounds
|
||||
@@ -396,20 +398,8 @@ open class FloatingPanelController: UIViewController {
|
||||
}
|
||||
|
||||
private func activateLayout(forceLayout: Bool = false) {
|
||||
floatingPanel.layoutAdapter.prepareLayout()
|
||||
|
||||
// preserve the current content offset if contentInsetAdjustmentBehavior is `.always`
|
||||
var contentOffset: CGPoint?
|
||||
if contentInsetAdjustmentBehavior == .always {
|
||||
contentOffset = trackingScrollView?.contentOffset
|
||||
}
|
||||
|
||||
floatingPanel.layoutAdapter.updateStaticConstraint()
|
||||
floatingPanel.layoutAdapter.activateLayout(for: floatingPanel.state, forceLayout: forceLayout)
|
||||
|
||||
if let contentOffset = contentOffset {
|
||||
trackingScrollView?.contentOffset = contentOffset
|
||||
}
|
||||
floatingPanel.activateLayout(forceLayout: forceLayout,
|
||||
contentInsetAdjustmentBehavior: contentInsetAdjustmentBehavior)
|
||||
}
|
||||
|
||||
func remove() {
|
||||
@@ -427,6 +417,9 @@ open class FloatingPanelController: UIViewController {
|
||||
// MARK: - Container view controller interface
|
||||
|
||||
/// Shows the surface view at the initial position defined by the current layout
|
||||
/// - Parameters:
|
||||
/// - animated: Pass true to animate the presentation; otherwise, pass false.
|
||||
/// - completion: The block to execute after the presentation finishes. This block has no return value and takes no parameters. You may specify nil for this parameter.
|
||||
@objc(show:completion:)
|
||||
public func show(animated: Bool = false, completion: (() -> Void)? = nil) {
|
||||
// Must apply the current layout here
|
||||
@@ -468,8 +461,9 @@ open class FloatingPanelController: UIViewController {
|
||||
/// - parent: A parent view controller object that displays FloatingPanelController's view. A container view controller object isn't applicable.
|
||||
/// - viewIndex: Insert the surface view managed by the controller below the specified view index. By default, the surface view will be added to the end of the parent list of subviews.
|
||||
/// - animated: Pass true to animate the presentation; otherwise, pass false.
|
||||
@objc(addPanelToParent:at:animated:)
|
||||
public func addPanel(toParent parent: UIViewController, at viewIndex: Int = -1, animated: Bool = false) {
|
||||
/// - completion: The block to execute after the presentation finishes. This block has no return value and takes no parameters. You may specify nil for this parameter.
|
||||
@objc(addPanelToParent:at:animated:completion:)
|
||||
public func addPanel(toParent parent: UIViewController, at viewIndex: Int = -1, animated: Bool = false, completion: (() -> Void)? = nil) {
|
||||
guard self.parent == nil else {
|
||||
log.warning("Already added to a parent(\(parent))")
|
||||
return
|
||||
@@ -498,8 +492,9 @@ open class FloatingPanelController: UIViewController {
|
||||
])
|
||||
|
||||
show(animated: animated) { [weak self] in
|
||||
guard let `self` = self else { return }
|
||||
guard let self = self else { return }
|
||||
self.didMove(toParent: parent)
|
||||
completion?()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -517,7 +512,7 @@ open class FloatingPanelController: UIViewController {
|
||||
delegate?.floatingPanelWillRemove?(self)
|
||||
|
||||
hide(animated: animated) { [weak self] in
|
||||
guard let `self` = self else { return }
|
||||
guard let self = self else { return }
|
||||
|
||||
self.willMove(toParent: nil)
|
||||
|
||||
@@ -531,13 +526,13 @@ open class FloatingPanelController: UIViewController {
|
||||
}
|
||||
|
||||
/// Moves the position to the specified position.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - to: Pass a FloatingPanelPosition value to move the surface view to the position.
|
||||
/// - animated: Pass true to animate the presentation; otherwise, pass false.
|
||||
/// - completion: The block to execute after the view controller has finished moving. This block has no return value and takes no parameters. You may specify nil for this parameter.
|
||||
@objc(moveToState:animated:completion:)
|
||||
public func move(to: FloatingPanelState, animated: Bool, completion: (() -> Void)? = nil) {
|
||||
assert(floatingPanel.layoutAdapter.vc != nil, "Use show(animated:completion)")
|
||||
floatingPanel.move(to: to, animated: animated, completion: completion)
|
||||
}
|
||||
|
||||
@@ -598,6 +593,14 @@ open class FloatingPanelController: UIViewController {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Accessibility
|
||||
|
||||
open override func accessibilityPerformEscape() -> Bool {
|
||||
guard isRemovalInteractionEnabled else { return false }
|
||||
dismiss(animated: true, completion: nil)
|
||||
return true
|
||||
}
|
||||
|
||||
// MARK: - Utilities
|
||||
|
||||
/// Updates the layout object from the delegate and lays out the views managed
|
||||
@@ -641,10 +644,31 @@ extension FloatingPanelController {
|
||||
#endif
|
||||
delegate?.floatingPanelDidMove?(self)
|
||||
}
|
||||
|
||||
func animatorForPresenting(to: FloatingPanelState) -> UIViewPropertyAnimator {
|
||||
if let animator = delegate?.floatingPanel?(self, animatorForPresentingTo: to) {
|
||||
return animator
|
||||
}
|
||||
let timingParameters = UISpringTimingParameters(decelerationRate: UIScrollView.DecelerationRate.fast.rawValue,
|
||||
frequencyResponse: 0.25)
|
||||
return UIViewPropertyAnimator(duration: 0.0,
|
||||
timingParameters: timingParameters)
|
||||
}
|
||||
|
||||
func animatorForDismissing(with velocity: CGVector) -> UIViewPropertyAnimator {
|
||||
if let animator = delegate?.floatingPanel?(self, animatorForDismissingWith: velocity) {
|
||||
return animator
|
||||
}
|
||||
let timingParameters = UISpringTimingParameters(decelerationRate: UIScrollView.DecelerationRate.fast.rawValue,
|
||||
frequencyResponse: 0.25,
|
||||
initialVelocity: velocity)
|
||||
return UIViewPropertyAnimator(duration: 0.0,
|
||||
timingParameters: timingParameters)
|
||||
}
|
||||
}
|
||||
|
||||
extension FloatingPanelController {
|
||||
private static let dismissSwizzling: Any? = {
|
||||
private static let dismissSwizzling: Void = {
|
||||
let aClass: AnyClass! = UIViewController.self //object_getClass(vc)
|
||||
if let imp = class_getMethodImplementation(aClass, #selector(dismiss(animated:completion:))),
|
||||
let originalAltMethod = class_getInstanceMethod(aClass, #selector(fp_original_dismiss(animated:completion:))) {
|
||||
@@ -653,10 +677,8 @@ extension FloatingPanelController {
|
||||
let originalMethod = class_getInstanceMethod(aClass, #selector(dismiss(animated:completion:)))
|
||||
let swizzledMethod = class_getInstanceMethod(aClass, #selector(fp_dismiss(animated:completion:)))
|
||||
if let originalMethod = originalMethod, let swizzledMethod = swizzledMethod {
|
||||
// switch implementation..
|
||||
method_exchangeImplementations(originalMethod, swizzledMethod)
|
||||
}
|
||||
return nil
|
||||
}()
|
||||
}
|
||||
|
||||
|
||||
+121
-61
@@ -24,7 +24,7 @@ class Core: NSObject, UIGestureRecognizerDelegate {
|
||||
didSet {
|
||||
log.debug("state changed: \(oldValue) -> \(state)")
|
||||
if let vc = ownerVC {
|
||||
vc.delegate?.floatingPanelDidChangePosition?(vc)
|
||||
vc.delegate?.floatingPanelDidChangeState?(vc)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -32,7 +32,8 @@ class Core: NSObject, UIGestureRecognizerDelegate {
|
||||
let panGestureRecognizer: FloatingPanelPanGestureRecognizer
|
||||
var isRemovalInteractionEnabled: Bool = false
|
||||
|
||||
fileprivate var animator: UIViewPropertyAnimator?
|
||||
fileprivate var isSuspended: Bool = false // Prevent a memory leak in the modal transition
|
||||
fileprivate var transitionAnimator: UIViewPropertyAnimator?
|
||||
fileprivate var moveAnimator: NumericSpringAnimator?
|
||||
|
||||
private var initialSurfaceLocation: CGPoint = .zero
|
||||
@@ -68,11 +69,8 @@ class Core: NSObject, UIGestureRecognizerDelegate {
|
||||
backdropView.backgroundColor = .black
|
||||
backdropView.alpha = 0.0
|
||||
|
||||
self.layoutAdapter = LayoutAdapter(vc: vc,
|
||||
surfaceView: surfaceView,
|
||||
backdropView: backdropView,
|
||||
layout: layout)
|
||||
self.behaviorAdapter = BehaviorAdapter(vc: vc, behavior: behavior)
|
||||
layoutAdapter = LayoutAdapter(vc: vc, layout: layout)
|
||||
behaviorAdapter = BehaviorAdapter(vc: vc, behavior: behavior)
|
||||
|
||||
panGestureRecognizer = FloatingPanelPanGestureRecognizer()
|
||||
|
||||
@@ -94,6 +92,11 @@ class Core: NSObject, UIGestureRecognizerDelegate {
|
||||
backdropView.addGestureRecognizer(tapGesture)
|
||||
}
|
||||
|
||||
deinit {
|
||||
// Release `NumericSpringAnimator.displayLink` from the run loop.
|
||||
self.moveAnimator?.stopAnimation(false)
|
||||
}
|
||||
|
||||
func move(to: FloatingPanelState, animated: Bool, completion: (() -> Void)? = nil) {
|
||||
move(from: state, to: to, animated: animated, completion: completion)
|
||||
}
|
||||
@@ -104,7 +107,7 @@ class Core: NSObject, UIGestureRecognizerDelegate {
|
||||
completion?()
|
||||
return
|
||||
}
|
||||
if state != layoutAdapter.edgeMostState {
|
||||
if state != layoutAdapter.mostExpandedState {
|
||||
lockScrollView()
|
||||
}
|
||||
tearDownActiveInteraction()
|
||||
@@ -112,8 +115,9 @@ class Core: NSObject, UIGestureRecognizerDelegate {
|
||||
interruptAnimationIfNeeded()
|
||||
|
||||
if animated {
|
||||
func updateScrollView() {
|
||||
if self.state == self.layoutAdapter.edgeMostState, abs(self.layoutAdapter.offsetFromEdgeMost) <= 1.0 {
|
||||
let updateScrollView: () -> Void = { [weak self] in
|
||||
guard let self = self else { return }
|
||||
if self.state == self.layoutAdapter.mostExpandedState, abs(self.layoutAdapter.offsetFromMostExpandedAnchor) <= 1.0 {
|
||||
self.unlockScrollView()
|
||||
} else {
|
||||
self.lockScrollView()
|
||||
@@ -123,14 +127,15 @@ class Core: NSObject, UIGestureRecognizerDelegate {
|
||||
let animator: UIViewPropertyAnimator
|
||||
switch (from, to) {
|
||||
case (.hidden, let to):
|
||||
animator = vc.delegate?.floatingPanel?(vc, animatorForPresentingTo: to)
|
||||
?? FloatingPanelDefaultBehavior().addPanelAnimator(vc, to: to)
|
||||
case (let from, .hidden):
|
||||
animator = vc.animatorForPresenting(to: to)
|
||||
case (_, .hidden):
|
||||
let animationVector = CGVector(dx: abs(removalVector.dx), dy: abs(removalVector.dy))
|
||||
animator = vc.delegate?.floatingPanel?(vc, animatorForDismissingWith: .zero)
|
||||
?? FloatingPanelDefaultBehavior().removePanelAnimator(vc, from: from, with: animationVector)
|
||||
animator = vc.animatorForDismissing(with: animationVector)
|
||||
default:
|
||||
move(to: to, with: 0) {
|
||||
move(to: to, with: 0) { [weak self] in
|
||||
guard let self = self else { return }
|
||||
|
||||
self.moveAnimator = nil
|
||||
updateScrollView()
|
||||
completion?()
|
||||
}
|
||||
@@ -142,7 +147,7 @@ class Core: NSObject, UIGestureRecognizerDelegate {
|
||||
&& layoutAdapter.isIntrinsicAnchor(state: to)
|
||||
|
||||
animator.addAnimations { [weak self] in
|
||||
guard let `self` = self else { return }
|
||||
guard let self = self else { return }
|
||||
|
||||
self.state = to
|
||||
self.updateLayout(to: to)
|
||||
@@ -153,18 +158,22 @@ class Core: NSObject, UIGestureRecognizerDelegate {
|
||||
}
|
||||
}
|
||||
animator.addCompletion { [weak self] _ in
|
||||
guard let `self` = self else { return }
|
||||
self.animator = nil
|
||||
guard let self = self else { return }
|
||||
|
||||
self.transitionAnimator = nil
|
||||
updateScrollView()
|
||||
self.ownerVC?.notifyDidMove()
|
||||
completion?()
|
||||
}
|
||||
self.animator = animator
|
||||
self.transitionAnimator = animator
|
||||
if isSuspended {
|
||||
return
|
||||
}
|
||||
animator.startAnimation()
|
||||
} else {
|
||||
self.state = to
|
||||
self.updateLayout(to: to)
|
||||
if self.state == self.layoutAdapter.edgeMostState {
|
||||
if self.state == self.layoutAdapter.mostExpandedState {
|
||||
self.unlockScrollView()
|
||||
} else {
|
||||
self.lockScrollView()
|
||||
@@ -177,8 +186,37 @@ class Core: NSObject, UIGestureRecognizerDelegate {
|
||||
|
||||
// MARK: - Layout update
|
||||
|
||||
func activateLayout(forceLayout: Bool = false,
|
||||
contentInsetAdjustmentBehavior: FloatingPanelController.ContentInsetAdjustmentBehavior) {
|
||||
layoutAdapter.prepareLayout()
|
||||
|
||||
// preserve the current content offset if contentInsetAdjustmentBehavior is `.always`
|
||||
var contentOffset: CGPoint?
|
||||
if contentInsetAdjustmentBehavior == .always {
|
||||
contentOffset = scrollView?.contentOffset
|
||||
}
|
||||
|
||||
layoutAdapter.updateStaticConstraint()
|
||||
layoutAdapter.activateLayout(for: state, forceLayout: true)
|
||||
|
||||
// Update the backdrop alpha only when called in `Controller.show(animated:completion:)`
|
||||
// Because that prevents a backdrop flicking just before presenting a panel(#466).
|
||||
if forceLayout {
|
||||
backdropView.alpha = getBackdropAlpha(for: state)
|
||||
}
|
||||
|
||||
if let contentOffset = contentOffset {
|
||||
scrollView?.contentOffset = contentOffset
|
||||
}
|
||||
}
|
||||
|
||||
private func updateLayout(to target: FloatingPanelState) {
|
||||
self.layoutAdapter.activateLayout(for: state, forceLayout: true)
|
||||
self.layoutAdapter.activateLayout(for: target, forceLayout: true)
|
||||
self.backdropView.alpha = self.getBackdropAlpha(for: target)
|
||||
}
|
||||
|
||||
private func getBackdropAlpha(for target: FloatingPanelState) -> CGFloat {
|
||||
return target == .hidden ? 0.0 : layoutAdapter.backdropAlpha(for: target)
|
||||
}
|
||||
|
||||
func getBackdropAlpha(at cur: CGFloat, with translation: CGFloat) -> CGFloat {
|
||||
@@ -187,8 +225,8 @@ class Core: NSObject, UIGestureRecognizerDelegate {
|
||||
|
||||
let segment = layoutAdapter.segment(at: cur, forward: forwardY)
|
||||
|
||||
let lowerState = segment.lower ?? layoutAdapter.edgeMostState
|
||||
let upperState = segment.upper ?? layoutAdapter.edgeLeastState
|
||||
let lowerState = segment.lower ?? layoutAdapter.mostExpandedState
|
||||
let upperState = segment.upper ?? layoutAdapter.leastExpandedState
|
||||
|
||||
let preState = forwardY ? lowerState : upperState
|
||||
let nextState = forwardY ? upperState : lowerState
|
||||
@@ -201,9 +239,8 @@ class Core: NSObject, UIGestureRecognizerDelegate {
|
||||
|
||||
if pre == next {
|
||||
return preAlpha
|
||||
} else {
|
||||
return preAlpha + max(min(1.0, 1.0 - (next - cur) / (next - pre) ), 0.0) * (nextAlpha - preAlpha)
|
||||
}
|
||||
return preAlpha + max(min(1.0, 1.0 - (next - cur) / (next - pre) ), 0.0) * (nextAlpha - preAlpha)
|
||||
}
|
||||
|
||||
// MARK: - UIGestureRecognizerDelegate
|
||||
@@ -340,7 +377,7 @@ class Core: NSObject, UIGestureRecognizerDelegate {
|
||||
let velocity = value(of: panGesture.velocity(in: panGesture.view))
|
||||
let location = panGesture.location(in: surfaceView)
|
||||
|
||||
let belowEdgeMost = 0 > layoutAdapter.offsetFromEdgeMost + (1.0 / surfaceView.fp_displayScale)
|
||||
let belowEdgeMost = 0 > layoutAdapter.offsetFromMostExpandedAnchor + (1.0 / surfaceView.fp_displayScale)
|
||||
|
||||
log.debug("""
|
||||
scroll gesture(\(state):\(panGesture.state)) -- \
|
||||
@@ -354,7 +391,7 @@ class Core: NSObject, UIGestureRecognizerDelegate {
|
||||
|
||||
if belowEdgeMost {
|
||||
// Scroll offset pinning
|
||||
if state == layoutAdapter.edgeMostState {
|
||||
if state == layoutAdapter.mostExpandedState {
|
||||
if interactionInProgress {
|
||||
log.debug("settle offset --", value(of: initialScrollOffset))
|
||||
stopScrolling(at: initialScrollOffset)
|
||||
@@ -372,7 +409,7 @@ class Core: NSObject, UIGestureRecognizerDelegate {
|
||||
if interactionInProgress {
|
||||
lockScrollView()
|
||||
} else {
|
||||
if state == layoutAdapter.edgeMostState, self.animator == nil {
|
||||
if state == layoutAdapter.mostExpandedState, self.transitionAnimator == nil {
|
||||
switch layoutAdapter.position {
|
||||
case .top, .left:
|
||||
if offsetDiff < 0 && velocity > 0 {
|
||||
@@ -400,14 +437,14 @@ class Core: NSObject, UIGestureRecognizerDelegate {
|
||||
return
|
||||
}
|
||||
}
|
||||
if state == layoutAdapter.edgeMostState {
|
||||
if state == layoutAdapter.mostExpandedState {
|
||||
// Adjust a small gap of the scroll offset just after swiping down starts in the grabber area.
|
||||
if grabberAreaFrame.contains(location), grabberAreaFrame.contains(initialLocation) {
|
||||
stopScrolling(at: initialScrollOffset)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if state == layoutAdapter.edgeMostState {
|
||||
if state == layoutAdapter.mostExpandedState {
|
||||
switch layoutAdapter.position {
|
||||
case .top, .left:
|
||||
if velocity < 0, !allowScrollPanGesture(for: scrollView) {
|
||||
@@ -492,16 +529,16 @@ class Core: NSObject, UIGestureRecognizerDelegate {
|
||||
animator.stopAnimation(true)
|
||||
endAttraction(false)
|
||||
}
|
||||
if let animator = self.animator {
|
||||
guard 0 >= layoutAdapter.offsetFromEdgeMost else { return }
|
||||
if let animator = self.transitionAnimator {
|
||||
guard 0 >= layoutAdapter.offsetFromMostExpandedAnchor else { return }
|
||||
log.debug("a panel animation(interruptible: \(animator.isInterruptible)) 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(layoutAdapter.offsetFromEdgeMost) <= 1.0 {
|
||||
layoutAdapter.surfaceLocation = layoutAdapter.surfaceLocation(for: layoutAdapter.edgeMostState)
|
||||
if abs(layoutAdapter.offsetFromMostExpandedAnchor) <= 1.0 {
|
||||
layoutAdapter.surfaceLocation = layoutAdapter.surfaceLocation(for: layoutAdapter.mostExpandedState)
|
||||
}
|
||||
animator.finishAnimation(at: .current)
|
||||
} else {
|
||||
@@ -527,22 +564,27 @@ class Core: NSObject, UIGestureRecognizerDelegate {
|
||||
}
|
||||
|
||||
guard
|
||||
state == layoutAdapter.edgeMostState, // When not top most(i.e. .full), don't scroll.
|
||||
state == layoutAdapter.mostExpandedState, // When not top most(i.e. .full), don't scroll.
|
||||
interactionInProgress == false, // When interaction already in progress, don't scroll.
|
||||
0 == layoutAdapter.offsetFromEdgeMost
|
||||
0 == layoutAdapter.offsetFromMostExpandedAnchor
|
||||
else {
|
||||
return false
|
||||
}
|
||||
|
||||
// When the current and initial point within grabber area, do scroll.
|
||||
// When the current point is within grabber area but the initial point is not, do scroll.
|
||||
if grabberAreaFrame.contains(point), !grabberAreaFrame.contains(initialLocation) {
|
||||
return true
|
||||
}
|
||||
|
||||
// When the initial point is within grabber area and the current point is out of surface, don't scroll.
|
||||
if grabberAreaFrame.contains(initialLocation), !surfaceView.frame.contains(point) {
|
||||
return false
|
||||
}
|
||||
|
||||
let scrollViewFrame = scrollView.convert(scrollView.bounds, to: surfaceView)
|
||||
guard
|
||||
scrollViewFrame.contains(initialLocation), // When initialLocation not in scrollView, don't scroll.
|
||||
!grabberAreaFrame.contains(point) // When point within grabber area, don't scroll.
|
||||
scrollViewFrame.contains(initialLocation), // When the initial point not in scrollView, don't scroll.
|
||||
!grabberAreaFrame.contains(point) // When point within grabber area, don't scroll.
|
||||
else {
|
||||
return false
|
||||
}
|
||||
@@ -570,6 +612,9 @@ class Core: NSObject, UIGestureRecognizerDelegate {
|
||||
if scrollView.isDecelerating {
|
||||
return true
|
||||
}
|
||||
if let tableView = (scrollView as? UITableView), tableView.isEditing {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
@@ -581,7 +626,7 @@ class Core: NSObject, UIGestureRecognizerDelegate {
|
||||
log.debug("panningBegan -- location = \(value(of: location))")
|
||||
|
||||
guard let scrollView = scrollView else { return }
|
||||
if state == layoutAdapter.edgeMostState {
|
||||
if state == layoutAdapter.mostExpandedState {
|
||||
if grabberAreaFrame.contains(location) {
|
||||
initialScrollOffset = scrollView.contentOffset
|
||||
}
|
||||
@@ -648,10 +693,11 @@ class Core: NSObject, UIGestureRecognizerDelegate {
|
||||
return
|
||||
}
|
||||
|
||||
stopScrollDeceleration = (0 > layoutAdapter.offsetFromEdgeMost + (1.0 / surfaceView.fp_displayScale)) // Projecting the dragging to the scroll dragging or not
|
||||
stopScrollDeceleration = (0 > layoutAdapter.offsetFromMostExpandedAnchor + (1.0 / surfaceView.fp_displayScale)) // Projecting the dragging to the scroll dragging or not
|
||||
if stopScrollDeceleration {
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
guard let `self` = self else { return }
|
||||
guard let self = self else { return }
|
||||
|
||||
self.stopScrolling(at: self.initialScrollOffset)
|
||||
}
|
||||
}
|
||||
@@ -697,14 +743,14 @@ class Core: NSObject, UIGestureRecognizerDelegate {
|
||||
|
||||
// Workaround: Disable a tracking scroll to prevent bouncing a scroll content in a panel animating
|
||||
let isScrollEnabled = scrollView?.isScrollEnabled
|
||||
if let scrollView = scrollView, targetPosition != .full {
|
||||
if let scrollView = scrollView, targetPosition != layoutAdapter.mostExpandedState {
|
||||
scrollView.isScrollEnabled = false
|
||||
}
|
||||
|
||||
startAttraction(to: targetPosition, with: velocity)
|
||||
|
||||
// Workaround: Reset `self.scrollView.isScrollEnabled`
|
||||
if let scrollView = scrollView, targetPosition != .full,
|
||||
if let scrollView = scrollView, targetPosition != layoutAdapter.mostExpandedState,
|
||||
let isScrollEnabled = isScrollEnabled {
|
||||
scrollView.isScrollEnabled = isScrollEnabled
|
||||
}
|
||||
@@ -717,15 +763,16 @@ class Core: NSObject, UIGestureRecognizerDelegate {
|
||||
if let result = vc.delegate?.floatingPanel?(vc, shouldRemoveAt: vc.surfaceLocation, with: velocityVector) {
|
||||
return result
|
||||
}
|
||||
let threshold = behaviorAdapter.removalInteractionVelocityThreshold
|
||||
switch layoutAdapter.position {
|
||||
case .top:
|
||||
return (velocityVector.dy <= -10.0)
|
||||
return (velocityVector.dy <= -threshold)
|
||||
case .left:
|
||||
return (velocityVector.dx <= -10.0)
|
||||
return (velocityVector.dx <= -threshold)
|
||||
case .bottom:
|
||||
return (velocityVector.dy >= 10.0)
|
||||
return (velocityVector.dy >= threshold)
|
||||
case .right:
|
||||
return (velocityVector.dx >= 10.0)
|
||||
return (velocityVector.dx >= threshold)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -737,7 +784,7 @@ class Core: NSObject, UIGestureRecognizerDelegate {
|
||||
var offset: CGPoint = .zero
|
||||
|
||||
initialSurfaceLocation = layoutAdapter.surfaceLocation
|
||||
if state == layoutAdapter.edgeMostState, let scrollView = scrollView {
|
||||
if state == layoutAdapter.mostExpandedState, let scrollView = scrollView {
|
||||
if grabberAreaFrame.contains(location) {
|
||||
initialScrollOffset = scrollView.contentOffset
|
||||
} else {
|
||||
@@ -782,7 +829,7 @@ class Core: NSObject, UIGestureRecognizerDelegate {
|
||||
interactionInProgress = false
|
||||
|
||||
// Prevent to keep a scroll view indicator visible at the half/tip position
|
||||
if targetPosition != layoutAdapter.edgeMostState {
|
||||
if targetPosition != layoutAdapter.mostExpandedState {
|
||||
lockScrollView()
|
||||
}
|
||||
|
||||
@@ -790,6 +837,7 @@ class Core: NSObject, UIGestureRecognizerDelegate {
|
||||
}
|
||||
|
||||
private func tearDownActiveInteraction() {
|
||||
guard panGestureRecognizer.isEnabled else { return }
|
||||
// Cancel the pan gesture so that panningEnd(with:velocity:) is called
|
||||
panGestureRecognizer.isEnabled = false
|
||||
panGestureRecognizer.isEnabled = true
|
||||
@@ -823,16 +871,19 @@ class Core: NSObject, UIGestureRecognizerDelegate {
|
||||
decelerationRate: behaviorAdapter.springDecelerationRate,
|
||||
responseTime: behaviorAdapter.springResponseTime,
|
||||
update: { [weak self] data in
|
||||
guard let self = self else { return }
|
||||
guard let self = self,
|
||||
let ownerVC = self.ownerVC // Ensure the owner vc is existing for `layoutAdapter.surfaceLocation`
|
||||
else { return }
|
||||
animationConstraint.constant = data.value
|
||||
let current = self.value(of: self.layoutAdapter.surfaceLocation)
|
||||
let translation = data.value - initialData.value
|
||||
self.backdropView.alpha = self.getBackdropAlpha(at: current, with: translation)
|
||||
self.ownerVC?.notifyDidMove()
|
||||
ownerVC.notifyDidMove()
|
||||
},
|
||||
completion: { [weak self] in
|
||||
guard let self = self else { return }
|
||||
self.layoutAdapter.activateLayout(for: targetPosition, forceLayout: true)
|
||||
guard let self = self,
|
||||
self.ownerVC != nil else { return }
|
||||
self.updateLayout(to: targetPosition)
|
||||
completion()
|
||||
})
|
||||
moveAnimator?.startAnimation()
|
||||
@@ -856,9 +907,9 @@ class Core: NSObject, UIGestureRecognizerDelegate {
|
||||
log.debug("""
|
||||
finishAnimation -- state = \(state) \
|
||||
surface location = \(layoutAdapter.surfaceLocation) \
|
||||
edge most position = \(layoutAdapter.surfaceLocation(for: layoutAdapter.edgeMostState))
|
||||
edge most position = \(layoutAdapter.surfaceLocation(for: layoutAdapter.mostExpandedState))
|
||||
""")
|
||||
if finished, state == layoutAdapter.edgeMostState, abs(layoutAdapter.offsetFromEdgeMost) <= 1.0 {
|
||||
if finished, state == layoutAdapter.mostExpandedState, abs(layoutAdapter.offsetFromMostExpandedAnchor) <= 1.0 {
|
||||
unlockScrollView()
|
||||
}
|
||||
}
|
||||
@@ -885,7 +936,7 @@ class Core: NSObject, UIGestureRecognizerDelegate {
|
||||
func targetPosition(from currentY: CGFloat, with velocity: CGFloat) -> (FloatingPanelState) {
|
||||
log.debug("targetPosition -- currentY = \(currentY), velocity = \(velocity)")
|
||||
|
||||
let sortedPositions = layoutAdapter.sortedDirectionalStates
|
||||
let sortedPositions = layoutAdapter.sortedAnchorStatesByCoordinate
|
||||
|
||||
guard sortedPositions.count > 1 else {
|
||||
return state
|
||||
@@ -893,7 +944,7 @@ class Core: NSObject, UIGestureRecognizerDelegate {
|
||||
|
||||
// Projection
|
||||
let decelerationRate = behaviorAdapter.momentumProjectionRate
|
||||
let baseY = abs(layoutAdapter.position(for: layoutAdapter.edgeLeastState) - layoutAdapter.position(for: layoutAdapter.edgeMostState))
|
||||
let baseY = abs(layoutAdapter.position(for: layoutAdapter.leastExpandedState) - layoutAdapter.position(for: layoutAdapter.mostExpandedState))
|
||||
let vecY = velocity / baseY
|
||||
var pY = project(initialVelocity: vecY, decelerationRate: decelerationRate) * baseY + currentY
|
||||
|
||||
@@ -989,7 +1040,7 @@ class Core: NSObject, UIGestureRecognizerDelegate {
|
||||
}
|
||||
|
||||
private func allowScrollPanGesture(for scrollView: UIScrollView) -> Bool {
|
||||
guard state == layoutAdapter.edgeMostState else { return false }
|
||||
guard state == layoutAdapter.mostExpandedState else { return false }
|
||||
var offsetY: CGFloat = 0
|
||||
switch layoutAdapter.position {
|
||||
case .top, .left:
|
||||
@@ -1021,7 +1072,7 @@ public final class FloatingPanelPanGestureRecognizer: UIPanGestureRecognizer {
|
||||
public override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
|
||||
super.touchesBegan(touches, with: event)
|
||||
initialLocation = touches.first?.location(in: view) ?? .zero
|
||||
if floatingPanel?.animator != nil || floatingPanel?.moveAnimator != nil {
|
||||
if floatingPanel?.transitionAnimator != nil || floatingPanel?.moveAnimator != nil {
|
||||
self.state = .began
|
||||
}
|
||||
}
|
||||
@@ -1182,3 +1233,12 @@ private class NumericSpringAnimator: NSObject {
|
||||
v = (v + h * o2 * (xt - x)) / det
|
||||
}
|
||||
}
|
||||
|
||||
extension FloatingPanelController {
|
||||
func suspendTransitionAnimator(_ suspended: Bool) {
|
||||
self.floatingPanel.isSuspended = suspended
|
||||
}
|
||||
var transitionAnimator: UIViewPropertyAnimator? {
|
||||
return self.floatingPanel.transitionAnimator
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,16 +2,38 @@
|
||||
|
||||
import UIKit
|
||||
|
||||
internal func displayTrunc(_ v: CGFloat, by s: CGFloat) -> CGFloat {
|
||||
let base = (1 / s)
|
||||
let t = v.rounded(.down)
|
||||
return t + ((v - t) / base).rounded(.toNearestOrAwayFromZero) * base
|
||||
// MARK: - CoreGraphics
|
||||
|
||||
extension CGFloat {
|
||||
/// Returns this value rounded to an logical pixel value by a display scale
|
||||
func rounded(by displayScale: CGFloat) -> CGFloat {
|
||||
return (self * displayScale).rounded(.toNearestOrAwayFromZero) / displayScale
|
||||
}
|
||||
func isEqual(to: CGFloat, on displayScale: CGFloat) -> Bool {
|
||||
return self.rounded(by: displayScale) == to.rounded(by: displayScale)
|
||||
}
|
||||
}
|
||||
|
||||
internal func displayEqual(_ lhs: CGFloat, _ rhs: CGFloat, by displayScale: CGFloat) -> Bool {
|
||||
return displayTrunc(lhs, by: displayScale) == displayTrunc(rhs, by: displayScale)
|
||||
extension CGPoint {
|
||||
static var leastNonzeroMagnitude: CGPoint {
|
||||
return CGPoint(x: CGFloat.leastNonzeroMagnitude, y: CGFloat.leastNonzeroMagnitude)
|
||||
}
|
||||
|
||||
static func + (lhs: CGPoint, rhs: CGPoint) -> CGPoint {
|
||||
return CGPoint(x: lhs.x + rhs.x, y: lhs.y + rhs.y)
|
||||
}
|
||||
|
||||
static func - (lhs: CGPoint, rhs: CGPoint) -> CGPoint {
|
||||
return CGPoint(x: lhs.x - rhs.x, y: lhs.y - rhs.y)
|
||||
}
|
||||
|
||||
static prefix func - (point: CGPoint) -> CGPoint {
|
||||
return CGPoint(x: -point.x, y: -point.y)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - UIKit
|
||||
|
||||
protocol LayoutGuideProvider {
|
||||
var topAnchor: NSLayoutYAxisAnchor { get }
|
||||
var leftAnchor: NSLayoutXAxisAnchor { get }
|
||||
@@ -164,24 +186,6 @@ extension UISpringTimingParameters {
|
||||
}
|
||||
}
|
||||
|
||||
extension CGPoint {
|
||||
static var leastNonzeroMagnitude: CGPoint {
|
||||
return CGPoint(x: CGFloat.leastNonzeroMagnitude, y: CGFloat.leastNonzeroMagnitude)
|
||||
}
|
||||
|
||||
static func + (lhs: CGPoint, rhs: CGPoint) -> CGPoint {
|
||||
return CGPoint(x: lhs.x + rhs.x, y: lhs.y + rhs.y)
|
||||
}
|
||||
|
||||
static func - (lhs: CGPoint, rhs: CGPoint) -> CGPoint {
|
||||
return CGPoint(x: lhs.x - rhs.x, y: lhs.y - rhs.y)
|
||||
}
|
||||
|
||||
static prefix func - (point: CGPoint) -> CGPoint {
|
||||
return CGPoint(x: -point.x, y: -point.y)
|
||||
}
|
||||
}
|
||||
|
||||
extension NSLayoutConstraint {
|
||||
static func activate(constraint: NSLayoutConstraint?) {
|
||||
guard let constraint = constraint else { return }
|
||||
@@ -201,3 +205,70 @@ extension UIEdgeInsets {
|
||||
return self.top + self.bottom
|
||||
}
|
||||
}
|
||||
|
||||
extension UIBezierPath {
|
||||
static func path(roundedRect rect: CGRect, appearance: SurfaceAppearance) -> UIBezierPath {
|
||||
let cornerRadius = appearance.cornerRadius;
|
||||
if #available(iOS 13.0, *) {
|
||||
if appearance.cornerCurve == .circular {
|
||||
let path = UIBezierPath()
|
||||
let start = CGPoint(x: rect.minX + cornerRadius, y: rect.minY)
|
||||
|
||||
path.move(to: start)
|
||||
|
||||
path.addLine(to: CGPoint(x: rect.maxX - cornerRadius, y: rect.minY))
|
||||
if cornerRadius > 0 {
|
||||
path .addArc(withCenter: CGPoint(x: rect.maxX - cornerRadius,
|
||||
y: rect.minY + cornerRadius),
|
||||
radius: cornerRadius,
|
||||
startAngle: -0.5 * .pi,
|
||||
endAngle: 0,
|
||||
clockwise: true)
|
||||
}
|
||||
|
||||
path.addLine(to: CGPoint(x: rect.maxX, y: rect.maxY - cornerRadius))
|
||||
|
||||
if cornerRadius > 0 {
|
||||
path.addArc(withCenter: CGPoint(x: rect.maxX - cornerRadius,
|
||||
y: rect.maxY - cornerRadius),
|
||||
radius: cornerRadius,
|
||||
startAngle: 0,
|
||||
endAngle: .pi * 0.5,
|
||||
clockwise: true)
|
||||
}
|
||||
|
||||
path.addLine(to: CGPoint(x: rect.minX + cornerRadius, y: rect.maxY))
|
||||
|
||||
if cornerRadius > 0 {
|
||||
path.addArc(withCenter: CGPoint(x: rect.minX + cornerRadius,
|
||||
y: rect.maxY - cornerRadius),
|
||||
radius: cornerRadius,
|
||||
startAngle: .pi * 0.5,
|
||||
endAngle: .pi,
|
||||
clockwise: true)
|
||||
}
|
||||
|
||||
path.addLine(to: CGPoint(x: rect.minX, y: rect.minY + cornerRadius))
|
||||
|
||||
if cornerRadius > 0 {
|
||||
path.addArc(withCenter: CGPoint(x: rect.minX + cornerRadius,
|
||||
y: rect.minY + cornerRadius),
|
||||
radius: cornerRadius,
|
||||
startAngle: .pi,
|
||||
endAngle: .pi * 1.5,
|
||||
clockwise: true)
|
||||
}
|
||||
|
||||
path.addLine(to: start)
|
||||
|
||||
path.close()
|
||||
|
||||
return path
|
||||
}
|
||||
}
|
||||
return UIBezierPath(roundedRect: rect,
|
||||
byRoundingCorners: [.allCorners],
|
||||
cornerRadii: CGSize(width: cornerRadius,
|
||||
height: cornerRadius))
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -15,7 +15,7 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>FMWK</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>2.0.0</string>
|
||||
<string>2.4.1</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||
</dict>
|
||||
|
||||
+95
-96
@@ -60,9 +60,7 @@ struct LayoutSegment {
|
||||
}
|
||||
|
||||
class LayoutAdapter {
|
||||
weak var vc: FloatingPanelController!
|
||||
private weak var surfaceView: SurfaceView!
|
||||
private weak var backdropView: BackdropView!
|
||||
private unowned var vc: FloatingPanelController
|
||||
private let defaultLayout = FloatingPanelBottomLayout()
|
||||
|
||||
fileprivate var layout: FloatingPanelLayout {
|
||||
@@ -71,16 +69,21 @@ class LayoutAdapter {
|
||||
}
|
||||
}
|
||||
|
||||
private var surfaceView: SurfaceView {
|
||||
return vc.surfaceView
|
||||
}
|
||||
private var backdropView: BackdropView {
|
||||
return vc.backdropView
|
||||
}
|
||||
private var safeAreaInsets: UIEdgeInsets {
|
||||
return vc?.fp_safeAreaInsets ?? .zero
|
||||
return vc.fp_safeAreaInsets
|
||||
}
|
||||
|
||||
private var initialConst: CGFloat = 0.0
|
||||
|
||||
private var fixedConstraints: [NSLayoutConstraint] = []
|
||||
private var fullConstraints: [NSLayoutConstraint] = []
|
||||
private var halfConstraints: [NSLayoutConstraint] = []
|
||||
private var tipConstraints: [NSLayoutConstraint] = []
|
||||
|
||||
private var stateConstraints: [FloatingPanelState: [NSLayoutConstraint]] = [:]
|
||||
private var offConstraints: [NSLayoutConstraint] = []
|
||||
private var fitToBoundsConstraint: NSLayoutConstraint?
|
||||
|
||||
@@ -89,10 +92,16 @@ class LayoutAdapter {
|
||||
|
||||
private var staticConstraint: NSLayoutConstraint?
|
||||
|
||||
private var activeStates: Set<FloatingPanelState> {
|
||||
private var anchorStates: Set<FloatingPanelState> {
|
||||
return Set(layout.anchors.keys)
|
||||
}
|
||||
|
||||
private var sortedAnchorStates: [FloatingPanelState] {
|
||||
return anchorStates.sorted(by: {
|
||||
return $0.order < $1.order
|
||||
})
|
||||
}
|
||||
|
||||
var initialState: FloatingPanelState {
|
||||
layout.initialState
|
||||
}
|
||||
@@ -101,18 +110,12 @@ class LayoutAdapter {
|
||||
layout.position
|
||||
}
|
||||
|
||||
var orderedStates: [FloatingPanelState] {
|
||||
return activeStates.sorted(by: {
|
||||
return $0.order < $1.order
|
||||
})
|
||||
}
|
||||
|
||||
var validStates: Set<FloatingPanelState> {
|
||||
return activeStates.union([.hidden])
|
||||
return anchorStates.union([.hidden])
|
||||
}
|
||||
|
||||
var sortedDirectionalStates: [FloatingPanelState] {
|
||||
return activeStates.sorted(by: {
|
||||
var sortedAnchorStatesByCoordinate: [FloatingPanelState] {
|
||||
return anchorStates.sorted(by: {
|
||||
switch position {
|
||||
case .top, .left:
|
||||
return $0.order < $1.order
|
||||
@@ -122,30 +125,26 @@ class LayoutAdapter {
|
||||
})
|
||||
}
|
||||
|
||||
private var directionalLeastState: FloatingPanelState {
|
||||
return sortedDirectionalStates.first ?? .hidden
|
||||
private var leastCoordinateState: FloatingPanelState {
|
||||
return sortedAnchorStatesByCoordinate.first ?? .hidden
|
||||
}
|
||||
|
||||
private var directionalMostState: FloatingPanelState {
|
||||
return sortedDirectionalStates.last ?? .hidden
|
||||
private var mostCoordinateState: FloatingPanelState {
|
||||
return sortedAnchorStatesByCoordinate.last ?? .hidden
|
||||
}
|
||||
|
||||
var edgeLeastState: FloatingPanelState {
|
||||
if orderedStates.count == 1 {
|
||||
var leastExpandedState: FloatingPanelState {
|
||||
if sortedAnchorStates.count == 1 {
|
||||
return .hidden
|
||||
}
|
||||
return orderedStates.first ?? .hidden
|
||||
return sortedAnchorStates.first ?? .hidden
|
||||
}
|
||||
|
||||
var edgeMostState: FloatingPanelState {
|
||||
if orderedStates.count == 1 {
|
||||
return orderedStates[0]
|
||||
var mostExpandedState: FloatingPanelState {
|
||||
if sortedAnchorStates.count == 1 {
|
||||
return sortedAnchorStates[0]
|
||||
}
|
||||
return orderedStates.last ?? .hidden
|
||||
}
|
||||
|
||||
var edgeMostY: CGFloat {
|
||||
return position(for: edgeMostState)
|
||||
return sortedAnchorStates.last ?? .hidden
|
||||
}
|
||||
|
||||
var adjustedContentInsets: UIEdgeInsets {
|
||||
@@ -220,7 +219,7 @@ class LayoutAdapter {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
pos = displayTrunc(edgePosition(surfaceView.frame), by: surfaceView.fp_displayScale)
|
||||
pos = edgePosition(surfaceView.frame).rounded(by: surfaceView.fp_displayScale)
|
||||
}
|
||||
switch position {
|
||||
case .top, .bottom:
|
||||
@@ -262,12 +261,12 @@ class LayoutAdapter {
|
||||
}
|
||||
}
|
||||
|
||||
var offsetFromEdgeMost: CGFloat {
|
||||
var offsetFromMostExpandedAnchor: CGFloat {
|
||||
switch position {
|
||||
case .top, .left:
|
||||
return edgePosition(surfaceView.presentationFrame) - position(for: directionalMostState)
|
||||
return edgePosition(surfaceView.presentationFrame) - position(for: mostExpandedState)
|
||||
case .bottom, .right:
|
||||
return position(for: directionalLeastState) - edgePosition(surfaceView.presentationFrame)
|
||||
return position(for: mostExpandedState) - edgePosition(surfaceView.presentationFrame)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -284,18 +283,13 @@ class LayoutAdapter {
|
||||
}
|
||||
}
|
||||
|
||||
init(vc: FloatingPanelController,
|
||||
surfaceView: SurfaceView,
|
||||
backdropView: BackdropView,
|
||||
layout: FloatingPanelLayout) {
|
||||
init(vc: FloatingPanelController, layout: FloatingPanelLayout) {
|
||||
self.vc = vc
|
||||
self.layout = layout
|
||||
self.surfaceView = surfaceView
|
||||
self.backdropView = backdropView
|
||||
}
|
||||
|
||||
func surfaceLocation(for state: FloatingPanelState) -> CGPoint {
|
||||
let pos = displayTrunc(position(for: state), by: surfaceView.fp_displayScale)
|
||||
let pos = position(for: state).rounded(by: surfaceView.fp_displayScale)
|
||||
switch layout.position {
|
||||
case .top, .bottom:
|
||||
return CGPoint(x: 0.0, y: pos)
|
||||
@@ -327,6 +321,22 @@ class LayoutAdapter {
|
||||
}
|
||||
return base - intrinsicLength + diff
|
||||
}
|
||||
case let anchor as FloatingPanelAdaptiveLayoutAnchor:
|
||||
let dimension = layout.position.mainDimension(anchor.contentLayoutGuide.layoutFrame.size)
|
||||
let diff = anchor.distance(from: dimension)
|
||||
var referenceBoundsLength = layout.position.mainDimension(bounds.size)
|
||||
switch layout.position {
|
||||
case .top, .left:
|
||||
if anchor.referenceGuide == .safeArea {
|
||||
referenceBoundsLength += position.inset(safeAreaInsets)
|
||||
}
|
||||
return dimension - diff
|
||||
case .bottom, .right:
|
||||
if anchor.referenceGuide == .safeArea {
|
||||
referenceBoundsLength -= position.inset(safeAreaInsets)
|
||||
}
|
||||
return referenceBoundsLength - dimension + diff
|
||||
}
|
||||
case let anchor as FloatingPanelLayoutAnchor:
|
||||
let referenceBounds = anchor.referenceGuide == .safeArea ? bounds.inset(by: safeAreaInsets) : bounds
|
||||
let diff = anchor.isAbsolute ? anchor.inset : position.mainDimension(referenceBounds.size) * anchor.inset
|
||||
@@ -364,7 +374,8 @@ class LayoutAdapter {
|
||||
|
||||
private func referenceEdge(of anchor: FloatingPanelLayoutAnchoring) -> FloatingPanelReferenceEdge {
|
||||
switch anchor {
|
||||
case is FloatingPanelIntrinsicLayoutAnchor:
|
||||
case is FloatingPanelIntrinsicLayoutAnchor,
|
||||
is FloatingPanelAdaptiveLayoutAnchor:
|
||||
switch position {
|
||||
case .top: return .top
|
||||
case .left: return .left
|
||||
@@ -436,25 +447,13 @@ class LayoutAdapter {
|
||||
}
|
||||
|
||||
private func updateStateConstraints() {
|
||||
NSLayoutConstraint.deactivate(fullConstraints + halfConstraints + tipConstraints + offConstraints)
|
||||
|
||||
if let fullAnchor = layout.anchors[.full] {
|
||||
fullConstraints = fullAnchor.layoutConstraints(vc, for: position)
|
||||
fullConstraints.forEach {
|
||||
$0.identifier = "FloatingPanel-full-constraint"
|
||||
}
|
||||
}
|
||||
if let halfAnchor = layout.anchors[.half] {
|
||||
halfConstraints = halfAnchor.layoutConstraints(vc, for: position)
|
||||
halfConstraints.forEach {
|
||||
$0.identifier = "FloatingPanel-half-constraint"
|
||||
}
|
||||
}
|
||||
if let tipAnchors = layout.anchors[.tip] {
|
||||
tipConstraints = tipAnchors.layoutConstraints(vc, for: position)
|
||||
tipConstraints.forEach {
|
||||
$0.identifier = "FloatingPanel-tip-constraint"
|
||||
}
|
||||
let allStateConstraints = stateConstraints.flatMap { $1 }
|
||||
NSLayoutConstraint.deactivate(allStateConstraints + offConstraints)
|
||||
stateConstraints.removeAll()
|
||||
for state in layout.anchors.keys {
|
||||
stateConstraints[state] = layout.anchors[state]?
|
||||
.layoutConstraints(vc, for: position)
|
||||
.map{ $0.identifier = "FloatingPanel-\(state)-constraint"; return $0 }
|
||||
}
|
||||
let hiddenAnchor = layout.anchors[.hidden] ?? self.hiddenAnchor
|
||||
offConstraints = hiddenAnchor.layoutConstraints(vc, for: position)
|
||||
@@ -471,7 +470,7 @@ class LayoutAdapter {
|
||||
|
||||
tearDownAttraction()
|
||||
|
||||
NSLayoutConstraint.deactivate(fullConstraints + halfConstraints + tipConstraints + offConstraints)
|
||||
NSLayoutConstraint.deactivate(stateConstraints.flatMap { $1 } + offConstraints)
|
||||
|
||||
initialConst = edgePosition(surfaceView.frame) + offset.y
|
||||
|
||||
@@ -487,7 +486,7 @@ class LayoutAdapter {
|
||||
constraint = surfaceView.leftAnchor.constraint(equalTo: vc.view.leftAnchor, constant: initialConst)
|
||||
}
|
||||
|
||||
constraint.priority = .defaultHigh
|
||||
constraint.priority = .required
|
||||
constraint.identifier = "FloatingPanel-interaction"
|
||||
|
||||
NSLayoutConstraint.activate([constraint])
|
||||
@@ -511,7 +510,7 @@ class LayoutAdapter {
|
||||
|
||||
let anchor = layout.anchors[state] ?? self.hiddenAnchor
|
||||
|
||||
NSLayoutConstraint.deactivate(fullConstraints + halfConstraints + tipConstraints + offConstraints)
|
||||
NSLayoutConstraint.deactivate(stateConstraints.flatMap { $1 } + offConstraints)
|
||||
NSLayoutConstraint.deactivate(constraint: interactionConstraint)
|
||||
interactionConstraint = nil
|
||||
|
||||
@@ -630,7 +629,6 @@ class LayoutAdapter {
|
||||
// The method is separated from prepareLayout(to:) for the rotation support
|
||||
// It must be called in FloatingPanelController.traitCollectionDidChange(_:)
|
||||
func updateStaticConstraint() {
|
||||
guard let vc = vc else { return }
|
||||
NSLayoutConstraint.deactivate(constraint: staticConstraint)
|
||||
staticConstraint = nil
|
||||
|
||||
@@ -639,20 +637,32 @@ class LayoutAdapter {
|
||||
return
|
||||
}
|
||||
|
||||
let anchor = layout.anchors[self.edgeMostState]!
|
||||
if anchor is FloatingPanelIntrinsicLayoutAnchor {
|
||||
var constant = layout.position.mainDimension(surfaceView.intrinsicContentSize)
|
||||
let anchor = layout.anchors[self.mostExpandedState]!
|
||||
let surfaceAnchor = position.mainDimensionAnchor(surfaceView)
|
||||
switch anchor {
|
||||
case let anchor as FloatingPanelIntrinsicLayoutAnchor:
|
||||
var constant = position.mainDimension(surfaceView.intrinsicContentSize)
|
||||
if anchor.referenceGuide == .safeArea {
|
||||
constant += position.inset(safeAreaInsets)
|
||||
}
|
||||
staticConstraint = position.mainDimensionAnchor(surfaceView).constraint(equalToConstant: constant)
|
||||
} else {
|
||||
staticConstraint = surfaceAnchor.constraint(equalToConstant: constant)
|
||||
case let anchor as FloatingPanelAdaptiveLayoutAnchor:
|
||||
let constant: CGFloat
|
||||
if anchor.referenceGuide == .safeArea {
|
||||
constant = position.inset(safeAreaInsets)
|
||||
} else {
|
||||
constant = 0.0
|
||||
}
|
||||
let baseAnchor = position.mainDimensionAnchor(anchor.contentLayoutGuide)
|
||||
staticConstraint = surfaceAnchor.constraint(equalTo: baseAnchor, constant: constant)
|
||||
default:
|
||||
switch position {
|
||||
case .top, .left:
|
||||
staticConstraint = position.mainDimensionAnchor(surfaceView).constraint(equalToConstant: position(for: self.directionalMostState))
|
||||
staticConstraint = surfaceAnchor.constraint(equalToConstant: position(for: self.mostCoordinateState))
|
||||
case .bottom, .right:
|
||||
staticConstraint = position.mainDimensionAnchor(vc.view).constraint(equalTo: position.mainDimensionAnchor(surfaceView),
|
||||
constant: position(for: self.directionalLeastState))
|
||||
let rootViewAnchor = position.mainDimensionAnchor(vc.view)
|
||||
staticConstraint = rootViewAnchor.constraint(equalTo: surfaceAnchor,
|
||||
constant: position(for: self.leastCoordinateState))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -673,8 +683,8 @@ class LayoutAdapter {
|
||||
log.debug("update surface location = \(surfaceLocation)")
|
||||
}
|
||||
|
||||
let minConst: CGFloat = position(for: directionalLeastState)
|
||||
let maxConst: CGFloat = position(for: directionalMostState)
|
||||
let minConst: CGFloat = position(for: leastCoordinateState)
|
||||
let maxConst: CGFloat = position(for: mostCoordinateState)
|
||||
|
||||
var const = initialConst + diff
|
||||
|
||||
@@ -730,8 +740,6 @@ class LayoutAdapter {
|
||||
|
||||
var state = state
|
||||
|
||||
setBackdropAlpha(of: state)
|
||||
|
||||
if validStates.contains(state) == false {
|
||||
state = layout.initialState
|
||||
}
|
||||
@@ -741,17 +749,16 @@ class LayoutAdapter {
|
||||
// on-screen and off-screen view which includes
|
||||
// UIStackView(i.e. Settings view in Samples.app)
|
||||
updateStateConstraints()
|
||||
|
||||
switch state {
|
||||
case .full:
|
||||
NSLayoutConstraint.activate(fullConstraints)
|
||||
case .half:
|
||||
NSLayoutConstraint.activate(halfConstraints)
|
||||
case .tip:
|
||||
NSLayoutConstraint.activate(tipConstraints)
|
||||
case .hidden:
|
||||
NSLayoutConstraint.activate(offConstraints)
|
||||
default:
|
||||
break
|
||||
if let constraints = stateConstraints[state] {
|
||||
NSLayoutConstraint.activate(constraints)
|
||||
} else {
|
||||
log.error("Couldn't find any constraints for \(state)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -762,21 +769,13 @@ class LayoutAdapter {
|
||||
surfaceView.superview?.layoutIfNeeded()
|
||||
}
|
||||
|
||||
private func setBackdropAlpha(of target: FloatingPanelState) {
|
||||
if target == .hidden {
|
||||
self.backdropView.alpha = 0.0
|
||||
} else {
|
||||
self.backdropView.alpha = backdropAlpha(for: target)
|
||||
}
|
||||
}
|
||||
|
||||
func backdropAlpha(for state: FloatingPanelState) -> CGFloat {
|
||||
return layout.backdropAlpha?(for: state) ?? defaultLayout.backdropAlpha(for: state)
|
||||
}
|
||||
|
||||
fileprivate func checkLayout() {
|
||||
// Verify layout configurations
|
||||
assert(activeStates.count > 0)
|
||||
assert(anchorStates.count > 0)
|
||||
assert(validStates.contains(layout.initialState),
|
||||
"Does not include an initial state (\(layout.initialState)) in (\(validStates))")
|
||||
/* This assertion does not work in a device rotating
|
||||
@@ -796,7 +795,7 @@ extension LayoutAdapter {
|
||||
/// |-------|-------|===o===| |-------|===o===|-------|
|
||||
/// pos: o/x, segment: =
|
||||
|
||||
let sortedStates = sortedDirectionalStates
|
||||
let sortedStates = sortedAnchorStatesByCoordinate
|
||||
|
||||
let upperIndex: Int?
|
||||
if forward {
|
||||
|
||||
@@ -8,22 +8,25 @@ import UIKit
|
||||
func layoutConstraints(_ fpc: FloatingPanelController, for position: FloatingPanelPosition) -> [NSLayoutConstraint]
|
||||
}
|
||||
|
||||
/// A layout anchor object that anchors a panel in a state.
|
||||
/// An object that defines how to settles a panel with insets from an edge of a reference rectangle.
|
||||
@objc final public class FloatingPanelLayoutAnchor: NSObject, FloatingPanelLayoutAnchoring /*, NSCopying */ {
|
||||
/// Initializes and returns a layout anchor object to specify an absolute inset value for the position of a panel.
|
||||
|
||||
/// Returns a layout anchor with the specified inset by an absolute value, edge and reference guide for a panel.
|
||||
///
|
||||
/// The inset is a distance from the edge of the specified layout guide.
|
||||
/// The inset is an amount to inset a panel from an edge of the reference guide. The edge refers to a panel
|
||||
/// positioning.
|
||||
@objc public init(absoluteInset: CGFloat, edge: FloatingPanelReferenceEdge, referenceGuide: FloatingPanelLayoutReferenceGuide) {
|
||||
self.inset = absoluteInset
|
||||
self.referenceGuide = referenceGuide
|
||||
self.referenceEdge = edge
|
||||
self.isAbsolute = true
|
||||
}
|
||||
/// Initializes and returns a layout anchor object to specify a fractional inset value for the position of a panel.
|
||||
|
||||
/// Returns a layout anchor with the specified inset by a fractional value, edge and reference guide for a panel.
|
||||
///
|
||||
/// The inset is a distance from the edge of the specified layout guide. The value is a floating-point number
|
||||
/// in the range 0.0 to 1.0, where 0.0 represents zero distance from the edge and 1.0 represents a distance
|
||||
/// to the opposite edge.
|
||||
/// The inset is an amount to inset a panel from the edge of the specified reference guide. The value is
|
||||
/// a floating-point number in the range 0.0 to 1.0, where 0.0 represents zero distance from the edge and
|
||||
/// 1.0 represents a distance to the opposite edge.
|
||||
@objc public init(fractionalInset: CGFloat, edge: FloatingPanelReferenceEdge, referenceGuide: FloatingPanelLayoutReferenceGuide) {
|
||||
self.inset = fractionalInset
|
||||
self.referenceGuide = referenceGuide
|
||||
@@ -91,19 +94,20 @@ public extension FloatingPanelLayoutAnchor {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// A layout anchor object that anchors a panel in a state using the intrinsic size for a content.
|
||||
/// An object that defines how to settles a panel with the intrinsic size for a content.
|
||||
@objc final public class FloatingPanelIntrinsicLayoutAnchor: NSObject, FloatingPanelLayoutAnchoring /*, NSCopying */ {
|
||||
/// Initializes and returns a layout anchor object to specify an absolute offset value for the position of a panel.
|
||||
|
||||
/// Returns a layout anchor with the specified offset by an absolute value and reference guide for a panel.
|
||||
///
|
||||
/// The offset is a distance from a position at which a panel displays the entire content.
|
||||
/// The offset is an amount to offset a position of panel that displays the entire content from an edge of
|
||||
/// the reference guide. The edge refers to a panel positioning.
|
||||
@objc public init(absoluteOffset offset: CGFloat, referenceGuide: FloatingPanelLayoutReferenceGuide = .safeArea) {
|
||||
self.offset = offset
|
||||
self.referenceGuide = referenceGuide
|
||||
self.isAbsolute = true
|
||||
}
|
||||
|
||||
/// Initializes and returns a layout anchor object to specify a fractional offset value for the position of a panel.
|
||||
/// Returns a layout anchor with the specified offset by a fractional value and reference guide for a panel.
|
||||
///
|
||||
/// The offset value is a floating-point number in the range 0.0 to 1.0, where 0.0 represents the full content
|
||||
/// is displayed and 0.5 represents the half of content is displayed.
|
||||
@@ -136,3 +140,62 @@ public extension FloatingPanelIntrinsicLayoutAnchor {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// An object that defines how to settles a panel with a layout guide of a content view.
|
||||
@objc final public class FloatingPanelAdaptiveLayoutAnchor: NSObject, FloatingPanelLayoutAnchoring /*, NSCopying */ {
|
||||
|
||||
/// Returns a layout anchor with the specified offset by an absolute value, layout guide to display content and reference guide for a panel.
|
||||
///
|
||||
/// The offset is an amount to offset a position of panel that displays the entire content of the specified guide from an edge of
|
||||
/// the reference guide. The edge refers to a panel positioning.
|
||||
@objc public init(absoluteOffset offset: CGFloat, contentLayout: UILayoutGuide, referenceGuide: FloatingPanelLayoutReferenceGuide = .safeArea) {
|
||||
self.offset = offset
|
||||
self.contentLayoutGuide = contentLayout
|
||||
self.referenceGuide = referenceGuide
|
||||
self.isAbsolute = true
|
||||
|
||||
}
|
||||
|
||||
/// Returns a layout anchor with the specified offset by a fractional value, layout guide to display content and reference guide for a panel.
|
||||
///
|
||||
/// The offset value is a floating-point number in the range 0.0 to 1.0, where 0.0 represents the full content
|
||||
/// is displayed and 0.5 represents the half of content is displayed.
|
||||
@objc public init(fractionalOffset offset: CGFloat, contentLayout: UILayoutGuide, referenceGuide: FloatingPanelLayoutReferenceGuide = .safeArea) {
|
||||
self.offset = offset
|
||||
self.contentLayoutGuide = contentLayout
|
||||
self.referenceGuide = referenceGuide
|
||||
self.isAbsolute = false
|
||||
}
|
||||
fileprivate let offset: CGFloat
|
||||
fileprivate let isAbsolute: Bool
|
||||
let contentLayoutGuide: UILayoutGuide
|
||||
@objc public let referenceGuide: FloatingPanelLayoutReferenceGuide
|
||||
}
|
||||
|
||||
public extension FloatingPanelAdaptiveLayoutAnchor {
|
||||
func layoutConstraints(_ vc: FloatingPanelController, for position: FloatingPanelPosition) -> [NSLayoutConstraint] {
|
||||
let layoutGuide = referenceGuide.layoutGuide(vc: vc)
|
||||
let offsetAnchor: NSLayoutDimension
|
||||
switch position {
|
||||
case .top:
|
||||
offsetAnchor = layoutGuide.topAnchor.anchorWithOffset(to: vc.surfaceView.bottomAnchor)
|
||||
case .left:
|
||||
offsetAnchor = layoutGuide.leftAnchor.anchorWithOffset(to: vc.surfaceView.rightAnchor)
|
||||
case .bottom:
|
||||
offsetAnchor = vc.surfaceView.topAnchor.anchorWithOffset(to: layoutGuide.bottomAnchor)
|
||||
case .right:
|
||||
offsetAnchor = vc.surfaceView.leftAnchor.anchorWithOffset(to: layoutGuide.rightAnchor)
|
||||
}
|
||||
if isAbsolute {
|
||||
return [offsetAnchor.constraint(equalTo: position.mainDimensionAnchor(contentLayoutGuide), constant: -offset)]
|
||||
} else {
|
||||
return [offsetAnchor.constraint(equalTo: position.mainDimensionAnchor(contentLayoutGuide), multiplier: (1 - offset))]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension FloatingPanelAdaptiveLayoutAnchor {
|
||||
func distance(from dimension: CGFloat) -> CGFloat {
|
||||
return isAbsolute ? offset : dimension * offset
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import Foundation
|
||||
import os.log
|
||||
|
||||
// Must be a variable to use `hook` property in testing
|
||||
var log = {
|
||||
return Logger()
|
||||
}()
|
||||
@@ -68,7 +69,7 @@ struct Logger {
|
||||
|
||||
hook?(log, level)
|
||||
|
||||
os_log("%@", log: osLog, type: level.osLogType, log)
|
||||
os_log("%{public}@", log: osLog, type: level.osLogType, log)
|
||||
}
|
||||
|
||||
private func getPrettyFunction(_ function: String, _ file: String) -> String {
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
import UIKit
|
||||
|
||||
@objc(FloatingPanelPassThroughView)
|
||||
class PassThroughView: UIView {
|
||||
@objc(FloatingPanelPassthroughView)
|
||||
class PassthroughView: UIView {
|
||||
public weak var eventForwardingView: UIView?
|
||||
public override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||
let hitView = super.hitTest(point, with: event)
|
||||
+3
-2
@@ -4,7 +4,7 @@ import Foundation
|
||||
|
||||
/// An object that represents the display state of a panel in a screen.
|
||||
@objc
|
||||
public class FloatingPanelState: NSObject, NSCopying, RawRepresentable {
|
||||
open class FloatingPanelState: NSObject, NSCopying, RawRepresentable {
|
||||
public typealias RawValue = String
|
||||
|
||||
required public init?(rawValue: RawValue) {
|
||||
@@ -13,6 +13,7 @@ public class FloatingPanelState: NSObject, NSCopying, RawRepresentable {
|
||||
super.init()
|
||||
}
|
||||
|
||||
@objc
|
||||
public init(rawValue: RawValue, order: Int) {
|
||||
self.rawValue = rawValue
|
||||
self.order = order
|
||||
@@ -33,7 +34,7 @@ public class FloatingPanelState: NSObject, NSCopying, RawRepresentable {
|
||||
}
|
||||
|
||||
public override var debugDescription: String {
|
||||
return description
|
||||
return "<FloatingPanel.FloatingPanelState: \(Unmanaged.passUnretained(self).toOpaque())>"
|
||||
}
|
||||
|
||||
/// A panel state indicates the entire panel is shown.
|
||||
|
||||
@@ -50,6 +50,12 @@ public class SurfaceAppearance: NSObject {
|
||||
/// On iOS 10, they are not automatically masked because of a UIVisualEffectView issue. See https://forums.developer.apple.com/thread/50854
|
||||
public var cornerRadius: CGFloat = 0.0
|
||||
|
||||
/// Defines the curve used for rendering the rounded corners of the layer.
|
||||
///
|
||||
/// Defaults to `.circular`.
|
||||
@available(iOS 13.0, *)
|
||||
public lazy var cornerCurve: CALayerCornerCurve = .circular
|
||||
|
||||
/// An array of shadows used to create drop shadows underneath a surface view.
|
||||
public var shadows: [Shadow] = [Shadow()]
|
||||
|
||||
@@ -311,8 +317,8 @@ public class SurfaceView: UIView {
|
||||
|
||||
containerView.backgroundColor = appearance.backgroundColor
|
||||
|
||||
updateShadow()
|
||||
updateCornerRadius()
|
||||
updateShadow()
|
||||
updateBorder()
|
||||
|
||||
grabberHandle.layer.cornerRadius = grabberHandleSize.height / 2
|
||||
@@ -336,10 +342,9 @@ public class SurfaceView: UIView {
|
||||
shadowLayer.frame = layer.bounds
|
||||
|
||||
let spread = shadow.spread
|
||||
let shadowPath = UIBezierPath(roundedRect: containerView.frame.insetBy(dx: -spread,
|
||||
dy: -spread),
|
||||
byRoundingCorners: [.allCorners],
|
||||
cornerRadii: CGSize(width: appearance.cornerRadius, height: 0))
|
||||
let shadowRect = containerView.frame.insetBy(dx: -spread, dy: -spread)
|
||||
let shadowPath = UIBezierPath.path(roundedRect: shadowRect,
|
||||
appearance: appearance)
|
||||
shadowLayer.shadowPath = shadowPath.cgPath
|
||||
shadowLayer.shadowColor = shadow.color.cgColor
|
||||
shadowLayer.shadowOffset = shadow.offset
|
||||
@@ -348,16 +353,16 @@ public class SurfaceView: UIView {
|
||||
shadowLayer.shadowOpacity = shadow.opacity
|
||||
|
||||
let mask = CAShapeLayer()
|
||||
let path = UIBezierPath(roundedRect: containerView.frame,
|
||||
byRoundingCorners: [.allCorners],
|
||||
cornerRadii: CGSize(width: appearance.cornerRadius, height: 0))
|
||||
let path = UIBezierPath.path(roundedRect: containerView.frame,
|
||||
appearance: appearance)
|
||||
let size = window?.bounds.size ?? CGSize(width: 1000.0, height: 1000.0)
|
||||
path.append(UIBezierPath(rect: layer.bounds.insetBy(dx: -size.width,
|
||||
dy: -size.height)))
|
||||
mask.fillRule = .evenOdd
|
||||
mask.path = path.cgPath
|
||||
if #available(iOS 13.0, *) {
|
||||
mask.cornerCurve = containerView.layer.cornerCurve
|
||||
containerView.layer.cornerCurve = appearance.cornerCurve
|
||||
mask.cornerCurve = appearance.cornerCurve
|
||||
}
|
||||
shadowLayer.mask = mask
|
||||
}
|
||||
|
||||
@@ -61,7 +61,7 @@ class PresentationController: UIPresentationController {
|
||||
addFloatingPanel()
|
||||
|
||||
// Forward touch events to the presenting view controller
|
||||
(fpc.view as? PassThroughView)?.eventForwardingView = presentingViewController.view
|
||||
(fpc.view as? PassthroughView)?.eventForwardingView = presentingViewController.view
|
||||
}
|
||||
|
||||
@objc func handleBackdrop(tapGesture: UITapGestureRecognizer) {
|
||||
@@ -86,19 +86,29 @@ class ModalPresentTransition: NSObject, UIViewControllerAnimatedTransitioning {
|
||||
let fpc = transitionContext?.viewController(forKey: .to) as? FloatingPanelController
|
||||
else { fatalError()}
|
||||
|
||||
let animator = fpc.delegate?.floatingPanel?(fpc, animatorForPresentingTo: fpc.layout.initialState)
|
||||
?? FloatingPanelDefaultBehavior().addPanelAnimator(fpc, to: fpc.layout.initialState)
|
||||
let animator = fpc.animatorForPresenting(to: fpc.layout.initialState)
|
||||
return TimeInterval(animator.duration)
|
||||
}
|
||||
|
||||
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
|
||||
func interruptibleAnimator(using transitionContext: UIViewControllerContextTransitioning) -> UIViewImplicitlyAnimating {
|
||||
guard
|
||||
let fpc = transitionContext.viewController(forKey: .to) as? FloatingPanelController
|
||||
else { fatalError() }
|
||||
|
||||
fpc.show(animated: true) {
|
||||
if let animator = fpc.transitionAnimator {
|
||||
return animator
|
||||
}
|
||||
|
||||
fpc.suspendTransitionAnimator(true)
|
||||
fpc.show(animated: true) { [weak fpc] in
|
||||
fpc?.suspendTransitionAnimator(false)
|
||||
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
|
||||
}
|
||||
return fpc.transitionAnimator!
|
||||
}
|
||||
|
||||
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
|
||||
self.interruptibleAnimator(using: transitionContext).startAnimation()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,19 +118,29 @@ class ModalDismissTransition: NSObject, UIViewControllerAnimatedTransitioning {
|
||||
let fpc = transitionContext?.viewController(forKey: .from) as? FloatingPanelController
|
||||
else { fatalError()}
|
||||
|
||||
let animator = fpc.delegate?.floatingPanel?(fpc, animatorForDismissingWith: .zero)
|
||||
?? FloatingPanelDefaultBehavior().removePanelAnimator(fpc, from: fpc.state, with: .zero)
|
||||
let animator = fpc.animatorForDismissing(with: .zero)
|
||||
return TimeInterval(animator.duration)
|
||||
}
|
||||
|
||||
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
|
||||
func interruptibleAnimator(using transitionContext: UIViewControllerContextTransitioning) -> UIViewImplicitlyAnimating {
|
||||
guard
|
||||
let fpc = transitionContext.viewController(forKey: .from) as? FloatingPanelController
|
||||
else { fatalError() }
|
||||
|
||||
fpc.hide(animated: true) {
|
||||
if let animator = fpc.transitionAnimator {
|
||||
return animator
|
||||
}
|
||||
|
||||
fpc.suspendTransitionAnimator(true)
|
||||
fpc.hide(animated: true) { [weak fpc] in
|
||||
fpc?.suspendTransitionAnimator(false)
|
||||
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
|
||||
}
|
||||
return fpc.transitionAnimator!
|
||||
}
|
||||
|
||||
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
|
||||
self.interruptibleAnimator(using: transitionContext).startAnimation()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -185,6 +185,57 @@ class CoreTests: XCTestCase {
|
||||
XCTAssertEqual(fpc.floatingPanel.getBackdropAlpha(at: halfPos, with: -1 * distance2), 0.0)
|
||||
}
|
||||
|
||||
|
||||
func test_updateBackdropAlpha() {
|
||||
class BackdropTestLayout: FloatingPanelTestLayout {
|
||||
override var initialState: FloatingPanelState { .hidden }
|
||||
func backdropAlpha(for state: FloatingPanelState) -> CGFloat {
|
||||
switch state {
|
||||
case .full: return 0.3
|
||||
case .half: return 0.0
|
||||
case .tip: return 0.3
|
||||
default: return 0.0
|
||||
}
|
||||
}
|
||||
}
|
||||
let fpc = FloatingPanelController()
|
||||
fpc.layout = BackdropTestLayout()
|
||||
|
||||
fpc.showForTest()
|
||||
|
||||
fpc.move(to: .full, animated: false)
|
||||
XCTAssertEqual(floor(fpc.backdropView.alpha * 1000_000) / 1000_000, 0.3)
|
||||
|
||||
fpc.move(to: .half, animated: false)
|
||||
XCTAssertEqual(fpc.backdropView.alpha, 0.0)
|
||||
|
||||
fpc.move(to: .tip, animated: false)
|
||||
XCTAssertEqual(floor(fpc.backdropView.alpha * 1000_000) / 1000_000, 0.3)
|
||||
|
||||
let exp1 = expectation(description: "move to full with animation")
|
||||
fpc.move(to: .full, animated: true) {
|
||||
exp1.fulfill()
|
||||
}
|
||||
wait(for: [exp1], timeout: 1.0)
|
||||
XCTAssertEqual(floor(fpc.backdropView.alpha * 1000_000) / 1000_000, 0.3)
|
||||
|
||||
let exp2 = expectation(description: "move to half with animation")
|
||||
fpc.move(to: .half, animated: true) {
|
||||
exp2.fulfill()
|
||||
}
|
||||
wait(for: [exp2], timeout: 1.0)
|
||||
XCTAssertEqual(fpc.backdropView.alpha, 0.0)
|
||||
|
||||
let exp3 = expectation(description: "move to tip with animation")
|
||||
fpc.move(to: .tip, animated: true) {
|
||||
exp3.fulfill()
|
||||
}
|
||||
fpc.contentMode = .fitToBounds
|
||||
XCTAssertEqual(fpc.backdropView.alpha, 0.0) // Must not affect the backdrop alpha by changing the content mode
|
||||
wait(for: [exp3], timeout: 1.0)
|
||||
XCTAssertEqual(floor(fpc.backdropView.alpha * 1000_000) / 1000_000, 0.3)
|
||||
}
|
||||
|
||||
func test_targetPosition_1positions() {
|
||||
class FloatingPanelLayout1Positions: FloatingPanelLayout {
|
||||
let initialState: FloatingPanelState = .full
|
||||
@@ -715,6 +766,13 @@ class CoreTests: XCTestCase {
|
||||
(#line, tipPos, CGPoint(x: 0.0, y: 1000.0), .hidden),
|
||||
])
|
||||
}
|
||||
|
||||
func test_keep_pan_gesture_disabled() {
|
||||
let fpc = FloatingPanelController()
|
||||
fpc.panGestureRecognizer.isEnabled = false
|
||||
fpc.showForTest()
|
||||
XCTAssertFalse(fpc.panGestureRecognizer.isEnabled)
|
||||
}
|
||||
}
|
||||
|
||||
private class FloatingPanelLayout3Positions: FloatingPanelTestLayout {
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
// Copyright 2018 the FloatingPanel authors. All rights reserved. MIT license.
|
||||
|
||||
import XCTest
|
||||
@testable import FloatingPanel
|
||||
|
||||
class ExtensionTests: XCTestCase {
|
||||
func test_roundedByDisplayScale() {
|
||||
XCTAssertEqual(CGFloat(333.222).rounded(by: 3), 333.3333333333333)
|
||||
XCTAssertNotEqual(CGFloat(333.5).rounded(by: 3), 333.66666666666674)
|
||||
XCTAssertTrue(CGFloat(333.5).isEqual(to: 333.66666666666674, on: 3.0))
|
||||
}
|
||||
}
|
||||
@@ -13,8 +13,8 @@ class LayoutTests: XCTestCase {
|
||||
override func tearDown() {}
|
||||
|
||||
func test_layoutAdapter_topAndBottomMostState() {
|
||||
XCTAssertEqual(fpc.floatingPanel.layoutAdapter.edgeMostState, .full)
|
||||
XCTAssertEqual(fpc.floatingPanel.layoutAdapter.edgeLeastState, .tip)
|
||||
XCTAssertEqual(fpc.floatingPanel.layoutAdapter.mostExpandedState, .full)
|
||||
XCTAssertEqual(fpc.floatingPanel.layoutAdapter.leastExpandedState, .tip)
|
||||
|
||||
class FloatingPanelLayoutWithHidden: FloatingPanelLayout {
|
||||
var anchors: [FloatingPanelState: FloatingPanelLayoutAnchoring] {
|
||||
@@ -38,12 +38,12 @@ class LayoutTests: XCTestCase {
|
||||
let position: FloatingPanelPosition = .bottom
|
||||
}
|
||||
fpc.layout = FloatingPanelLayoutWithHidden()
|
||||
XCTAssertEqual(fpc.floatingPanel.layoutAdapter.edgeMostState, .full)
|
||||
XCTAssertEqual(fpc.floatingPanel.layoutAdapter.edgeLeastState, .hidden)
|
||||
XCTAssertEqual(fpc.floatingPanel.layoutAdapter.mostExpandedState, .full)
|
||||
XCTAssertEqual(fpc.floatingPanel.layoutAdapter.leastExpandedState, .hidden)
|
||||
|
||||
fpc.layout = FloatingPanelLayout2Positions()
|
||||
XCTAssertEqual(fpc.floatingPanel.layoutAdapter.edgeMostState, .half)
|
||||
XCTAssertEqual(fpc.floatingPanel.layoutAdapter.edgeLeastState, .tip)
|
||||
XCTAssertEqual(fpc.floatingPanel.layoutAdapter.mostExpandedState, .half)
|
||||
XCTAssertEqual(fpc.floatingPanel.layoutAdapter.leastExpandedState, .tip)
|
||||
}
|
||||
|
||||
func test_layoutSegment_3position() {
|
||||
|
||||
@@ -18,7 +18,7 @@ extension FloatingPanelController {
|
||||
class FloatingPanelTestDelegate: FloatingPanelControllerDelegate {
|
||||
var position: FloatingPanelState = .hidden
|
||||
var didMoveCallback: ((FloatingPanelController) -> Void)?
|
||||
func floatingPanelDidChangePosition(_ vc: FloatingPanelController) {
|
||||
func floatingPanelDidChangeState(_ vc: FloatingPanelController) {
|
||||
position = vc.state
|
||||
}
|
||||
func floatingPanelDidMove(_ vc: FloatingPanelController) {
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
// Copyright 2018 the FloatingPanel authors. All rights reserved. MIT license.
|
||||
|
||||
import XCTest
|
||||
@testable import FloatingPanel
|
||||
|
||||
class UtilTests: XCTestCase {
|
||||
func test_displayTrunc() {
|
||||
XCTAssertEqual(displayTrunc(333.222, by: 3), 333.3333333333333)
|
||||
XCTAssertNotEqual(displayTrunc(333.5, by: 3), 333.66666666666674)
|
||||
XCTAssertTrue(displayEqual(333.5, 333.66666666666674, by: 3))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user