Compare commits

..

33 Commits

Author SHA1 Message Date
Shin Yamamoto 16fea625be Version 2.3.0 2021-02-23 13:45:46 +09:00
Shin Yamamoto 6c69694cfe Update readme 2021-02-23 13:45:46 +09:00
Shin Yamamoto 561d783479 Prevent a memory leak in a nested function (#441)
To fix a crash "Layout.swift: Fatal error: Attempted to read an unowned reference but the object was already deallocated"  which is reported at issue #440.
2021-02-23 09:37:35 +09:00
Shin Yamamoto 1bd2e60200 Enable to add custom panel states (#438)
* Support over 3 states in LayoutAdapter
* Allow to inherite FloatingPanelState
* Support a custome FloatingPanelState in ObjC
* Replace Menu enum with UseCase enum in Samples.app
* Rename UIExtensions to Extensions
* Add CustomState use case in Samples app
2021-02-15 21:05:12 +09:00
Shin Yamamoto ec0e1b2dad Remove class keyword
class keyword will be deprecated and in Swift 5 doesn't need it for a
protocol attributed by @objc.
2021-02-12 12:15:58 +09:00
Shin Yamamoto 9958fc5017 Prevent the potential memory leaks in the modal transition (#429)
This dismisses the frame 'FloatingPanel Core.move(from:to:animated:completion:)'
in the following memory leaks

> BoardServices -[BSXPCServiceConnectionEventHandler remoteTarget]
> BoardServices __63+[BSXPCServiceConnectionProxy createImplementationForProtocol:]_block_invoke

These leaks happens when a panel showes and hides using "Show Multi Panel Modal"
in the Samples app.
2021-02-06 09:16:11 +09:00
Shin Yamamoto 11dfc0d2f3 Fix workaround for bouncing a scroll content 2021-02-06 09:05:40 +09:00
Shin Yamamoto 34246d1f37 Merge pull request #435 from SCENEE/release-2.2.0
Prepare v2.2.0
2021-01-24 16:08:13 +09:00
Shin Yamamoto 3d6c0220e0 Version 2.2.0 2021-01-22 07:45:12 +09:00
Shin Yamamoto ecbd318186 Add a note for log variable 2021-01-22 07:45:12 +09:00
Shin Yamamoto 34809bd8ea Show debug logs in console.app 2021-01-22 07:45:12 +09:00
Shin Yamamoto aa5fbe9e94 Move the main ci to github actions (#437)
Because of the migration from travis-ci.org to travis-ci.com. As a
bonus, ci builds on github actions are much faster.

This repo still needs travis-ci for testing on iOS 10/11/12 simulators
on Xcode11.x. So this doesn't remove .travis.yml
2021-01-22 07:44:30 +09:00
nickcheng 9fbb7df15a Fixed the crash when ownerVC is nil. (#436)
`ownerVC` should be checked in the completion as well.
2021-01-20 23:10:43 +09:00
Shin Yamamoto d55f9a0abf Revise the swizzling prop to make it nonnull 2021-01-15 21:54:21 +09:00
Shin Yamamoto a932f3b782 Stop moving a panel while the tracking table view is editing (#431)
To fix https://github.com/SCENEE/FloatingPanel/issues/427
2021-01-11 22:46:34 +09:00
Shin Yamamoto 9ea95d69a1 Fix an issue where not dragging a panel by priority of Layout.interactionConstraint (#428)
I applied `.defaultHigh` priority which might be safe to prevent
an ambiguous layout error. But that causes this issue, so I have to
use `.required` priority.

See more detail [#424
issue](https://github.com/SCENEE/FloatingPanel/issues/424).
2021-01-11 20:13:04 +09:00
Shin Yamamoto e783b92905 Fix a crash by the move animator (#423)
* Prevent a possible memory leak at Core.move(from:to:animated:completion:)
* Replace ``self`` with `self`.
* Change `LayoutAdapter.vc` property to an unowned reference
* Prevent a crash at LayoutAdapter.surfaceLocation called in `NumericSpringAnimator.update` closure.
2021-01-09 12:20:53 +09:00
Shin Yamamoto d380fdeb13 Address the grabber area detection in scroll tracking (#407)
Allows to expand the panel while touching in the grabber area initially.
2021-01-09 11:59:02 +09:00
Fumito Nakazawa 987bf79121 Fix swiftformat (#426) 2021-01-08 22:20:38 +09:00
Federico Zanetello be0ebd0fae Add cornerCurve option to SurfaceAppearance (#417)
* Add cornerCurve option to SurfaceAppearance
* Add cornerCurve usage in Maps example

Co-authored-by: Shin Yamamoto <shin@scenee.com>
2021-01-05 18:20:58 +09:00
Benjamin Otto a6538b7a2a Add optional removalInteractionVelocityThreshold value to Behavior (#425)
Added an optional variable `removalInteractionVelocityThreshold` to `Behavior`, which let’s the user configure the velocity threshold for the default `floatingPanel:shouldRemoveAt:with` implementation.
2021-01-04 23:09:33 +09:00
Shin Yamamoto 40194d91c0 Add a description for the backdropAlpha(for:) API (#416) 2020-12-24 23:53:42 +09:00
Federico Zanetello c8b2b33de0 fix example typo (#418) 2020-12-22 18:22:19 +09:00
Shin Yamamoto 7114f545ff Merge pull request #414 from SCENEE/release-2.1.0
Prepare version 2.1.0
2020-12-12 12:43:52 +09:00
Shin Yamamoto b4423bcaa2 Version 2.1.0 2020-12-07 19:25:47 +09:00
Shin Yamamoto b34fd41650 Fix Maps samlpe (#411)
* Fix the detail vc layout in Maps sample
* Deactivate search bar when the detail vc shows in Maps sample
2020-12-05 11:36:54 +09:00
Ryan McLeod 199a77182b Work magic numbers out of Adaptive Panel (Custom Layout Guide) example (#412) 2020-12-05 11:36:21 +09:00
Shin Yamamoto 0a0f00172d Add FloatingPanelAdaptiveLayoutAnchor (#390)
* Rename PassThroughView to PassthroughView
* Refactor LayoutAnchor initializer
* Add FloatignPanelAdaptiveLayoutAnchor
* Add samples for FloatingPanelAdaptiveLayoutAnchor
* Revise updateStaticConstraint
2020-12-01 19:33:19 +09:00
Shin Yamamoto 25ca9487fb Open the default behavior class (#406)
* Allow open access to FloatingPanelDefaultBehavior
* Move the default presenting & dismissing animators
* Add the public initializer of FloatingPanelDefaultBehavior
2020-11-30 21:21:21 +09:00
Shin Yamamoto 231ee4c9af Create FUNDING.yml for GitHub Sponsors
According to #400 request.
2020-11-28 10:32:03 +09:00
Ryan McLeod c872218446 Add escape gesture to close modal for VoiceOver users (#383)
* Allow dismissing of panel with Accessibility escape gesture
2020-11-07 11:29:23 +09:00
Takao Horiguchi 8e8c6527d4 update readme url for Transitioning.swift (#398) 2020-10-23 19:24:10 +09:00
Shin Yamamoto 3b4e237eba Merge pull request #397 from SCENEE/release-2.0.1
Release 2.0.1
2020-10-21 20:41:34 +09:00
33 changed files with 860 additions and 309 deletions
+1
View File
@@ -0,0 +1 @@
github: SCENEE
+81
View File
@@ -0,0 +1,81 @@
name: ci
on:
push:
branches:
- master
pull_request:
branches:
- '*'
jobs:
build:
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.3.app/Contents/Developer
testing:
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.3"
run: xcodebuild clean test -scheme FloatingPanel -workspace FloatingPanel.xcworkspace -destination 'platform=iOS Simulator,OS=14.3,name=iPhone 12 Pro'
env:
DEVELOPER_DIR: /Applications/Xcode_12.3.app/Contents/Developer
example:
runs-on: macOS-10.15
env:
DEVELOPER_DIR: /Applications/Xcode_12.3.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-10.15
env:
DEVELOPER_DIR: /Applications/Xcode_12.3.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-10.15
env:
# Carthage doesn't fix issues on Xcode 12: https://github.com/Carthage/Carthage/releases/tag/0.36.0
DEVELOPER_DIR: /Applications/Xcode_11.7.app/Contents/Developer
steps:
- uses: actions/checkout@v1
- name: "Carthage build"
run: carthage build --no-skip-current
cocoapods:
runs-on: macOS-10.15
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
View File
@@ -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
-54
View File
@@ -8,18 +8,6 @@ env:
- 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
@@ -30,45 +18,3 @@ jobs:
- 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
@@ -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:
+18 -7
View File
@@ -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)
}
@@ -14,9 +14,11 @@
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 /* UseCases.swift in Sources */ = {isa = PBXBuildFile; fileRef = 546341A025C6415100CA0596 /* UseCases.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 */; };
54B51116216AFE5F0033A6F3 /* Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54B51115216AFE5F0033A6F3 /* Extensions.swift */; };
54CDC5D8215BBE23007D205C /* UIComponents.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CDC5D7215BBE23007D205C /* UIComponents.swift */; };
/* End PBXBuildFile section */
@@ -65,8 +67,10 @@
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 /* UseCases.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UseCases.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>"; };
54B51115216AFE5F0033A6F3 /* Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Extensions.swift; sourceTree = "<group>"; };
54CDC5D7215BBE23007D205C /* UIComponents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIComponents.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
@@ -125,8 +129,9 @@
545DB9F121511E6300CA77B8 /* Main.storyboard */,
545DB9ED21511E6300CA77B8 /* AppDelegate.swift */,
545DB9EF21511E6300CA77B8 /* ViewController.swift */,
54B51115216AFE5F0033A6F3 /* UIExtensions.swift */,
54B51115216AFE5F0033A6F3 /* Extensions.swift */,
54CDC5D7215BBE23007D205C /* UIComponents.swift */,
546341AA25C6421000CA0596 /* UseCases */,
545DB9F921511E6400CA77B8 /* Info.plist */,
);
path = Sources;
@@ -150,6 +155,15 @@
path = UITests;
sourceTree = "<group>";
};
546341AA25C6421000CA0596 /* UseCases */ = {
isa = PBXGroup;
children = (
546341A025C6415100CA0596 /* UseCases.swift */,
546341AB25C6426500CA0596 /* CustomState.swift */,
);
path = UseCases;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
@@ -304,7 +318,9 @@
buildActionMask = 2147483647;
files = (
54CDC5D8215BBE23007D205C /* UIComponents.swift in Sources */,
54B51116216AFE5F0033A6F3 /* UIExtensions.swift in Sources */,
54B51116216AFE5F0033A6F3 /* Extensions.swift in Sources */,
546341AC25C6426500CA0596 /* CustomState.swift in Sources */,
546341A125C6415100CA0596 /* UseCases.swift in Sources */,
545DB9F021511E6300CA77B8 /* ViewController.swift in Sources */,
545DB9EE21511E6300CA77B8 /* AppDelegate.swift in Sources */,
);
@@ -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,12 @@
<?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">
<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="RoN-h0-uBD">
<device id="retina5_9" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="17125"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="17505"/>
<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>
@@ -293,6 +294,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>
@@ -725,4 +791,19 @@ Section 1.10.33 of "de Finibus Bonorum et Malorum", written by Cicero in 45 BC
<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>
@@ -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,78 @@
// Copyright 2018-Present Shin Yamamoto. All rights reserved. MIT license.
import Foundation
enum UseCases: 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"
case .showDetail: return "DetailViewController"
case .showModal: return "ModalViewController"
case .showMultiPanelModal: return nil
case .showPanelInSheetModal: return nil
case .showPanelModal: return nil
case .showTabBar: return "TabBarViewController"
case .showPageView: return nil
case .showPageContentView: return nil
case .showNestedScrollView: return "NestedScrollViewController"
case .showRemovablePanel: return "DetailViewController"
case .showIntrinsicView: return "IntrinsicViewController"
case .showContentInset: return nil
case .showContainerMargins: return nil
case .showNavigationController: return "RootNavigationController"
case .showTopPositionedPanel: return nil
case .showAdaptivePanel,
.showAdaptivePanelWithCustomGuide:
return "ImageViewController"
case .showCustomStatePanel:
return nil
}
}
}
+89 -76
View File
@@ -6,71 +6,7 @@ import FloatingPanel
class SampleListViewController: UIViewController {
@IBOutlet weak var tableView: UITableView!
enum Menu: 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 showBottomEdgeInteraction
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 .showBottomEdgeInteraction: return "Show bottom edge interaction"
}
}
var storyboardID: String? {
switch self {
case .trackingTableView: return nil
case .trackingTextView: return "ConsoleViewController"
case .showDetail: return "DetailViewController"
case .showModal: return "ModalViewController"
case .showMultiPanelModal: return nil
case .showPanelInSheetModal: return nil
case .showPanelModal: return nil
case .showTabBar: return "TabBarViewController"
case .showPageView: return nil
case .showPageContentView: return nil
case .showNestedScrollView: return "NestedScrollViewController"
case .showRemovablePanel: return "DetailViewController"
case .showIntrinsicView: return "IntrinsicViewController"
case .showContentInset: return nil
case .showContainerMargins: return nil
case .showNavigationController: return "RootNavigationController"
case .showBottomEdgeInteraction: return nil
}
}
}
var currentMenu: Menu = .trackingTableView
var currentMenu: UseCases = .trackingTableView
var mainPanelVC: FloatingPanelController!
var detailPanelVC: FloatingPanelController!
@@ -156,7 +92,7 @@ class SampleListViewController: UIViewController {
mainPanelVC.backdropView.dismissalTapGestureRecognizer.isEnabled = true
case .showNavigationController:
mainPanelVC.contentInsetAdjustmentBehavior = .never
case .showBottomEdgeInteraction: // For debug
case .showTopPositionedPanel: // For debug
let contentVC = UIViewController()
contentVC.view.backgroundColor = .red
mainPanelVC.set(contentViewController: contentVC)
@@ -184,6 +120,17 @@ class SampleListViewController: UIViewController {
rootVC.loadViewIfNeeded()
mainPanelVC.track(scrollView: rootVC.tableView)
}
case let contentVC as ImageViewController:
if #available(iOS 11.0, *) {
let mode: ImageViewController.Mode = (currentMenu == .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
}
@@ -236,19 +183,19 @@ extension SampleListViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if #available(iOS 11.0, *) {
if navigationController?.navigationBar.prefersLargeTitles == true {
return Menu.allCases.count + 30
return UseCases.allCases.count + 30
} else {
return Menu.allCases.count
return UseCases.allCases.count
}
} else {
return Menu.allCases.count
return UseCases.allCases.count
}
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
if Menu.allCases.count > indexPath.row {
let menu = Menu.allCases[indexPath.row]
if UseCases.allCases.count > indexPath.row {
let menu = UseCases.allCases[indexPath.row]
cell.textLabel?.text = menu.name
} else {
cell.textLabel?.text = "\(indexPath.row) row"
@@ -259,8 +206,8 @@ extension SampleListViewController: UITableViewDataSource {
extension SampleListViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
guard Menu.allCases.count > indexPath.row else { return }
let menu = Menu.allCases[indexPath.row]
guard UseCases.allCases.count > indexPath.row else { return }
let menu = UseCases.allCases[indexPath.row]
let contentVC: UIViewController = {
guard let storyboardID = menu.storyboardID else { return DebugTableViewController() }
guard let vc = self.storyboard?.instantiateViewController(withIdentifier: storyboardID) else { fatalError() }
@@ -419,8 +366,8 @@ extension SampleListViewController: FloatingPanelControllerDelegate {
}
switch currentMenu {
case .showBottomEdgeInteraction:
return BottomEdgeInteractionLayout()
case .showTopPositionedPanel:
return TopPositionedPanelLayout()
case .showRemovablePanel:
return newCollection.verticalSizeClass == .compact ? RemovablePanelLandscapeLayout() : RemovablePanelLayout()
case .showIntrinsicView:
@@ -432,6 +379,8 @@ extension SampleListViewController: FloatingPanelControllerDelegate {
fallthrough
case .showContentInset:
return FloatingPanelBottomLayout()
case .showCustomStatePanel:
return FloatingPanelLayoutWithCustomState()
default:
return (newCollection.verticalSizeClass == .compact) ? FloatingPanelBottomLayout() : self
}
@@ -506,7 +455,7 @@ extension SampleListViewController: UIPageViewControllerDelegate {
}
}
class BottomEdgeInteractionLayout: FloatingPanelLayout {
class TopPositionedPanelLayout: FloatingPanelLayout {
let position: FloatingPanelPosition = .top
let initialState: FloatingPanelState = .full
@@ -1298,3 +1247,67 @@ final class MultiPanelController: FloatingPanelController, FloatingPanelControll
}
}
}
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
}
}
}
@@ -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
@@ -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 -1
View File
@@ -1,7 +1,7 @@
Pod::Spec.new do |s|
s.name = "FloatingPanel"
s.version = "2.0.1"
s.version = "2.3.0"
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.
+4 -4
View File
@@ -9,7 +9,7 @@
/* 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 */; };
@@ -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>"; };
@@ -142,7 +142,7 @@
5469F4B324B30F3500537F8A /* LayoutReferences.swift */,
5469F4AF24B30E1500537F8A /* LayoutAnchoring.swift */,
5450EEE321646DF500135936 /* Behavior.swift */,
54352E9721A521CA00CBCA08 /* PassThroughView.swift */,
54352E9721A521CA00CBCA08 /* PassthroughView.swift */,
54CDC5D2215B6D5A007D205C /* SurfaceView.swift */,
54CDC5D4215B6D8D007D205C /* BackdropView.swift */,
545DBA2A2152383100CA77B8 /* GrabberView.swift */,
@@ -328,7 +328,7 @@
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 */,
+44 -2
View File
@@ -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)
@@ -71,7 +73,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 +165,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 +372,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
+20 -19
View File
@@ -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)
+34 -8
View File
@@ -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
@@ -274,7 +274,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
@@ -498,7 +498,7 @@ 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)
}
}
@@ -517,7 +517,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)
@@ -537,7 +537,6 @@ open class FloatingPanelController: UIViewController {
/// - 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 +597,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 +648,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 +681,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
}()
}
+59 -30
View File
@@ -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)
}
@@ -112,7 +115,8 @@ class Core: NSObject, UIGestureRecognizerDelegate {
interruptAnimationIfNeeded()
if animated {
func updateScrollView() {
let updateScrollView: () -> Void = { [weak self] in
guard let self = self else { return }
if self.state == self.layoutAdapter.edgeMostState, abs(self.layoutAdapter.offsetFromEdgeMost) <= 1.0 {
self.unlockScrollView()
} else {
@@ -123,14 +127,14 @@ 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?()
@@ -143,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)
@@ -154,13 +158,17 @@ 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
@@ -373,7 +381,7 @@ class Core: NSObject, UIGestureRecognizerDelegate {
if interactionInProgress {
lockScrollView()
} else {
if state == layoutAdapter.edgeMostState, self.animator == nil {
if state == layoutAdapter.edgeMostState, self.transitionAnimator == nil {
switch layoutAdapter.position {
case .top, .left:
if offsetDiff < 0 && velocity > 0 {
@@ -493,7 +501,7 @@ class Core: NSObject, UIGestureRecognizerDelegate {
animator.stopAnimation(true)
endAttraction(false)
}
if let animator = self.animator {
if let animator = self.transitionAnimator {
guard 0 >= layoutAdapter.offsetFromEdgeMost else { return }
log.debug("a panel animation(interruptible: \(animator.isInterruptible)) interrupted!!!")
if animator.isInterruptible {
@@ -535,15 +543,20 @@ class Core: NSObject, UIGestureRecognizerDelegate {
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
}
@@ -571,6 +584,9 @@ class Core: NSObject, UIGestureRecognizerDelegate {
if scrollView.isDecelerating {
return true
}
if let tableView = (scrollView as? UITableView), tableView.isEditing {
return true
}
return false
}
@@ -652,7 +668,8 @@ class Core: NSObject, UIGestureRecognizerDelegate {
stopScrollDeceleration = (0 > layoutAdapter.offsetFromEdgeMost + (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)
}
}
@@ -698,14 +715,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.edgeMostState {
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.edgeMostState,
let isScrollEnabled = isScrollEnabled {
scrollView.isScrollEnabled = isScrollEnabled
}
@@ -718,7 +735,7 @@ class Core: NSObject, UIGestureRecognizerDelegate {
if let result = vc.delegate?.floatingPanel?(vc, shouldRemoveAt: vc.surfaceLocation, with: velocityVector) {
return result
}
let threshold = CGFloat(5.5)
let threshold = behaviorAdapter.removalInteractionVelocityThreshold
switch layoutAdapter.position {
case .top:
return (velocityVector.dy <= -threshold)
@@ -825,15 +842,18 @@ 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 }
guard let self = self,
self.ownerVC != nil else { return }
self.layoutAdapter.activateLayout(for: targetPosition, forceLayout: true)
completion()
})
@@ -1023,7 +1043,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
}
}
@@ -1184,3 +1204,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
}
}
+1 -1
View File
@@ -15,7 +15,7 @@
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>2.0.1</string>
<string>2.3.0</string>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
</dict>
+64 -51
View File
@@ -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?
@@ -284,14 +287,9 @@ 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 {
@@ -327,6 +325,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 +378,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 +451,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 +474,7 @@ class LayoutAdapter {
tearDownAttraction()
NSLayoutConstraint.deactivate(fullConstraints + halfConstraints + tipConstraints + offConstraints)
NSLayoutConstraint.deactivate(stateConstraints.flatMap { $1 } + offConstraints)
initialConst = edgePosition(surfaceView.frame) + offset.y
@@ -487,7 +490,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 +514,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 +633,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
@@ -640,19 +642,31 @@ class LayoutAdapter {
}
let anchor = layout.anchors[self.edgeMostState]!
if anchor is FloatingPanelIntrinsicLayoutAnchor {
var constant = layout.position.mainDimension(surfaceView.intrinsicContentSize)
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.directionalMostState))
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.directionalLeastState))
}
}
@@ -741,17 +755,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)")
}
}
}
+75 -12
View File
@@ -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
}
}
+2 -1
View File
@@ -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
View File
@@ -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.
+14 -9
View File
@@ -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
}
+29 -9
View File
@@ -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()
}
}
+67
View File
@@ -201,3 +201,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))
}
}