Compare commits

...

75 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
Shin Yamamoto 9b1cb68f0a Version 2.0.1 2020-10-19 19:22:53 +09:00
Shin Yamamoto 4ba8acaf08 Ease the default velocity for a panel removal (#395)
Because the current threshold(10.0) makes it hard to remove a panel.

Resolve #389
2020-10-19 19:20:16 +09:00
Shin Yamamoto 02d8d4516c Reset the moveAnimator prop after a move animation (#396)
* Reset the moveAnimator prop after a move animation

If `moveAnimator` isn't null, `FloatingPanelPanGestureRecognizer.touchesBegan`
detects `began` state quickly so that it doesn’t allow to work a tap gesture or
tap action in a panel.

Resolve #392

* Add 2 buttons in DebugTableViewController
2020-10-19 19:17:53 +09:00
Federico Zanetello 94829d2749 add missing commas (#387) 2020-10-07 20:11:04 +09:00
Shin Yamamoto 1522a0990f Merge pull request #372 from SCENEE/v2-dev
Release FloatingPanel 2.0.0
2020-10-03 09:01:19 +09:00
Shin Yamamoto 0a613a7339 Version 2.0.0 2020-10-01 08:26:01 +09:00
Shin Yamamoto b4a518fe3e Suppress 'brew update' error on travis ci 2020-09-30 21:53:47 +09:00
Shin Yamamoto 538e046368 Change SurfaceView.contentView to an optional type 2020-09-30 20:19:19 +09:00
Shin Yamamoto bb44d14370 Double lay out the surface view for uistackview's intrinsic behavior 2020-09-30 20:19:19 +09:00
Shin Yamamoto 230ec689d4 Prevent a crash in a device rotation 2020-09-22 15:20:42 +09:00
Shin Yamamoto c7605a94b9 Fix a content view layout using intrinsic anchor 2020-09-21 15:00:50 +09:00
Shin Yamamoto 864104de3e Recalculate state constraints in layout activation for intrinsic layout
Because sometimes UIView.systemLayoutSizeFitting() returns a different
size between an onscreen and offscreen view which includes UIStackView.

This behavior depends on UIStackView behavior and the lib can not match
the difference.
2020-09-21 15:00:50 +09:00
Shin Yamamoto a8e1cfa2bc Fix intrinsic layout 2020-09-21 15:00:50 +09:00
Shin Yamamoto 207dd27113 Fix Samples app 2020-09-21 15:00:50 +09:00
Shin Yamamoto 802b6f9b6b Ease time out conditions for ControllerTests.test_moveTo_bottomEdge() 2020-09-19 16:06:17 +09:00
Shin Yamamoto 0ff9d5dd9b ci: reduce the build time 2020-09-19 15:12:47 +09:00
Shin Yamamoto 6e509481bb Ease time out conditions in ControllerTests.test_moveTo() 2020-09-19 14:00:14 +09:00
Shin Yamamoto 054a21f3f8 ci: support xcode 12 and fix errors
- Since April 2020 Xcode 11 is required to submit an app so this drops
  Xcode 10.3(Swift 5.0) build.
- Specify `-workspace` for testing
- Travis CI doesn't have Xcode 11.7 environment.
2020-09-19 13:16:09 +09:00
Shin Yamamoto 2e3bbaeec2 Merge pull request #386 from SCENEE/release-1.7.6
Release 1.7.6
2020-09-19 12:10:10 +09:00
Shin Yamamoto 18e739fc7b Merge branch 'release-1.7.6' into v2-dev 2020-09-19 11:24:39 +09:00
Shin Yamamoto ca7596e1ca Version 1.7.6 2020-09-19 11:09:30 +09:00
Shin Yamamoto 38103da2eb Update travis ci config 2020-09-05 14:46:23 +09:00
Shin Yamamoto 0c2275def1 Address test failures on iOS 10 and 11 2020-09-05 14:46:23 +09:00
Shin Yamamoto a0da1b99c0 Use BackdropView.dismissalTapGestureRecognizer to dismiss panels in Samples 2020-09-05 14:46:23 +09:00
Shin Yamamoto 427814839a Fix typo 2020-09-05 14:46:23 +09:00
Shin Yamamoto 007f9af3eb Enable the removal interaction at any positions upon the conditions (#335)
If a library consumer allows a panel projectable movement with the
FloatingPanelBehavior object, the panel is able to invoke the removal
interaction when the next moving position projected the momentum is
hidden.
2020-09-03 23:07:33 +09:00
Michal Raška da4e1d26d3 Fix quick pull down (#385) 2020-09-03 21:41:16 +09:00
Shin Yamamoto 2ce1375ce7 Fix an issue where keyboard opens above image picker (#381)
This issue was reported in
https://github.com/SCENEE/FloatingPanel/issues/369.

The cause of that is the containerViewWillLayoutSubviews() of
FloatingPanelPresentationController is called in presenting a
UIImagePickerViewController. As a result, a FloatingPanelController
view is added unnecessarily.

I don't know the reason why the method is called, but this patch
resolves the issue.

By the way, the issue doesn't happen when a FloatingPanelController
shows as a child view controller
2020-09-03 20:42:21 +09:00
Shin Yamamoto 016bf3e185 Revise the migration guide according to @zntfdr reviews 2020-08-31 21:12:35 +09:00
Shin Yamamoto 214942f918 Fix the panel behavior of 'Show Detail Panel' on 'fitToBounds' mode in Samples app 2020-08-29 16:17:02 +09:00
Shin Yamamoto af53ac1964 Update README 2020-08-29 15:23:41 +09:00
Shin Yamamoto 991166534f Add the migration guide for FloatingPanel 2.0 2020-08-29 15:19:32 +09:00
Shin Yamamoto 39feab437d Fix layouts using backdropAlpaFor(position:) in Samples app 2020-08-29 13:28:15 +09:00
Shin Yamamoto ba8d37e8e0 Update .gitignore 2020-08-22 15:33:30 +09:00
Shin Yamamoto 885493f2e5 Add doc comments 2020-08-21 17:42:02 +09:00
Shin Yamamoto 65fd04975d Clean up core code 2020-08-12 12:26:18 +09:00
Shin Yamamoto 1a45f43232 Update the file license headers
I reconsidered the author part because it's better to make the ownership
obvious to get rid of problems according to the unclearness.
2020-08-12 09:37:52 +09:00
Greg Hazel 28c384aa0d use 'prominent' blur effect (#379)
'prominent' blue effect is adaptive to dark mode.
2020-08-10 12:36:47 +09:00
Shin Yamamoto 99c922bf3a Update doc comment 2020-08-10 12:22:14 +09:00
Shin Yamamoto 009033be79 Fix typo 2020-08-10 12:22:14 +09:00
Shin Yamamoto 5d2e6dd417 Fix method names in core 2020-08-10 12:22:14 +09:00
Christopher Truman 9c71a47d9b Small typo fixes (#378) 2020-08-10 09:55:10 +09:00
45 changed files with 1616 additions and 607 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
View File
@@ -17,6 +17,7 @@ DerivedData/
*.perspectivev3
!default.perspectivev3
xcuserdata/
IDEWorkspaceChecks.plis
## Other
*.moved-aside
+2 -2
View File
@@ -1,3 +1,3 @@
--header "// Copyright 2018 the FloatingPanel authors. 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
--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,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
+6 -60
View File
@@ -2,73 +2,19 @@ language: objective-c
branches:
only:
- master
cache:
directories:
- /usr/local/Homebrew
- $HOME/Library/Caches/Homebrew
before_cache:
- brew cleanup
env:
global:
- LANG=en_US.UTF-8
- LC_ALL=en_US.UTF-8
jobs:
include:
- stage: "Builds"
osx_image: xcode9.4
script: xcodebuild -scheme FloatingPanel SWIFT_VERSION=4.1 clean build
name: "Swift 4.1"
- script: xcodebuild -scheme FloatingPanel SWIFT_VERSION=4.2 clean build
osx_image: xcode10
name: "Swift 4.2"
- script: xcodebuild -scheme FloatingPanel SWIFT_VERSION=5.0 clean build
osx_image: xcode10.3
name: "Swift 5.0"
- script: xcodebuild -scheme FloatingPanel SWIFT_VERSION=5.1 SUPPORTS_MACCATALYST=NO clean build
# SUPPORTS_MACCATALYST=NO because Xcode 11 runs on macOS 10.14 in Travis CI
osx_image: xcode11
name: "Swift 5.1"
- stage: "Tests"
osx_image: xcode10.3
script: xcodebuild clean test -scheme FloatingPanel -workspace FloatingPanel.xcworkspace -destination 'platform=iOS Simulator,OS=10.3.1,name=iPhone SE'
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: xcode10.3
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.2,name=iPhone X'
osx_image: xcode10.3
name: "iPhone X (iOS 12.2)"
- script: xcodebuild clean test -scheme FloatingPanel -workspace FloatingPanel.xcworkspace -destination 'platform=iOS Simulator,OS=13.0,name=iPhone 11'
osx_image: xcode11
name: "iPhone X (iOS 13.0)"
- stage: Build examples
osx_image: xcode11
script: xcodebuild -scheme Maps -sdk iphonesimulator clean build
name: "Maps"
- script: xcodebuild -scheme Stocks -sdk iphonesimulator clean build
osx_image: xcode11
name: "Stocks"
- script: xcodebuild -scheme Samples -sdk iphonesimulator clean build
osx_image: xcode11
name: "Samples"
- script: xcodebuild -scheme SamplesObjC -sdk iphonesimulator clean build
osx_image: xcode11
name: "SamplesObjC"
- stage: Carthage
osx_image: xcode11
before_install:
- brew update
- brew outdated carthage || brew upgrade carthage
script:
- carthage build --no-skip-current
- stage: CocoaPods
osx_image: xcode11
before_install:
- gem install cocoapods
script:
- pod spec lint --allow-warnings
- pod lib lint --allow-warnings
- 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)"
View File
@@ -0,0 +1,294 @@
# FloatingPanel 2.0 Migration Guide
FloatingPanel 2.0 is the latest major release of FloatingPanel. As a major release, following Semantic Versioning conventions, 2.0 introduces API-breaking changes.
This guide is provided in order to ease the transition of existing applications using FloatingPanel 1.x to the latest APIs, as well as explain the design and structure of new and updated functionality.
## Updated Minimum Requirements
* Swift 5.0
* iOS 11 (iOS 10 is still the deployment target, but not tested well)
* Xcode 11.0
## Benefits of Upgrading
* __Top, left and right positioned panel__
* FloatingPanel is not just a library for a bottom positioned panel, but also top, left and right positioned ones.
* __Objective-C compatibility__
* The entire APIs are exposed in Objective-C. So you can use them in Objective-C directly.
* __Flexible and explicit layout customization__
* `FloatingPanelLayout` is redesigned. There is no implicit rules to lay out a panel anymore.
* __New spring animation without UIViewPropertyAnimator__
* The new spring animation uses [Numeric springing](http://allenchou.net/2015/04/game-math-precise-control-over-numeric-springing/) which is a very powerful tool for procedural animation. Therefore a library consumer is easy to modify a panel behavior by 2 paramters of the deceleration rate and response time.
* __Handle the panel position anytime__
* `floatingPanelDidMove(_:)` delegate method is also called while a panel is moving. The method behavior becomes same as `scrollViewDidScroll(_:)` in `UIScrollViewDelegate`. And in the method a library consumer is able to change a panel location.
* __Update the removal interaction's invocation__
* Now you can invoke the removal interaction at any time where you want. There is no restrictions in the library.
* __Fix many issues depending on API design__
* See the following sections for details.
## API Name Changes
* `FloatingPanelPosition` is now `FloatingPanelState`.
* `FloatingPanelPosition` in v2 is used to specify a panel position(top, left, bottom and right) in a screen.
* `FloatingPanelSurfaceView` is `SurfaceView` only in Swift.
* `FloatingPanelBackdropView` is `BackdropView` only in Swift.
* `FloatingPanelGrabberHandleView` is `GrabberView` only in Swift.
* "decelerate" term is replaced with "attract" because the panel's behavior is not unidirectional, but going back and forth so that it is settled to a location.
## `FloatingPanelController`
* `layout` and `behavior` properties can be changed directly without using the delegate methods.
```swift
fpc.behavior = SearchPaneliPadBehavior()
fpc.layout = SearchPaneliPadLayout()
fpc.invalidateLayout() // If needed
```
* The second argument of `addPanel(toParent:)` changes to specify an index of subviews of a view in which a panel is added.
```diff
- public func addPanel(toParent parent: UIViewController, belowView: UIView? = nil, animated: Bool = false) {
+ public func addPanel(toParent parent: UIViewController, at viewIndex: Int = -1, animated: Bool = false) {
```
* `surfaceOriginY` is now `surfaceLocation`.
* `updateLayout` is now `invalidateLayout`.
* The scroll tracking API is changed a bit to support multiple scroll view tracking in the future.
* Now `untrack(scrollView:)` is used to disable the scroll tracking.
## `FloatingPanelControllerDelegate`
* `floatingPanelDidEndDragging(_ vc:willAttract:)` is added to check whether a panel will continue to move after dragging.
* `floatingPanelDidMove(_:)` behavior changes. The method is also called in the spring animation.
* The removal interaction delegate is updated.
* `floatingPanel(_:shouldRemoveAt:with:)` is added to determine whether it invokes the removal interaction in any state.
* `floatingPanelWillRemove(_:)` is added.
* `floatingPanel(_: FloatingPanelController, layoutFor size: CGSize)` is added to respond to a layout change in regular size classes on iPad.
```swift
func floatingPanel(_ fpc: FloatingPanelController, layoutFor size: CGSize) -> FloatingPanelLayout {
if aCondition(for: size) {
return SearchPanelLayout()
}
return SearchPanel2Layout()
}
```
* The `targetState` argument type of `floatingPanelWillEndDragging(_:withVelocity:targetState:)` is changed from `FloatingPanelState` to `UnsafeMutablePointer<FloatingPanelState>` to modify a target state on demand.
```swift
func floatingPanelWillEndDragging(_ vc: FloatingPanelController, withVelocity velocity: CGPoint, targetState: UnsafeMutablePointer<FloatingPanelState>) {
switch targetState.pointee {
case .full:
// do something...
case .half:
if aCondition {
targetState.pointee = .tip
}
default:
break
}
}
```
### Deprecated APIs
* `floatingPanel(_:behaviorFor:)`
* Please update `FloatingPanelController.behavior` directly.
* `floatingPanel(_:shouldRecognizeSimultaneouslyWith:)`
* Please use `FloatingPanelController.panGestureRecognizer.delegateProxy`.
## `FloatingPanelLayout`
* `position` property is added to determine a panel position.
* `initialPosition` is now `initialState`.
* `supportedPositions` and `insetFor(position:)` are replaced with `anchors` property.
* `backdropAlphaFor(position:)` is now `backdropAlpha(for:)`.
```swift
class SearchPanelPadLayout: FloatingPanelLayout {
let position: FloatingPanelPosition = .top
let initialState: FloatingPanelState = .tip
var anchors: [FloatingPanelState : FloatingPanelLayoutAnchoring] {
return [
...
]
}
func backdropAlpha(for state: FloatingPanelState) -> CGFloat {
return 0.3
}
...
}
```
### New `FloatingPanelLayoutAnchoring` classes
The following objects adopting `FloatingPanelLayoutAnchoring` protocol are added to configure the flexible and explicit layout.
#### `FloatingPanelLayoutAnchor`
This class is used to specify a panel layout using insets from a rectangle area of the superview or safe area.
* `FloatingPanelFullScreenLayout` is replaced with anchors using `.superview` reference guide.
* `FloatingPanelLayoutAnchor(fractionalInset:edge:referenceGuide:)` lets you lay out a panel at a relative position in a reference rectangle area.
```swift
// Before:
class MyPanelLayout: FloatingPanelLayout {
var initialPosition: FloatingPanelPosition {
return .half
}
func insetFor(position: FloatingPanelPosition) -> CGFloat? {
switch position {
case .full: return 16.0
case .half: return 262.0
case .tip: return 44.0
case .hidden: return nil
}
}
}
// After:
class MyPanelLayout: 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(fractionalInset: 0.5, edge: .bottom, referenceGuide: .superview),
.tip: FloatingPanelLayoutAnchor(absoluteInset: 44.0, edge: .bottom, referenceGuide: .safeArea)
]
}
}
```
#### `FloatingPanelIntrinsicLayoutAnchor`
This class is used to specify a panel layout using offsets from the intrinsic size layout.
* This replaces `FloatingPanelIntrinsicLayout`.
* This is also able to configure a fractional layout in the intrinsic size.
```swift
// Before:
class MyPanelIntrinsicLayout: FloatingPanelIntrinsicLayout {
var initialPosition: FloatingPanelPosition {
return .half
}
func insetFor(position: FloatingPanelPosition) -> CGFloat? {
switch position {
case .full: return 16.0
case .half: return 262.0
case .tip: return 44.0
case .hidden: return nil
}
}
}
// After:
class MyPanelIntrinsicLayout: FloatingPanelLayout {
var position: FloatingPanelPosition = .bottom
var initialState: FloatingPanelState { .full }
var anchors: [FloatingPanelState : FloatingPanelLayoutAnchoring] {
return [
.full: FloatingPanelIntrinsicLayoutAnchor(absoluteOffset: 16.0, referenceGuide: .safeArea),
.half: FloatingPanelIntrinsicLayoutAnchor(fractionalOffset: 0.5, referenceGuide: .safeArea),
.tip: FloatingPanelLayoutAnchor(absoluteInset: 44.0, edge: .bottom, referenceGuide: .safeArea)
]
}
}
```
### Deprecated APIs
* `.topInteractionBuffer` and `.bottomInteractionBuffer`.
* Please control the max/min range of the motion in `floatingPanelDidMove(_:)` delegate method as below.
```swift
func floatingPanelDidMove(_ fpc: FloatingPanelController) {
if fpc.isAttracting == false {
let loc = fpc.surfaceLocation
let minY = fpc.surfaceLocation(for: .full).y - 6.0
let maxY = fpc.surfaceLocation(for: .tip).y + 6.0
fpc.surfaceLocation = CGPoint(x: loc.x, y: min(max(loc.y, minY), maxY))
}
}
```
## `FloatingPanelBehavior`
* `.springDecelerationRate` and `.springResponseTime` properties are added to control the new spring effect of Numeric springing.
### Deprecated APIs
* `addAnimator(_:to:)`, `removeAnimator(_:from:)`
* They are moved into `floatingPanel(_:animatorForPresentingTo:)` and `floatingPanel(_:animatorForDismissingWith:)` of `FloatingPanelControllerDelegate` because they are used for view transitions.
* `interactionAnimator(_:to:with:)`, `moveAnimator(_:from:to:)`
* They are removed because the animators are replaced with the new spring effect.
* `removalVelocity`, `removalProgress`
* They are replaced with `floatingPanel(_:shouldRemoveAt:with:)` of `FloatingPanelControllerDelegate`
* `removalInteractionAnimator(_:with:)`
* It is integrated with `floatingPanel(_:animatorForDismissingWith:)` of `FloatingPanelControllerDelegate`.
## `SurfaceView`
* `SurfaceAppearance` class and `SurfaceView.appearance` property are added to specify the rounding corners, shadows and background color.
* `SurfaceView.appearance` property avoids `Ambiguous use of 'cornerRadius'` error, for instance.
* `SurfaceAppearance` enables to apply layered box shadows into a surface to materialize it.
```swift
// Before:
fpc.surfaceView.cornerRadius = 6.0
fpc.surfaceView.backgroundColor = .clear
fpc.surfaceView.shadowHidden = false
fpc.surfaceView.shadowColor = .black
fpc.surfaceView.shadowOffset = CGSize(width: 0, height: 16)
fpc.surfaceView.shadowRadius = 16.0
// After:
let appearance = SurfaceAppearance()
appearance.cornerRadius = 8.0
appearance.backgroundColor = .clear
let shadow = SurfaceAppearance.Shadow()
shadow.color = .black
shadow.offset = CGSize(width: 0, height: 16)
shadow.radius = 16
shadow.spread = 8
appearance.shadows = [shadow]
fpc.surfaceView.appearance = appearance
```
* These properties are changed for the top, left and right positioned panel.
* `grabberTopPadding` is now `grabberHandlePadding`.
* `topGrabberBarHeight` is now `grabberAreaOffset`.
* `grabberHandleWidth` and `grabberHandleHeight` are replaced with `grabberHandleSize`.
## `BackdropView`
* The dismissal action of the backdrop is disabled by default.
* You can enable it to set `BackdropView.dismissalTapGestureRecognizer.isEnabled` to `true`.
## `FloatingPanelPanGestureRecognizer`
* `delegateProxy` property is added to intercept the gesture recognizer delegate.
```swift
func layoutPanelForPad() {
fpc.behavior = SearchPaneliPadBehavior()
fpc.panGestureRecognizer.delegateProxy = self
}
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRequireFailureOf otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return false
}
```
## Miscellaneous
* `UISpringTimingParameters(decelerationRate:frequencyResponse:initialVelocity:)` initializer is added.
* The directory structure and file names in the Xcode project changes.
@@ -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>
@@ -25,9 +25,10 @@
<rect key="frame" x="0.0" y="0.0" width="375" height="44"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
</view>
<blurEffect style="light"/>
<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"/>
@@ -230,9 +230,10 @@
<constraint firstItem="Zcj-SE-gb8" firstAttribute="leading" secondItem="ED1-gT-FBj" secondAttribute="leading" id="wMb-L2-Z0W"/>
</constraints>
</view>
<blurEffect style="extraLight"/>
<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:
+20 -9
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)
UIView.animate(withDuration: 0.25) { [weak self] in
self?.fpc.move(to: .half, animated: false)
}
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,10 +1,12 @@
<?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="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="16087"/>
<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>
@@ -44,7 +46,7 @@
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Title" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="M0G-C8-hAO">
<rect key="frame" x="15" y="0.0" width="345" height="43.666667938232422"/>
<rect key="frame" x="16" y="0.0" width="343" height="43.666667938232422"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/>
@@ -56,6 +58,7 @@
</prototypes>
</tableView>
</subviews>
<viewLayoutGuide key="safeArea" id="39L-Nq-qfp"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstItem="7IS-PU-x0P" firstAttribute="top" secondItem="Smh-Bd-AAc" secondAttribute="top" id="6yd-jv-ey3"/>
@@ -63,7 +66,6 @@
<constraint firstItem="7IS-PU-x0P" firstAttribute="bottom" secondItem="Smh-Bd-AAc" secondAttribute="bottom" id="fNW-DP-lhV"/>
<constraint firstItem="7IS-PU-x0P" firstAttribute="trailing" secondItem="39L-Nq-qfp" secondAttribute="trailing" id="vfY-Rc-FOI"/>
</constraints>
<viewLayoutGuide key="safeArea" id="39L-Nq-qfp"/>
</view>
<navigationItem key="navigationItem" title="Samples" id="wCF-su-7up">
<barButtonItem key="rightBarButtonItem" title="Settings" id="rbH-U3-XyA">
@@ -88,66 +90,76 @@
<rect key="frame" x="0.0" y="0.0" width="375" height="197.33333333333334"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" distribution="fillProportionally" alignment="center" spacing="16" translatesAutoresizingMaskIntoConstraints="NO" id="n93-ZL-fmC">
<rect key="frame" x="32" y="16" width="311" height="181.33333333333334"/>
<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"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Version: 1.0" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="WmC-Tq-NDN">
<rect key="frame" x="118.33333333333334" y="0.0" width="74.333333333333343" height="21"/>
<fontDescription key="fontDescription" type="system" pointSize="14"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="UINavigationBar" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Ulg-gS-ah0">
<rect key="frame" x="90.666666666666686" y="37" width="130" height="25"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleHeadline"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<stackView opaque="NO" contentMode="scaleToFill" alignment="center" spacing="126" translatesAutoresizingMaskIntoConstraints="NO" id="uEf-g4-CeU">
<rect key="frame" x="23.333333333333343" y="78" width="264.66666666666663" height="38"/>
<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"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Large Titles" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="ogl-S5-4tJ">
<rect key="frame" x="0.0" y="8.9999999999999982" width="89.666666666666671" height="20.333333333333329"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<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"/>
<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"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleTitle2"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<switch opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" contentHorizontalAlignment="center" contentVerticalAlignment="center" translatesAutoresizingMaskIntoConstraints="NO" id="js8-Qv-lUC">
<rect key="frame" x="215.66666666666666" y="3.6666666666666714" width="50.999999999999972" height="31"/>
<connections>
<action selector="toggleLargeTitle:" destination="C1X-9Z-TyQ" eventType="valueChanged" id="FJS-Ty-mCY"/>
</connections>
</switch>
</subviews>
</stackView>
<stackView opaque="NO" contentMode="scaleToFill" spacing="126" translatesAutoresizingMaskIntoConstraints="NO" id="ZtZ-Dz-4cC">
<rect key="frame" x="23.333333333333343" y="132" width="264.66666666666663" height="49.333333333333343"/>
<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"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Translucent" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Z5i-rm-QgL">
<rect key="frame" x="0.0" y="0.0" width="89.666666666666671" height="49.333333333333336"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<switch opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" contentHorizontalAlignment="center" contentVerticalAlignment="center" translatesAutoresizingMaskIntoConstraints="NO" id="s6b-j9-8Kw">
<rect key="frame" x="215.66666666666666" y="0.0" width="50.999999999999972" height="49.333333333333336"/>
<connections>
<action selector="toggleTranslucent:" destination="C1X-9Z-TyQ" eventType="valueChanged" id="nL4-3L-9hh"/>
</connections>
</switch>
<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"/>
<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"/>
<connections>
<action selector="toggleLargeTitle:" destination="C1X-9Z-TyQ" eventType="valueChanged" id="FJS-Ty-mCY"/>
</connections>
</switch>
</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"/>
<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"/>
<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"/>
<connections>
<action selector="toggleTranslucent:" destination="C1X-9Z-TyQ" eventType="valueChanged" id="nL4-3L-9hh"/>
</connections>
</switch>
</subviews>
</stackView>
</subviews>
</stackView>
</subviews>
</stackView>
</subviews>
<viewLayoutGuide key="safeArea" id="0hr-ty-yWm"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstItem="0hr-ty-yWm" firstAttribute="bottom" secondItem="n93-ZL-fmC" secondAttribute="bottom" id="2Ey-ou-E1M"/>
<constraint firstAttribute="bottom" secondItem="n93-ZL-fmC" secondAttribute="bottom" constant="32" id="2Ey-ou-E1M"/>
<constraint firstAttribute="trailing" secondItem="n93-ZL-fmC" secondAttribute="trailing" constant="32" id="DdZ-eB-F5s"/>
<constraint firstItem="n93-ZL-fmC" firstAttribute="leading" secondItem="af9-Zr-Ppc" secondAttribute="leading" constant="32" id="TyK-GP-Ari"/>
<constraint firstItem="n93-ZL-fmC" firstAttribute="top" secondItem="af9-Zr-Ppc" secondAttribute="topMargin" constant="16" id="mbC-6H-z9M"/>
</constraints>
<viewLayoutGuide key="safeArea" id="0hr-ty-yWm"/>
</view>
<size key="freeformSize" width="375" height="197.33000000000001"/>
<connections>
@@ -168,7 +180,7 @@
<rect key="frame" x="0.0" y="0.0" width="375" height="812"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="IvG-yp-yzI">
<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"/>
<state key="normal" title="Close"/>
<connections>
@@ -177,13 +189,13 @@
</connections>
</button>
</subviews>
<viewLayoutGuide key="safeArea" id="954-Dk-zvc"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstItem="IvG-yp-yzI" firstAttribute="top" secondItem="954-Dk-zvc" secondAttribute="top" id="18k-sV-PgT"/>
<constraint firstItem="954-Dk-zvc" firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="IvG-yp-yzI" secondAttribute="trailing" id="mpr-u5-MZu"/>
<constraint firstItem="IvG-yp-yzI" firstAttribute="leading" secondItem="954-Dk-zvc" secondAttribute="leading" constant="20" id="pYt-jE-CTF"/>
</constraints>
<viewLayoutGuide key="safeArea" id="954-Dk-zvc"/>
</view>
<tabBarItem key="tabBarItem" tag="1" title="Layout 2" id="qb3-RB-B28"/>
</viewController>
@@ -199,7 +211,7 @@
<rect key="frame" x="0.0" y="0.0" width="375" height="812"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="NbG-e8-HdI">
<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"/>
<state key="normal" title="Close"/>
<connections>
@@ -208,13 +220,13 @@
</connections>
</button>
</subviews>
<viewLayoutGuide key="safeArea" id="0ao-SI-QZW"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstItem="0ao-SI-QZW" firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="NbG-e8-HdI" secondAttribute="trailing" id="K9F-6x-KWn"/>
<constraint firstItem="NbG-e8-HdI" firstAttribute="top" secondItem="0ao-SI-QZW" secondAttribute="top" id="nsE-so-rTl"/>
<constraint firstItem="NbG-e8-HdI" firstAttribute="leading" secondItem="0ao-SI-QZW" secondAttribute="leading" constant="20" id="sF4-Dm-aoY"/>
</constraints>
<viewLayoutGuide key="safeArea" id="0ao-SI-QZW"/>
</view>
<tabBarItem key="tabBarItem" tag="2" title="Layout 3" id="RJD-TF-Sdh"/>
</viewController>
@@ -230,7 +242,7 @@
<rect key="frame" x="0.0" y="0.0" width="375" height="812"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="eFN-tN-4Ct">
<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"/>
<state key="normal" title="Close"/>
<connections>
@@ -239,13 +251,13 @@
</connections>
</button>
</subviews>
<viewLayoutGuide key="safeArea" id="5Ns-4l-Ufg"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstItem="eFN-tN-4Ct" firstAttribute="leading" secondItem="5Ns-4l-Ufg" secondAttribute="leading" constant="20" id="5BT-yZ-EKe"/>
<constraint firstItem="5Ns-4l-Ufg" firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="eFN-tN-4Ct" secondAttribute="trailing" id="OzZ-Dz-RNF"/>
<constraint firstItem="eFN-tN-4Ct" firstAttribute="top" secondItem="5Ns-4l-Ufg" secondAttribute="top" id="hUV-3a-XkY"/>
</constraints>
<viewLayoutGuide key="safeArea" id="5Ns-4l-Ufg"/>
</view>
<tabBarItem key="tabBarItem" title="Layout 1" id="HEV-kf-jxH"/>
</viewController>
@@ -262,20 +274,19 @@
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="1000" verticalHuggingPriority="1000" horizontalCompressionResistancePriority="1000" verticalCompressionResistancePriority="1000" text="Change this text" textAlignment="center" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="ge4-RW-Gmz">
<rect key="frame" x="24" y="24" width="327" height="20.333333333333329"/>
<rect key="frame" x="125.66666666666669" y="24" width="124" height="20.333333333333329"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
</subviews>
<viewLayoutGuide key="safeArea" id="ouu-g9-OiX"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstAttribute="trailing" secondItem="ge4-RW-Gmz" secondAttribute="trailing" constant="24" id="V59-MD-Lcg"/>
<constraint firstItem="ge4-RW-Gmz" firstAttribute="leading" secondItem="eLM-xc-d9e" secondAttribute="leading" constant="24" id="hAO-P0-7Kw"/>
<constraint firstItem="ge4-RW-Gmz" firstAttribute="top" secondItem="eLM-xc-d9e" secondAttribute="top" constant="24" id="j0s-fd-MYj"/>
<constraint firstAttribute="bottom" relation="greaterThanOrEqual" secondItem="ge4-RW-Gmz" secondAttribute="bottom" constant="24" id="tEn-PO-nVD"/>
<constraint firstItem="ge4-RW-Gmz" firstAttribute="centerX" secondItem="eLM-xc-d9e" secondAttribute="centerX" id="vh3-l7-uY8"/>
</constraints>
<viewLayoutGuide key="safeArea" id="ouu-g9-OiX"/>
</view>
<size key="freeformSize" width="375" height="778"/>
</viewController>
@@ -283,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>
@@ -314,7 +390,7 @@
<rect key="frame" x="0.0" y="724" width="375" height="0.0"/>
<color key="backgroundColor" white="0.66666666666666663" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</view>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="sbF-Az-7sy">
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="sbF-Az-7sy">
<rect key="frame" x="20" y="0.0" width="39" height="30"/>
<state key="normal" title="Close"/>
<connections>
@@ -324,35 +400,35 @@
<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"/>
<subviews>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="i9x-x5-n1q">
<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"/>
<state key="normal" title="Move to full"/>
<connections>
<action selector="moveToFullWithSender:" destination="bYI-y3-Rzb" eventType="touchUpInside" id="TDe-3J-gIR"/>
</connections>
</button>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="2u5-cH-RAN">
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="2u5-cH-RAN">
<rect key="frame" x="0.0" y="74" width="85" height="30"/>
<state key="normal" title="Move to half"/>
<connections>
<action selector="moveToHalfWithSender:" destination="bYI-y3-Rzb" eventType="touchUpInside" id="12s-o7-Et5"/>
</connections>
</button>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="M4A-iO-RIE">
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="M4A-iO-RIE">
<rect key="frame" x="0.0" y="148" width="77" height="30"/>
<state key="normal" title="Move to tip"/>
<connections>
<action selector="moveToTipWithSender:" destination="bYI-y3-Rzb" eventType="touchUpInside" id="BmL-91-9ai"/>
</connections>
</button>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="swr-XM-GzZ">
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="swr-XM-GzZ">
<rect key="frame" x="0.0" y="222" width="106" height="30"/>
<state key="normal" title="Move to hidden"/>
<connections>
<action selector="moveToHiddenWithSender:" destination="bYI-y3-Rzb" eventType="touchUpInside" id="jfJ-0f-fdk"/>
</connections>
</button>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="szf-HE-QTk">
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="szf-HE-QTk">
<rect key="frame" x="0.0" y="296" width="96" height="30"/>
<state key="normal" title="Update layout"/>
<connections>
@@ -362,6 +438,7 @@
</subviews>
</stackView>
</subviews>
<viewLayoutGuide key="safeArea" id="kjr-TP-fcM"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstItem="sbF-Az-7sy" firstAttribute="top" secondItem="kjr-TP-fcM" secondAttribute="top" id="3VR-hj-zeQ"/>
@@ -373,7 +450,6 @@
<constraint firstItem="9p4-06-y2T" firstAttribute="centerX" secondItem="kjr-TP-fcM" secondAttribute="centerX" id="l8t-p3-ETf"/>
<constraint firstItem="vut-mK-Y4t" firstAttribute="top" secondItem="kjr-TP-fcM" secondAttribute="bottom" id="rMy-JT-t4B"/>
</constraints>
<viewLayoutGuide key="safeArea" id="kjr-TP-fcM"/>
</view>
<size key="freeformSize" width="375" height="778"/>
<connections>
@@ -466,6 +542,7 @@
</constraints>
</scrollView>
</subviews>
<viewLayoutGuide key="safeArea" id="ufS-Rf-F2F"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<gestureRecognizers/>
<constraints>
@@ -474,7 +551,6 @@
<constraint firstAttribute="bottom" secondItem="sBe-tN-uMi" secondAttribute="bottom" id="jzB-47-P7e"/>
<constraint firstItem="ufS-Rf-F2F" firstAttribute="trailing" secondItem="sBe-tN-uMi" secondAttribute="trailing" id="nHG-wg-pLP"/>
</constraints>
<viewLayoutGuide key="safeArea" id="ufS-Rf-F2F"/>
<connections>
<outletCollection property="gestureRecognizers" destination="tOa-bf-zGz" appends="YES" id="zle-Sz-M3U"/>
<outletCollection property="gestureRecognizers" destination="SCk-hG-weZ" appends="YES" id="OcK-FK-Lac"/>
@@ -525,7 +601,7 @@
<rect key="frame" x="0.0" y="44" width="375" height="734"/>
<color key="backgroundColor" red="0.0078431372550000003" green="0.72156862749999995" blue="0.45882352939999999" alpha="1" colorSpace="calibratedRGB"/>
</view>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="noi-1a-5bZ" customClass="CloseButton" customModule="Samples" customModuleProvider="target">
<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"/>
<constraints>
<constraint firstAttribute="height" constant="44" id="0jg-5D-A1F"/>
@@ -536,16 +612,16 @@
</connections>
</button>
<stackView opaque="NO" contentMode="scaleToFill" distribution="fillProportionally" alignment="center" spacing="8" translatesAutoresizingMaskIntoConstraints="NO" id="qux-uG-4o2">
<rect key="frame" x="8" y="52" width="148.33333333333334" height="31"/>
<rect key="frame" x="8" y="52" 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.333333333333329" height="20.333333333333332"/>
<rect key="frame" x="0.0" y="5.3333333333333357" width="91" height="20.333333333333332"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<switch opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" contentHorizontalAlignment="center" contentVerticalAlignment="center" on="YES" translatesAutoresizingMaskIntoConstraints="NO" id="0MA-lV-KjS">
<rect key="frame" x="99.333333333333329" y="0.0" width="50.999999999999986" height="31"/>
<rect key="frame" x="99" y="0.0" width="51" height="31"/>
<connections>
<action selector="modeChanged:" destination="YC8-ae-15L" eventType="valueChanged" id="IQ8-u2-Rib"/>
</connections>
@@ -555,14 +631,14 @@
<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"/>
<subviews>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="c5r-jU-haj">
<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"/>
<state key="normal" title="Show"/>
<connections>
<action selector="buttonPressed:" destination="YC8-ae-15L" eventType="touchUpInside" id="Mi1-o6-TWt"/>
</connections>
</button>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="wmd-ab-Nz3">
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="wmd-ab-Nz3">
<rect key="frame" x="0.0" y="52" width="114" height="30"/>
<state key="normal" title="Present Modallly"/>
<connections>
@@ -570,7 +646,7 @@
<segue destination="bYI-y3-Rzb" kind="presentation" identifier="PresentModallySegue" id="3yq-HE-Tgn"/>
</connections>
</button>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="01L-lp-oy6">
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="01L-lp-oy6">
<rect key="frame" x="0.0" y="104" width="114" height="30"/>
<state key="normal" title="Update Layout"/>
<connections>
@@ -580,6 +656,7 @@
</subviews>
</stackView>
</subviews>
<viewLayoutGuide key="safeArea" id="aOK-7l-cA6"/>
<color key="backgroundColor" red="1" green="0.83234566450000003" blue="0.47320586440000001" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<gestureRecognizers/>
<constraints>
@@ -598,7 +675,6 @@
<constraint firstItem="8yw-OC-Ubk" firstAttribute="top" relation="greaterThanOrEqual" secondItem="tP3-oJ-4EB" secondAttribute="bottom" constant="88" id="vKQ-h9-uKt"/>
<constraint firstItem="qux-uG-4o2" firstAttribute="leading" secondItem="g7l-kO-y7q" secondAttribute="leading" constant="8" id="zXb-R9-bMO"/>
</constraints>
<viewLayoutGuide key="safeArea" id="aOK-7l-cA6"/>
<connections>
<outletCollection property="gestureRecognizers" destination="6Ca-p8-7uF" appends="YES" id="xOy-f1-NZE"/>
<outletCollection property="gestureRecognizers" destination="SPY-Vr-XDT" appends="YES" id="vgS-Am-jhQ"/>
@@ -686,6 +762,7 @@ Section 1.10.33 of "de Finibus Bonorum et Malorum", written by Cicero in 45 BC
<textInputTraits key="textInputTraits" autocapitalizationType="sentences"/>
</textView>
</subviews>
<viewLayoutGuide key="safeArea" id="5ET-zC-lCb"/>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstItem="rN1-HL-YHv" firstAttribute="leading" secondItem="5ET-zC-lCb" secondAttribute="leading" id="7V3-KL-vXd"/>
@@ -693,7 +770,6 @@ Section 1.10.33 of "de Finibus Bonorum et Malorum", written by Cicero in 45 BC
<constraint firstItem="rN1-HL-YHv" firstAttribute="top" secondItem="9YG-0j-Zzg" secondAttribute="top" constant="17" id="fiO-LL-nSC"/>
<constraint firstItem="rN1-HL-YHv" firstAttribute="trailing" secondItem="5ET-zC-lCb" secondAttribute="trailing" id="lfg-EE-euw"/>
</constraints>
<viewLayoutGuide key="safeArea" id="5ET-zC-lCb"/>
</view>
<size key="freeformSize" width="375" height="778"/>
<connections>
@@ -707,7 +783,27 @@ 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>
@@ -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
}
}
}
+115 -101
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!
@@ -142,6 +78,8 @@ class SampleListViewController: UIViewController {
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(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 = self
@@ -151,12 +89,10 @@ class SampleListViewController: UIViewController {
}
case .showRemovablePanel, .showIntrinsicView:
mainPanelVC.isRemovalInteractionEnabled = true
let backdropTapGesture = UITapGestureRecognizer(target: self, action: #selector(handleBackdrop(tapGesture:)))
mainPanelVC.backdropView.addGestureRecognizer(backdropTapGesture)
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
}
@@ -208,18 +155,6 @@ class SampleListViewController: UIViewController {
}
}
@objc func handleBackdrop(tapGesture: UITapGestureRecognizer) {
switch tapGesture.view {
case mainPanelVC.backdropView:
mainPanelVC.hide(animated: true, completion: nil)
case settingsPanelVC.backdropView:
settingsPanelVC.removePanelFromParent(animated: true)
settingsPanelVC = nil
default:
break
}
}
// MARK:- Actions
@IBAction func showDebugMenu(_ sender: UIBarButtonItem) {
guard settingsPanelVC == nil else { return }
@@ -231,10 +166,7 @@ class SampleListViewController: UIViewController {
settingsPanelVC.surfaceView.appearance = appearance
settingsPanelVC.isRemovalInteractionEnabled = true
let backdropTapGesture = UITapGestureRecognizer(target: self, action: #selector(handleBackdrop(tapGesture:)))
settingsPanelVC.backdropView.addGestureRecognizer(backdropTapGesture)
settingsPanelVC.backdropView.dismissalTapGestureRecognizer.isEnabled = true
settingsPanelVC.delegate = self
let contentVC = storyboard?.instantiateViewController(withIdentifier: "SettingsViewController")
@@ -251,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"
@@ -274,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() }
@@ -302,7 +234,7 @@ extension SampleListViewController: UITableViewDelegate {
detailPanelVC.set(contentViewController: contentVC)
detailPanelVC.contentMode = .fitToBounds
(contentVC as? DetailViewController)?.intrinsicHeightConstraint.priority = .defaultLow
(contentVC as? DetailViewController)?.intrinsicHeightConstraint.isActive = false
// Add FloatingPanel to self.view
detailPanelVC.addPanel(toParent: self, animated: true)
@@ -353,6 +285,7 @@ extension SampleListViewController: UITableViewDelegate {
let appearance = SurfaceAppearance()
appearance.cornerRadius = 38.5
fpc.surfaceView.appearance = appearance
fpc.backdropView.dismissalTapGestureRecognizer.isEnabled = true
fpc.isRemovalInteractionEnabled = true
@@ -433,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:
@@ -446,6 +379,8 @@ extension SampleListViewController: FloatingPanelControllerDelegate {
fallthrough
case .showContentInset:
return FloatingPanelBottomLayout()
case .showCustomStatePanel:
return FloatingPanelLayoutWithCustomState()
default:
return (newCollection.verticalSizeClass == .compact) ? FloatingPanelBottomLayout() : self
}
@@ -480,7 +415,7 @@ extension SampleListViewController: UIGestureRecognizerDelegate {
/**
- Attention: `FloatingPanelLayout` must not be applied by the parent view
controller of a floating panel. But here `SampleListViewController` adopts it
controller of a panel. But here `SampleListViewController` adopts it
purposely to check if the library prints an appropriate warning.
*/
extension SampleListViewController: FloatingPanelLayout {
@@ -520,7 +455,7 @@ extension SampleListViewController: UIPageViewControllerDelegate {
}
}
class BottomEdgeInteractionLayout: FloatingPanelLayout {
class TopPositionedPanelLayout: FloatingPanelLayout {
let position: FloatingPanelPosition = .top
let initialState: FloatingPanelState = .full
@@ -553,7 +488,7 @@ class RemovablePanelLayout: FloatingPanelLayout {
]
}
func backdropAlphaFor(position: FloatingPanelState) -> CGFloat {
func backdropAlpha(for state: FloatingPanelState) -> CGFloat {
return 0.3
}
}
@@ -569,7 +504,7 @@ class RemovablePanelLandscapeLayout: FloatingPanelLayout {
]
}
func backdropAlphaFor(position: FloatingPanelState) -> CGFloat {
func backdropAlpha(for state: FloatingPanelState) -> CGFloat {
return 0.3
}
}
@@ -584,7 +519,7 @@ class ModalPanelLayout: FloatingPanelLayout {
]
}
func backdropAlphaFor(position: FloatingPanelState) -> CGFloat {
func backdropAlpha(for state: FloatingPanelState) -> CGFloat {
return 0.3
}
}
@@ -700,6 +635,8 @@ class DebugTableViewController: InspectableViewController {
case animateScroll = "Animate Scroll"
case changeContentSize = "Change content size"
case reorder = "Reorder"
case moveToFull = "Move to Full"
case moveToHalf = "Move to Half"
}
var reorderButton: UIButton!
@@ -741,6 +678,10 @@ class DebugTableViewController: InspectableViewController {
case .reorder:
button.addTarget(self, action: #selector(reorderItems), for: .touchUpInside)
reorderButton = button
case .moveToFull:
button.addTarget(self, action: #selector(moveToFull), for: .touchUpInside)
case .moveToHalf:
button.addTarget(self, action: #selector(moveToHalf), for: .touchUpInside)
}
buttonStackView.addArrangedSubview(button)
}
@@ -801,6 +742,14 @@ class DebugTableViewController: InspectableViewController {
tableView.reloadData()
}
@objc func moveToFull() {
(self.parent as! FloatingPanelController).move(to: .full, animated: true)
}
@objc func moveToHalf() {
(self.parent as! FloatingPanelController).move(to: .half, animated: true)
}
@objc func close(sender: UIButton) {
// Remove FloatingPanel from a view
(self.parent as! FloatingPanelController).removePanelFromParent(animated: true, completion: nil)
@@ -1188,9 +1137,10 @@ class ThreeTabBarPanelLayout: FloatingPanelLayout {
]
}
func backdropAlphaFor(position: FloatingPanelState) -> CGFloat {
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)
@@ -1297,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],
};
}
+2 -2
View File
@@ -1,7 +1,7 @@
Pod::Spec.new do |s|
s.name = "FloatingPanel"
s.version = "2.0.0-beta.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.
@@ -14,7 +14,7 @@ The new interface displays the related contents and utilities in parallel as a u
s.platform = :ios, "10.0"
s.source = { :git => "https://github.com/SCENEE/FloatingPanel.git", :tag => s.version.to_s }
s.source_files = "Sources/*.swift"
s.swift_versions = ['5.1', '5.2']
s.swift_versions = ['5.1', '5.2', '5.3']
s.framework = "UIKit"
+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 */,
+3 -3
View File
@@ -10,9 +10,6 @@
<Group
location = "group:Examples"
name = "Examples">
<FileRef
location = "group:SamplesObjC/SamplesObjC.xcodeproj">
</FileRef>
<FileRef
location = "group:Maps/Maps.xcodeproj">
</FileRef>
@@ -22,5 +19,8 @@
<FileRef
location = "group:Samples/Samples.xcodeproj">
</FileRef>
<FileRef
location = "group:SamplesObjC/SamplesObjC.xcodeproj">
</FileRef>
</Group>
</Workspace>
+2 -2
View File
@@ -1,6 +1,6 @@
MIT License
Copyright (c) 2018 Shin Yamamoto
Copyright (c) 2018-Present Shin Yamamoto
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
@@ -18,4 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
SOFTWARE.
+81 -22
View File
@@ -21,7 +21,7 @@ The new interface displays the related contents and utilities in parallel as a u
- [Installation](#installation)
- [CocoaPods](#cocoapods)
- [Carthage](#carthage)
- [Swift Package Manager with Xcode](#swift-package-manager-with-xcode)
- [Swift Package Manager](#swift-package-manager)
- [Getting Started](#getting-started)
- [Add a floating panel as a child view controller](#add-a-floating-panel-as-a-child-view-controller)
- [Present a floating panel as a modality](#present-a-floating-panel-as-a-modality)
@@ -30,11 +30,13 @@ The new interface displays the related contents and utilities in parallel as a u
- [Show/Hide a floating panel in a view with your view hierarchy](#showhide-a-floating-panel-in-a-view-with-your-view-hierarchy)
- [Scale the content view when the surface position changes](#scale-the-content-view-when-the-surface-position-changes)
- [Customize the layout with `FloatingPanelLayout` protocol](#customize-the-layout-with-floatingpanellayout-protocol)
- [Change the initial layout.](#change-the-initial-layout)
- [Change the initial layout](#change-the-initial-layout)
- [Update your panel layout](#update-your-panel-layout)
- [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
@@ -81,12 +83,16 @@ Examples are here.
- [Examples/Maps](https://github.com/SCENEE/FloatingPanel/tree/master/Examples/Maps) like Apple Maps.app.
- [Examples/Stocks](https://github.com/SCENEE/FloatingPanel/tree/master/Examples/Stocks) like Apple Stocks.app.
- [Examples/Samples](https://github.com/SCENEE/FloatingPanel/tree/master/Examples/Samples)
- [Examples/SamplesObjC](https://github.com/SCENEE/FloatingPanel/tree/master/Examples/SamplesObjC)
## Requirements
FloatingPanel is written in Swift 5.0+. Compatible with iOS 10.0+.
FloatingPanel is written in Swift 5.0+. Compatible with iOS 11.0+.
:pencil2: You would like to use Swift 4.0. Please use FloatingPanel v1.
The deployment is still iOS 10, but it is recommended to use this library on iOS 11+.
:pencil2: You would like to use Swift 4.0. Please use FloatingPanel v1.
## Installation
@@ -109,7 +115,7 @@ For [Carthage](https://github.com/Carthage/Carthage), add the following to your
github "scenee/FloatingPanel"
```
### Swift Package Manager with Xcode
### Swift Package Manager
Follow [this doc](https://developer.apple.com/documentation/swift_packages/adding_package_dependencies_to_your_app).
@@ -159,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
@@ -247,7 +253,7 @@ Otherwise, `FloatingPanelController` fixes the content by the height of the top
### Customize the layout with `FloatingPanelLayout` protocol
#### Change the initial layout.
#### Change the initial layout
```swift
class ViewController: UIViewController, FloatingPanelControllerDelegate {
@@ -258,8 +264,8 @@ class ViewController: UIViewController, FloatingPanelControllerDelegate {
}
class MyFloatingPanelLayout: FloatingPanelLayout {
let position = .bottom
let initialState = .tip
let position: FloatingPanelPosition = .bottom
let initialState: FloatingPanelState = .tip
var anchors: [FloatingPanelState: FloatingPanelLayoutAnchoring] {
return [
.full: FloatingPanelLayoutAnchor(absoluteInset: 16.0, edge: .top, referenceGuide: .safeArea),
@@ -272,7 +278,7 @@ class MyFloatingPanelLayout: FloatingPanelLayout {
### Update your panel layout
There are 2 ways to update the panel layout.
There are 2 ways to update the panel layout.
1. Manually set `FloatingPanelController.layout` to the new layout object directly.
@@ -281,7 +287,7 @@ fpc.layout = MyPanelLayout()
fpc.invalidateLayout() // If needed
```
2. Returns an appropriate layout object in `floatingPanel(_:layoutFor:)` delegate.
2. Returns an appropriate layout object in one of 2 `floatingPanel(_:layoutFor:)` delegates.
```swift
class ViewController: UIViewController, FloatingPanelControllerDelegate {
@@ -289,6 +295,11 @@ class ViewController: UIViewController, FloatingPanelControllerDelegate {
func floatingPanel(_ vc: FloatingPanelController, layoutFor newCollection: UITraitCollection) -> FloatingPanelLayout {
return MyFloatingPanelLayout()
}
// OR
func floatingPanel(_ vc: FloatingPanelController, layoutFor size: CGSize) -> FloatingPanelLayout {
return MyFloatingPanelLayout()
}
}
```
@@ -303,8 +314,8 @@ class ViewController: UIViewController, FloatingPanelControllerDelegate {
}
class LandscapePanelLayout: FloatingPanelLayout {
let position = .bottom
let initialState = .tip
let position: FloatingPanelPosition = .bottom
let initialState: FloatingPanelState = .tip
var anchors: [FloatingPanelState: FloatingPanelLayoutAnchoring] {
return [
.full: FloatingPanelLayoutAnchor(absoluteInset: 16.0, edge: .top, referenceGuide: .safeArea),
@@ -327,19 +338,19 @@ class LandscapePanelLayout: FloatingPanelLayout {
```swift
class IntrinsicPanelLayout: FloatingPanelLayout {
let position = .bottom
let initialState = .full
let position: FloatingPanelPosition = .bottom
let initialState: FloatingPanelState = .full
var anchors: [FloatingPanelState: FloatingPanelLayoutAnchoring] {
return [
.full: FloatingPanelIntrinsicLayoutAnchor(absoluteInset: 0, referenceGuide: .safeArea),
.half: FloatingPanelIntrinsicLayoutAnchor(fractionalInset: 0.5, referenceGuide: .safeArea),
.full: FloatingPanelIntrinsicLayoutAnchor(absoluteOffset: 0, referenceGuide: .safeArea),
.half: FloatingPanelIntrinsicLayoutAnchor(fractionalOffset: 0.5, referenceGuide: .safeArea),
]
}
...
}
```
✏️ `FloatingPanelIntrinsicLayout` is deprecated on v1.
:pencil2: `FloatingPanelIntrinsicLayout` is deprecated on v1.
#### Specify an anchor for each state by an inset of the `FloatingPanelController.view` frame
@@ -359,7 +370,47 @@ class MyFullScreenLayout: FloatingPanelLayout {
}
```
✏️ `FloatingPanelFullScreenLayout` is deprecated on v1.
: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
@@ -383,7 +434,7 @@ class CustomPanelBehavior: FloatingPanelBehavior {
}
```
✏️ `floatingPanel(_ vc:behaviorFor:)` is deprecated on v1.
:pencil2: `floatingPanel(_ vc:behaviorFor:)` is deprecated on v1.
#### Activate the rubber-band effect on panel edges
@@ -521,7 +572,7 @@ func floatingPanelDidChangePosition(_ vc: FloatingPanelController) {
#### Interrupt the delegate methods of `FloatingPanelController.panGestureRecognizer`
`FloatingPanelPanGestureRecognizer.delegateProxy` is able to override the delegate methods by your own delegate object.
If you are set `FloatingPanelController.panGestureRecognizer.delegateProxy` to an object adopting `UIGestureRecognizerDelegate`, it overrides delegate methods of the pan gesture recognizer.
```swift
class MyGestureRecognizerDelegate: UIGestureRecognizerDelegate {
@@ -579,6 +630,14 @@ func searchBarTextDidBeginEditing(_ searchBar: UISearchBar) {
}
```
You can also use a view animation to move a panel.
```swift
UIView.animate(withDuration: 0.25) {
self.fpc.move(to: .half, animated: false)
}
```
### Work your contents together with a floating panel behavior
```swift
+4 -2
View File
@@ -1,9 +1,11 @@
// Copyright 2018 the FloatingPanel authors. All rights reserved. MIT license.
// Copyright 2018-Present Shin Yamamoto. All rights reserved. MIT license.
import UIKit
/// A view that presents a backdrop interface behind a floating panel.
/// A view that presents a backdrop interface behind a panel.
@objc(FloatingPanelBackdropView)
public class BackdropView: UIView {
/// The gesture recognizer for tap gestures to dismiss a panel.
public var dismissalTapGestureRecognizer: UITapGestureRecognizer!
}
+28 -23
View File
@@ -1,7 +1,8 @@
// Copyright 2018 the FloatingPanel authors. All rights reserved. MIT license.
// Copyright 2018-Present Shin Yamamoto. All rights reserved. MIT license.
import UIKit
/// An interface for generating behavior information to fine-tune the behavior of a panel.
@objc
public protocol FloatingPanelBehavior {
/// A floating-point value that determines the rate of oscillation magnitude reduction after the user lifts their finger.
@@ -24,7 +25,7 @@ public protocol FloatingPanelBehavior {
@objc optional
var momentumProjectionRate: CGFloat { get }
/// Asks the behavior if the floating panel should project a momentum of a user interaction to move the proposed position.
/// Asks the behavior if a panel should project a momentum of a user interaction to move the proposed position.
///
/// The default implementation of this method returns true. This method is called for a layout to support all positions(tip, half and full).
/// Therefore, `proposedTargetPosition` can only be `FloatingPanelState.tip` or `FloatingPanelState.full`.
@@ -33,50 +34,50 @@ public protocol FloatingPanelBehavior {
/// Returns the progress to redirect to the previous position.
///
/// The progress is represented by a floating-point value between 0.0 and 1.0, inclusive, where 1.0 indicates the floating panel is impossible to move to the next position. The default value is 0.5. Values less than 0.0 and greater than 1.0 are pinned to those limits.
/// The progress is represented by a floating-point value between 0.0 and 1.0, inclusive, where 1.0 indicates a panel is impossible to move to the next position. The default value is 0.5. Values less than 0.0 and greater than 1.0 are pinned to those limits.
@objc optional
func redirectionalProgress(_ fpc: FloatingPanelController, from: FloatingPanelState, to: FloatingPanelState) -> CGFloat
/// Asks the behavior whether the rubber band effect is enabled in moving over a given edge of the surface view.
///
/// This method allows the behavior to activate the rubber band effect to a given edge of the surface view. By default, the effect is disabled.
/// 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 }
}
class FloatingPanelDefaultBehavior: FloatingPanelBehavior {
var springDecelerationRate: CGFloat {
/// 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.
open class FloatingPanelDefaultBehavior: FloatingPanelBehavior {
public init() {}
open var springDecelerationRate: CGFloat {
return UIScrollView.DecelerationRate.fast.rawValue + 0.001
}
var springResponseTime: CGFloat {
open var springResponseTime: CGFloat {
return 0.4
}
var momentumProjectionRate: CGFloat {
open var momentumProjectionRate: CGFloat {
return UIScrollView.DecelerationRate.normal.rawValue
}
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))
}
func allowsRubberBanding(for edge: UIRectEdge) -> Bool {
open func allowsRubberBanding(for edge: UIRectEdge) -> Bool {
return false
}
open var removalInteractionVelocityThreshold: CGFloat = 5.5
}
class BehaviorAdapter {
@@ -99,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)
+84 -40
View File
@@ -1,30 +1,33 @@
// Copyright 2018 the FloatingPanel authors. All rights reserved. MIT license.
// Copyright 2018-Present Shin Yamamoto. All rights reserved. MIT license.
import UIKit
@objc public protocol FloatingPanelControllerDelegate: class {
// Returns a FloatingPanelLayout object. If you use the default one, you can use a `FloatingPanelDefaultLayout` object.
/// 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 {
/// 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
// Returns a FloatingPanelLayout object. If you use the default one, you can use a `FloatingPanelDefaultLayout` object.
/// Returns a FloatingPanelLayout object. If you use the default one, you can use a `FloatingPanelBottomLayout` object.
@objc(floatingPanel:layoutForSize:) optional
func floatingPanel(_ fpc: FloatingPanelController, layoutFor size: CGSize) -> FloatingPanelLayout
/// Returns a UIViewPropertyAnimator object to add/present a floating panel to a position.
/// Returns a UIViewPropertyAnimator object to add/present the panel to a position.
///
/// Default is the spring animator with 0.25 sec duration and `UIScrollView.DecelerationRate.fast` deceleration rate.
/// Default is the spring animation with 0.25 secs.
@objc(floatingPanel:animatorForPresentingToState:) optional
func floatingPanel(_ fpc: FloatingPanelController, animatorForPresentingTo state: FloatingPanelState) -> UIViewPropertyAnimator
/// Returns a UIViewPropertyAnimator object to remove/dismiss a floating panel from a position.
/// Returns a UIViewPropertyAnimator object to remove/dismiss a panel from a position.
///
/// Default is the spring animator with 0.25 sec duration and `UIScrollView.DecelerationRate.fast` deceleration rate.
/// Default is the spring animator with 0.25 secs.
@objc(floatingPanel:animatorForDismissingWithVelocity:) optional
func floatingPanel(_ fpc: FloatingPanelController, animatorForDismissingWith velocity: CGVector) -> UIViewPropertyAnimator
/// Called when the floating panel has changed to a new position. Can be called inside an animation block, so any
/// view properties set inside this function will be automatically animated alongside the panel.
/// 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.
@objc optional
func floatingPanelDidChangePosition(_ fpc: FloatingPanelController)
@@ -32,45 +35,50 @@ import UIKit
@objc optional
func floatingPanelShouldBeginDragging(_ fpc: FloatingPanelController) -> Bool
/// Called when the user drags the surface or the surface is attracted to a state anchor.
@objc optional
func floatingPanelDidMove(_ fpc: FloatingPanelController) // any surface frame changes in dragging
// called on start of dragging (may require some time and or distance to move)
/// Called on start of dragging (may require some time and or distance to move)
@objc optional
func floatingPanelWillBeginDragging(_ fpc: FloatingPanelController)
// called on finger up if the user dragged. velocity is in points/second.
/// Called on finger up if the user dragged. velocity is in points/second.
@objc optional
func floatingPanelWillEndDragging(_ fpc: FloatingPanelController, withVelocity velocity: CGPoint, targetState: UnsafeMutablePointer<FloatingPanelState>)
// called on finger up if the user dragged. decelerate is true if it will continue moving afterwards
/// Called on finger up if the user dragged.
///
/// If `attract` is true, it will continue moving afterwards to a nearby state anchor.
@objc optional
func floatingPanelDidEndDragging(_ fpc: FloatingPanelController, willDecelerate decelerate: Bool)
func floatingPanelDidEndDragging(_ fpc: FloatingPanelController, willAttract attract: Bool)
/// Called when it is about to be attracted to a state anchor.
@objc optional
func floatingPanelWillBeginAttracting(_ fpc: FloatingPanelController, to state: FloatingPanelState) // called on finger up as a panel are moving
/// Called when attracting it is completed.
@objc optional
func floatingPanelDidEndAttracting(_ fpc: FloatingPanelController) // called when a panel stops
/// Asks the delegate whether the panel should be removed when dragging ended at the specified location
/// Asks the delegate whether a panel should be removed when dragging ended at the specified location
///
/// This delegate method is called only when `FloatingPanelController.isRemovalInteractionEnabled` is set to true.
/// This delegate method is called only where `FloatingPanelController.isRemovalInteractionEnabled` is `true`.
/// The velocity vector is calculated from the distance to a point of the hidden state and the pan gesture's velocity.
@objc(floatingPanel:shouldRemoveAtLocation:withVelocity:)
optional
func floatingPanel(_ fpc: FloatingPanelController, shouldRemoveAt location: CGPoint, with velocity: CGVector) -> Bool
// called on start to remove its view controller from the parent view controller.
/// Called on start to remove its view controller from the parent view controller.
@objc(floatingPanelWillRemove:)
optional
func floatingPanelWillRemove(_ fpc: FloatingPanelController)
// called when its view controller are removed from the parent view controller.
/// Called when a panel is removed from the parent view controller.
@objc optional
func floatingPanelDidRemove(_ fpc: FloatingPanelController)
/// Asks the delegate for a content offset of the tracked scroll view to be pinned when a floating panel moves
/// Asks the delegate for a content offset of the tracking scroll view to be pinned when a panel moves
///
/// If you do not implement this method, the controller uses a value of the content offset plus the content insets
/// of the tracked scroll view. Your implementation of this method can return a value for a navigation bar with a large
@@ -83,7 +91,7 @@ import UIKit
}
///
/// A container view controller to display a floating panel to present contents in parallel as a user wants.
/// A container view controller to display a panel to present contents in parallel as a user wants.
///
@objc
open class FloatingPanelController: UIViewController {
@@ -103,7 +111,7 @@ open class FloatingPanelController: UIViewController {
case fitToBounds
}
/// The delegate of the floating panel controller object.
/// The delegate of a panel controller object.
@objc
public weak var delegate: FloatingPanelControllerDelegate?{
didSet{
@@ -135,12 +143,13 @@ open class FloatingPanelController: UIViewController {
return floatingPanel.panGestureRecognizer
}
/// The current position of the floating panel controller's contents.
/// The current position of a panel controller's contents.
@objc
public var state: FloatingPanelState {
return floatingPanel.state
}
/// A Boolean value indicating whether a panel controller is attracting the surface to a state anchor.
@objc
public var isAttracting: Bool {
return floatingPanel.isAttracting
@@ -189,7 +198,7 @@ open class FloatingPanelController: UIViewController {
@objc(isRemovalInteractionEnabled) get { return floatingPanel.isRemovalInteractionEnabled }
}
/// The view controller responsible for the content portion of the floating panel.
/// The view controller responsible for the content portion of a panel.
@objc
public var contentViewController: UIViewController? {
set { set(contentViewController: newValue) }
@@ -202,6 +211,7 @@ open class FloatingPanelController: UIViewController {
return floatingPanel.targetPosition(from: currentY, with: .zero)
}
/// Constants that define how a panel content fills in the surface.
@objc
public var contentMode: ContentMode = .static {
didSet {
@@ -227,7 +237,7 @@ open class FloatingPanelController: UIViewController {
setUp()
}
/// Initialize a newly created floating panel controller.
/// Initialize a newly created panel controller.
@objc
public init(delegate: FloatingPanelControllerDelegate? = nil) {
super.init(nibName: nil, bundle: nil)
@@ -264,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
@@ -279,7 +289,7 @@ open class FloatingPanelController: UIViewController {
open override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
if #available(iOS 11.0, *) {
// Ensure the panel's static constraint after rotating a device in static mode
// Ensure to update the static constraint of a panel after rotating a device in static mode
if contentMode == .static {
floatingPanel.layoutAdapter.updateStaticConstraint()
}
@@ -289,7 +299,6 @@ open class FloatingPanelController: UIViewController {
self.update(safeAreaInsets: fp_safeAreaInsets)
}
}
floatingPanel.layoutAdapter.checkLayout()
}
open override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
@@ -431,7 +440,7 @@ open class FloatingPanelController: UIViewController {
// 2. The safe area top inset can be variable on the large title navigation bar(iOS11+).
// That's why it needs the observation to keep `adjustedContentInsets` correct.
safeAreaInsetsObservation = self.view.observe(\.safeAreaInsets, options: [.initial, .new, .old]) { [weak self] (_, change) in
// Use `self.view.safeAreaInsets` becauese `change.newValue` can be nil in particular case when
// Use `self.view.safeAreaInsets` because `change.newValue` can be nil in particular case when
// is reported in https://github.com/SCENEE/FloatingPanel/issues/330
guard let self = self, change.oldValue != self.view.safeAreaInsets else { return }
self.update(safeAreaInsets: self.view.safeAreaInsets)
@@ -468,8 +477,8 @@ open class FloatingPanelController: UIViewController {
assert((parent is UINavigationController) == false, "UINavigationController displays only one child view controller at a time.")
assert((parent is UITabBarController) == false, "UITabBarController displays child view controllers with a radio-style selection interface")
assert((parent is UISplitViewController) == false, "UISplitViewController manages two child view controllers in a master-detail interface")
assert((parent is UITableViewController) == false, "UITableViewController should not be the parent because the view is a table view so that a floating panel doens't work well")
assert((parent is UICollectionViewController) == false, "UICollectionViewController should not be the parent because the view is a collection view so that a floating panel doens't work well")
assert((parent is UITableViewController) == false, "UITableViewController should not be the parent because the view is a table view so that a panel doesn't work well")
assert((parent is UICollectionViewController) == false, "UICollectionViewController should not be the parent because the view is a collection view so that a panel doesn't work well")
if viewIndex < 0 {
parent.view.addSubview(self.view)
@@ -489,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)
}
}
@@ -508,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)
@@ -528,11 +537,10 @@ 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)
}
/// Sets the view controller responsible for the content portion of the floating panel.
/// Sets the view controller responsible for the content portion of a panel.
public func set(contentViewController: UIViewController?) {
if let vc = _contentViewController {
vc.willMove(toParent: nil)
@@ -580,7 +588,7 @@ open class FloatingPanelController: UIViewController {
}
}
/// Cancel tracking the specifiy scroll view.
/// Cancel tracking the specify scroll view.
///
@objc(untrackScrollView:)
public func untrack(scrollView: UIScrollView) {
@@ -589,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
@@ -596,19 +612,28 @@ open class FloatingPanelController: UIViewController {
///
/// This method updates the `FloatingPanelLayout` object from the delegate and
/// then it calls `layoutIfNeeded()` of the root view to force the view
/// to update the floating panel's layout immediately. It can be called in an
/// to update the layout immediately. It can be called in an
/// animation block.
@objc
public func invalidateLayout() {
activateLayout(forceLayout: true)
}
/// Returns the y-coordinate of the point at the origin of the surface view.
/// Returns the surface's position in a panel controller's view for the specified state.
///
/// If a panel is top positioned, this returns a point of the bottom-left corner of the surface. If it is left positioned
/// this returns a point of top-right corner of the surface. If it is bottom or right positioned, this returns a point
/// of the top-left corner of the surface.
@objc
public func surfaceLocation(for state: FloatingPanelState) -> CGPoint {
return floatingPanel.layoutAdapter.surfaceLocation(for: state)
}
/// The surface's position in a panel controller's view.
///
/// If a panel is top positioned, this returns a point of the bottom-left corner of the surface. If it is left positioned
/// this returns a point of top-right corner of the surface. If it is bottom or right positioned, this returns a point
/// of the top-left corner of the surface.
@objc
public var surfaceLocation: CGPoint {
get { floatingPanel.layoutAdapter.surfaceLocation }
@@ -623,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:))) {
@@ -635,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
}()
}
+124 -76
View File
@@ -1,13 +1,12 @@
// Copyright 2018 the FloatingPanel authors. All rights reserved. MIT license.
// Copyright 2018-Present Shin Yamamoto. All rights reserved. MIT license.
import UIKit
///
/// FloatingPanel presentation model
/// The presentation model of FloatingPanel
///
class Core: NSObject, UIGestureRecognizerDelegate {
// MUST be a weak reference to prevent UI freeze on the presentation modally
weak var viewcontroller: FloatingPanelController?
private weak var ownerVC: FloatingPanelController?
let surfaceView: SurfaceView
let backdropView: BackdropView
@@ -24,7 +23,7 @@ class Core: NSObject, UIGestureRecognizerDelegate {
private(set) var state: FloatingPanelState = .hidden {
didSet {
log.debug("state changed: \(oldValue) -> \(state)")
if let vc = viewcontroller {
if let vc = ownerVC {
vc.delegate?.floatingPanelDidChangePosition?(vc)
}
}
@@ -33,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
@@ -51,7 +51,7 @@ class Core: NSObject, UIGestureRecognizerDelegate {
// Scroll handling
private var initialScrollOffset: CGPoint = .zero
private var stopScrollDeceleration: Bool = false
private var scrollBouncable = false
private var scrollBounce = false
private var scrollIndictorVisible = false
private var grabberAreaFrame: CGRect {
return surfaceView.grabberAreaFrame
@@ -60,7 +60,7 @@ class Core: NSObject, UIGestureRecognizerDelegate {
// MARK: - Interface
init(_ vc: FloatingPanelController, layout: FloatingPanelLayout, behavior: FloatingPanelBehavior) {
viewcontroller = vc
ownerVC = vc
surfaceView = SurfaceView()
surfaceView.backgroundColor = .white
@@ -69,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()
@@ -95,13 +92,18 @@ 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)
}
private func move(from: FloatingPanelState, to: FloatingPanelState, animated: Bool, completion: (() -> Void)? = nil) {
assert(layoutAdapter.validStates.contains(to), "Can't move to '\(to)' state because it's not valid in the layout")
guard let vc = viewcontroller else {
guard let vc = ownerVC else {
completion?()
return
}
@@ -113,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 {
@@ -124,34 +127,48 @@ 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?()
}
return
}
let shouldDoubleLayout = from == .hidden
&& surfaceView.hasStackView()
&& 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)
if shouldDoubleLayout {
log.info("Lay out the surface again to modify an intrinsic size error according to UIStackView")
self.updateLayout(to: to)
}
}
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.viewcontroller?.notifyDidMove()
self.ownerVC?.notifyDidMove()
completion?()
}
self.animator = animator
self.transitionAnimator = animator
if isSuspended {
return
}
animator.startAnimation()
} else {
self.state = to
@@ -162,7 +179,7 @@ class Core: NSObject, UIGestureRecognizerDelegate {
self.lockScrollView()
}
viewcontroller?.notifyDidMove()
ownerVC?.notifyDidMove()
completion?()
}
}
@@ -177,7 +194,7 @@ class Core: NSObject, UIGestureRecognizerDelegate {
/* log.debug("currentY: \(currentY) translation: \(translation)") */
let forwardY = (translation >= 0)
let segment = layoutAdapter.segument(at: cur, forward: forwardY)
let segment = layoutAdapter.segment(at: cur, forward: forwardY)
let lowerState = segment.lower ?? layoutAdapter.edgeMostState
let upperState = segment.upper ?? layoutAdapter.edgeLeastState
@@ -212,7 +229,7 @@ class Core: NSObject, UIGestureRecognizerDelegate {
switch otherGestureRecognizer {
case is FloatingPanelPanGestureRecognizer:
// All visiable panels' pan gesture should be recognized simultaneously.
// All visible panels' pan gesture should be recognized simultaneously.
return true
case is UIPanGestureRecognizer,
is UISwipeGestureRecognizer,
@@ -228,7 +245,7 @@ class Core: NSObject, UIGestureRecognizerDelegate {
default:
// Should recognize tap/long press gestures in parallel when the surface view is at an anchor position.
let adapterY = layoutAdapter.position(for: state)
return abs(value(of: layoutAdapter.surfaceLocation) - adapterY) < (1.0 / surfaceView.traitCollection.displayScale)
return abs(value(of: layoutAdapter.surfaceLocation) - adapterY) < (1.0 / surfaceView.fp_displayScale)
}
}
@@ -238,7 +255,7 @@ class Core: NSObject, UIGestureRecognizerDelegate {
}
if otherGestureRecognizer is FloatingPanelPanGestureRecognizer {
// If this panel is the farthest descendant of visiable panels,
// If this panel is the farthest descendant of visible panels,
// its ancestors' pan gesture must wait for its pan gesture to fail
if let view = otherGestureRecognizer.view, surfaceView.isDescendant(of: view) {
return true
@@ -290,7 +307,7 @@ class Core: NSObject, UIGestureRecognizerDelegate {
switch otherGestureRecognizer {
case is FloatingPanelPanGestureRecognizer:
// If this panel is the farthest descendant of visiable panels,
// If this panel is the farthest descendant of visible panels,
// its pan gesture does not require its ancestors' pan gesture to fail
if let view = otherGestureRecognizer.view, surfaceView.isDescendant(of: view) {
return false
@@ -321,7 +338,7 @@ class Core: NSObject, UIGestureRecognizerDelegate {
@objc func handleBackdrop(tapGesture: UITapGestureRecognizer) {
removalVector = .zero
viewcontroller?.remove()
ownerVC?.remove()
}
@objc func handle(panGesture: UIPanGestureRecognizer) {
@@ -332,7 +349,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.traitCollection.displayScale)
let belowEdgeMost = 0 > layoutAdapter.offsetFromEdgeMost + (1.0 / surfaceView.fp_displayScale)
log.debug("""
scroll gesture(\(state):\(panGesture.state)) -- \
@@ -364,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 {
@@ -438,7 +455,7 @@ class Core: NSObject, UIGestureRecognizerDelegate {
""")
if interactionInProgress == false, isAttracting == false,
let vc = viewcontroller, vc.delegate?.floatingPanelShouldBeginDragging?(vc) == false {
let vc = ownerVC, vc.delegate?.floatingPanelShouldBeginDragging?(vc) == false {
return
}
@@ -484,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 {
@@ -526,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
}
@@ -562,6 +584,9 @@ class Core: NSObject, UIGestureRecognizerDelegate {
if scrollView.isDecelerating {
return true
}
if let tableView = (scrollView as? UITableView), tableView.isEditing {
return true
}
return false
}
@@ -599,7 +624,7 @@ class Core: NSObject, UIGestureRecognizerDelegate {
guard (pre != cur) else { return }
if let vc = viewcontroller {
if let vc = ownerVC {
vc.delegate?.floatingPanelDidMove?(vc)
}
}
@@ -640,10 +665,11 @@ class Core: NSObject, UIGestureRecognizerDelegate {
return
}
stopScrollDeceleration = (0 > layoutAdapter.offsetFromEdgeMost + (1.0 / surfaceView.traitCollection.displayScale)) // Projecting the dragging to the scroll dragging or not
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)
}
}
@@ -662,19 +688,19 @@ class Core: NSObject, UIGestureRecognizerDelegate {
case .left, .right:
removalVector = (distToHidden != 0) ? CGVector(dx: velocity.x/distToHidden, dy: 0.0) : .zero
}
if shoulRemove(with: removalVector) {
viewcontroller?.remove()
if shouldRemove(with: removalVector) {
ownerVC?.remove()
return
}
}
if let vc = viewcontroller {
if let vc = ownerVC {
vc.delegate?.floatingPanelWillEndDragging?(vc, withVelocity: velocity, targetState: &targetPosition)
}
guard shouldDecelerate(to: targetPosition) else {
if let vc = viewcontroller {
vc.delegate?.floatingPanelDidEndDragging?(vc, willDecelerate: false)
guard shouldAttract(to: targetPosition) else {
if let vc = ownerVC {
vc.delegate?.floatingPanelDidEndDragging?(vc, willAttract: false)
}
self.state = targetPosition
@@ -683,20 +709,20 @@ class Core: NSObject, UIGestureRecognizerDelegate {
return
}
if let vc = viewcontroller {
vc.delegate?.floatingPanelDidEndDragging?(vc, willDecelerate: true)
if let vc = ownerVC {
vc.delegate?.floatingPanelDidEndDragging?(vc, willAttract: true)
}
// 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
}
startAttration(to: targetPosition, with: velocity)
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
}
@@ -704,20 +730,21 @@ class Core: NSObject, UIGestureRecognizerDelegate {
// MARK: - Behavior
private func shoulRemove(with velocityVector: CGVector) -> Bool {
guard let vc = viewcontroller else { return false }
private func shouldRemove(with velocityVector: CGVector) -> Bool {
guard let vc = ownerVC else { return false }
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)
}
}
@@ -753,7 +780,7 @@ class Core: NSObject, UIGestureRecognizerDelegate {
initialTranslation = translation
if let vc = viewcontroller {
if let vc = ownerVC {
vc.delegate?.floatingPanelWillBeginDragging?(vc)
}
@@ -787,16 +814,16 @@ class Core: NSObject, UIGestureRecognizerDelegate {
panGestureRecognizer.isEnabled = true
}
private func shouldDecelerate(to targetState: FloatingPanelState) -> Bool {
private func shouldAttract(to targetState: FloatingPanelState) -> Bool {
if layoutAdapter.position(for: targetState) == value(of: layoutAdapter.surfaceLocation) {
return false
}
return true
}
private func startAttration(to targetPosition: FloatingPanelState, with velocity: CGPoint) {
private func startAttraction(to targetPosition: FloatingPanelState, with velocity: CGPoint) {
log.debug("startAnimation to \(targetPosition) -- velocity = \(value(of: velocity))")
guard let vc = viewcontroller else { return }
guard let vc = ownerVC else { return }
isAttracting = true
vc.delegate?.floatingPanelWillBeginAttracting?(vc, to: targetPosition)
@@ -811,19 +838,22 @@ class Core: NSObject, UIGestureRecognizerDelegate {
moveAnimator = NumericSpringAnimator(
initialData: initialData,
target: target,
displayScale: surfaceView.traitCollection.displayScale,
displayScale: surfaceView.fp_displayScale,
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.viewcontroller?.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()
})
@@ -835,7 +865,7 @@ class Core: NSObject, UIGestureRecognizerDelegate {
self.isAttracting = false
self.moveAnimator = nil
if let vc = viewcontroller {
if let vc = ownerVC {
vc.delegate?.floatingPanelDidEndAttracting?(vc)
}
@@ -892,7 +922,7 @@ class Core: NSObject, UIGestureRecognizerDelegate {
let distance = (currentY - layoutAdapter.position(for: state))
let forwardY = velocity == 0 ? distance > 0 : velocity > 0
let segment = layoutAdapter.segument(at: pY, forward: forwardY)
let segment = layoutAdapter.segment(at: pY, forward: forwardY)
var fromPos: FloatingPanelState
var toPos: FloatingPanelState
@@ -902,7 +932,7 @@ class Core: NSObject, UIGestureRecognizerDelegate {
if behaviorAdapter.shouldProjectMomentum(to: toPos) == false {
log.debug("targetPosition -- negate projection: distance = \(distance)")
let segment = layoutAdapter.segument(at: currentY, forward: forwardY)
let segment = layoutAdapter.segment(at: currentY, forward: forwardY)
var (lowerPos, upperPos) = (segment.lower ?? sortedPositions.first!, segment.upper ?? sortedPositions.last!)
// Equate the segment out of {top,bottom} most state to the {top,bottom} most segment
if lowerPos == upperPos {
@@ -939,7 +969,7 @@ class Core: NSObject, UIGestureRecognizerDelegate {
}
log.debug("lock scroll view")
scrollBouncable = scrollView.bounces
scrollBounce = scrollView.bounces
scrollIndictorVisible = scrollView.showsVerticalScrollIndicator
scrollView.isDirectionalLockEnabled = true
@@ -952,7 +982,7 @@ class Core: NSObject, UIGestureRecognizerDelegate {
log.debug("unlock scroll view")
scrollView.isDirectionalLockEnabled = false
scrollView.bounces = scrollBouncable
scrollView.bounces = scrollBounce
scrollView.showsVerticalScrollIndicator = scrollIndictorVisible
}
@@ -965,7 +995,7 @@ class Core: NSObject, UIGestureRecognizerDelegate {
}
private func contentOffsetForPinning(of scrollView: UIScrollView) -> CGPoint {
if let vc = viewcontroller, let origin = vc.delegate?.floatingPanel?(vc, contentOffsetForPinning: scrollView) {
if let vc = ownerVC, let origin = vc.delegate?.floatingPanel?(vc, contentOffsetForPinning: scrollView) {
return origin
}
switch layoutAdapter.position {
@@ -1005,6 +1035,7 @@ class Core: NSObject, UIGestureRecognizerDelegate {
}
}
/// A gesture recognizer that looks for panning (dragging) gestures in a panel.
public final class FloatingPanelPanGestureRecognizer: UIPanGestureRecognizer {
fileprivate weak var floatingPanel: Core?
fileprivate var initialLocation: CGPoint = .zero
@@ -1012,10 +1043,14 @@ 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
}
}
/// The delegate of the gesture recognizer.
///
/// - Note: The delegate is used by FloatingPanel itself. If you set your own delegate object, an
/// exception is raised. If you want to handle the methods of UIGestureRecognizerDelegate, you can use `delegateProxy`.
public override weak var delegate: UIGestureRecognizerDelegate? {
get {
return super.delegate
@@ -1031,6 +1066,10 @@ public final class FloatingPanelPanGestureRecognizer: UIPanGestureRecognizer {
super.delegate = newValue
}
}
/// An object to intercept the delegate of the gesture recognizer.
///
/// If an object adopting `UIGestureRecognizerDelegate` is set, the delegate methods are proxied to it.
public weak var delegateProxy: UIGestureRecognizerDelegate? {
didSet {
self.delegate = floatingPanel // Update the cached IMP
@@ -1040,7 +1079,7 @@ public final class FloatingPanelPanGestureRecognizer: UIPanGestureRecognizer {
// MARK: - Animator
class NumericSpringAnimator: NSObject {
private class NumericSpringAnimator: NSObject {
struct Data {
let value: CGFloat
let velocity: CGFloat
@@ -1165,3 +1204,12 @@ 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 -1
View File
@@ -1,7 +1,8 @@
// Copyright 2018 the FloatingPanel authors. All rights reserved. MIT license.
// Copyright 2018-Present Shin Yamamoto. All rights reserved. MIT license.
import UIKit
/// A view that presents a grabber handle in the surface of a panel.
@objc(FloatingPanelGrabberView)
public class GrabberView: UIView {
+1 -1
View File
@@ -15,7 +15,7 @@
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>2.0.0-beta.1</string>
<string>2.3.0</string>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
</dict>
+118 -86
View File
@@ -1,29 +1,26 @@
// Copyright 2018 the FloatingPanel authors. All rights reserved. MIT license.
// Copyright 2018-Present Shin Yamamoto. All rights reserved. MIT license.
import UIKit
/// An interface for generating layout information for a panel.
@objc public protocol FloatingPanelLayout {
/// The position of the panel in the view of `FloatingPanelController`.
/// Returns the position of a panel in a `FloatingPanelController` view .
@objc var position: FloatingPanelPosition { get }
/// The initial state when the layout is applied.
/// Returns the initial state when a panel is presented.
@objc var initialState: FloatingPanelState { get }
/// The layout anchors to specify the snapping locations for each state.
/// Returns the layout anchors to specify the snapping locations for each state.
@objc var anchors: [FloatingPanelState: FloatingPanelLayoutAnchoring] { get }
/// Returns X-axis and width layout constraints of the surface view of a floating panel.
/// You must not include any Y-axis and height layout constraints of the surface view
/// because their constraints will be configured by the floating panel controller.
/// By default, the width of a surface view fits a safe area.
/// Returns layout constraints to determine the cross dimension of a panel.
@objc optional func prepareLayout(surfaceView: UIView, in view: UIView) -> [NSLayoutConstraint]
/// Returns a CGFloat value to determine the backdrop view's alpha for a state.
///
/// Default is 0.3 at full state, otherwise 0.0.
/// Returns the alpha value of the backdrop of a panel for each state.
@objc optional func backdropAlpha(for state: FloatingPanelState) -> CGFloat
}
/// A layout object that lays out a panel in bottom sheet style.
@objcMembers
open class FloatingPanelBottomLayout: NSObject, FloatingPanelLayout {
public override init() {
@@ -63,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 {
@@ -74,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?
@@ -223,8 +223,7 @@ class LayoutAdapter {
}
}
} else {
let displayScale = surfaceView.traitCollection.displayScale
pos = displayTrunc(edgePosition(surfaceView.frame), by: displayScale)
pos = displayTrunc(edgePosition(surfaceView.frame), by: surfaceView.fp_displayScale)
}
switch position {
case .top, .bottom:
@@ -288,18 +287,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.traitCollection.displayScale)
let pos = displayTrunc(position(for: state), by: surfaceView.fp_displayScale)
switch layout.position {
case .top, .bottom:
return CGPoint(x: 0.0, y: pos)
@@ -313,19 +307,39 @@ class LayoutAdapter {
let anchor = layout.anchors[state] ?? self.hiddenAnchor
switch anchor {
case let ianchor as FloatingPanelIntrinsicLayoutAnchor:
let surfaceIntrinsicLength = position.mainDimension(surfaceView.intrinsicContentSize)
let diff = ianchor.isAbsolute ? ianchor.offset : surfaceIntrinsicLength * ianchor.offset
case let anchor as FloatingPanelIntrinsicLayoutAnchor:
let intrinsicLength = position.mainDimension(surfaceView.intrinsicContentSize)
let diff = anchor.isAbsolute ? anchor.offset : intrinsicLength * anchor.offset
var referenceBoundsLength = position.mainDimension(bounds.size)
switch position {
case .top, .left:
return referenceBoundsLength - surfaceIntrinsicLength - diff
var base: CGFloat = 0.0
if anchor.referenceGuide == .safeArea {
base += position.inset(safeAreaInsets)
}
return base + intrinsicLength - diff
case .bottom, .right:
var base = position.mainDimension(bounds.size)
if anchor.referenceGuide == .safeArea {
base -= position.inset(safeAreaInsets)
}
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 - surfaceIntrinsicLength + diff
return referenceBoundsLength - dimension + diff
}
case let anchor as FloatingPanelLayoutAnchor:
let referenceBounds = anchor.referenceGuide == .safeArea ? bounds.inset(by: safeAreaInsets) : bounds
@@ -343,7 +357,11 @@ class LayoutAdapter {
default:
fatalError("Unsupported a FloatingPanelLayoutAnchoring object")
}
}
}
func isIntrinsicAnchor(state: FloatingPanelState) -> Bool {
return layout.anchors[state] is FloatingPanelIntrinsicLayoutAnchor
}
private func edgePosition(_ frame: CGRect) -> CGFloat {
switch position {
@@ -360,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
@@ -428,25 +447,17 @@ class LayoutAdapter {
fitToBoundsConstraint?.priority = .defaultHigh
}
NSLayoutConstraint.deactivate(fullConstraints + halfConstraints + tipConstraints + offConstraints)
updateStateConstraints()
}
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"
}
private func updateStateConstraints() {
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)
@@ -463,7 +474,7 @@ class LayoutAdapter {
tearDownAttraction()
NSLayoutConstraint.deactivate(fullConstraints + halfConstraints + tipConstraints + offConstraints)
NSLayoutConstraint.deactivate(stateConstraints.flatMap { $1 } + offConstraints)
initialConst = edgePosition(surfaceView.frame) + offset.y
@@ -479,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])
@@ -491,8 +502,8 @@ class LayoutAdapter {
// unsatisfiable constraints
if self.interactionConstraint == nil {
// Actiavate `interactiveTopConstraint` for `fitToBounds` mode.
// It goes throught this path when the pan gesture state jumps
// Activate `interactiveTopConstraint` for `fitToBounds` mode.
// It goes through this path when the pan gesture state jumps
// from .begin to .end.
startInteraction(at: state)
}
@@ -503,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
@@ -622,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
@@ -632,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))
}
}
@@ -671,16 +693,16 @@ class LayoutAdapter {
var const = initialConst + diff
let base = position.mainDimension(vc.view.bounds.size)
// Rubberbanding top buffer
// Rubber-banding top buffer
if allowsRubberBanding(.top), const < minConst {
let buffer = minConst - const
const = minConst - rubberbandEffect(for: buffer, base: base)
const = minConst - rubberBandEffect(for: buffer, base: base)
}
// Rubberbanding bottom buffer
// Rubber-banding bottom buffer
if allowsRubberBanding(.bottom), const > maxConst {
let buffer = const - maxConst
const = maxConst + rubberbandEffect(for: buffer, base: base)
const = maxConst + rubberBandEffect(for: buffer, base: base)
}
if overflow == false {
@@ -694,7 +716,7 @@ class LayoutAdapter {
// x = distance from the edge
// c = constant value, UIScrollView uses 0.55
// d = dimension, either width or height
private func rubberbandEffect(for buffer: CGFloat, base: CGFloat) -> CGFloat {
private func rubberBandEffect(for buffer: CGFloat, base: CGFloat) -> CGFloat {
return (1.0 - (1.0 / ((buffer * 0.55 / base) + 1.0))) * base
}
@@ -728,18 +750,21 @@ class LayoutAdapter {
state = layout.initialState
}
NSLayoutConstraint.deactivate(fullConstraints + halfConstraints + tipConstraints + offConstraints)
// Recalculate the intrinsic size of a content view. This is because
// UIView.systemLayoutSizeFitting() returns a different size between an
// 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,25 +787,27 @@ class LayoutAdapter {
return layout.backdropAlpha?(for: state) ?? defaultLayout.backdropAlpha(for: state)
}
func checkLayout() {
fileprivate func checkLayout() {
// Verify layout configurations
assert(activeStates.count > 0)
assert(validStates.contains(layout.initialState),
"Does not include an initial state (\(layout.initialState)) in (\(validStates))")
let statePosOrder = activeStates.sorted(by: { position(for: $0) < position(for: $1) })
assert(sortedDirectionalStates == statePosOrder,
/* This assertion does not work in a device rotating
let statePosOrder = activeStates.sorted(by: { position(for: $0) < position(for: $1) })
assert(sortedDirectionalStates == statePosOrder,
"Check your layout anchors because the state order(\(statePosOrder)) must be (\(sortedDirectionalStates))).")
*/
}
}
extension LayoutAdapter {
func segument(at pos: CGFloat, forward: Bool) -> LayoutSegment {
func segment(at pos: CGFloat, forward: Bool) -> LayoutSegment {
/// ----------------------->Y
/// --> forward <-- backward
/// |-------|===o===|-------| |-------|-------|===o===|
/// |-------|-------x=======| |-------|=======x-------|
/// |-------|-------|===o===| |-------|===o===|-------|
/// pos: o/x, seguement: =
/// pos: o/x, segment: =
let sortedStates = sortedDirectionalStates
@@ -804,7 +831,12 @@ extension LayoutAdapter {
extension FloatingPanelController {
var _layout: FloatingPanelLayout {
get { floatingPanel.layoutAdapter.layout }
set { floatingPanel.layoutAdapter.layout = newValue}
get {
floatingPanel.layoutAdapter.layout
}
set {
floatingPanel.layoutAdapter.layout = newValue
floatingPanel.layoutAdapter.checkLayout()
}
}
}
+88 -4
View File
@@ -1,19 +1,32 @@
// Copyright 2018 the FloatingPanel authors. All rights reserved. MIT license.
// Copyright 2018-Present Shin Yamamoto. All rights reserved. MIT license.
import UIKit
/// An interface for implementing custom layout anchor objects.
@objc public protocol FloatingPanelLayoutAnchoring {
var referenceGuide: FloatingPanelLayoutReferenceGuide { get }
func layoutConstraints(_ fpc: FloatingPanelController, for position: FloatingPanelPosition) -> [NSLayoutConstraint]
}
/// 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 */ {
/// Returns a layout anchor with the specified inset by an absolute value, edge and reference guide for a panel.
///
/// 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
}
/// Returns a layout anchor with the specified inset by a fractional value, edge and reference guide for a panel.
///
/// 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
@@ -22,8 +35,9 @@ import UIKit
}
let inset: CGFloat
let isAbsolute: Bool
/// The reference rectangle area for the inset.
@objc public let referenceGuide: FloatingPanelLayoutReferenceGuide
@objc public let referenceEdge: FloatingPanelReferenceEdge
@objc let referenceEdge: FloatingPanelReferenceEdge
}
public extension FloatingPanelLayoutAnchor {
@@ -80,14 +94,23 @@ public extension FloatingPanelLayoutAnchor {
}
}
/// An object that defines how to settles a panel with the intrinsic size for a content.
@objc final public class FloatingPanelIntrinsicLayoutAnchor: NSObject, FloatingPanelLayoutAnchoring /*, NSCopying */ {
/// Returns a layout anchor with the specified offset by an absolute value and reference guide for a panel.
///
/// 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
}
// offset = 0.0 -> All content visible
// offset = 1.0 -> All content invisible
/// 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.
@objc public init(fractionalOffset offset: CGFloat, referenceGuide: FloatingPanelLayoutReferenceGuide = .safeArea) {
self.offset = offset
self.referenceGuide = referenceGuide
@@ -95,6 +118,8 @@ public extension FloatingPanelLayoutAnchor {
}
let offset: CGFloat
let isAbsolute: Bool
/// The reference rectangle area for the offset
@objc public let referenceGuide: FloatingPanelLayoutReferenceGuide
}
@@ -115,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 -1
View File
@@ -1,7 +1,8 @@
// Copyright 2018 the FloatingPanel authors. All rights reserved. MIT license.
// Copyright 2018-Present Shin Yamamoto. All rights reserved. MIT license.
import UIKit
/// Constants that specify the edge of the container of a panel.
@objc public enum FloatingPanelReferenceEdge: Int {
case top
case left
@@ -26,6 +27,7 @@ extension FloatingPanelReferenceEdge {
}
}
/// Constants that specify a layout guide to lay out a panel.
@objc public enum FloatingPanelLayoutReferenceGuide: Int {
case superview = 0
case safeArea = 1
+3 -2
View File
@@ -1,8 +1,9 @@
// Copyright 2018 the FloatingPanel authors. All rights reserved. MIT license.
// Copyright 2018-Present Shin Yamamoto. All rights reserved. MIT license.
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 {
@@ -1,9 +1,9 @@
// Copyright 2018 the FloatingPanel authors. All rights reserved. MIT license.
// Copyright 2018-Present Shin Yamamoto. All rights reserved. MIT license.
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)
+2 -1
View File
@@ -1,7 +1,8 @@
// Copyright 2018 the FloatingPanel authors. All rights reserved. MIT license.
// Copyright 2018-Present Shin Yamamoto. All rights reserved. MIT license.
import UIKit
/// Constants describing the position of a panel in a screen
@objc public enum FloatingPanelPosition: Int {
case top
case left
+11 -3
View File
@@ -1,9 +1,10 @@
// Copyright 2018 the FloatingPanel authors. All rights reserved. MIT license.
// Copyright 2018-Present Shin Yamamoto. All rights reserved. MIT license.
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) {
@@ -12,13 +13,16 @@ public class FloatingPanelState: NSObject, NSCopying, RawRepresentable {
super.init()
}
@objc
public init(rawValue: RawValue, order: Int) {
self.rawValue = rawValue
self.order = order
super.init()
}
/// The corresponding value of the raw type.
public let rawValue: RawValue
/// The sorting order for states
public let order: Int
public func copy(with zone: NSZone? = nil) -> Any {
@@ -30,12 +34,16 @@ 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.
@objc(Full) public static let full: FloatingPanelState = FloatingPanelState(rawValue: "full", order: 1000)
/// A panel state indicates the half of a panel is shown.
@objc(Half) public static let half: FloatingPanelState = FloatingPanelState(rawValue: "half", order: 500)
/// A panel state indicates the tip of a panel is shown.
@objc(Tip) public static let tip: FloatingPanelState = FloatingPanelState(rawValue: "tip", order: 100)
/// A panel state indicates it is hidden.
@objc(Hidden) public static let hidden: FloatingPanelState = FloatingPanelState(rawValue: "hidden", order: 0)
}
+44 -31
View File
@@ -1,38 +1,41 @@
// Copyright 2018 the FloatingPanel authors. All rights reserved. MIT license.
// Copyright 2018-Present Shin Yamamoto. All rights reserved. MIT license.
import UIKit
/// An object for customizing the appearance of a surface view
@objc(FloatingPanelSurfaceAppearance)
@objcMembers
public class SurfaceAppearance: NSObject {
/// An object that represents information to render a shadow
@objc(FloatingPanelSurfaceAppearanceShadow)
public class Shadow: NSObject {
/// A Boolean indicating whether the surface shadow is displayed.
/// A Boolean indicating whether a shadow is displayed.
@objc
public var hidden: Bool = false
/// The color of the surface shadow.
/// The color of a shadow.
@objc
public var color: UIColor = .black
/// The offset (in points) of the surface shadow.
/// The offset (in points) of a shadow.
@objc
public var offset: CGSize = CGSize(width: 0.0, height: 1.0)
/// The opacity of the surface shadow.
/// The opacity of a shadow.
@objc
public var opacity: Float = 0.2
/// The blur radius (in points) used to render the surface shadow.
/// The blur radius (in points) used to render a shadow.
@objc
public var radius: CGFloat = 3
// The inflated amount of the surface shadow prior to applying the blur.
/// The inflated amount of a shadow prior to applying the blur.
@objc
public var spread: CGFloat = 0
}
/// The background color.
/// The background color of a surface view
public var backgroundColor: UIColor? = {
if #available(iOS 13, *) {
return UIColor.systemBackground
@@ -41,29 +44,35 @@ public class SurfaceAppearance: NSObject {
}
}()
/// The radius to use when drawing top rounded corners.
/// The radius to use when drawing the top rounded corners.
///
/// `self.contentView` is masked with the top rounded corners automatically on iOS 11 and later.
/// On iOS 10, they are not automatically masked because of a UIVisualEffectView issue. See https://forums.developer.apple.com/thread/50854
public var cornerRadius: CGFloat = 0.0
/// 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()]
/// The width of the surface border.
/// The border width of a surface view.
public var borderColor: UIColor?
/// The color of the surface border.
/// The border color of a surface view.
public var borderWidth: CGFloat = 0.0
}
/// A view that presents a surface interface in a floating panel.
/// A view that presents a surface interface in a panel.
@objc(FloatingPanelSurfaceView)
@objcMembers
public class SurfaceView: UIView {
/// A FloatingPanelGrabberView object displayed at the top of the surface view.
/// A `FloatingPanelGrabberView` object displayed at the top of the surface view.
///
/// To use a custom grabber handle, hide this and then add the custom one
/// to the surface view at appropriate coordinates.
/// To use a custom grabber, hide this and then add it to the surface view at appropriate point.
public let grabberHandle = GrabberView()
/// Offset of the grabber handle from the interactive edge.
@@ -71,18 +80,18 @@ public class SurfaceView: UIView {
setNeedsUpdateConstraints()
} }
/// The offset from the interactive edge which prevents the conetent scroll
/// The offset from the move edge to prevent the content scroll
public var grabberAreaOffset: CGFloat = 36.0
/// The grabber handle size
///
/// On left/right position, the width dimension is used as the height of `grabberHandle`, and vice versa.
/// On left/right positioned panel the width dimension is used as the height of `grabberHandle`, and vice versa.
public var grabberHandleSize: CGSize = CGSize(width: 36.0, height: 5.0) { didSet {
setNeedsUpdateConstraints()
} }
/// A root view of a content view controller
public weak var contentView: UIView!
/// The content view to be assigned a view of the content view controller of `FloatingPanelController`
public weak var contentView: UIView?
/// The content insets specifying the insets around the content view.
public var contentPadding: UIEdgeInsets = .zero {
@@ -97,6 +106,7 @@ public class SurfaceView: UIView {
set { appearance.backgroundColor = newValue; setNeedsLayout() }
}
/// The appearance settings for a surface view.
public var appearance = SurfaceAppearance() { didSet {
shadowLayers = appearance.shadows.map { _ in CAShapeLayer() }
setNeedsLayout()
@@ -107,7 +117,7 @@ public class SurfaceView: UIView {
setNeedsUpdateConstraints()
} }
/// The view presents an actual surface shape.
/// The view that displays an actual surface shape.
///
/// It renders the background color, border line and top rounded corners,
/// specified by other properties. The reason why they're not be applied to
@@ -118,7 +128,7 @@ public class SurfaceView: UIView {
var containerOverflow: CGFloat = 0.0 { // Must not call setNeedsLayout()
didSet {
// Calling setNeedsUpdateConstraints() is necessary to fix a layout break
// when the contentMode is changed after laying out a floating panel, for instance,
// when the contentMode is changed after laying out a panel, for instance,
// after calling viewDidAppear(_:) of the parent view controller.
setNeedsUpdateConstraints()
}
@@ -307,8 +317,8 @@ public class SurfaceView: UIView {
containerView.backgroundColor = appearance.backgroundColor
updateShadow()
updateCornerRadius()
updateShadow()
updateBorder()
grabberHandle.layer.cornerRadius = grabberHandleSize.height / 2
@@ -332,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
@@ -344,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
}
@@ -415,7 +424,7 @@ public class SurfaceView: UIView {
rightConstraint,
bottomConstraint,
].map {
$0.priority = .defaultHigh;
$0.priority = .required - 1;
$0.identifier = "FloatingPanel-surface-content"
return $0;
})
@@ -424,4 +433,8 @@ public class SurfaceView: UIView {
self.contentViewRightConstraint = rightConstraint
self.contentViewBottomConstraint = bottomConstraint
}
func hasStackView() -> Bool {
return contentView?.subviews.reduce(false) { $0 || ($1 is UIStackView) } ?? false
}
}
+40 -13
View File
@@ -1,4 +1,4 @@
// Copyright 2018 the FloatingPanel authors. All rights reserved. MIT license.
// Copyright 2018-Present Shin Yamamoto. All rights reserved. MIT license.
import UIKit
@@ -21,7 +21,7 @@ class ModalTransition: NSObject, UIViewControllerTransitioningDelegate {
class PresentationController: UIPresentationController {
override func presentationTransitionWillBegin() {
// Must call here even if duplicating on in containerViewWillLayoutSubviews()
// Because it let the floating panel present correctly with the presentation animation
// Because it let the panel present correctly with the presentation animation
addFloatingPanel()
}
@@ -44,8 +44,15 @@ class PresentationController: UIPresentationController {
override func containerViewWillLayoutSubviews() {
guard
let fpc = presentedViewController as? FloatingPanelController
else { fatalError() }
let fpc = presentedViewController as? FloatingPanelController,
/**
This condition fixes https://github.com/SCENEE/FloatingPanel/issues/369.
The issue is that this method is called in presenting a
UIImagePickerViewController and then a FloatingPanelController
view is added unnecessarily.
*/
fpc.presentedViewController == nil
else { return }
/*
* Layout the views managed by `FloatingPanelController` here for the
@@ -54,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) {
@@ -79,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()
}
}
@@ -101,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()
}
}
+83 -3
View File
@@ -1,4 +1,4 @@
// Copyright 2018 the FloatingPanel authors. All rights reserved. MIT license.
// Copyright 2018-Present Shin Yamamoto. All rights reserved. MIT license.
import UIKit
@@ -72,7 +72,7 @@ extension UIViewController {
}
// The reason why UIView has no extensions of safe area insets and top/bottom guides
// is for iOS10 compat.
// is for iOS10 compatibility.
extension UIView {
var fp_safeAreaLayoutGuide: LayoutGuideProvider {
if #available(iOS 11.0, *) {
@@ -85,6 +85,19 @@ extension UIView {
var presentationFrame: CGRect {
return layer.presentation()?.frame ?? frame
}
/// Returns non-zero displayScale
///
/// On iOS 11 or earlier the `traitCollection.displayScale` of a view can be
/// 0.0(indicating unspecified) when its view hasn't been added yet into a view tree in a window.
/// So this method returns `UIScreen.main` scale if the scale value is zero, for testing mainly.
var fp_displayScale: CGFloat {
let ret = traitCollection.displayScale
if ret == 0.0 {
return UIScreen.main.scale
}
return ret
}
}
extension UIView {
@@ -112,7 +125,7 @@ extension UIGestureRecognizer.State: CustomDebugStringConvertible {
case .changed: return "changed"
case .failed: return "failed"
case .cancelled: return "cancelled"
case .ended: return "endeded"
case .ended: return "ended"
case .possible: return "possible"
@unknown default: return ""
}
@@ -188,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))
}
}
+7 -7
View File
@@ -86,7 +86,7 @@ class ControllerTests: XCTestCase {
}
XCTAssertEqual(fpc.state, .full)
XCTAssertEqual(delegate.position, .full)
wait(for: [exp], timeout: 0.5)
wait(for: [exp], timeout: 1.0)
}
XCTContext.runActivity(named: "move to half(animated)") { act in
@@ -97,7 +97,7 @@ class ControllerTests: XCTestCase {
}
XCTAssertEqual(fpc.state, .half)
XCTAssertEqual(delegate.position, .half)
wait(for: [exp], timeout: 0.8)
wait(for: [exp], timeout: 1.0)
}
XCTContext.runActivity(named: "move to tip(animated)") { act in
@@ -108,7 +108,7 @@ class ControllerTests: XCTestCase {
}
XCTAssertEqual(fpc.state, .tip)
XCTAssertEqual(delegate.position, .tip)
wait(for: [exp], timeout: 0.8)
wait(for: [exp], timeout: 1.0)
}
fpc.move(to: .hidden, animated: true)
@@ -159,7 +159,7 @@ class ControllerTests: XCTestCase {
}
XCTAssertEqual(fpc.state, .full)
XCTAssertEqual(delegate.position, .full)
wait(for: [exp], timeout: 0.5)
wait(for: [exp], timeout: 1.0)
}
XCTContext.runActivity(named: "move to half(animated)") { act in
@@ -170,7 +170,7 @@ class ControllerTests: XCTestCase {
}
XCTAssertEqual(fpc.state, .half)
XCTAssertEqual(delegate.position, .half)
wait(for: [exp], timeout: 0.8)
wait(for: [exp], timeout: 1.0)
}
XCTContext.runActivity(named: "move to tip(animated)") { act in
@@ -181,7 +181,7 @@ class ControllerTests: XCTestCase {
}
XCTAssertEqual(fpc.state, .tip)
XCTAssertEqual(delegate.position, .tip)
wait(for: [exp], timeout: 0.8)
wait(for: [exp], timeout: 1.0)
}
fpc.move(to: .hidden, animated: true)
@@ -314,7 +314,7 @@ class ControllerTests: XCTestCase {
fpc.move(to: .full, animated: false)
XCTAssertEqual(fpc.surfaceView.frame.height, fpc.view.bounds.height - fpc.surfaceLocation(for: .full).y)
fpc.move(to: .half, animated: false)
print(1 / fpc.surfaceView.traitCollection.displayScale)
print(1 / fpc.surfaceView.fp_displayScale)
XCTAssertEqual(fpc.surfaceView.frame.height, fpc.view.bounds.height - fpc.surfaceLocation(for: .half).y)
fpc.move(to: .tip, animated: false)
XCTAssertEqual(fpc.surfaceView.frame.height, fpc.view.bounds.height - fpc.surfaceLocation(for: .tip).y)
+13 -5
View File
@@ -469,7 +469,11 @@ class LayoutTests: XCTestCase {
let c = prop.anchor.layoutConstraints(fpc, for: position)[0]
XCTAssertEqual(c.multiplier, CGFloat(prop.result.multiplier), line: UInt(prop.result.0))
XCTAssertTrue(c.firstAnchor is NSLayoutAnchor<NSLayoutDimension>, line: UInt(prop.result.0))
XCTAssertEqual(c.secondAnchor, prop.result.secondAnchor, line: UInt(prop.result.0))
// On iOS 10, `c.secondAnchor` can't be equal object to `prop.result.secondAnchor`
// because there is no safe area on iOS 10 and `fp_safeAreaLayoutGuide` emulates it.
if #available(iOS 11, *) {
XCTAssertEqual(c.secondAnchor, prop.result.secondAnchor, line: UInt(prop.result.0))
}
print(c)
}
}
@@ -532,7 +536,11 @@ class LayoutTests: XCTestCase {
let c = prop.anchor.layoutConstraints(fpc, for: position)[0]
XCTAssertEqual(c.multiplier, CGFloat(prop.result.multiplier), line: UInt(prop.result.0))
XCTAssertTrue(c.firstAnchor is NSLayoutAnchor<NSLayoutDimension>, line: UInt(prop.result.0))
XCTAssertEqual(c.secondAnchor, prop.result.secondAnchor, line: UInt(prop.result.0))
// On iOS 10, `c.secondAnchor` can't be equal object to `prop.result.secondAnchor`
// because there is no safe area on iOS 10 and `fp_safeAreaLayoutGuide` emulates it.
if #available(iOS 11, *) {
XCTAssertEqual(c.secondAnchor, prop.result.secondAnchor, line: UInt(prop.result.0))
}
print(c)
}
}
@@ -641,9 +649,9 @@ class LayoutTests: XCTestCase {
private typealias LayoutSegmentTestParameter = (UInt, pos: CGFloat, forwardY: Bool, lower: FloatingPanelState?, upper: FloatingPanelState?)
private func assertLayoutSegment(_ floatingPanel: Core, with params: [LayoutSegmentTestParameter]) {
params.forEach { (line, pos, forwardY, lowr, upper) in
let segument = floatingPanel.layoutAdapter.segument(at: pos, forward: forwardY)
XCTAssertEqual(segument.lower, lowr, line: line)
XCTAssertEqual(segument.upper, upper, line: line)
let segment = floatingPanel.layoutAdapter.segment(at: pos, forward: forwardY)
XCTAssertEqual(segment.lower, lowr, line: line)
XCTAssertEqual(segment.upper, upper, line: line)
}
}
+8 -8
View File
@@ -59,7 +59,7 @@ class SurfaceViewTests: XCTestCase {
surface.containerOverflow = height
surface.setNeedsLayout()
surface.layoutIfNeeded()
XCTAssertEqual(surface.contentView.frame, surface.bounds)
XCTAssertEqual(surface.contentView?.frame ?? .zero, surface.bounds)
}
XCTContext.runActivity(named: "Top sheet") { _ in
@@ -75,7 +75,7 @@ class SurfaceViewTests: XCTestCase {
surface.setNeedsLayout()
surface.layoutIfNeeded()
XCTAssertEqual(surface.containerView.frame, CGRect(x: 0.0, y: -height, width: 320.0, height: 480.0 * 3))
XCTAssertEqual(surface.convert(surface.contentView.frame, from: surface.containerView),
XCTAssertEqual(surface.convert(surface.contentView?.frame ?? .zero, from: surface.containerView),
surface.bounds)
}
}
@@ -146,22 +146,22 @@ class SurfaceViewTests: XCTestCase {
let contentView = UIView()
surface.set(contentView: contentView)
surface.layoutIfNeeded()
XCTAssertEqual(surface.contentView.frame, surface.bounds)
XCTAssertEqual(surface.contentView?.frame ?? .zero, surface.bounds)
surface.contentPadding = UIEdgeInsets(top: 16.0, left: 16.0, bottom: 16.0, right: 16.0)
surface.setNeedsLayout()
surface.layoutIfNeeded()
XCTAssertEqual(surface.contentView.frame, surface.bounds.inset(by: surface.contentPadding))
XCTAssertEqual(surface.contentView?.frame ?? .zero, surface.bounds.inset(by: surface.contentPadding))
}
XCTContext.runActivity(named: "Bottom sheet") { _ in
let surface = SurfaceView(frame: CGRect(x: 0.0, y: 0.0, width: 320.0, height: 480.0))
let contentView = UIView()
surface.set(contentView: contentView)
surface.layoutIfNeeded()
XCTAssertEqual(surface.contentView.frame, surface.bounds)
XCTAssertEqual(surface.contentView?.frame ?? .zero, surface.bounds)
surface.contentPadding = UIEdgeInsets(top: 16.0, left: 16.0, bottom: 16.0, right: 16.0)
surface.setNeedsLayout()
surface.layoutIfNeeded()
XCTAssertEqual(surface.contentView.frame, surface.bounds.inset(by: surface.contentPadding))
XCTAssertEqual(surface.contentView?.frame ?? .zero, surface.bounds.inset(by: surface.contentPadding))
}
}
@@ -170,13 +170,13 @@ class SurfaceViewTests: XCTestCase {
let contentView = UIView()
surface.set(contentView: contentView)
surface.layoutIfNeeded()
XCTAssertEqual(surface.contentView.frame, surface.bounds)
XCTAssertEqual(surface.contentView?.frame ?? .zero, surface.bounds)
surface.containerMargins = UIEdgeInsets(top: 16.0, left: 16.0, bottom: 16.0, right: 16.0)
surface.contentPadding = UIEdgeInsets(top: 16.0, left: 16.0, bottom: 16.0, right: 16.0)
surface.setNeedsLayout()
surface.layoutIfNeeded()
XCTAssertEqual(surface.containerView.frame, surface.bounds.inset(by: surface.containerMargins))
XCTAssertEqual(surface.contentView.frame, surface.containerView.bounds.inset(by: surface.contentPadding))
XCTAssertEqual(surface.contentView?.frame ?? .zero, surface.containerView.bounds.inset(by: surface.contentPadding))
}
func test_surfaceView_cornderRaduis() {