Compare commits

...

80 Commits

Author SHA1 Message Date
Shin Yamamoto c87d6c42c9 Address #661 issue since v2.8.0
See this commend for more detail:
https://github.com/scenee/FloatingPanel/issues/661#issuecomment-2818064324
2025-04-21 20:25:58 +09:00
Shin Yamamoto dfa9a77816 Fix a miss spell 2025-04-21 20:18:12 +09:00
Shin Yamamoto afff000d8c Allow slight deviation when checking for anchor position.
This change addresses the 2nd issue reported in #633. The previous attempt
in commit b0fd0d4 was intended to fix this, but it has a regression.
This change resolves the issue without introducing any regressions.
2025-04-21 20:18:02 +09:00
Shin Yamamoto dd49fdea5e Revert "Disallow interrupting the panel interaction while bouncing over the most expanded state (#652)"
This reverts commit b0fd0d4427.

This change had a problem normal cases. For example, in Maps example a
panel interaction jumps occurs because of this.
2025-04-21 18:09:24 +09:00
Shin Yamamoto a1f20cedb1 Version 2.8.7 2025-04-05 14:11:55 +09:00
Shin Yamamoto 9592baa16c ci: use macos-15 for all testing to use Xcode 16.2 2025-03-13 09:33:17 +09:00
Shin Yamamoto 370e306904 ci: use Xcode 16.2 (#653)
* Added '--verbose' in cocoapods job
* Fixed 'error: No simulator runtime version'. Some of the example apps cannot be built on github actions.
> /Users/runner/work/FloatingPanel/FloatingPanel/Examples/Samples/Sources/Assets.xcassets: error: No simulator runtime version from [<DVTBuildVersion 21A342>, <DVTBuildVersion 21C62>, <DVTBuildVersion 21E213>, <DVTBuildVersion 21F79>, <DVTBuildVersion 22B81>] available to use with iphonesimulator SDK version <DVTBuildVersion 22C146>
* Used macos-15 to fix random build fails
2025-03-07 20:42:16 +09:00
Shin Yamamoto 479cce4546 Reset initialScrollOffset after the attracting animation ends (#659)
* Stop pinning the scroll offset in moving programmatically
* Add 'optional' string interpolation
* Add comments
* Add CoreTests.test_initial_scroll_offset_reset()
* ci: remove macos-12 jobs for the deprecation
* ci: name circleci jobs
2025-03-07 18:57:21 +09:00
Shin Yamamoto b0fd0d4427 Disallow interrupting the panel interaction while bouncing over the most expanded state (#652)
I decided to disallow interrupting panel interactions while bouncing over the most expanded state in order to fix the 2nd issue in #633, https://github.com/scenee/FloatingPanel/issues/633#issuecomment-2324666767.
2024-11-09 13:16:47 +09:00
Shin Yamamoto b6e8928b1a Version 2.8.6 2024-09-07 10:12:44 +09:00
Shin Yamamoto 3a3d53424c Fix doc comments' errors (#644)
* Fix doc comment errors in ObjC APIs
* Improve doc comments in LayoutAnchoring.swift
2024-09-06 13:49:56 +09:00
Shin Yamamoto 71f419a3cd Version 2.8.5 2024-08-05 22:07:48 +09:00
Ivan Levin 0e27410460 Replace fatal errors in transitionDuration delegate methods (#642)
This PR modifies the transitionDuration(using:) method in FloatingPanelController to return 0.0 instead of calling fatalError when the FloatingPanelController instance is not found. This change is crucial for several reasons:

1. Avoiding Crashes in Production: Using fatalError in production can lead to unexpected crashes, which are detrimental to user experience. It's safer to return a default value like 0.0 and handle the scenario gracefully.

2. Improved Stability: By returning 0.0, the application can continue running, allowing for better stability and user satisfaction. This approach also aligns with the principle of fail-safety, where the system continues operating under error conditions.

3. Real-World Case: In our large-scale project, we encountered a crash at this specific point due to the fatalError. Given the size and complexity of our application, it has been challenging to pinpoint the exact cause. Switching to a return value of 0.0 would significantly help us mitigate the issue and maintain app stability while we investigate further.

4. Maintainability: Returning a default value makes the codebase more maintainable and easier to debug, as it avoids abrupt termination and allows for logging or other error handling mechanisms.
2024-08-05 22:06:43 +09:00
Shin Yamamoto 29185a47bd Version 2.8.4 2024-07-06 15:59:52 +09:00
Shin Yamamoto 6821b26706 Fix an inappropriate condition to determine scrolling content or not (#633)
Removed the `pre > .zero` condition from `FloatingPanelLayoutAnchor` as it was not appropriate for zero or negative `absoluteInset` values. 

Added documentation for `shouldScrollingContentInMoving(from:to:)` to prevent similar mistakes in the future.
2024-07-04 10:08:17 +09:00
Shin Yamamoto 5bdbe0f0ea test: increase timeout durations 2024-07-03 22:55:32 +09:00
Shin Yamamoto afe2a9bced ci: remove macos-11 runner
The macOS 11 runner image was removed on 6/28/2024.
https://github.blog/changelog/2024-05-20-actions-upcoming-changes-to-github-hosted-macos-runners/

Therefore Carthage support was removed from README since the 'carthage' job can
no longer be run and then 'carthage' build can not be checked.
2024-07-03 22:13:37 +09:00
Shin Yamamoto c0d88af234 ci: update actions/checkout to v4 2024-07-02 21:09:56 +09:00
Shin Yamamoto 3b4c1bd51c test: increase timeout durations 2024-07-02 20:48:44 +09:00
Shin Yamamoto 3c9f556533 ci: remove test-ios13_7-iPhone_11_Pro job
Because all Intel-based macOS resource were deprecated on June 28, 2024.
2024-07-02 15:31:26 +09:00
Shin Yamamoto 7f1a74825d Remove an unused variable 2024-07-02 00:01:01 +09:00
Shin Yamamoto 22d46c5260 Version 2.8.3 2024-06-12 09:21:33 +09:00
Will Bishop 466aaf21dd Fix a compilation error in Xcode 16 (#636)
This patch fixed the error at SurfaceView#L63 in Xcode 16 (#635):

> 'CALayerCornerCurve' is only available in application extensions for iOS 13.0 or newer.
2024-06-12 09:19:04 +09:00
Shin Yamamoto 56e71ac580 Fix a broken panel layout with a compositional collection view (#634)
* Fix #628 
* Add a new sample to test UICollectionView using a compositional layout
2024-06-08 16:23:29 +09:00
Shin Yamamoto f45b6aaa3f ci: use Xcode 15.4 (#631) 2024-05-18 11:49:18 +09:00
Shin Yamamoto e473c3c440 Fix the scroll tracking of WKWebView on iOS 17.4 (#630)
* Fix a scroll tracking issue of WKWebView on iOS 17.4
* Add 'Scroll tracking(List CollectionView)' use case in Samples app
2024-05-18 10:29:12 +09:00
Shin Yamamoto 8f2be39bf4 Version 2.8.2 2024-02-18 15:25:08 +09:00
Shin Yamamoto 92c10830ff ci: use Xcode 15.2 on the GitHub Actions (#619)
- Replaced 'runsOn' with 'runs-on'.
- Supported build jobs for Xcode 15.2.
- Didn't use `macos-11` as possible which was deprecated.
2024-02-18 14:46:32 +09:00
Shin Yamamoto dcb89f58c3 Add CoreTest.test_handleGesture_endWithoutAttraction() 2024-02-17 09:04:09 +09:00
Shin Yamamoto d39c4b54d1 Enable to define and use a subclass object of BackdropView (#617)
* Enable to create a subclass of BackdropView
* Add a custom backdrop sample in the Samples example
2024-02-16 22:07:14 +09:00
Ortwin Gentz, FutureTap 504182ceae Fix scroll locking behavior (#615)
use a separate `scrollLocked` var instead of abusing the scrollView's properties to store the locked state;
fixes an issue where the scroll indicators were no longer visible because lockScrollView() was executed twice before unlockScrollView() was called (due to the user changing `showsVerticalScrollIndicator` mid-animation);
2024-02-16 22:04:10 +09:00
Shin Yamamoto bc1cfe444b Fix a bug state was not changed property after v2.8.1
The state was not changed after moving a panel without attractive
interaction. For example, a panel is moved from half to full and
the scroll content continues to scroll with its deceleration
animation. We can test it on 'Show tracking(TextView)' in Samples app.

dbef6a6 commit causes this issue.
2024-02-03 13:36:36 +09:00
Shin Yamamoto 0e0f773df7 Possible fix for #586 2024-02-03 10:27:21 +09:00
Shin Yamamoto be2be99537 Fix a typo 2023-12-02 09:03:31 +09:00
Shin Yamamoto afcf1ced36 ci: support xcode 15.0.1 2023-11-18 11:51:15 +09:00
Shin Yamamoto 5b33d3d5ff Version 2.8.1 2023-11-04 21:17:09 +09:00
Shin Yamamoto dbef6a691a Fix an invalid behavior after switching to a new layout object (#611)
* Added a test for the use case, ControllerTests.test_switching_layout()
2023-11-04 13:25:25 +09:00
Shin Yamamoto dd238884bf Version 2.8.0 2023-10-08 14:51:26 +09:00
Shin Yamamoto 046ed3df5b Modify the doc comment of 'floatingPanel(_:shouldAllowToScroll:in:)' 2023-10-08 14:51:26 +09:00
Shin Yamamoto 4996ce1a84 Remove unnecessary notes 2023-10-08 14:51:26 +09:00
Shin Yamamoto b8f7ff825d Revise the doc comment of shouldProjectMomentum delegate method 2023-10-08 14:51:26 +09:00
Shin Yamamoto 6fb9a9b3a2 Add a new section for the new API 2023-09-20 08:58:02 +09:00
Shin Yamamoto 34ebb3bf19 Use alerts syntax in the README 2023-09-19 15:18:10 +09:00
Shin Yamamoto dbf665526d ci: disable parallel testing to improve stability 2023-09-19 09:43:04 +09:00
Shin Yamamoto 6c7f529eff Add 'state' parameter into 'floatingPanel(_:shouldAllowToScroll:)'
This is because the `state` argument of `Core.isScrollable(state:)` is
not always equal to `FloatingPanelController.state` property. Therefore,
the API should pass the `state` property of `Core.isScrollable(state:)`.
2023-09-19 09:41:07 +09:00
Shin Yamamoto c414d3a2a6 Fix errors of offset value from a state position
Sometimes an offset value has an error less than 1 pt, for example,
-0.666... 0.333... etc. This is due to `surfaceView.presentationFrame`.
2023-09-13 22:04:18 +09:00
Shin Yamamoto 8eba647d75 Fix a panel not moving when picked up in certain area
On `DebugTableViewController` in the Samples app, the panel does not move
when a finger move up from the menu area to the grabber area.
`scrollViewFrame` is not necessary due to the same condition is already
checked at Core.swift:L578.
2023-09-13 22:04:18 +09:00
Shin Yamamoto c3568067b7 Reverse 'LayoutAdapter.offsetFromMostExpandedAnchor' direction 2023-09-13 22:04:18 +09:00
Shin Yamamoto 72580f089d Fix scroll offset reset when moving in grabber area
In addition, `scrollFrame` conversion is removed because it's not really
necessary.
2023-09-13 22:04:16 +09:00
Shin Yamamoto c508ec892d Return false in shouldScrollViewHandleTouch if initial touch is outside scroll view 2023-09-13 22:03:29 +09:00
Shin Yamamoto ff2d4a48f1 Enable content scrolling in non-expanded states (#455)
The new `floatingPanel(_:shouldAllowToScroll)` delegate method allows the
library user to determine whether the content scrolls or not in certain
state. `Core.isScrollable(state:)` and `LayoutAdpter.offset(from:)` are
added for this feature.
2023-09-13 20:32:48 +09:00
Shin Yamamoto 62364eb6d5 Update the minimum deployment target to 11.0 2023-09-06 21:57:23 +09:00
Shin Yamamoto ce5469a69d Fix a compile error on swift 5.4 or earlier
The error detail is here:

> Extensions.swift:11:18: error: cannot convert value of type 'CGFloat' to
> expected argument type 'Double'
>
>         let v = (self * p).rounded(.towardZero) / p
>                  ^
>                  Double( )
2023-09-03 23:25:50 +09:00
Shin Yamamoto 93c31fd71d Fix CGFloat.rounded(by displayScale) for a floating point error
Where a value is -0.16666666666674246, -0.0 is the rounded value to be
expected. However the current implementation returned -0.3333333333333.
This is because the floating point error. So this patch truncates it.
2023-09-01 22:34:48 +09:00
Shin Yamamoto 461f637818 Add isLooselyLocked flag 2023-09-01 22:34:48 +09:00
Shin Yamamoto 6b3b18b8ed Care left/right positioned panels on the scroll indicator lock/unlock 2023-09-01 22:34:48 +09:00
Shin Yamamoto b5ca468397 Fix the hidden scroll indicator in Maps example
In Maps example, the scroll indicator of the table view doesn't show
even if `UIScrollView.showsVerticalScrollIndicator` is set to `true`.
This is due to the occurrence of two loose scroll locks before the
scroll content is displayed.
2023-09-01 22:34:45 +09:00
Shin Yamamoto 80956bfac6 Display the scroll indicator of table view in Maps example
The top inset of its scroll indicator has an unexpected top margin.
`UITableView` seems to append a top inset in addition to its scroll
insets. However, it's important to display the indicator for the library
testing. This allows us to verify whether the scroll indicator shows and
hides as expected.
2023-09-01 22:32:12 +09:00
Shin Yamamoto 5d382c440f Rename outdated compilation condition name to latest 2023-09-01 21:13:06 +09:00
Shin Yamamoto d28c939a4c Fix active compilation conditinos in unit testing 2023-09-01 21:11:51 +09:00
Shin Yamamoto 8bd02145cf Fix typo 2023-09-01 21:11:51 +09:00
Shin Yamamoto debeca1fb2 Version 2.7.0 2023-08-21 19:28:57 +09:00
Shin Yamamoto 9da54f9fc1 Call floatingPanelWillRemove when a panel removes from a window 2023-08-20 22:23:41 +09:00
Shin Yamamoto a13d053867 Update the doc comment of floatingPanelDidEndDragging method 2023-08-20 22:23:41 +09:00
Shin Yamamoto eda7201fe8 Remove unnecessary code 2023-08-18 12:09:34 +09:00
Shin Yamamoto b1edef49a4 ci: update the push trigger of github actions 2023-08-17 09:02:35 +09:00
Shin Yamamoto a2db94a8c4 Fix invalid scroll offsets after moving between states
Sometimes, the content offset of the tracking scroll view would become
less than the content inset (e.g. when a panel moves from half to full
state displaying content with a top bar similar to 'Show Navigation
Controller' in the Samples app). This resulted in the content getting
fixed in an unintended position.

For this issue, this commit completely changes the scroll offset pinning
logic from one shot pinning by DispatchQueue at Core:L43-L56

That old logic was added when the UIViewPropertyAnimator was used to move
the panel. But now, the custom animator using CADisplayLink allows
fine-grained control of panel movement and then the scroll offset is
able to be pinned during its panel transitions.
2023-08-17 09:02:35 +09:00
Shin Yamamoto 57495cff84 Fix log prints 2023-08-14 22:32:14 +09:00
Shin Yamamoto eff5cde844 Remove libswiftCoreGraphics.tbd to fix a crash of SamplesObjC.app
Sometimes SamplesObjC app crashes by the following error:
> dyld[21380]: Library not loaded: @rpath/libswiftCoreGraphics.dylib
>   Reason: tried: '/usr/lib/system/introspection/libswiftCoreGraphics.dylib'
>   (no such file, not in dyld cache),
2023-08-14 22:32:14 +09:00
Shin Yamamoto c365eadf1e Retain scroll view position while moving between states (#587)
Previously, the panel might not consistently keep its scroll content
offset when moving from its most expanded state to another.

Changes made in this commit:

* Keep the content offset of tracking scroll view in the following cases.
  A panel is moved...
  1. Outside of the tracking scroll view.
  2. Inside of a navigation bar/toolbar over the tracking scroll view.

* Stopped the scroll offset reset of the `stopScrollDeceleration` flag
  in the  `panningEnd` method when the panel transitions from its most
  expanded state because there is no issue without the reset.
2023-08-14 22:28:19 +09:00
Shin Yamamoto 5d02681b05 Add 'Expand top margin' switcher in DebugTextViewController 2023-08-11 16:46:42 +09:00
Shin Yamamoto 6e17ff734a Modify shouldProjectMomentum(_:{proposedTargetPosition => proposedState}:) 2023-08-11 16:45:23 +09:00
Shin Yamamoto 02ed923e7b Replace 'Position' words 2023-08-11 16:45:23 +09:00
Shin Yamamoto 421335d98c Make the 'Show Panel over Window' panel removable 2023-08-11 16:09:47 +09:00
Shin Yamamoto 2618f49556 Add GestureTests 2023-08-11 15:01:25 +09:00
Shin Yamamoto 328116600f Update the minimum deployment target to iOS 11.0 on the Unit test target 2023-08-11 15:01:25 +09:00
Shin Yamamoto a10b1426cd Allow a 'delegateProxy' object to access the default implementations 2023-08-11 15:01:25 +09:00
Shin Yamamoto 5468856a93 Set isAttracting to true while moveAnimator is active
And make it calls the related delegate methods
2023-08-11 15:01:24 +09:00
Shin Yamamoto 8f3a7de321 Call the 'floatingPanelDidEndDragging' method after 'state' property changes
This change allows the library user to get the correct state in
'floatingPanelDidEndDragging' method while `attract` is false.
2023-08-11 15:01:01 +09:00
Shin Yamamoto 2de1fb9ac8 Call the 'floatingPanelDidMove' delegate at the end of a move interaction 2023-08-11 15:01:01 +09:00
44 changed files with 1814 additions and 758 deletions
+30 -8
View File
@@ -1,21 +1,43 @@
version: 2.1
jobs:
build-swift_5_7:
macos:
xcode: 13.4.1
steps:
- checkout
- run: xcodebuild -scheme FloatingPanel -workspace FloatingPanel.xcworkspace SWIFT_VERSION=5.7 clean build
build-swiftpm_ios15_7:
macos:
xcode: 13.4.1
steps:
- checkout
- run: swift build -Xswiftc "-sdk" -Xswiftc "`xcrun --sdk iphonesimulator --show-sdk-path`" -Xswiftc "-target" -Xswiftc "x86_64-apple-ios15.7-simulator"
- run: swift build -Xswiftc "-sdk" -Xswiftc "`xcrun --sdk iphonesimulator --show-sdk-path`" -Xswiftc "-target" -Xswiftc "arm64-apple-ios15.7-simulator"
test-ios15_5-iPhone_13_Pro:
macos:
xcode: 13.4.1
steps:
- checkout
- run: xcodebuild clean test -scheme FloatingPanel -workspace FloatingPanel.xcworkspace -destination 'platform=iOS Simulator,OS=15.5,name=iPhone 13 Pro'
test-ios14_5-iPhone_12_Pro:
macos:
xcode: 13.4.1
steps:
- checkout
- run: xcodebuild clean test -scheme FloatingPanel -workspace FloatingPanel.xcworkspace -destination 'platform=iOS Simulator,OS=14.5,name=iPhone 12 Pro'
test-ios13_7-iPhone_11_Pro:
macos:
xcode: 12.5.1
steps:
- checkout
- run: xcodebuild clean test -scheme FloatingPanel -workspace FloatingPanel.xcworkspace -destination 'platform=iOS Simulator,OS=13.7,name=iPhone 11 Pro'
workflows:
test:
jobs:
- test-ios14_5-iPhone_12_Pro
- test-ios13_7-iPhone_11_Pro
- build-swift_5_7:
name: build (5.7, 13.4.1)
- build-swiftpm_ios15_7:
name: swiftpm ({x86_64,arm64}-apple-ios15.5-simulator, 13.4.1)
- test-ios14_5-iPhone_12_Pro:
name: test (15.5, 13.4.1, iPhone 12 Pro)
- test-ios15_5-iPhone_13_Pro:
name: test (14.5, 13.4.1, iPhone 13 Pro)
+99 -55
View File
@@ -4,71 +4,75 @@ on:
push:
branches:
- master
- next
pull_request:
branches:
- '*'
jobs:
build:
runs-on: ${{ matrix.runsOn }}
runs-on: ${{ matrix.runs-on }}
env:
DEVELOPER_DIR: /Applications/Xcode_${{ matrix.xcode }}.app/Contents/Developer
strategy:
fail-fast: false
matrix:
include:
- swift: "5"
xcode: "16.2"
runs-on: macos-15
- swift: "5.10"
xcode: "15.4"
runs-on: macos-14
- swift: "5.9"
xcode: "15.2"
runs-on: macos-13
- swift: "5.8"
xcode: "14.3"
runsOn: macos-13
- swift: "5.7"
xcode: "14.1"
runsOn: macos-12
- swift: "5.6"
xcode: "13.4.1"
runsOn: macos-12
- swift: "5.5"
xcode: "13.2.1"
runsOn: macos-11
- swift: "5.4"
xcode: "12.5.1"
runsOn: macos-11
- swift: "5.3"
xcode: "12.4"
runsOn: macos-11
- swift: "5.2"
xcode: "11.7"
runsOn: macos-11
xcode: "14.3.1"
runs-on: macos-13
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Building in Swift ${{ matrix.swift }}
run: xcodebuild -scheme FloatingPanel SWIFT_VERSION=${{ matrix.swift }} clean build
test:
runs-on: ${{ matrix.runsOn }}
runs-on: ${{ matrix.runs-on }}
env:
DEVELOPER_DIR: /Applications/Xcode_${{ matrix.xcode }}.app/Contents/Developer
strategy:
fail-fast: false
matrix:
include:
- os: "18.2"
xcode: "16.2"
sim: "iPhone 16 Pro"
parallel: NO # Stop random test job failures
runs-on: macos-15
- os: "17.5"
xcode: "15.4"
sim: "iPhone 15 Pro"
parallel: NO # Stop random test job failures
runs-on: macos-14
- os: "16.4"
xcode: "14.3.1"
sim: "iPhone 14 Pro"
runsOn: macos-13
- os: "15.5"
xcode: "13.4.1"
sim: "iPhone 13 Pro"
runsOn: macos-12
parallel: NO # Stop random test job failures
runs-on: macos-13
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Testing in iOS ${{ matrix.os }}
run: xcodebuild clean test -scheme FloatingPanel -workspace FloatingPanel.xcworkspace -destination 'platform=iOS Simulator,OS=${{ matrix.os }},name=${{ matrix.sim }}'
run: |
xcodebuild clean test \
-workspace FloatingPanel.xcworkspace \
-scheme FloatingPanel \
-destination 'platform=iOS Simulator,OS=${{ matrix.os }},name=${{ matrix.sim }}' \
-parallel-testing-enabled '${{ matrix.parallel }}'
timeout-minutes: 20
example:
runs-on: macos-12
runs-on: macos-15
env:
DEVELOPER_DIR: /Applications/Xcode_14.1.app/Contents/Developer
DEVELOPER_DIR: /Applications/Xcode_16.2.app/Contents/Developer
strategy:
fail-fast: false
matrix:
@@ -78,41 +82,81 @@ jobs:
- example: "Stocks"
- example: "Samples"
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Building ${{ matrix.example }}
run: xcodebuild -workspace FloatingPanel.xcworkspace -scheme ${{ matrix.example }} -sdk iphonesimulator clean build
# Need to use iphonesimulator18.1 because randomly 18.2<DVTBuildVersion 22C146> isn't available.
run: |
xcodebuild clean build \
-workspace FloatingPanel.xcworkspace \
-scheme ${{ matrix.example }} \
-sdk iphonesimulator
swiftpm:
runs-on: macos-12
runs-on: macos-15
env:
DEVELOPER_DIR: /Applications/Xcode_14.1.app/Contents/Developer
DEVELOPER_DIR: /Applications/Xcode_${{ matrix.xcode }}.app/Contents/Developer
strategy:
fail-fast: false
matrix:
xcode: ["16.2", "15.4"]
platform: [iphoneos, iphonesimulator]
arch: [x86_64, arm64]
exclude:
- platform: iphoneos
arch: x86_64
include:
# 18.2
- platform: iphoneos
xcode: "16.2"
sys: "ios18.2"
- platform: iphonesimulator
xcode: "16.2"
sys: "ios18.2-simulator"
# 17.2
- platform: iphoneos
xcode: "15.4"
sys: "ios17.2"
- platform: iphonesimulator
xcode: "15.4"
sys: "ios17.2-simulator"
steps:
- uses: actions/checkout@v4
- name: "Swift Package Manager build"
run: |
xcrun swift build \
--sdk "$(xcrun --sdk ${{ matrix.platform }} --show-sdk-path)" \
-Xswiftc "-target" -Xswiftc "${{ matrix.arch }}-apple-${{ matrix.sys }}"
swiftpm_old:
runs-on: ${{ matrix.runs-on }}
env:
DEVELOPER_DIR: /Applications/Xcode_${{ matrix.xcode }}.app/Contents/Developer
strategy:
fail-fast: false
matrix:
include:
# 15.7
- target: "x86_64-apple-ios15.7-simulator"
- target: "arm64-apple-ios15.7-simulator"
# 16.1
- target: "x86_64-apple-ios16.1-simulator"
- target: "arm64-apple-ios16.1-simulator"
# 16.4
- target: "x86_64-apple-ios16.4-simulator"
xcode: "14.3.1"
runs-on: macos-13
- target: "arm64-apple-ios16.4-simulator"
xcode: "14.3.1"
runs-on: macos-13
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: "Swift Package Manager build"
run: swift build -Xswiftc "-sdk" -Xswiftc "`xcrun --sdk iphonesimulator --show-sdk-path`" -Xswiftc "-target" -Xswiftc "${{ matrix.target }}"
carthage:
runs-on: macos-11
steps:
- uses: actions/checkout@v3
- name: "Carthage build"
run: carthage build --use-xcframeworks --no-skip-current
run: |
swift build \
-Xswiftc "-sdk" -Xswiftc "`xcrun --sdk iphonesimulator --show-sdk-path`" \
-Xswiftc "-target" -Xswiftc "${{ matrix.target }}"
cocoapods:
runs-on: macos-12
runs-on: macos-15
env:
DEVELOPER_DIR: /Applications/Xcode_16.2.app/Contents/Developer
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: "CocoaPods: pod lib lint"
run: pod lib lint --allow-warnings
run: pod lib lint --allow-warnings --verbose
- name: "CocoaPods: pod spec lint"
run: pod spec lint --allow-warnings
run: pod spec lint --allow-warnings --verbose
+33 -31
View File
@@ -1,7 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="19455" 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="21701" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="BYZ-38-t0r">
<device id="retina6_12" orientation="portrait" appearance="light"/>
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="19454"/>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="21679"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
@@ -11,16 +13,16 @@
<objects>
<viewController id="BYZ-38-t0r" customClass="MainViewController" customModule="Maps" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
<rect key="frame" x="0.0" y="0.0" width="393" height="852"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<mapView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" mapType="standard" translatesAutoresizingMaskIntoConstraints="NO" id="5Jw-n2-Cpw">
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
<rect key="frame" x="0.0" y="0.0" width="393" height="852"/>
</mapView>
<visualEffectView opaque="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="d9i-3g-8Ja">
<rect key="frame" x="0.0" y="0.0" width="600" height="0.0"/>
<rect key="frame" x="0.0" y="0.0" width="393" height="59"/>
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" id="lMa-xa-AVV">
<rect key="frame" x="0.0" y="0.0" width="600" height="0.0"/>
<rect key="frame" x="0.0" y="0.0" width="393" height="59"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
</view>
<blurEffect style="prominent"/>
@@ -52,31 +54,31 @@
<objects>
<viewController storyboardIdentifier="SearchViewController" useStoryboardIdentifierAsRestorationIdentifier="YES" id="0S1-Lk-JgE" customClass="SearchViewController" customModule="Maps" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="Ncl-E9-yRn">
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
<rect key="frame" x="0.0" y="0.0" width="393" height="852"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<visualEffectView opaque="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="Ye3-uU-bq3">
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
<rect key="frame" x="0.0" y="0.0" width="393" height="852"/>
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" id="ED1-gT-FBj">
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
<rect key="frame" x="0.0" y="0.0" width="393" height="852"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<searchBar contentMode="redraw" searchBarStyle="minimal" translatesAutoresizingMaskIntoConstraints="NO" id="Zcj-SE-gb8">
<rect key="frame" x="0.0" y="6" width="600" height="51"/>
<rect key="frame" x="0.0" y="6" width="393" height="56"/>
<textInputTraits key="textInputTraits"/>
</searchBar>
<tableView clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" showsHorizontalScrollIndicator="NO" showsVerticalScrollIndicator="NO" dataMode="prototypes" style="plain" separatorStyle="default" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="28" sectionFooterHeight="28" translatesAutoresizingMaskIntoConstraints="NO" id="D7r-re-InH">
<rect key="frame" x="0.0" y="61" width="600" height="539"/>
<tableView clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" showsHorizontalScrollIndicator="NO" dataMode="prototypes" style="plain" separatorStyle="default" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="28" sectionFooterHeight="28" translatesAutoresizingMaskIntoConstraints="NO" id="D7r-re-InH">
<rect key="frame" x="0.0" y="66" width="393" height="786"/>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<view key="tableHeaderView" contentMode="scaleToFill" id="u28-LY-hIh" customClass="SearchHeaderView" customModule="Maps" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="600" height="116"/>
<rect key="frame" x="0.0" y="0.0" width="393" height="116"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<subviews>
<stackView opaque="NO" contentMode="scaleToFill" distribution="equalSpacing" baselineRelativeArrangement="YES" translatesAutoresizingMaskIntoConstraints="NO" id="era-8w-yA1">
<rect key="frame" x="24" y="10.5" width="552" height="97.5"/>
<rect key="frame" x="24" y="10.666666666666664" width="345" height="97.333333333333343"/>
<subviews>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="6" translatesAutoresizingMaskIntoConstraints="NO" id="auI-1v-Yfk">
<rect key="frame" x="0.0" y="0.0" width="60" height="97.5"/>
<rect key="frame" x="0.0" y="0.0" width="60" height="97.333333333333329"/>
<subviews>
<imageView userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="food" translatesAutoresizingMaskIntoConstraints="NO" id="ErN-bC-qTx">
<rect key="frame" x="0.0" y="0.0" width="60" height="60"/>
@@ -86,7 +88,7 @@
</constraints>
</imageView>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Food &amp; Drinks" textAlignment="center" lineBreakMode="tailTruncation" numberOfLines="2" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="nx2-fW-xAm">
<rect key="frame" x="0.0" y="66" width="60" height="31.5"/>
<rect key="frame" x="0.0" y="66" width="60" height="31.333333333333329"/>
<fontDescription key="fontDescription" type="system" pointSize="13"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
@@ -94,7 +96,7 @@
</subviews>
</stackView>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="6" translatesAutoresizingMaskIntoConstraints="NO" id="0vd-sD-XKv">
<rect key="frame" x="164" y="0.0" width="60" height="97.5"/>
<rect key="frame" x="95" y="0.0" width="60" height="97.333333333333329"/>
<subviews>
<imageView userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="shopping" translatesAutoresizingMaskIntoConstraints="NO" id="xcm-St-HAo">
<rect key="frame" x="0.0" y="0.0" width="60" height="60"/>
@@ -104,7 +106,7 @@
</constraints>
</imageView>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" verticalCompressionResistancePriority="1000" textAlignment="center" lineBreakMode="tailTruncation" numberOfLines="2" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="H7q-q2-ga5">
<rect key="frame" x="0.0" y="66" width="60" height="31.5"/>
<rect key="frame" x="0.0" y="66" width="60" height="31.333333333333329"/>
<string key="text">Shopping
</string>
<fontDescription key="fontDescription" type="system" pointSize="13"/>
@@ -114,7 +116,7 @@
</subviews>
</stackView>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="6" translatesAutoresizingMaskIntoConstraints="NO" id="Jd8-YL-b5s">
<rect key="frame" x="328" y="0.0" width="60" height="97.5"/>
<rect key="frame" x="190" y="0.0" width="60" height="97.333333333333329"/>
<subviews>
<imageView userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="fun" translatesAutoresizingMaskIntoConstraints="NO" id="bMJ-Jn-Gi8">
<rect key="frame" x="0.0" y="0.0" width="60" height="60"/>
@@ -124,7 +126,7 @@
</constraints>
</imageView>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" textAlignment="center" lineBreakMode="tailTruncation" numberOfLines="2" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="kKh-45-FZ2">
<rect key="frame" x="0.0" y="66" width="60" height="31.5"/>
<rect key="frame" x="0.0" y="66" width="60" height="31.333333333333329"/>
<string key="text">Fun
</string>
<fontDescription key="fontDescription" type="system" pointSize="13"/>
@@ -134,7 +136,7 @@
</subviews>
</stackView>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="6" translatesAutoresizingMaskIntoConstraints="NO" id="dTL-e1-Arz">
<rect key="frame" x="492" y="0.0" width="60" height="97.5"/>
<rect key="frame" x="285" y="0.0" width="60" height="97.333333333333329"/>
<subviews>
<imageView userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="travel" translatesAutoresizingMaskIntoConstraints="NO" id="8h3-fo-pC3">
<rect key="frame" x="0.0" y="0.0" width="60" height="60"/>
@@ -144,7 +146,7 @@
</constraints>
</imageView>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" textAlignment="center" lineBreakMode="tailTruncation" numberOfLines="2" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="WBT-Vj-7QA">
<rect key="frame" x="0.0" y="66" width="60" height="31.5"/>
<rect key="frame" x="0.0" y="66" width="60" height="31.333333333333329"/>
<string key="text">Travel
</string>
<fontDescription key="fontDescription" type="system" pointSize="13"/>
@@ -166,10 +168,10 @@
</view>
<prototypes>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" selectionStyle="blue" hidesAccessoryWhenEditing="NO" indentationLevel="1" indentationWidth="0.0" reuseIdentifier="Cell" rowHeight="70" id="LzC-B9-Adb" customClass="SearchCell" customModule="Maps" customModuleProvider="target">
<rect key="frame" x="0.0" y="160.5" width="600" height="70"/>
<rect key="frame" x="0.0" y="166" width="393" height="70"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="LzC-B9-Adb" id="evr-60-laS">
<rect key="frame" x="0.0" y="0.0" width="600" height="70"/>
<rect key="frame" x="0.0" y="0.0" width="393" height="70"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<imageView userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="like" translatesAutoresizingMaskIntoConstraints="NO" id="GEk-yE-lLq">
@@ -181,16 +183,16 @@
</constraints>
</imageView>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" distribution="fillEqually" spacing="2" translatesAutoresizingMaskIntoConstraints="NO" id="Gfl-Oy-rsy">
<rect key="frame" x="57" y="12" width="528" height="46"/>
<rect key="frame" x="57" y="12" width="321" height="46"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" tag="1" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Favorites" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Spf-8L-Ne6">
<rect key="frame" x="0.0" y="0.0" width="528" height="22"/>
<rect key="frame" x="0.0" y="0.0" width="321" height="22"/>
<fontDescription key="fontDescription" type="boldSystem" pointSize="20"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="0 Places" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="gyo-3V-7U8">
<rect key="frame" x="0.0" y="24" width="528" height="22"/>
<rect key="frame" x="0.0" y="24" width="321" height="22"/>
<fontDescription key="fontDescription" type="system" pointSize="13"/>
<color key="textColor" red="0.57647058819999997" green="0.57647058819999997" blue="0.57647058819999997" alpha="1" colorSpace="calibratedRGB"/>
<nil key="highlightedColor"/>
@@ -255,18 +257,18 @@
<objects>
<viewController storyboardIdentifier="DetailViewController" id="Tp2-MF-IFz" customClass="DetailViewController" customModule="Maps" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="FmO-AT-4Y7">
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
<rect key="frame" x="0.0" y="0.0" width="393" height="852"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<visualEffectView opaque="NO" contentMode="scaleToFill" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="c3d-2e-0b1">
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
<rect key="frame" x="-1" y="-1" width="393" height="852"/>
<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="600" height="600"/>
<rect key="frame" x="0.0" y="0.0" width="393" height="852"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<tableView clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" showsHorizontalScrollIndicator="NO" showsVerticalScrollIndicator="NO" dataMode="prototypes" style="plain" separatorStyle="default" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="28" sectionFooterHeight="28" translatesAutoresizingMaskIntoConstraints="NO" id="kP7-56-wlG">
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
<rect key="frame" x="0.0" y="0.0" width="393" height="852"/>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</tableView>
</subviews>
+13 -12
View File
@@ -156,6 +156,14 @@ class SearchPanelPhoneDelegate: NSObject, FloatingPanelControllerDelegate, UIGes
self.owner = owner
}
func floatingPanel(
_ fpc: FloatingPanelController,
shouldAllowToScroll scrollView: UIScrollView,
in state: FloatingPanelState
) -> Bool {
return state == .full || state == .half
}
func floatingPanel(_ vc: FloatingPanelController, layoutFor newCollection: UITraitCollection) -> FloatingPanelLayout {
switch newCollection.verticalSizeClass {
case .compact:
@@ -215,17 +223,10 @@ class SearchPanelLandscapeLayout: FloatingPanelLayout {
.tip: FloatingPanelLayoutAnchor(absoluteInset: 69.0, edge: .bottom, referenceGuide: .safeArea),
]
func prepareLayout(surfaceView: UIView, in view: UIView) -> [NSLayoutConstraint] {
if #available(iOS 11.0, *) {
return [
surfaceView.leftAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leftAnchor, constant: 8.0),
surfaceView.widthAnchor.constraint(equalToConstant: 291),
]
} else {
return [
surfaceView.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 8.0),
surfaceView.widthAnchor.constraint(equalToConstant: 291),
]
}
return [
surfaceView.leftAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leftAnchor, constant: 8.0),
surfaceView.widthAnchor.constraint(equalToConstant: 291),
]
}
func backdropAlpha(for state: FloatingPanelState) -> CGFloat {
return 0.0
@@ -319,7 +320,7 @@ class SearchPaneliPadBehavior: FloatingPanelBehavior {
var momentumProjectionRate: CGFloat {
return UIScrollView.DecelerationRate.fast.rawValue
}
func shouldProjectMomentum(_ fpc: FloatingPanelController, to proposedTargetPosition: FloatingPanelState) -> Bool {
func shouldProjectMomentum(_ fpc: FloatingPanelController, to proposedState: FloatingPanelState) -> Bool {
return true
}
}
+4 -30
View File
@@ -81,9 +81,6 @@ class SearchViewController: UIViewController, UITableViewDataSource {
var items: [LocationItem] = []
// For iOS 10 only
private lazy var shadowLayer: CAShapeLayer = CAShapeLayer()
override func viewDidLoad() {
super.viewDidLoad()
tableView.dataSource = self
@@ -92,29 +89,6 @@ class SearchViewController: UIViewController, UITableViewDataSource {
hideHeader(animated: false)
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
if #available(iOS 11, *) {
} else {
// Exmaple: Add rounding corners on iOS 10
visualEffectView.layer.cornerRadius = 9.0
visualEffectView.clipsToBounds = true
// Exmaple: Add shadow manually on iOS 10
view.layer.insertSublayer(shadowLayer, at: 0)
let rect = visualEffectView.frame
let path = UIBezierPath(roundedRect: rect,
byRoundingCorners: [.topLeft, .topRight],
cornerRadii: CGSize(width: 9.0, height: 9.0))
shadowLayer.frame = visualEffectView.frame
shadowLayer.shadowPath = path.cgPath
shadowLayer.shadowColor = UIColor.black.cgColor
shadowLayer.shadowOffset = CGSize(width: 0.0, height: 1.0)
shadowLayer.shadowOpacity = 0.2
shadowLayer.shadowRadius = 3.0
}
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return items.count
}
@@ -130,16 +104,16 @@ class SearchViewController: UIViewController, UITableViewDataSource {
}
func showHeader(animated: Bool) {
changeHeader(height: 116.0, aniamted: animated)
changeHeader(height: 116.0, animated: animated)
}
func hideHeader(animated: Bool) {
changeHeader(height: 0.0, aniamted: animated)
changeHeader(height: 0.0, animated: animated)
}
private func changeHeader(height: CGFloat, aniamted: Bool) {
private func changeHeader(height: CGFloat, animated: Bool) {
guard let headerView = tableView.tableHeaderView, headerView.bounds.height != height else { return }
if aniamted == false {
if animated == false {
updateHeader(height: height)
return
}
@@ -18,7 +18,7 @@
5442E24A25FC53C100A26F43 /* DebugTextViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5442E24925FC53C100A26F43 /* DebugTextViewController.swift */; };
5442E25225FC541700A26F43 /* NestedScrollViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5442E25125FC541700A26F43 /* NestedScrollViewController.swift */; };
54496C59263A7E5A0031E0C8 /* UseCaseController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54496C58263A7E5A0031E0C8 /* UseCaseController.swift */; };
544BC56826CC918200D0A436 /* AdaptiveLayoutTestViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 544BC56726CC918200D0A436 /* AdaptiveLayoutTestViewController.swift */; };
544BC56826CC918200D0A436 /* TableViewControllerForAdaptiveLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 544BC56726CC918200D0A436 /* TableViewControllerForAdaptiveLayout.swift */; };
545DB9EE21511E6300CA77B8 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 545DB9ED21511E6300CA77B8 /* AppDelegate.swift */; };
545DB9F021511E6300CA77B8 /* MainViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 545DB9EF21511E6300CA77B8 /* MainViewController.swift */; };
545DB9F321511E6300CA77B8 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 545DB9F121511E6300CA77B8 /* Main.storyboard */; };
@@ -30,8 +30,11 @@
549D23CC233C7779008EF4D7 /* FloatingPanel.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 549D23CA233C7779008EF4D7 /* FloatingPanel.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
54B51116216AFE5F0033A6F3 /* Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54B51115216AFE5F0033A6F3 /* Extensions.swift */; };
54CDC5D8215BBE23007D205C /* SupplementaryViews.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CDC5D7215BBE23007D205C /* SupplementaryViews.swift */; };
54E58CB72BF8A8D900408EA9 /* CollectionViewControllerForAdaptiveLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54E58CB62BF8A8D900408EA9 /* CollectionViewControllerForAdaptiveLayout.swift */; };
54EAD35B263A75EB006A36EA /* PanelLayouts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54EAD35A263A75EB006A36EA /* PanelLayouts.swift */; };
54EAD365263A765F006A36EA /* PagePanelController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54EAD364263A765F006A36EA /* PagePanelController.swift */; };
54F185822BF4AD0000916F57 /* DebugListCollectionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54F185812BF4AD0000916F57 /* DebugListCollectionViewController.swift */; };
54F185842BF4B82E00916F57 /* UnavailableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54F185832BF4B82E00916F57 /* UnavailableViewController.swift */; };
5D82A6AD28D1843C006A44BA /* libswiftCoreGraphics.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 5D82A6AC28D18438006A44BA /* libswiftCoreGraphics.tbd */; };
/* End PBXBuildFile section */
@@ -61,7 +64,7 @@
5442E24925FC53C100A26F43 /* DebugTextViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebugTextViewController.swift; sourceTree = "<group>"; };
5442E25125FC541700A26F43 /* NestedScrollViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NestedScrollViewController.swift; sourceTree = "<group>"; };
54496C58263A7E5A0031E0C8 /* UseCaseController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UseCaseController.swift; sourceTree = "<group>"; };
544BC56726CC918200D0A436 /* AdaptiveLayoutTestViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdaptiveLayoutTestViewController.swift; sourceTree = "<group>"; };
544BC56726CC918200D0A436 /* TableViewControllerForAdaptiveLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TableViewControllerForAdaptiveLayout.swift; sourceTree = "<group>"; };
545DB9EA21511E6300CA77B8 /* Samples.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Samples.app; sourceTree = BUILT_PRODUCTS_DIR; };
545DB9ED21511E6300CA77B8 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
545DB9EF21511E6300CA77B8 /* MainViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainViewController.swift; sourceTree = "<group>"; };
@@ -74,8 +77,11 @@
549D23CA233C7779008EF4D7 /* FloatingPanel.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = FloatingPanel.framework; sourceTree = BUILT_PRODUCTS_DIR; };
54B51115216AFE5F0033A6F3 /* Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Extensions.swift; sourceTree = "<group>"; };
54CDC5D7215BBE23007D205C /* SupplementaryViews.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SupplementaryViews.swift; sourceTree = "<group>"; };
54E58CB62BF8A8D900408EA9 /* CollectionViewControllerForAdaptiveLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollectionViewControllerForAdaptiveLayout.swift; sourceTree = "<group>"; };
54EAD35A263A75EB006A36EA /* PanelLayouts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PanelLayouts.swift; sourceTree = "<group>"; };
54EAD364263A765F006A36EA /* PagePanelController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PagePanelController.swift; sourceTree = "<group>"; };
54F185812BF4AD0000916F57 /* DebugListCollectionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebugListCollectionViewController.swift; sourceTree = "<group>"; };
54F185832BF4B82E00916F57 /* UnavailableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnavailableViewController.swift; sourceTree = "<group>"; };
5D82A6AC28D18438006A44BA /* libswiftCoreGraphics.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libswiftCoreGraphics.tbd; path = usr/lib/swift/libswiftCoreGraphics.tbd; sourceTree = SDKROOT; };
/* End PBXFileReference section */
@@ -95,17 +101,18 @@
5442E22225FC519700A26F43 /* ContentViewControllers */ = {
isa = PBXGroup;
children = (
54E58CB52BF8A88600408EA9 /* AdaptiveLayout */,
5442E23F25FC533800A26F43 /* DebugTableViewController.swift */,
5442E24925FC53C100A26F43 /* DebugTextViewController.swift */,
54F185812BF4AD0000916F57 /* DebugListCollectionViewController.swift */,
5442E23325FC528400A26F43 /* DetailViewController.swift */,
5442E24325FC538200A26F43 /* InspectorViewController.swift */,
5442E22325FC51AF00A26F43 /* ImageViewController.swift */,
5442E25125FC541700A26F43 /* NestedScrollViewController.swift */,
5442E23925FC52CD00A26F43 /* ModalViewController.swift */,
5442E22725FC51E200A26F43 /* MultiPanelController.swift */,
5442E22B25FC521F00A26F43 /* SettingsViewController.swift */,
5442E22F25FC525200A26F43 /* TabBarViewController.swift */,
544BC56726CC918200D0A436 /* AdaptiveLayoutTestViewController.swift */,
54F185832BF4B82E00916F57 /* UnavailableViewController.swift */,
);
path = ContentViewControllers;
sourceTree = "<group>";
@@ -157,6 +164,16 @@
path = UseCases;
sourceTree = "<group>";
};
54E58CB52BF8A88600408EA9 /* AdaptiveLayout */ = {
isa = PBXGroup;
children = (
5442E22325FC51AF00A26F43 /* ImageViewController.swift */,
544BC56726CC918200D0A436 /* TableViewControllerForAdaptiveLayout.swift */,
54E58CB62BF8A8D900408EA9 /* CollectionViewControllerForAdaptiveLayout.swift */,
);
path = AdaptiveLayout;
sourceTree = "<group>";
};
5D82A6AB28D18438006A44BA /* Frameworks */ = {
isa = PBXGroup;
children = (
@@ -238,15 +255,17 @@
buildActionMask = 2147483647;
files = (
5442E23425FC528400A26F43 /* DetailViewController.swift in Sources */,
54E58CB72BF8A8D900408EA9 /* CollectionViewControllerForAdaptiveLayout.swift in Sources */,
54496C59263A7E5A0031E0C8 /* UseCaseController.swift in Sources */,
54CDC5D8215BBE23007D205C /* SupplementaryViews.swift in Sources */,
54B51116216AFE5F0033A6F3 /* Extensions.swift in Sources */,
544BC56826CC918200D0A436 /* AdaptiveLayoutTestViewController.swift in Sources */,
544BC56826CC918200D0A436 /* TableViewControllerForAdaptiveLayout.swift in Sources */,
5442E24A25FC53C100A26F43 /* DebugTextViewController.swift in Sources */,
546341AC25C6426500CA0596 /* CustomState.swift in Sources */,
5442E23A25FC52CD00A26F43 /* ModalViewController.swift in Sources */,
5442E22425FC51AF00A26F43 /* ImageViewController.swift in Sources */,
5442E24025FC533800A26F43 /* DebugTableViewController.swift in Sources */,
54F185822BF4AD0000916F57 /* DebugListCollectionViewController.swift in Sources */,
5442E25225FC541700A26F43 /* NestedScrollViewController.swift in Sources */,
5442E22825FC51E200A26F43 /* MultiPanelController.swift in Sources */,
546341A125C6415100CA0596 /* UseCase.swift in Sources */,
@@ -257,6 +276,7 @@
54EAD365263A765F006A36EA /* PagePanelController.swift in Sources */,
5442E24425FC538200A26F43 /* InspectorViewController.swift in Sources */,
5442E22C25FC521F00A26F43 /* SettingsViewController.swift in Sources */,
54F185842BF4B82E00916F57 /* UnavailableViewController.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -1,9 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="21507" 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="32700.99.1234" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="RoN-h0-uBD">
<device id="retina6_1" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="21505"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="22685"/>
<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"/>
@@ -383,22 +383,22 @@
<objects>
<viewController storyboardIdentifier="ModalViewController" id="bYI-y3-Rzb" customClass="ModalViewController" customModule="Samples" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="qwo-GK-p1U">
<rect key="frame" x="0.0" y="0.0" width="375" height="720"/>
<rect key="frame" x="0.0" y="0.0" width="375" height="768"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="vut-mK-Y4t" customClass="SafeAreaView" customModule="Samples" customModuleProvider="target">
<rect key="frame" x="0.0" y="720" width="375" height="0.0"/>
<rect key="frame" x="0.0" y="768" width="375" height="0.0"/>
<color key="backgroundColor" white="0.66666666666666663" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</view>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="sbF-Az-7sy">
<rect key="frame" x="20" y="48" width="39" height="30"/>
<rect key="frame" x="20" y="0.0" width="39" height="30"/>
<state key="normal" title="Close"/>
<connections>
<action selector="closeWithSender:" destination="bYI-y3-Rzb" eventType="touchUpInside" id="MSC-ch-YJK"/>
</connections>
</button>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" alignment="top" spacing="44" translatesAutoresizingMaskIntoConstraints="NO" id="9p4-06-y2T">
<rect key="frame" x="134.5" y="136" width="106" height="326"/>
<rect key="frame" x="134.5" y="88" width="106" height="326"/>
<subviews>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="i9x-x5-n1q">
<rect key="frame" x="0.0" y="0.0" width="80" height="30"/>
@@ -598,7 +598,7 @@
</constraints>
</view>
<view alpha="0.5" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="Kva-Z7-0qY" customClass="OnSafeAreaView" customModule="Samples" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="375" height="778"/>
<rect key="frame" x="0.0" y="0.0" width="375" height="744"/>
<color key="backgroundColor" red="0.0078431372550000003" green="0.72156862749999995" blue="0.45882352939999999" alpha="1" colorSpace="calibratedRGB"/>
</view>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="noi-1a-5bZ" customClass="CloseButton" customModule="Samples" customModuleProvider="target">
@@ -761,14 +761,33 @@ Section 1.10.33 of "de Finibus Bonorum et Malorum", written by Cicero in 45 BC
<fontDescription key="fontDescription" name="CourierNewPSMT" family="Courier New" pointSize="14"/>
<textInputTraits key="textInputTraits" autocapitalizationType="sentences"/>
</textView>
<stackView opaque="NO" contentMode="scaleToFill" distribution="fillProportionally" alignment="center" spacing="8" translatesAutoresizingMaskIntoConstraints="NO" id="wUo-kb-NIn">
<rect key="frame" x="159" y="16" width="200" height="31"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Expand top margin" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="OC3-od-ldC">
<rect key="frame" x="0.0" y="5.5" width="143" height="20.5"/>
<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="XFC-sq-pWj">
<rect key="frame" x="151" y="0.0" width="51" height="31"/>
<connections>
<action selector="toggleTopMargin:" destination="tvD-nO-QUb" eventType="valueChanged" id="XWo-eX-0Jn"/>
</connections>
</switch>
</subviews>
</stackView>
</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"/>
<constraint firstItem="5ET-zC-lCb" firstAttribute="trailing" secondItem="wUo-kb-NIn" secondAttribute="trailing" constant="16" id="CtG-H5-tAI"/>
<constraint firstAttribute="bottom" secondItem="rN1-HL-YHv" secondAttribute="bottom" id="efD-U5-Tet"/>
<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"/>
<constraint firstItem="wUo-kb-NIn" firstAttribute="top" secondItem="9YG-0j-Zzg" secondAttribute="top" constant="16" id="ogC-1W-upw"/>
</constraints>
</view>
<size key="freeformSize" width="375" height="778"/>
@@ -780,12 +799,12 @@ Section 1.10.33 of "de Finibus Bonorum et Malorum", written by Cicero in 45 BC
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="x1h-y1-h8q" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="-1" y="734"/>
<point key="canvasLocation" x="-2.1739130434782612" y="733.92857142857144"/>
</scene>
<!--Adaptive Layout Test View Controller-->
<!--Table View Controller For Adaptive Layout-->
<scene sceneID="rDI-lU-wEx">
<objects>
<viewController storyboardIdentifier="AdaptiveLayoutTestViewController" id="5nC-6E-bXf" customClass="AdaptiveLayoutTestViewController" customModule="Samples" customModuleProvider="target" sceneMemberID="viewController">
<viewController storyboardIdentifier="TableViewControllerForAdaptiveLayout" id="5nC-6E-bXf" customClass="TableViewControllerForAdaptiveLayout" customModule="Samples" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="jXL-Ss-NCJ">
<rect key="frame" x="0.0" y="0.0" width="375" height="778"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
@@ -842,13 +861,13 @@ Section 1.10.33 of "de Finibus Bonorum et Malorum", written by Cicero in 45 BC
<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"/>
<color red="1" green="0.58431372550000005" 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"/>
<color red="0.68627450980000004" green="0.32156862749999998" blue="0.87058823529999996" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</systemColor>
<systemColor name="systemTealColor">
<color red="0.18823529411764706" green="0.69019607843137254" blue="0.7803921568627451" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<color red="0.18823529410000001" green="0.69019607839999997" blue="0.78039215689999997" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</systemColor>
</resources>
</document>
@@ -0,0 +1,175 @@
// Copyright 2018-Present Shin Yamamoto. All rights reserved. MIT license.
import UIKit
import FloatingPanel
@available(iOS 13.0, *)
class CollectionViewControllerForAdaptiveLayout: UIViewController {
class PanelLayout: FloatingPanelLayout {
let position: FloatingPanelPosition = .bottom
let initialState: FloatingPanelState = .full
private unowned var targetGuide: UILayoutGuide
init(targetGuide: UILayoutGuide) {
self.targetGuide = targetGuide
}
var anchors: [FloatingPanelState : FloatingPanelLayoutAnchoring] {
return [
.full: FloatingPanelAdaptiveLayoutAnchor(
absoluteOffset: 0.0,
contentLayout: targetGuide,
referenceGuide: .superview,
contentBoundingGuide: .safeArea
),
.half: FloatingPanelAdaptiveLayoutAnchor(
fractionalOffset: 0.5,
contentLayout: targetGuide,
referenceGuide: .superview,
contentBoundingGuide: .safeArea
),
]
}
}
enum LayoutType {
case flow
case compositional
}
weak var collectionView: UICollectionView!
var layoutType: LayoutType = .flow
override func viewDidLoad() {
super.viewDidLoad()
setupCollectionView()
}
private func setupCollectionView() {
let collectionViewLayout = {
switch layoutType {
case .flow:
CollectionViewLayoutFactory.flowLayout
case .compositional:
CollectionViewLayoutFactory.compositionalLayout
}
}()
let collectionView = IntrinsicCollectionView(
frame: .zero,
collectionViewLayout: collectionViewLayout
)
collectionView.delegate = self
collectionView.dataSource = self
collectionView.backgroundColor = .yellow
collectionView.register(Cell.self, forCellWithReuseIdentifier: Cell.reuseIdentifier)
view.addSubview(collectionView)
self.collectionView = collectionView
collectionView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
collectionView.topAnchor.constraint(equalTo: view.topAnchor),
collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
collectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
collectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor)
])
}
}
@available(iOS 13.0, *)
extension CollectionViewControllerForAdaptiveLayout: UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 5 // Only three cells needed to fill the space
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: Cell.reuseIdentifier, for: indexPath) as! Cell
cell.configure(text: "Item \(indexPath.row)")
return cell
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let width = collectionView.frame.width
return CGSize(width: width, height: 100)
}
}
@available(iOS 13.0, *)
extension CollectionViewControllerForAdaptiveLayout {
enum CollectionViewLayoutFactory {
static var flowLayout: UICollectionViewLayout {
let layout = UICollectionViewFlowLayout()
layout.scrollDirection = .vertical
layout.minimumLineSpacing = 8 // Vertical spacing between rows
return layout
}
@available(iOS 13.0, *)
static var compositionalLayout: UICollectionViewLayout {
UICollectionViewCompositionalLayout { (sectionIndex: Int, layoutEnvironment: NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection? in
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .absolute(100))
let item = NSCollectionLayoutItem(layoutSize: itemSize)
let group = NSCollectionLayoutGroup.horizontal(layoutSize: itemSize, subitems: [item])
let section = NSCollectionLayoutSection(group: group)
section.interGroupSpacing = 8 // Spacing between each group/item
section.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0)
return section
}
}
}
private final class Cell: UICollectionViewCell {
static let reuseIdentifier = "Cell"
private let label: UILabel = {
let label = UILabel()
label.textAlignment = .center
label.textColor = .white
return label
}()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
private func commonInit() {
backgroundColor = .systemBlue
addSubview(label)
label.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
label.centerXAnchor.constraint(equalTo: centerXAnchor),
label.centerYAnchor.constraint(equalTo: centerYAnchor)
])
}
func configure(text: String) {
label.text = text
}
}
private final class IntrinsicCollectionView: UICollectionView {
override public var contentSize: CGSize {
didSet {
invalidateIntrinsicContentSize()
}
}
override public var intrinsicContentSize: CGSize {
layoutIfNeeded()
return CGSize(width: UIView.noIntrinsicMetric, height: contentSize.height)
}
}
}
@@ -37,7 +37,6 @@ final class ImageViewController: UIViewController {
case withHeaderFooter
}
@available(iOS 11.0, *)
func layoutGuideFor(mode: Mode) -> UILayoutGuide {
switch mode {
case .onlyImage:
@@ -3,7 +3,7 @@
import UIKit
import FloatingPanel
final class AdaptiveLayoutTestViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
final class TableViewControllerForAdaptiveLayout: UIViewController, UITableViewDataSource, UITableViewDelegate {
class PanelLayout: FloatingPanelLayout {
let position: FloatingPanelPosition = .bottom
let initialState: FloatingPanelState = .full
@@ -75,7 +75,6 @@ final class AdaptiveLayoutTestViewController: UIViewController, UITableViewDataS
}
class IntrinsicTableView: UITableView {
override var contentSize:CGSize {
didSet {
invalidateIntrinsicContentSize()
@@ -0,0 +1,77 @@
// Copyright 2018 the FloatingPanel authors. All rights reserved. MIT license.
import UIKit
@available(iOS 14, *)
class DebugListCollectionViewController: UIViewController {
enum Section {
case main
}
var dataSource: UICollectionViewDiffableDataSource<Section, Int>! = nil
var collectionView: UICollectionView! = nil
override func viewDidLoad() {
super.viewDidLoad()
navigationItem.title = "List"
configureHierarchy()
configureDataSource()
}
}
@available(iOS 14, *)
extension DebugListCollectionViewController {
/// - Tag: List
private func createLayout() -> UICollectionViewLayout {
var config = UICollectionLayoutListConfiguration(appearance: .insetGrouped)
config.trailingSwipeActionsConfigurationProvider = { indexPath -> UISwipeActionsConfiguration? in
return UISwipeActionsConfiguration(
actions: [UIContextualAction(
style: .destructive,
title: "Delete",
handler: { _, _, completion in
// Do nothing now
}
)]
)
}
return UICollectionViewCompositionalLayout.list(using: config)
}
}
@available(iOS 14, *)
extension DebugListCollectionViewController {
private func configureHierarchy() {
collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: createLayout())
collectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
view.addSubview(collectionView)
collectionView.delegate = self
}
private func configureDataSource() {
let cellRegistration = UICollectionView.CellRegistration<UICollectionViewListCell, Int> { (cell, indexPath, item) in
var content = cell.defaultContentConfiguration()
content.text = "\(item)"
cell.contentConfiguration = content
}
dataSource = UICollectionViewDiffableDataSource<Section, Int>(collectionView: collectionView) {
(collectionView: UICollectionView, indexPath: IndexPath, identifier: Int) -> UICollectionViewCell? in
return collectionView.dequeueConfiguredReusableCell(using: cellRegistration, for: indexPath, item: identifier)
}
var snapshot = NSDiffableDataSourceSnapshot<Section, Int>()
snapshot.appendSections([.main])
snapshot.appendItems(Array(0..<94))
dataSource.apply(snapshot, animatingDifferences: false)
}
}
@available(iOS 14, *)
extension DebugListCollectionViewController: UICollectionViewDelegate {
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
collectionView.deselectItem(at: indexPath, animated: true)
}
}
@@ -11,9 +11,7 @@ final class DebugTextViewController: UIViewController, UITextViewDelegate {
textView.delegate = self
print("viewDidLoad: TextView --- ", textView.contentOffset, textView.contentInset)
if #available(iOS 11.0, *) {
textView.contentInsetAdjustmentBehavior = .never
}
textView.contentInsetAdjustmentBehavior = .never
}
override func viewWillLayoutSubviews() {
@@ -32,8 +30,14 @@ final class DebugTextViewController: UIViewController, UITextViewDelegate {
func scrollViewDidScroll(_ scrollView: UIScrollView) {
print("TextView --- ", scrollView.contentOffset, scrollView.contentInset)
if #available(iOS 11.0, *) {
print("TextView --- ", scrollView.adjustedContentInset)
print("TextView --- ", scrollView.adjustedContentInset)
}
@IBAction func toggleTopMargin(_ sender: UISwitch) {
if sender.isOn {
textViewTopConstraint.constant = 160
} else {
textViewTopConstraint.constant = 16
}
}
@@ -14,21 +14,17 @@ final class SettingsViewController: InspectableViewController {
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
if #available(iOS 11.0, *) {
let prefersLargeTitles = navigationController!.navigationBar.prefersLargeTitles
largeTitlesSwitch.setOn(prefersLargeTitles, animated: false)
} else {
largeTitlesSwitch.isEnabled = false
}
let prefersLargeTitles = navigationController!.navigationBar.prefersLargeTitles
largeTitlesSwitch.setOn(prefersLargeTitles, animated: false)
let isTranslucent = navigationController!.navigationBar.isTranslucent
translucentSwitch.setOn(isTranslucent, animated: false)
}
@IBAction func toggleLargeTitle(_ sender: UISwitch) {
if #available(iOS 11.0, *) {
navigationController?.navigationBar.prefersLargeTitles = sender.isOn
}
navigationController?.navigationBar.prefersLargeTitles = sender.isOn
}
@IBAction func toggleTranslucent(_ sender: UISwitch) {
// White non-translucent navigation bar, supports dark appearance
if #available(iOS 15, *) {
@@ -238,13 +238,8 @@ class ThreeTabBarPanelLayout: FloatingPanelLayout {
}
func prepareLayout(surfaceView: UIView, in view: UIView) -> [NSLayoutConstraint] {
if #available(iOS 11.0, *) {
leftConstraint = surfaceView.leftAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leftAnchor, constant: 0.0)
rightConstraint = surfaceView.rightAnchor.constraint(equalTo: view.safeAreaLayoutGuide.rightAnchor, constant: 0.0)
} else {
leftConstraint = surfaceView.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 0.0)
rightConstraint = surfaceView.rightAnchor.constraint(equalTo: view.rightAnchor, constant: 0.0)
}
leftConstraint = surfaceView.leftAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leftAnchor, constant: 0.0)
rightConstraint = surfaceView.rightAnchor.constraint(equalTo: view.safeAreaLayoutGuide.rightAnchor, constant: 0.0)
return [ leftConstraint, rightConstraint ]
}
}
@@ -0,0 +1,24 @@
// Copyright 2018 the FloatingPanel authors. All rights reserved. MIT license.
import UIKit
class UnavailableViewController: UIViewController {
weak var label: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
let label = UILabel()
label.text = "Unavailable content"
label.numberOfLines = 0
label.textAlignment = .center
label.frame = view.bounds
label.autoresizingMask = [
.flexibleTopMargin,
.flexibleLeftMargin,
.flexibleBottomMargin,
.flexibleRightMargin
]
view.addSubview(label)
self.label = label
}
}
+2 -14
View File
@@ -33,22 +33,10 @@ class CustomLayoutGuide: LayoutGuideProvider {
extension UIViewController {
var layoutInsets: UIEdgeInsets {
if #available(iOS 11.0, *) {
return view.safeAreaInsets
} else {
return UIEdgeInsets(top: topLayoutGuide.length,
left: 0.0,
bottom: bottomLayoutGuide.length,
right: 0.0)
}
return view.safeAreaInsets
}
var layoutGuide: LayoutGuideProvider {
if #available(iOS 11.0, *) {
return view!.safeAreaLayoutGuide
} else {
return CustomLayoutGuide(topAnchor: topLayoutGuide.bottomAnchor,
bottomAnchor: bottomLayoutGuide.topAnchor)
}
return view.safeAreaLayoutGuide
}
}
@@ -15,14 +15,11 @@ extension MainViewController {
tableView.dataSource = self
tableView.delegate = self
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "Cell")
automaticallyAdjustsScrollViewInsets = false
let searchController = UISearchController(searchResultsController: nil)
if #available(iOS 11.0, *) {
navigationItem.searchController = searchController
navigationItem.hidesSearchBarWhenScrolling = false
navigationItem.largeTitleDisplayMode = .automatic
}
navigationItem.searchController = searchController
navigationItem.hidesSearchBarWhenScrolling = false
navigationItem.largeTitleDisplayMode = .automatic
var insets = UIEdgeInsets.zero
insets.bottom += 69.0
tableView.contentInset = insets
@@ -33,12 +30,10 @@ extension MainViewController {
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
if #available(iOS 11.0, *) {
if let observation = navigationController?.navigationBar.observe(\.prefersLargeTitles, changeHandler: { (bar, _) in
self.tableView.reloadData()
}) {
observations.append(observation)
}
if let observation = navigationController?.navigationBar.observe(\.prefersLargeTitles, changeHandler: { (bar, _) in
self.tableView.reloadData()
}) {
observations.append(observation)
}
}
@@ -56,12 +51,8 @@ extension MainViewController {
extension MainViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if #available(iOS 11.0, *) {
if navigationController?.navigationBar.prefersLargeTitles == true {
return UseCase.allCases.count + 30
} else {
return UseCase.allCases.count
}
if navigationController?.navigationBar.prefersLargeTitles == true {
return UseCase.allCases.count + 30
} else {
return UseCase.allCases.count
}
@@ -76,3 +76,18 @@ class ModalPanelLayout: FloatingPanelLayout {
return 0.3
}
}
class ModalPanelLayout2: FloatingPanelLayout {
let position: FloatingPanelPosition = .bottom
let initialState: FloatingPanelState = .half
var anchors: [FloatingPanelState: FloatingPanelLayoutAnchoring] {
[
.full: FloatingPanelLayoutAnchor(fractionalInset: 0.0, edge: .top, referenceGuide: .superview),
.half: FloatingPanelLayoutAnchor(fractionalInset: 0.5, edge: .bottom, referenceGuide: .superview)
]
}
func backdropAlpha(for _: FloatingPanelState) -> CGFloat {
0.6
}
}
@@ -5,9 +5,11 @@ import UIKit
enum UseCase: Int, CaseIterable {
case trackingTableView
case trackingTextView
case trackingCollectionViewList
case showDetail
case showModal
case showPanelModal
case showPanelModal2
case showMultiPanelModal
case showPanelInSheetModal
case showOnWindow
@@ -22,18 +24,23 @@ enum UseCase: Int, CaseIterable {
case showNavigationController
case showTopPositionedPanel
case showAdaptivePanel
case showAdaptivePanelWithCustomGuide
case showAdaptivePanelWithTableView
case showAdaptivePanelWithCollectionView
case showAdaptivePanelWithCompositionalCollectionView
case showCustomStatePanel
case showCustomBackdrop
}
extension UseCase {
var name: String {
switch self {
case .trackingTableView: return "Scroll tracking(TableView)"
case .trackingCollectionViewList: return "Scroll tracking(List CollectionView)"
case .trackingTextView: return "Scroll tracking(TextView)"
case .showDetail: return "Show Detail Panel"
case .showModal: return "Show Modal"
case .showPanelModal: return "Show Panel Modal"
case .showPanelModal2: return "Show Panel Modal 2"
case .showMultiPanelModal: return "Show Multi Panel Modal"
case .showOnWindow: return "Show Panel over Window"
case .showPanelInSheetModal: return "Show Panel in Sheet Modal"
@@ -48,8 +55,11 @@ extension UseCase {
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 .showAdaptivePanelWithTableView: return "Show Adaptive Panel (TableView)"
case .showAdaptivePanelWithCollectionView: return "Show Adaptive Panel (CollectionView)"
case .showAdaptivePanelWithCompositionalCollectionView: return "Show Adaptive Panel (Compositional CollectionView)"
case .showCustomStatePanel: return "Show Panel with Custom state"
case .showCustomBackdrop: return "Show Panel with Custom Backdrop"
}
}
}
@@ -63,13 +73,21 @@ extension UseCase {
private var content: Content {
switch self {
case .trackingTableView: return .viewController(DebugTableViewController())
case .trackingCollectionViewList:
if #available(iOS 14, *) {
return .viewController(DebugListCollectionViewController())
} else {
let msg = "UICollectionLayoutListConfiguration is unavailable.\nBuild this app on iOS 14 and later."
return makeUnavailableViewContent(message: msg)
}
case .trackingTextView: return .storyboard("ConsoleViewController") // Storyboard only
case .showDetail: return .storyboard(String(describing: DetailViewController.self))
case .showModal: return .storyboard(String(describing: ModalViewController.self))
case .showPanelModal: return .viewController(DebugTableViewController())
case .showPanelModal2: return .storyboard("ConsoleViewController")
case .showMultiPanelModal: return .viewController(DebugTableViewController())
case .showOnWindow: return .viewController(DebugTableViewController())
case .showPanelInSheetModal: return .viewController(DebugTableViewController())
case .showPanelModal: return .viewController(DebugTableViewController())
case .showTabBar: return .storyboard(String(describing: TabBarViewController.self))
case .showPageView: return .viewController(DebugTableViewController())
case .showPageContentView: return .viewController(DebugTableViewController())
@@ -81,8 +99,19 @@ extension UseCase {
case .showNavigationController: return .storyboard("RootNavigationController") // Storyboard only
case .showTopPositionedPanel: return .viewController(DebugTableViewController())
case .showAdaptivePanel: return .storyboard(String(describing: ImageViewController.self))
case .showAdaptivePanelWithCustomGuide: return .storyboard(String(describing: AdaptiveLayoutTestViewController.self))
case .showAdaptivePanelWithTableView: return .storyboard(String(describing: TableViewControllerForAdaptiveLayout.self))
case .showAdaptivePanelWithCollectionView,
.showAdaptivePanelWithCompositionalCollectionView:
if #available(iOS 13, *) {
let vc = CollectionViewControllerForAdaptiveLayout()
vc.layoutType = self == .showAdaptivePanelWithCollectionView ? .flow : .compositional
return .viewController(vc)
} else {
let msg = "Compositional layout is unavailable.\nBuild this app on iOS 13 and later."
return makeUnavailableViewContent(message: msg)
}
case .showCustomStatePanel: return .viewController(DebugTableViewController())
case .showCustomBackdrop: return .viewController(UIViewController())
}
}
@@ -95,4 +124,11 @@ extension UseCase {
return vc
}
}
private func makeUnavailableViewContent(message: String) -> Content {
let vc = UnavailableViewController()
vc.loadViewIfNeeded()
vc.label.text = message
return .viewController(vc)
}
}
@@ -52,6 +52,30 @@ extension UseCaseController {
fpc.ext_trackScrollView(in: contentVC)
addMain(panel: fpc)
case .trackingCollectionViewList:
let fpc = FloatingPanelController()
fpc.delegate = self
fpc.contentInsetAdjustmentBehavior = .always
fpc.surfaceView.appearance = {
let appearance = SurfaceAppearance()
appearance.cornerRadius = 6.0
return appearance
}()
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(UseCaseController.handleSurface(tapGesture:)))
tapGesture.cancelsTouchesInView = false
tapGesture.numberOfTapsRequired = 2
// Prevents a delay to response a tap in menus of DebugTableViewController.
tapGesture.delaysTouchesEnded = false
fpc.surfaceView.addGestureRecognizer(tapGesture)
fpc.set(contentViewController: contentVC)
if #available(iOS 14, *),
let scrollView = (fpc.contentViewController as? DebugListCollectionViewController)?.collectionView {
fpc.track(scrollView: scrollView)
}
addMain(panel: fpc)
case .trackingTextView:
let fpc = FloatingPanelController()
fpc.delegate = self
@@ -154,6 +178,14 @@ extension UseCaseController {
mainVC.present(fpc, animated: true, completion: nil)
case .showPanelModal2:
let fpc = FloatingPanelController()
fpc.set(contentViewController: contentVC)
fpc.delegate = self
fpc.track(scrollView: (contentVC as? DebugTextViewController)!.textView)
mainVC.present(fpc, animated: true, completion: nil)
case .showMultiPanelModal:
let fpc = MultiPanelController()
mainVC.present(fpc, animated: true, completion: nil)
@@ -161,6 +193,7 @@ extension UseCaseController {
case .showOnWindow:
let fpc = overWindowPanelVC
fpc.backdropView.dismissalTapGestureRecognizer.isEnabled = true
fpc.isRemovalInteractionEnabled = true
fpc.set(contentViewController: contentVC)
fpc.ext_trackScrollView(in: contentVC)
@@ -177,10 +210,10 @@ extension UseCaseController {
fpc.set(contentViewController: contentVC)
fpc.delegate = self
let apprearance = SurfaceAppearance()
apprearance.cornerRadius = 38.5
apprearance.shadows = []
fpc.surfaceView.appearance = apprearance
let appearance = SurfaceAppearance()
appearance.cornerRadius = 38.5
appearance.shadows = []
fpc.surfaceView.appearance = appearance
fpc.isRemovalInteractionEnabled = true
let mvc = UIViewController()
@@ -238,13 +271,13 @@ extension UseCaseController {
fpc.set(contentViewController: contentVC)
fpc.ext_trackScrollView(in: contentVC)
if case let contentVC as ImageViewController = contentVC {
let mode: ImageViewController.Mode = (useCase == .showAdaptivePanelWithCustomGuide) ? .withHeaderFooter : .onlyImage
let mode: ImageViewController.Mode = (useCase == .showAdaptivePanelWithTableView) ? .withHeaderFooter : .onlyImage
let layoutGuide = contentVC.layoutGuideFor(mode: mode)
fpc.layout = ImageViewController.PanelLayout(targetGuide: layoutGuide)
}
addMain(panel: fpc)
case .showAdaptivePanelWithCustomGuide:
case .showAdaptivePanelWithTableView:
let fpc = FloatingPanelController()
fpc.isRemovalInteractionEnabled = true
fpc.contentInsetAdjustmentBehavior = .always
@@ -254,10 +287,25 @@ extension UseCaseController {
return appearance
}()
fpc.set(contentViewController: contentVC)
fpc.ext_trackScrollView(in: contentVC)
fpc.layout = AdaptiveLayoutTestViewController.PanelLayout(targetGuide: contentVC.view.makeBoundsLayoutGuide())
fpc.track(scrollView: (contentVC as! TableViewControllerForAdaptiveLayout).tableView)
fpc.layout = TableViewControllerForAdaptiveLayout.PanelLayout(targetGuide: contentVC.view.makeBoundsLayoutGuide())
addMain(panel: fpc)
case .showAdaptivePanelWithCollectionView, .showAdaptivePanelWithCompositionalCollectionView:
let fpc = FloatingPanelController()
fpc.isRemovalInteractionEnabled = true
fpc.contentInsetAdjustmentBehavior = .always
fpc.surfaceView.appearance = {
let appearance = SurfaceAppearance()
appearance.cornerRadius = 6.0
return appearance
}()
fpc.set(contentViewController: contentVC)
if #available(iOS 13, *) {
fpc.track(scrollView: (contentVC as! CollectionViewControllerForAdaptiveLayout).collectionView)
fpc.layout = CollectionViewControllerForAdaptiveLayout.PanelLayout(targetGuide: contentVC.view.makeBoundsLayoutGuide())
}
addMain(panel: fpc)
case .showCustomStatePanel:
@@ -272,6 +320,52 @@ extension UseCaseController {
fpc.set(contentViewController: contentVC)
fpc.ext_trackScrollView(in: contentVC)
addMain(panel: fpc)
case .showCustomBackdrop:
class BlurBackdropView: BackdropView {
var effectView: UIVisualEffectView!
override var alpha: CGFloat {
set {
effectView.alpha = newValue
}
get {
effectView.alpha
}
}
override init() {
super.init()
let effect = UIBlurEffect(style: .prominent)
let effectView = UIVisualEffectView(effect: effect)
addSubview(effectView)
effectView.frame = bounds
effectView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
self.effectView = effectView
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
class CustomBottomLayout: FloatingPanelBottomLayout {
override var anchors: [FloatingPanelState: FloatingPanelLayoutAnchoring] {
return [
.full: FloatingPanelLayoutAnchor(absoluteInset: 100.0, edge: .top, referenceGuide: .safeArea),
.half: FloatingPanelLayoutAnchor(fractionalInset: 0.5, edge: .bottom, referenceGuide: .safeArea),
.tip: FloatingPanelLayoutAnchor(fractionalInset: 0.1, edge: .bottom, referenceGuide: .safeArea),
]
}
override func backdropAlpha(for state: FloatingPanelState) -> CGFloat {
return state == .full ? 0.8 : 0.0
}
}
let fpc = FloatingPanelController()
fpc.delegate = self
fpc.set(contentViewController: contentVC)
fpc.backdropView = BlurBackdropView()
fpc.layout = CustomBottomLayout()
addMain(panel: fpc)
}
}
@@ -321,8 +415,16 @@ extension UseCaseController {
}
extension UseCaseController: FloatingPanelControllerDelegate {
func floatingPanel(
_ fpc: FloatingPanelController,
shouldAllowToScroll scrollView: UIScrollView,
in state: FloatingPanelState
) -> Bool {
return state == .full || state == .half
}
func floatingPanel(_ vc: FloatingPanelController, contentOffsetForPinning trackingScrollView: UIScrollView) -> CGPoint {
if useCase == .showNavigationController, #available(iOS 11.0, *) {
if useCase == .showNavigationController {
// 148.0 is the SafeArea's top value for a navigation bar with a large title.
return CGPoint(x: 0.0, y: 0.0 - trackingScrollView.contentInset.top - 148.0)
}
@@ -341,6 +443,8 @@ extension UseCaseController: FloatingPanelControllerDelegate {
return newCollection.verticalSizeClass == .compact ? RemovablePanelLandscapeLayout() : RemovablePanelLayout()
case .showIntrinsicView:
return IntrinsicPanelLayout()
case .showPanelModal2:
return ModalPanelLayout2()
case .showPanelModal:
if vc != mainPanelVC && vc != detailPanelVC {
return ModalPanelLayout()
@@ -403,7 +507,7 @@ private extension FloatingPanelController {
case let contentVC as ImageViewController:
track(scrollView: contentVC.scrollView)
case let contentVC as AdaptiveLayoutTestViewController:
case let contentVC as TableViewControllerForAdaptiveLayout:
track(scrollView: contentVC.tableView)
default:
@@ -15,7 +15,6 @@
545BA71421BA3217007F7846 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 545BA71321BA3217007F7846 /* main.m */; };
545BA72621BA3BAF007F7846 /* FloatingPanel.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 545BA72521BA3BAF007F7846 /* FloatingPanel.framework */; };
545BA72721BA3BAF007F7846 /* FloatingPanel.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 545BA72521BA3BAF007F7846 /* FloatingPanel.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
5D82A6B028D18447006A44BA /* libswiftCoreGraphics.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 5D82A6AF28D18443006A44BA /* libswiftCoreGraphics.tbd */; };
/* End PBXBuildFile section */
/* Begin PBXCopyFilesBuildPhase section */
@@ -53,7 +52,6 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
5D82A6B028D18447006A44BA /* libswiftCoreGraphics.tbd in Frameworks */,
545BA72621BA3BAF007F7846 /* FloatingPanel.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
@@ -81,7 +81,7 @@ class MainViewController: UIViewController, FloatingPanelControllerDelegate {
}
private func hideStockTickerBanner() {
// Dimiss top bar with dissolve animation
// Dismiss top bar with dissolve animation
UIView.animate(withDuration: 0.25) {
self.topBannerView.alpha = 0.0
self.labelStackView.alpha = 1.0
+5 -5
View File
@@ -1,18 +1,18 @@
Pod::Spec.new do |s|
s.name = "FloatingPanel"
s.version = "2.6.6"
s.version = "2.8.7"
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.
The new interface displays the related contents and utilities in parallel as a user wants.
DESC
s.homepage = "https://github.com/SCENEE/FloatingPanel"
s.homepage = "https://github.com/scenee/FloatingPanel"
s.author = "Shin Yamamoto"
s.social_media_url = "https://twitter.com/scenee"
s.social_media_url = "https://x.com/scenee"
s.platform = :ios, "10.0"
s.source = { :git => "https://github.com/SCENEE/FloatingPanel.git", :tag => s.version.to_s }
s.platform = :ios, "11.0"
s.source = { :git => "https://github.com/scenee/FloatingPanel.git", :tag => s.version.to_s }
s.source_files = "Sources/*.swift"
s.swift_version = '5.0'
+17 -7
View File
@@ -21,6 +21,7 @@
5469F4B024B30E1500537F8A /* LayoutAnchoring.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5469F4AF24B30E1500537F8A /* LayoutAnchoring.swift */; };
5469F4B224B30F1100537F8A /* Position.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5469F4B124B30F1100537F8A /* Position.swift */; };
5469F4B424B30F3500537F8A /* LayoutProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5469F4B324B30F3500537F8A /* LayoutProperties.swift */; };
547F7A9C2A6E946000303905 /* GestureTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 547F7A9B2A6E946000303905 /* GestureTests.swift */; };
549C371F2361E15E007D8058 /* ExtensionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 549C371E2361E15D007D8058 /* ExtensionTests.swift */; };
549E944522CF295D0050AECF /* StateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 549E944422CF295D0050AECF /* StateTests.swift */; };
54A6B6B122968B530077F348 /* CoreTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54A6B6B022968B530077F348 /* CoreTests.swift */; };
@@ -63,6 +64,7 @@
5469F4AF24B30E1500537F8A /* LayoutAnchoring.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LayoutAnchoring.swift; sourceTree = "<group>"; };
5469F4B124B30F1100537F8A /* Position.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Position.swift; sourceTree = "<group>"; };
5469F4B324B30F3500537F8A /* LayoutProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LayoutProperties.swift; sourceTree = "<group>"; };
547F7A9B2A6E946000303905 /* GestureTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GestureTests.swift; sourceTree = "<group>"; };
549C371E2361E15D007D8058 /* ExtensionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExtensionTests.swift; sourceTree = "<group>"; };
549E944422CF295D0050AECF /* StateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StateTests.swift; sourceTree = "<group>"; };
54A6B6B022968B530077F348 /* CoreTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreTests.swift; sourceTree = "<group>"; };
@@ -146,6 +148,7 @@
children = (
54A6B6B022968B530077F348 /* CoreTests.swift */,
545DB9CF2151169500CA77B8 /* ControllerTests.swift */,
547F7A9B2A6E946000303905 /* GestureTests.swift */,
542753C522C49A6E00D17955 /* LayoutTests.swift */,
54A6B6B72296A8520077F348 /* SurfaceViewTests.swift */,
549E944422CF295D0050AECF /* StateTests.swift */,
@@ -304,6 +307,7 @@
542753C622C49A6E00D17955 /* LayoutTests.swift in Sources */,
54A6B6B82296A8520077F348 /* SurfaceViewTests.swift in Sources */,
546055BF2333C4740069F400 /* TestSupports.swift in Sources */,
547F7A9C2A6E946000303905 /* GestureTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -458,12 +462,13 @@
DYLIB_INSTALL_NAME_BASE = "@rpath";
INFOPLIST_FILE = Sources/Info.plist;
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
IPHONEOS_DEPLOYMENT_TARGET = 10.0;
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@loader_path/Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = "$(RECOMMENDED_MACOSX_DEPLOYMENT_TARGET)";
PRODUCT_BUNDLE_IDENTIFIER = com.scenee.FloatingPanel;
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
SKIP_INSTALL = YES;
@@ -489,12 +494,13 @@
DYLIB_INSTALL_NAME_BASE = "@rpath";
INFOPLIST_FILE = Sources/Info.plist;
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
IPHONEOS_DEPLOYMENT_TARGET = 10.0;
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@loader_path/Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = "$(RECOMMENDED_MACOSX_DEPLOYMENT_TARGET)";
PRODUCT_BUNDLE_IDENTIFIER = com.scenee.FloatingPanel;
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
SKIP_INSTALL = YES;
@@ -511,12 +517,13 @@
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = "";
INFOPLIST_FILE = Tests/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 10.0;
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@loader_path/Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = "$(RECOMMENDED_MACOSX_DEPLOYMENT_TARGET)";
PRODUCT_BUNDLE_IDENTIFIER = com.scenee.FloatingPanelTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
@@ -531,12 +538,13 @@
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = "";
INFOPLIST_FILE = Tests/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 10.0;
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@loader_path/Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = "$(RECOMMENDED_MACOSX_DEPLOYMENT_TARGET)";
PRODUCT_BUNDLE_IDENTIFIER = com.scenee.FloatingPanelTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
@@ -623,16 +631,17 @@
DYLIB_INSTALL_NAME_BASE = "@rpath";
INFOPLIST_FILE = Sources/Info.plist;
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
IPHONEOS_DEPLOYMENT_TARGET = 10.0;
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@loader_path/Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = "$(RECOMMENDED_MACOSX_DEPLOYMENT_TARGET)";
PRODUCT_BUNDLE_IDENTIFIER = com.scenee.FloatingPanel;
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
SKIP_INSTALL = YES;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "TEST DEBUG __FP_LOG";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "TEST DEBUG FP_LOG";
SWIFT_COMPILATION_MODE = singlefile;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
@@ -647,12 +656,13 @@
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = "";
INFOPLIST_FILE = Tests/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 10.0;
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@loader_path/Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = "$(RECOMMENDED_MACOSX_DEPLOYMENT_TARGET)";
PRODUCT_BUNDLE_IDENTIFIER = com.scenee.FloatingPanelTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
+1 -1
View File
@@ -6,7 +6,7 @@ import PackageDescription
let package = Package(
name: "FloatingPanel",
platforms: [
.iOS(.v10)
.iOS(.v11)
],
products: [
// Products define the executables and libraries produced by a package, and make them visible to other packages.
+71 -77
View File
@@ -2,14 +2,13 @@
[![Platform](https://img.shields.io/cocoapods/p/FloatingPanel.svg)](https://cocoapods.org/pods/FloatingPanel)
[![Version](https://img.shields.io/cocoapods/v/FloatingPanel.svg)](https://cocoapods.org/pods/FloatingPanel)
![GitHub Workflow Status (with branch)](https://img.shields.io/github/actions/workflow/status/scenee/FloatingPanel/ci.yml?branch=master)
[![Carthage compatible](https://img.shields.io/badge/carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage)
# FloatingPanel
FloatingPanel is a simple and easy-to-use UI component designed for a user interface featured in Apple Maps, Shortcuts and Stocks app.
The user interface displays related content and utilities alongside the main content.
Please see also [the API reference](https://floatingpanel.github.io/2.6.6/documentation/floatingpanel/) for more details.
Please see also [the API reference@SPI](https://swiftpackageindex.com/scenee/FloatingPanel/2.8.7/documentation/floatingpanel) for more details.
![Maps](https://github.com/SCENEE/FloatingPanel/blob/master/assets/maps.gif)
![Stocks](https://github.com/SCENEE/FloatingPanel/blob/master/assets/stocks.gif)
@@ -21,47 +20,46 @@ Please see also [the API reference](https://floatingpanel.github.io/2.6.6/docume
- [Features](#features)
- [Requirements](#requirements)
- [Installation](#installation)
- [CocoaPods](#cocoapods)
- [Carthage](#carthage)
- [Swift Package Manager](#swift-package-manager)
- [CocoaPods](#cocoapods)
- [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)
- [Add a floating panel as a child view controller](#add-a-floating-panel-as-a-child-view-controller)
- [Present a floating panel as a modality](#present-a-floating-panel-as-a-modality)
- [View hierarchy](#view-hierarchy)
- [Usage](#usage)
- [Show/Hide a floating panel in a view with your view hierarchy](#showhide-a-floating-panel-in-a-view-with-your-view-hierarchy)
- [Scale the content view when the surface position changes](#scale-the-content-view-when-the-surface-position-changes)
- [Customize the layout with `FloatingPanelLayout` protocol](#customize-the-layout-with-floatingpanellayout-protocol)
- [Change the initial 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)
- [Manage the projection of a pan gesture momentum](#manage-the-projection-of-a-pan-gesture-momentum)
- [Specify the panel move's boundary](#specify-the-panel-moves-boundary)
- [Customize the surface design](#customize-the-surface-design)
- [Modify your surface appearance](#modify-your-surface-appearance)
- [Use a custom grabber handle](#use-a-custom-grabber-handle)
- [Customize layout of the grabber handle](#customize-layout-of-the-grabber-handle)
- [Customize content padding from surface edges](#customize-content-padding-from-surface-edges)
- [Customize margins of the surface edges](#customize-margins-of-the-surface-edges)
- [Customize gestures](#customize-gestures)
- [Suppress the panel interaction](#suppress-the-panel-interaction)
- [Add tap gestures to the surface view](#add-tap-gestures-to-the-surface-view)
- [Interrupt the delegate methods of `FloatingPanelController.panGestureRecognizer`](#interrupt-the-delegate-methods-of-floatingpanelcontrollerpangesturerecognizer)
- [Create an additional floating panel for a detail](#create-an-additional-floating-panel-for-a-detail)
- [Move a position with an animation](#move-a-position-with-an-animation)
- [Work your contents together with a floating panel behavior](#work-your-contents-together-with-a-floating-panel-behavior)
- [Enabling the tap-to-dismiss action of the backdrop view](#enabling-the-tap-to-dismiss-action-of-the-backdrop-view)
- [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)
- [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)
- [Manage the projection of a pan gesture momentum](#manage-the-projection-of-a-pan-gesture-momentum)
- [Specify the panel move's boundary](#specify-the-panel-moves-boundary)
- [Customize the surface design](#customize-the-surface-design)
- [Modify your surface appearance](#modify-your-surface-appearance)
- [Use a custom grabber handle](#use-a-custom-grabber-handle)
- [Customize layout of the grabber handle](#customize-layout-of-the-grabber-handle)
- [Customize content padding from surface edges](#customize-content-padding-from-surface-edges)
- [Customize margins of the surface edges](#customize-margins-of-the-surface-edges)
- [Customize gestures](#customize-gestures)
- [Suppress the panel interaction](#suppress-the-panel-interaction)
- [Add tap gestures to the surface view](#add-tap-gestures-to-the-surface-view)
- [Interrupt the delegate methods of `FloatingPanelController.panGestureRecognizer`](#interrupt-the-delegate-methods-of-floatingpanelcontrollerpangesturerecognizer)
- [Create an additional floating panel for a detail](#create-an-additional-floating-panel-for-a-detail)
- [Move a position with an animation](#move-a-position-with-an-animation)
- [Work your contents together with a floating panel behavior](#work-your-contents-together-with-a-floating-panel-behavior)
- [Enabling the tap-to-dismiss action of the backdrop view](#enabling-the-tap-to-dismiss-action-of-the-backdrop-view)
- [Allow to scroll content of the tracking scroll view in addition to the most expanded state](#allow-to-scroll-content-of-the-tracking-scroll-view-in-addition-to-the-most-expanded-state)
- [Notes](#notes)
- ['Show' or 'Show Detail' Segues from `FloatingPanelController`'s content view controller](#show-or-show-detail-segues-from-floatingpanelcontrollers-content-view-controller)
- [UISearchController issue](#uisearchcontroller-issue)
- [FloatingPanelSurfaceView's issue on iOS 10](#floatingpanelsurfaceviews-issue-on-ios-10)
- ['Show' or 'Show Detail' Segues from `FloatingPanelController`'s content view controller](#show-or-show-detail-segues-from-floatingpanelcontrollers-content-view-controller)
- [UISearchController issue](#uisearchcontroller-issue)
- [Maintainer](#maintainer)
- [License](#license)
@@ -93,10 +91,6 @@ Examples can be found here:
FloatingPanel is written in Swift 5.0+ and compatible with iOS 11.0+.
While it still supports iOS 10, it is recommended to use this library on iOS 11+.
:pencil2: If you'd like to use Swift 4.0, please use FloatingPanel v1.
## Installation
### CocoaPods
@@ -108,16 +102,6 @@ it, simply add the following line to your Podfile:
pod 'FloatingPanel'
```
:pencil2: FloatingPanel v1.7.0 or later requires CocoaPods v1.7.0+ for `swift_versions` support.
### Carthage
For [Carthage](https://github.com/Carthage/Carthage), add the following to your `Cartfile`:
```ogdl
github "scenee/FloatingPanel"
```
### Swift Package Manager
Follow [this doc](https://developer.apple.com/documentation/swift_packages/adding_package_dependencies_to_your_app).
@@ -168,7 +152,8 @@ 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 [Transitioning](https://github.com/SCENEE/FloatingPanel/blob/master/Sources/Transitioning.swift).
> [!NOTE]
> 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
@@ -252,7 +237,8 @@ fpc.contentMode = .fitToBounds
Otherwise, `FloatingPanelController` fixes the content by the height of the top most position.
:pencil2: In `.fitToBounds` mode, the surface height changes as following a user interaction so that you have a responsibility to configure Auto Layout constrains not to break the layout of a content view by the elastic surface height.
> [!NOTE]
> In `.fitToBounds` mode, the surface height changes as following a user interaction so that you have a responsibility to configure Auto Layout constrains not to break the layout of a content view by the elastic surface height.
### Customize the layout with `FloatingPanelLayout` protocol
@@ -350,7 +336,8 @@ class IntrinsicPanelLayout: FloatingPanelLayout {
}
```
:pencil2: `FloatingPanelIntrinsicLayout` is deprecated on v1.
> [!WARNING]
> `FloatingPanelIntrinsicLayout` is deprecated on v1.
#### Specify an anchor for each state by an inset of the `FloatingPanelController.view` frame
@@ -367,7 +354,8 @@ class MyFullScreenLayout: FloatingPanelLayout {
}
```
:pencil2: `FloatingPanelFullScreenLayout` is deprecated on v1.
> [!WARNING]
> `FloatingPanelFullScreenLayout` is deprecated on v1.
#### Change the backdrop alpha
@@ -425,13 +413,14 @@ class ViewController: UIViewController, FloatingPanelControllerDelegate {
class CustomPanelBehavior: FloatingPanelBehavior {
let springDecelerationRate = UIScrollView.DecelerationRate.fast.rawValue + 0.02
let springResponseTime = 0.4
func shouldProjectMomentum(_ fpc: FloatingPanelController, to proposedTargetPosition: FloatingPanelState) -> Bool {
func shouldProjectMomentum(_ fpc: FloatingPanelController, to proposedState: FloatingPanelState) -> Bool {
return true
}
}
```
:pencil2: `floatingPanel(_ vc:behaviorFor:)` is deprecated on v1.
> [!WARNING]
> `floatingPanel(_ vc:behaviorFor:)` is deprecated on v1.
#### Activate the rubber-band effect on panel edges
@@ -451,7 +440,7 @@ This allows full projectional panel behavior. For example, a user can swipe up a
```swift
class MyPanelBehavior: FloatingPanelBehavior {
...
func shouldProjectMomentum(_ fpc: FloatingPanelController, to proposedTargetPosition: FloatingPanelPosition) -> Bool {
func shouldProjectMomentum(_ fpc: FloatingPanelController, to proposedState: FloatingPanelPosition) -> Bool {
return true
}
}
@@ -473,7 +462,8 @@ func floatingPanelDidMove(_ vc: FloatingPanelController) {
}
```
:pencil2: `{top,bottom}InteractionBuffer` property is removed from `FloatingPanelLayout` since v2.
> [!WARNING]
> `{top,bottom}InteractionBuffer` property is removed from `FloatingPanelLayout` since v2.
### Customize the surface design
@@ -514,7 +504,8 @@ fpc.surfaceView.grabberHandlePadding = 10.0
fpc.surfaceView.grabberHandleSize = .init(width: 44.0, height: 12.0)
```
:pencil2: Note that `grabberHandleSize` width and height are reversed in the left/right position.
> [!NOTE]
> `grabberHandleSize` width and height are reversed in the left/right position.
#### Customize content padding from surface edges
@@ -663,6 +654,24 @@ The tap-to-dismiss action is disabled by default. So it needs to be enabled as b
fpc.backdropView.dismissalTapGestureRecognizer.isEnabled = true
```
### Allow to scroll content of the tracking scroll view in addition to the most expanded state
Just define conditions to allow content scrolling in `floatingPanel(:_:shouldAllowToScroll:in)` delegate method. If the returned value is true, the scroll content scrolls when its scroll position is not at the top of the content.
```swift
class MyViewController: FloatingPanelControllerDelegate {
...
func floatingPanel(
_ fpc: FloatingPanelController,
shouldAllowToScroll trackingScrollView: UIScrollView,
in state: FloatingPanelState
) -> Bool {
return state == .full || state == .half
}
}
```
## Notes
### 'Show' or 'Show Detail' Segues from `FloatingPanelController`'s content view controller
@@ -703,21 +712,6 @@ It's a great way to decouple between a floating panel and the content VC.
Because `UISearchController` automatically presents itself modally when a user interacts with the search bar, and then it swaps the superview of the search bar to the view managed by itself while it displays. As a result, `FloatingPanelController` can't control the search bar when it's active, as you can see from [the screen shot](https://github.com/SCENEE/FloatingPanel/issues/248#issuecomment-521263831).
### FloatingPanelSurfaceView's issue on iOS 10
* On iOS 10, `FloatingPanelSurfaceView.cornerRadius` isn't not automatically masked with the top rounded corners because of `UIVisualEffectView` issue. See https://forums.developer.apple.com/thread/50854.
So you need to draw top rounding corners of your content. Here is an example in Examples/Maps.
```swift
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
if #available(iOS 10, *) {
visualEffectView.layer.cornerRadius = 9.0
visualEffectView.clipsToBounds = true
}
}
```
* If you sets clear color to `FloatingPanelSurfaceView.backgroundColor`, please note the bottom overflow of your content on bouncing at full position. To prevent it, you need to expand your content. For example, See Example/Maps App's Auto Layout settings of `UIVisualEffectView` in Main.storyboard.
## Maintainer
Shin Yamamoto <shin@scenee.com> | [@scenee](https://twitter.com/scenee)
+3 -3
View File
@@ -4,7 +4,7 @@ import UIKit
/// A view that presents a backdrop interface behind a panel.
@objc(FloatingPanelBackdropView)
public class BackdropView: UIView {
open class BackdropView: UIView {
/// The gesture recognizer for tap gestures to dismiss a panel.
///
@@ -12,14 +12,14 @@ public class BackdropView: UIView {
/// To dismiss a panel by tap gestures on the backdrop, `dismissalTapGestureRecognizer.isEnabled` is set to true.
@objc public var dismissalTapGestureRecognizer: UITapGestureRecognizer
init() {
public init() {
dismissalTapGestureRecognizer = UITapGestureRecognizer()
dismissalTapGestureRecognizer.isEnabled = false
super.init(frame: .zero)
addGestureRecognizer(dismissalTapGestureRecognizer)
}
required init?(coder: NSCoder) {
required public init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
+8 -4
View File
@@ -25,12 +25,16 @@ public protocol FloatingPanelBehavior {
@objc optional
var momentumProjectionRate: CGFloat { get }
/// Asks the behavior if a 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 state.
///
/// 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`.
/// The default implementation of this method returns `false`. This method is called for called
/// for all states defined by the current layout object.
@objc optional
func shouldProjectMomentum(_ fpc: FloatingPanelController, to proposedTargetPosition: FloatingPanelState) -> Bool
func shouldProjectMomentum(
_ fpc: FloatingPanelController,
to proposedState: FloatingPanelState
) -> Bool
/// Returns the progress to redirect to the previous position.
///
+72 -45
View File
@@ -52,7 +52,12 @@ import os.log
/// Called on finger up if the user dragged.
///
/// If `attract` is true, it will continue moving afterwards to a nearby state anchor.
/// If `attract` is true, the panel continues moving towards the nearby state
/// anchor. Otherwise, it stops at the closest state anchor.
///
/// - Note: If `attract` is false, ``FloatingPanelController.state`` property has
/// already changed to the closest anchor's state by the time this delegate method
/// is called.
@objc optional
func floatingPanelDidEndDragging(_ fpc: FloatingPanelController, willAttract attract: Bool)
@@ -91,6 +96,35 @@ import os.log
@objc(floatingPanel:contentOffsetForPinningScrollView:)
optional
func floatingPanel(_ fpc: FloatingPanelController, contentOffsetForPinning trackingScrollView: UIScrollView) -> CGPoint
/// Returns a Boolean value that determines whether the tracking scroll view should
/// scroll or not
///
///
/// If you return true, the scroll content scrolls when its scroll position is not
/// at the top of the content. If the delegate doesnt implement this method, its
/// content can be scrolled only in the most expanded state.
///
/// Basically, the decision to scroll is based on the `state` property like the
/// following code.
/// ```swift
/// func floatingPanel(
/// _ fpc: FloatingPanelController,
/// shouldAllowToScroll scrollView: UIScrollView,
/// in state: FloatingPanelState
/// ) -> Bool {
/// return state == .full || state == .half
/// }
/// ```
///
/// - Attention: It is recommended that this method always returns the most expanded state(i.e.
/// .full). If it excludes the state, the panel might do unexpected behaviors.
@objc(floatingPanel:shouldAllowToScroll:in:)
optional func floatingPanel(
_ fpc: FloatingPanelController,
shouldAllowToScroll scrollView: UIScrollView,
in state: FloatingPanelState
) -> Bool
}
///
@@ -115,7 +149,7 @@ open class FloatingPanelController: UIViewController {
}
/// The delegate of a panel controller object.
@objc
@objc
public weak var delegate: FloatingPanelControllerDelegate?{
didSet{
didUpdateDelegate()
@@ -124,14 +158,15 @@ open class FloatingPanelController: UIViewController {
/// Returns the surface view managed by the controller object. It's the same as `self.view`.
@objc
public var surfaceView: SurfaceView! {
public var surfaceView: SurfaceView {
return floatingPanel.surfaceView
}
/// Returns the backdrop view managed by the controller object.
@objc
public var backdropView: BackdropView! {
return floatingPanel.backdropView
public var backdropView: BackdropView {
set { floatingPanel.backdropView = newValue }
get { return floatingPanel.backdropView }
}
/// Returns the scroll view that the controller tracks.
@@ -196,7 +231,7 @@ open class FloatingPanelController: UIViewController {
/// The behavior for determining the adjusted content offsets.
///
/// This property specifies how the content area of the tracking scroll view is modified using ``adjustedContentInsets``. The default value of this property is FloatingPanelController.ContentInsetAdjustmentBehavior.always.
@objc
@objc
public var contentInsetAdjustmentBehavior: ContentInsetAdjustmentBehavior = .always
/// A Boolean value that determines whether the removal interaction is enabled.
@@ -216,7 +251,7 @@ open class FloatingPanelController: UIViewController {
/// The NearbyState determines that finger's nearby state.
public var nearbyState: FloatingPanelState {
let currentY = surfaceLocation.y
return floatingPanel.targetPosition(from: currentY, with: .zero)
return floatingPanel.targetState(from: currentY, with: .zero)
}
/// Constants that define how a panel content fills in the surface.
@@ -296,19 +331,19 @@ open class FloatingPanelController: UIViewController {
open override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
if #available(iOS 11.0, *) {
// Ensure to update the static constraint of a panel after rotating a device in static mode
if contentMode == .static {
floatingPanel.layoutAdapter.updateStaticConstraint()
}
} else {
// Because {top,bottom}LayoutGuide is managed as a view
if floatingPanel.isAttracting == false {
self.update(safeAreaInsets: fp_safeAreaInsets)
}
// Ensure to update the static constraint of a panel after rotating a device in static mode
if contentMode == .static {
floatingPanel.layoutAdapter.updateStaticConstraint()
}
}
open override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
// Need to call this method just after the view appears, as the safe area is not
// correctly set before this time, for example, `show(animated:completion:)`.
floatingPanel.adjustScrollContentInsetIfNeeded()
}
open override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
@@ -390,6 +425,7 @@ open class FloatingPanelController: UIViewController {
}
floatingPanel.layoutAdapter.updateStaticConstraint()
floatingPanel.adjustScrollContentInsetIfNeeded()
if let contentOffset = contentOffset {
trackingScrollView?.contentOffset = contentOffset
@@ -420,6 +456,7 @@ open class FloatingPanelController: UIViewController {
} else if parent != nil {
removePanelFromParent(animated: true)
} else {
delegate?.floatingPanelWillRemove?(self)
hide(animated: true) { [weak self] in
guard let self = self else { return }
self.view.removeFromSuperview()
@@ -439,28 +476,23 @@ open class FloatingPanelController: UIViewController {
// Must apply the current layout here
activateLayout(forceLayout: true)
if #available(iOS 11.0, *) {
// Must track the safeAreaInsets of `self.view` to update the layout.
// There are 2 reasons.
// 1. This or the parent VC doesn't call viewSafeAreaInsetsDidChange() on the bottom
// inset's update expectedly.
// 2. The safe area top inset can be variable on the large title navigation bar(iOS11+).
// That's why it needs the observation to keep `adjustedContentInsets` correct.
safeAreaInsetsObservation = self.view.observe(\.safeAreaInsets, options: [.initial, .new, .old]) { [weak self] (_, change) in
// 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 }
// Must track the safeAreaInsets of `self.view` to update the layout.
// There are 2 reasons.
// 1. This or the parent VC doesn't call viewSafeAreaInsetsDidChange() on the bottom
// inset's update expectedly.
// 2. The safe area top inset can be variable on the large title navigation bar(iOS11+).
// That's why it needs the observation to keep `adjustedContentInsets` correct.
safeAreaInsetsObservation = self.view.observe(\.safeAreaInsets, options: [.initial, .new, .old]) { [weak self] (_, change) in
// 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 }
// Sometimes the bounding rectangle of the controlled view becomes invalid when the screen is rotated.
// This results in its safeAreaInsets change. In that case, `self.update(safeAreaInsets:)` leads
// an unsatisfied constraints error. So this method should not be called with those bounds.
guard self.view.bounds.height > 0 && self.view.bounds.width > 0 else { return }
// Sometimes the bounding rectangle of the controlled view becomes invalid when the screen is rotated.
// This results in its safeAreaInsets change. In that case, `self.update(safeAreaInsets:)` leads
// an unsatisfied constraints error. So this method should not be called with those bounds.
guard self.view.bounds.height > 0 && self.view.bounds.width > 0 else { return }
self.update(safeAreaInsets: self.view.safeAreaInsets)
}
} else {
// KVOs for topLayoutGuide & bottomLayoutGuide are not effective.
// Instead, update(safeAreaInsets:) is called at `viewDidLayoutSubviews()`
self.update(safeAreaInsets: self.view.safeAreaInsets)
}
move(to: floatingPanel.layoutAdapter.initialState,
@@ -592,18 +624,12 @@ open class FloatingPanelController: UIViewController {
switch contentInsetAdjustmentBehavior {
case .always:
if #available(iOS 11.0, *) {
scrollView.contentInsetAdjustmentBehavior = .never
} else {
children.forEach { (vc) in
vc.automaticallyAdjustsScrollViewInsets = false
}
}
scrollView.contentInsetAdjustmentBehavior = .never
default:
break
}
}
/// [Experimental] Allows the panel to move as its tracking scroll view bounces.
///
/// This method must be called in the delegate method, `UIScrollViewDelegate.scrollViewDidScroll(_:)`,
@@ -707,6 +733,7 @@ private var originalDismissImp: IMP?
private typealias DismissFunction = @convention(c) (AnyObject, Selector, Bool, (() -> Void)?) -> Void
extension FloatingPanelController {
private static let dismissSwizzling: Void = {
guard originalDismissImp == nil else { return }
let aClass: AnyClass! = UIViewController.self //object_getClass(vc)
if let originalMethod = class_getInstanceMethod(aClass, #selector(dismiss(animated:completion:))),
let swizzledImp = class_getMethodImplementation(aClass, #selector(__swizzled_dismiss(animated:completion:))) {
+343 -186
View File
@@ -10,7 +10,13 @@ class Core: NSObject, UIGestureRecognizerDelegate {
private weak var ownerVC: FloatingPanelController?
let surfaceView: SurfaceView
let backdropView: BackdropView
var backdropView: BackdropView {
didSet {
backdropView.dismissalTapGestureRecognizer
.addTarget(self, action: #selector(handleBackdrop(tapGesture:)))
}
}
let layoutAdapter: LayoutAdapter
let behaviorAdapter: BehaviorAdapter
@@ -24,6 +30,7 @@ class Core: NSObject, UIGestureRecognizerDelegate {
scrollBounce = cur.bounces
scrollIndictorVisible = cur.showsVerticalScrollIndicator
}
scrollLocked = false
} else {
if let pre = oldValue {
pre.isDirectionalLockEnabled = false
@@ -37,13 +44,14 @@ class Core: NSObject, UIGestureRecognizerDelegate {
private(set) var state: FloatingPanelState = .hidden {
didSet {
os_log(msg, log: devLog, type: .debug, "state changed: \(oldValue) -> \(state)")
if let vc = ownerVC {
vc.delegate?.floatingPanelDidChangeState?(vc)
if let fpc = ownerVC {
fpc.delegate?.floatingPanelDidChangeState?(fpc)
}
}
}
let panGestureRecognizer: FloatingPanelPanGestureRecognizer
let panGestureDelegateRouter: FloatingPanelPanGestureRecognizer.DelegateRouter
var isRemovalInteractionEnabled: Bool = false
fileprivate var isSuspended: Bool = false // Prevent a memory leak in the modal transition
@@ -63,11 +71,11 @@ class Core: NSObject, UIGestureRecognizerDelegate {
var removalVector: CGVector = .zero
// Scroll handling
private var initialScrollOffset: CGPoint = .zero
private var stopScrollDeceleration: Bool = false
private var initialScrollOffset: CGPoint?
private var scrollBounce = false
private var scrollIndictorVisible = false
private var scrollBounceThreshold: CGFloat = -30.0
private var scrollLocked = false
// MARK: - Interface
@@ -86,17 +94,19 @@ class Core: NSObject, UIGestureRecognizerDelegate {
behaviorAdapter = BehaviorAdapter(vc: vc, behavior: behavior)
panGestureRecognizer = FloatingPanelPanGestureRecognizer()
if #available(iOS 11.0, *) {
panGestureRecognizer.name = "FloatingPanelPanGestureRecognizer"
}
panGestureDelegateRouter = FloatingPanelPanGestureRecognizer.DelegateRouter(panGestureRecognizer: panGestureRecognizer)
super.init()
panGestureRecognizer.floatingPanel = self
panGestureRecognizer.set(floatingPanel: self)
surfaceView.addGestureRecognizer(panGestureRecognizer)
panGestureRecognizer.addTarget(self, action: #selector(handle(panGesture:)))
panGestureRecognizer.delegate = self
// Assign the delegate router to `FloatingPanelPanGestureRecognizer.delegate` only after setting
// `FloatingPanelPanGestureRecognizer.floatingPanel` property.
// This is because `delegateOrigin` is used at the time of assignment to its `delegate` property
// through the delegate router.
panGestureRecognizer.delegate = panGestureDelegateRouter
// Set the tap-to-dismiss action of the backdrop view.
// It's disabled by default. See also BackdropView.dismissalTapGestureRecognizer.
@@ -118,7 +128,7 @@ class Core: NSObject, UIGestureRecognizerDelegate {
completion?()
return
}
if state != layoutAdapter.mostExpandedState {
if !isScrollable(state: state) {
lockScrollView()
}
tearDownActiveInteraction()
@@ -128,7 +138,7 @@ class Core: NSObject, UIGestureRecognizerDelegate {
if animated {
let updateScrollView: () -> Void = { [weak self] in
guard let self = self else { return }
if self.state == self.layoutAdapter.mostExpandedState, 0 == self.layoutAdapter.offsetFromMostExpandedAnchor {
if self.isScrollable(state: self.state), 0 == self.layoutAdapter.offset(from: self.state) {
self.unlockScrollView()
} else {
self.lockScrollView()
@@ -143,10 +153,8 @@ class Core: NSObject, UIGestureRecognizerDelegate {
let animationVector = CGVector(dx: abs(removalVector.dx), dy: abs(removalVector.dy))
animator = vc.animatorForDismissing(with: animationVector)
default:
move(to: to, with: 0) { [weak self] in
guard let self = self else { return }
self.moveAnimator = nil
startAttraction(to: to, with: .zero) { [weak self] in
self?.endAttraction(false)
updateScrollView()
completion?()
}
@@ -184,7 +192,7 @@ class Core: NSObject, UIGestureRecognizerDelegate {
} else {
self.state = to
self.updateLayout(to: to)
if self.state == self.layoutAdapter.mostExpandedState {
if isScrollable(state: state) {
self.unlockScrollView()
} else {
self.lockScrollView()
@@ -209,6 +217,9 @@ class Core: NSObject, UIGestureRecognizerDelegate {
contentOffset = scrollView?.contentOffset
}
if layoutAdapter.validStates.contains(state) == false {
state = layoutAdapter.initialState
}
layoutAdapter.updateStaticConstraint()
layoutAdapter.activateLayout(for: state, forceLayout: forceLayout)
@@ -221,11 +232,14 @@ class Core: NSObject, UIGestureRecognizerDelegate {
if let contentOffset = contentOffset {
scrollView?.contentOffset = contentOffset
}
adjustScrollContentInsetIfNeeded()
}
private func updateLayout(to target: FloatingPanelState) {
self.layoutAdapter.activateLayout(for: target, forceLayout: true)
self.backdropView.alpha = self.getBackdropAlpha(for: target)
layoutAdapter.activateLayout(for: target, forceLayout: true)
backdropView.alpha = getBackdropAlpha(for: target)
adjustScrollContentInsetIfNeeded()
}
private func getBackdropAlpha(for target: FloatingPanelState) -> CGFloat {
@@ -260,10 +274,6 @@ class Core: NSObject, UIGestureRecognizerDelegate {
public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer,
shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
if let result = panGestureRecognizer.delegateProxy?.gestureRecognizer?(gestureRecognizer, shouldRecognizeSimultaneouslyWith: otherGestureRecognizer) {
return result
}
guard gestureRecognizer == panGestureRecognizer else { return false }
/* os_log(msg, log: devLog, type: .debug, "shouldRecognizeSimultaneouslyWith", otherGestureRecognizer) */
@@ -291,10 +301,6 @@ class Core: NSObject, UIGestureRecognizerDelegate {
}
public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldBeRequiredToFailBy otherGestureRecognizer: UIGestureRecognizer) -> Bool {
if let result = panGestureRecognizer.delegateProxy?.gestureRecognizer?(gestureRecognizer, shouldBeRequiredToFailBy: otherGestureRecognizer) {
return result
}
if otherGestureRecognizer is FloatingPanelPanGestureRecognizer {
// If this panel is the farthest descendant of visible panels,
// its ancestors' pan gesture must wait for its pan gesture to fail
@@ -302,8 +308,7 @@ class Core: NSObject, UIGestureRecognizerDelegate {
return true
}
}
if #available(iOS 11.0, *),
otherGestureRecognizer.name == "_UISheetInteractionBackgroundDismissRecognizer" {
if otherGestureRecognizer.name == "_UISheetInteractionBackgroundDismissRecognizer" {
// The dismiss gesture of a sheet modal should not begin until the pan gesture fails.
return true
}
@@ -316,10 +321,6 @@ class Core: NSObject, UIGestureRecognizerDelegate {
}
public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRequireFailureOf otherGestureRecognizer: UIGestureRecognizer) -> Bool {
if let result = panGestureRecognizer.delegateProxy?.gestureRecognizer?(gestureRecognizer, shouldRequireFailureOf: otherGestureRecognizer) {
return result
}
guard gestureRecognizer == panGestureRecognizer else { return false }
// Should begin the pan gesture without waiting for the tracking scroll view's gestures.
@@ -339,7 +340,9 @@ class Core: NSObject, UIGestureRecognizerDelegate {
if surfaceView.grabberAreaContains(gestureRecognizer.location(in: surfaceView)) {
return false
}
guard state == layoutAdapter.mostExpandedState else { return false }
guard isScrollable(state: state) else { return false }
// The condition where offset > 0 must not be included here. Because it will stop recognizing
// the panel pan gesture if a user starts scrolling content from an offset greater than 0.
return allowScrollPanGesture(of: scrollView) { offset in offset <= scrollBounceThreshold }
@@ -362,8 +365,7 @@ class Core: NSObject, UIGestureRecognizerDelegate {
is UIRotationGestureRecognizer,
is UIScreenEdgePanGestureRecognizer,
is UIPinchGestureRecognizer:
if #available(iOS 11.0, *),
otherGestureRecognizer.name == "_UISheetInteractionBackgroundDismissRecognizer" {
if otherGestureRecognizer.name == "_UISheetInteractionBackgroundDismissRecognizer" {
// Should begin the pan gesture without waiting the dismiss gesture of a sheet modal.
return false
}
@@ -393,7 +395,7 @@ class Core: NSObject, UIGestureRecognizerDelegate {
let velocity = value(of: panGesture.velocity(in: panGesture.view))
let location = panGesture.location(in: surfaceView)
let insideMostExpandedAnchor = 0 > layoutAdapter.offsetFromMostExpandedAnchor
let insideMostExpandedAnchor = 0 < layoutAdapter.offsetFromMostExpandedAnchor
os_log(msg, log: devLog, type: .debug, """
scroll gesture(\(state):\(panGesture.state)) -- \
@@ -404,21 +406,31 @@ class Core: NSObject, UIGestureRecognizerDelegate {
"""
)
let offsetDiff = value(of: scrollView.contentOffset - contentOffsetForPinning(of: scrollView))
let baseOffset = contentOffsetForPinning(of: scrollView)
let offsetDiff = value(of: scrollView.contentOffset - baseOffset)
if insideMostExpandedAnchor {
// Scroll offset pinning
if state == layoutAdapter.mostExpandedState {
// Prevent scrolling if needed
if isScrollable(state: state), let initialScrollOffset = initialScrollOffset {
if interactionInProgress {
os_log(msg, log: devLog, type: .debug, "settle offset -- \(value(of: initialScrollOffset))")
// Return content offset to initial offset to prevent scrolling
stopScrolling(at: initialScrollOffset)
} else {
if surfaceView.grabberAreaContains(location) {
if surfaceView.grabberAreaContains(initialLocation) {
// Preserve the current content offset in moving from full.
stopScrolling(at: initialScrollOffset)
}
/// When the scroll offset is at the pinned offset and a panel is moved, the content
/// must be fixed at the pinned position without scrolling. According to the scroll
/// pan gesture behavior, the content might have already scrolled a bit by the time
/// this handler is called. Thus `initialScrollOffset` property is used here.
if value(of: initialScrollOffset - baseOffset) == 0.0 {
stopScrolling(at: initialScrollOffset)
}
}
} else {
} else if let initialScrollOffset = initialScrollOffset {
// Return content offset to initial offset to prevent scrolling
stopScrolling(at: initialScrollOffset)
}
@@ -426,7 +438,9 @@ class Core: NSObject, UIGestureRecognizerDelegate {
if interactionInProgress {
lockScrollView()
} else {
if state == layoutAdapter.mostExpandedState, self.transitionAnimator == nil {
// Put back the scroll indicator and bounce of tracking scroll view
// for scrollable states, not most expanded state.
if isScrollable(state: state), self.transitionAnimator == nil {
switch layoutAdapter.position {
case .top, .left:
if offsetDiff < 0 && velocity > 0 {
@@ -440,6 +454,7 @@ class Core: NSObject, UIGestureRecognizerDelegate {
}
}
} else {
// Here handles seamless scrolling at the most expanded position
if interactionInProgress {
// Show a scroll indicator at the top in dragging.
switch layoutAdapter.position {
@@ -454,14 +469,15 @@ class Core: NSObject, UIGestureRecognizerDelegate {
return
}
}
if state == layoutAdapter.mostExpandedState {
if isScrollable(state: state) {
// Adjust a small gap of the scroll offset just after swiping down starts in the grabber area.
if surfaceView.grabberAreaContains(location), surfaceView.grabberAreaContains(initialLocation) {
if surfaceView.grabberAreaContains(location), surfaceView.grabberAreaContains(initialLocation),
let initialScrollOffset = initialScrollOffset {
stopScrolling(at: initialScrollOffset)
}
}
} else {
if state == layoutAdapter.mostExpandedState {
if isScrollable(state: state) {
let allowScroll = allowScrollPanGesture(of: scrollView) { offset in
offset <= scrollBounceThreshold || 0 < offset
}
@@ -484,7 +500,8 @@ class Core: NSObject, UIGestureRecognizerDelegate {
}
}
// Adjust a small gap of the scroll offset just before swiping down starts in the grabber area,
if surfaceView.grabberAreaContains(location), surfaceView.grabberAreaContains(initialLocation) {
if surfaceView.grabberAreaContains(location), surfaceView.grabberAreaContains(initialLocation),
let initialScrollOffset = initialScrollOffset {
stopScrolling(at: initialScrollOffset)
}
}
@@ -492,7 +509,9 @@ class Core: NSObject, UIGestureRecognizerDelegate {
}
case panGestureRecognizer:
let translation = panGesture.translation(in: panGestureRecognizer.view!.superview)
// The touch velocity in the surface view
let velocity = panGesture.velocity(in: panGesture.view)
// The touch location in the surface view
let location = panGesture.location(in: panGesture.view)
os_log(msg, log: devLog, type: .debug, """
@@ -550,7 +569,7 @@ class Core: NSObject, UIGestureRecognizerDelegate {
endAttraction(false)
}
if let animator = self.transitionAnimator {
guard 0 >= layoutAdapter.offsetFromMostExpandedAnchor else { return }
guard 0 <= layoutAdapter.offsetFromMostExpandedAnchor else { return }
os_log(msg, log: devLog, type: .debug, "a panel animation(interruptible: \(animator.isInterruptible)) interrupted!!!")
if animator.isInterruptible {
animator.stopAnimation(false)
@@ -569,13 +588,18 @@ class Core: NSObject, UIGestureRecognizerDelegate {
private func shouldScrollViewHandleTouch(_ scrollView: UIScrollView?, point: CGPoint, velocity: CGFloat) -> Bool {
// When no scrollView, nothing to handle.
guard let scrollView = scrollView else { return false }
guard let scrollView = scrollView, scrollView.frame.contains(initialLocation) else { return false }
// For _UISwipeActionPanGestureRecognizer
if let scrollGestureRecognizers = scrollView.gestureRecognizers {
// Prevents moving a panel on swipe actions using _UISwipeActionPanGestureRecognizer.
// [Warning] Do not apply this to WKWebView. Since iOS 17.4, WKWebView has an additional pan
// gesture recognizer besides UIScrollViewPanGestureRecognizer. Applying this to WKWebView
// will block panel movements because another pan gesture isn't `scrollView.panGestureRecognizer`.
if let scrollGestureRecognizers = scrollView.gestureRecognizers,
scrollView is UITableView || scrollView is UICollectionView {
for gesture in scrollGestureRecognizers {
guard gesture.state == .began || gesture.state == .changed
else { continue }
guard gesture.state == .began || gesture.state == .changed else {
continue
}
if gesture != scrollView.panGestureRecognizer {
return true
@@ -584,27 +608,10 @@ class Core: NSObject, UIGestureRecognizerDelegate {
}
guard
state == layoutAdapter.mostExpandedState, // When not top most(i.e. .full), don't scroll.
interactionInProgress == false, // When interaction already in progress, don't scroll.
0 == layoutAdapter.offsetFromMostExpandedAnchor
else {
return false
}
// When the current point is within grabber area but the initial point is not, do scroll.
if surfaceView.grabberAreaContains(point), !surfaceView.grabberAreaContains(initialLocation) {
return true
}
// When the initial point is within grabber area and the current point is out of surface, don't scroll.
if surfaceView.grabberAreaContains(initialLocation), !surfaceView.frame.contains(point) {
return false
}
let scrollViewFrame = scrollView.convert(scrollView.bounds, to: surfaceView)
guard
scrollViewFrame.contains(initialLocation), // When the initial point not in scrollView, don't scroll.
!surfaceView.grabberAreaContains(point) // When point within grabber area, don't scroll.
isScrollable(state: state), // When not top most(i.e. .full), don't scroll.
interactionInProgress == false, // When interaction already in progress, don't scroll.
abs(layoutAdapter.offset(from: state)) < 1, // Indistinguishably close to an anchor point.
!surfaceView.grabberAreaContains(initialLocation) // When the initial point is within grabber area, don't scroll
else {
return false
}
@@ -617,14 +624,14 @@ class Core: NSObject, UIGestureRecognizerDelegate {
if offset < 0.0 {
return true
}
if velocity >= 0 {
if velocity >= 0, offset > 0.0 {
return true
}
case .bottom, .right:
if offset > 0.0 {
return true
}
if velocity <= 0 {
if velocity <= 0, offset < 0.0 {
return true
}
}
@@ -646,24 +653,24 @@ class Core: NSObject, UIGestureRecognizerDelegate {
os_log(msg, log: devLog, type: .debug, "panningBegan -- location = \(value(of: location))")
guard let scrollView = scrollView else { return }
if state == layoutAdapter.mostExpandedState {
if surfaceView.grabberAreaContains(location) {
initialScrollOffset = scrollView.contentOffset
}
} else {
initialScrollOffset = scrollView.contentOffset
}
initialScrollOffset = scrollView.contentOffset
}
private func panningChange(with translation: CGPoint) {
os_log(msg, log: devLog, type: .debug, "panningChange -- translation = \(value(of: translation))")
let pre = value(of: layoutAdapter.surfaceLocation)
let diff = value(of: translation - initialTranslation)
let next = pre + diff
layoutAdapter.updateInteractiveEdgeConstraint(diff: diff,
scrollingContent: shouldScrollingContentInMoving(from: pre, to: next),
allowsRubberBanding: behaviorAdapter.allowsRubberBanding(for:))
os_log(msg, log: devLog, type: .debug, """
panningChange -- translation = \(value(of: translation)), diff = \(diff), pre = \(pre), next = \(next)
""")
layoutAdapter.updateInteractiveEdgeConstraint(
diff: diff,
scrollingContent: shouldScrollingContentInMoving(from: pre, to: next),
allowsRubberBanding: behaviorAdapter.allowsRubberBanding(for:)
)
let cur = value(of: layoutAdapter.surfaceLocation)
@@ -676,31 +683,36 @@ class Core: NSObject, UIGestureRecognizerDelegate {
}
}
private func shouldScrollingContentInMoving(from pre: CGFloat, to next: CGFloat) -> Bool {
/// Determines if the content should scroll while the surface is moving from `cur` to `target`.
///
/// - Note: `cur` argument starts from an anchor location of surface view in a state. For example,
/// it starts from zero if the state is full whose FloatingPanelLayoutAnchor.absoluteInset is zero
/// and there is no additional safe area insets like a navigation bar. Therefore, `cur` argument
/// can be minus if the absoluteInset is minus with such a condition.
private func shouldScrollingContentInMoving(from cur: CGFloat, to target: CGFloat) -> Bool {
// Don't allow scrolling if the initial panning location is in the grabber area.
if surfaceView.grabberAreaContains(initialLocation) {
return false
}
if let scrollView = scrollView, scrollView.panGestureRecognizer.state == .changed {
if let sv = scrollView, sv.panGestureRecognizer.state == .changed {
let (contentSize, bounds, alwaysBounceHorizontal, alwaysBounceVertical)
= (sv.contentSize, sv.bounds, sv.alwaysBounceHorizontal, sv.alwaysBounceVertical)
switch layoutAdapter.position {
case .top:
if pre > .zero, pre < next,
scrollView.contentSize.height > scrollView.bounds.height || scrollView.alwaysBounceVertical {
if cur < target, contentSize.height > bounds.height || alwaysBounceVertical {
return true
}
case .left:
if pre > .zero, pre < next,
scrollView.contentSize.width > scrollView.bounds.width || scrollView.alwaysBounceHorizontal {
if cur < target, contentSize.width > bounds.width || alwaysBounceHorizontal {
return true
}
case .bottom:
if pre > .zero, pre > next,
scrollView.contentSize.height > scrollView.bounds.height || scrollView.alwaysBounceVertical {
if cur > target, contentSize.height > bounds.height || alwaysBounceVertical {
return true
}
case .right:
if pre > .zero, pre > next,
scrollView.contentSize.width > scrollView.bounds.width || scrollView.alwaysBounceHorizontal {
if cur > target, contentSize.width > bounds.width || alwaysBounceHorizontal {
return true
}
}
@@ -716,23 +728,11 @@ class Core: NSObject, UIGestureRecognizerDelegate {
return
}
// Determine whether the panel's dragging should be projected onto the scroll content scrolling
stopScrollDeceleration = 0 > layoutAdapter.offsetFromMostExpandedAnchor
if stopScrollDeceleration {
os_log(msg, log: devLog, type: .debug, "panningEnd -- will stop scrolling at initialScrollOffset = \(initialScrollOffset)")
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
self.stopScrolling(at: self.initialScrollOffset)
os_log(msg, log: devLog, type: .debug, "panningEnd -- did stop scrolling at initialScrollOffset = \(self.initialScrollOffset)")
}
}
let currentPos = value(of: layoutAdapter.surfaceLocation)
let mainVelocity = value(of: velocity)
var targetPosition = self.targetPosition(from: currentPos, with: mainVelocity)
var target = self.targetState(from: currentPos, with: mainVelocity)
endInteraction(for: targetPosition)
endInteraction(for: target)
if isRemovalInteractionEnabled {
let distToHidden = CGFloat(abs(currentPos - layoutAdapter.position(for: .hidden)))
@@ -749,17 +749,11 @@ class Core: NSObject, UIGestureRecognizerDelegate {
}
if let vc = ownerVC {
vc.delegate?.floatingPanelWillEndDragging?(vc, withVelocity: velocity, targetState: &targetPosition)
vc.delegate?.floatingPanelWillEndDragging?(vc, withVelocity: velocity, targetState: &target)
}
guard shouldAttract(to: targetPosition) else {
if let vc = ownerVC {
vc.delegate?.floatingPanelDidEndDragging?(vc, willAttract: false)
}
self.state = targetPosition
self.updateLayout(to: targetPosition)
self.unlockScrollView()
guard shouldAttract(to: target) else {
self.endWithoutAttraction(target)
return
}
@@ -767,7 +761,9 @@ class Core: NSObject, UIGestureRecognizerDelegate {
vc.delegate?.floatingPanelDidEndDragging?(vc, willAttract: true)
}
startAttraction(to: targetPosition, with: velocity)
startAttraction(to: target, with: velocity) { [weak self] in
self?.endAttraction(true)
}
}
// MARK: - Behavior
@@ -798,12 +794,40 @@ class Core: NSObject, UIGestureRecognizerDelegate {
var offset: CGPoint = .zero
initialSurfaceLocation = layoutAdapter.surfaceLocation
if state == layoutAdapter.mostExpandedState, let scrollView = scrollView {
if surfaceView.grabberAreaContains(location) {
if isScrollable(state: state), let scrollView = scrollView {
ifLabel: if surfaceView.grabberAreaContains(initialLocation) {
initialScrollOffset = scrollView.contentOffset
} else {
} else if scrollView.frame.contains(initialLocation) {
let pinningOffset = contentOffsetForPinning(of: scrollView)
// This code block handles the scenario where there's a navigation bar or toolbar
// above the tracking scroll view with corresponding content insets set, and users
// move the panel by interacting with these bars. One case of the scenario can be
// tested with 'Show Navigation Controller' in Samples.app
do {
// Adjust the location by subtracting scrollView's origin to reference the frame
// rectangle of the scroll view itself.
let _location = scrollView.convert(location, from: surfaceView) - scrollView.bounds.origin
os_log(msg, log: devLog, type: .debug, "startInteraction -- location in scroll view = \(_location))")
// Keep the scroll content offset if the current touch position is inside its
// content inset area.
switch layoutAdapter.position {
case .top, .left:
let base = value(of: scrollView.bounds.size)
if value(of: pinningOffset) + (base - value(of: _location)) < 0 {
initialScrollOffset = scrollView.contentOffset
break ifLabel
}
case .bottom, .right:
if value(of: pinningOffset) + value(of: _location) < 0 {
initialScrollOffset = scrollView.contentOffset
break ifLabel
}
}
}
// `initialScrollOffset` must be reset to the pinning offset because the value of `scrollView.contentOffset`,
// for instance, is a value in [-30, 0) on a bottom positioned panel with `allowScrollPanGesture(of:condition:)`.
// If it's not reset, the following logic to shift the surface frame will not work and then the scroll
@@ -822,8 +846,10 @@ class Core: NSObject, UIGestureRecognizerDelegate {
offset = -offsetDiff
}
}
} else {
initialScrollOffset = scrollView.contentOffset
}
os_log(msg, log: devLog, type: .debug, "initial scroll offset -- \(initialScrollOffset)")
os_log(msg, log: devLog, type: .debug, "initial scroll offset -- \(optional: initialScrollOffset)")
}
initialTranslation = translation
@@ -839,8 +865,8 @@ class Core: NSObject, UIGestureRecognizerDelegate {
lockScrollView()
}
private func endInteraction(for targetPosition: FloatingPanelState) {
os_log(msg, log: devLog, type: .debug, "endInteraction to \(targetPosition)")
private func endInteraction(for state: FloatingPanelState) {
os_log(msg, log: devLog, type: .debug, "endInteraction to \(state)")
if let scrollView = scrollView {
os_log(msg, log: devLog, type: .debug, "endInteraction -- scroll offset = \(scrollView.contentOffset)")
@@ -849,11 +875,11 @@ class Core: NSObject, UIGestureRecognizerDelegate {
interactionInProgress = false
// Prevent to keep a scroll view indicator visible at the half/tip position
if targetPosition != layoutAdapter.mostExpandedState {
if !isScrollable(state: state) {
lockScrollView()
}
layoutAdapter.endInteraction(at: targetPosition)
layoutAdapter.endInteraction(at: state)
}
private func tearDownActiveInteraction() {
@@ -863,26 +889,24 @@ class Core: NSObject, UIGestureRecognizerDelegate {
panGestureRecognizer.isEnabled = true
}
private func shouldAttract(to targetState: FloatingPanelState) -> Bool {
if layoutAdapter.position(for: targetState) == value(of: layoutAdapter.surfaceLocation) {
private func shouldAttract(to state: FloatingPanelState) -> Bool {
if layoutAdapter.position(for: state) == value(of: layoutAdapter.surfaceLocation) {
return false
}
return true
}
private func startAttraction(to targetPosition: FloatingPanelState, with velocity: CGPoint) {
os_log(msg, log: devLog, type: .debug, "startAnimation to \(targetPosition) -- velocity = \(value(of: velocity))")
private func startAttraction(to state: FloatingPanelState, with velocity: CGPoint, completion: @escaping (() -> Void)) {
os_log(msg, log: devLog, type: .debug, "startAnimation to \(state) -- velocity = \(value(of: velocity))")
guard let vc = ownerVC else { return }
isAttracting = true
vc.delegate?.floatingPanelWillBeginAttracting?(vc, to: targetPosition)
move(to: targetPosition, with: value(of: velocity)) {
self.endAttraction(true)
}
vc.delegate?.floatingPanelWillBeginAttracting?(vc, to: state)
move(to: state, with: value(of: velocity), completion: completion)
}
private func move(to targetPosition: FloatingPanelState, with velocity: CGFloat, completion: @escaping (() -> Void)) {
let (animationConstraint, target) = layoutAdapter.setUpAttraction(to: targetPosition)
private func move(to state: FloatingPanelState, with velocity: CGFloat, completion: @escaping (() -> Void)) {
let (animationConstraint, target) = layoutAdapter.setUpAttraction(to: state)
let initialData = NumericSpringAnimator.Data(value: animationConstraint.constant, velocity: velocity)
moveAnimator = NumericSpringAnimator(
initialData: initialData,
@@ -895,25 +919,43 @@ class Core: NSObject, UIGestureRecognizerDelegate {
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)
// Pin the offset of the tracking scroll view while moving by this animator
if let scrollView = self.scrollView, let initialScrollOffset = self.initialScrollOffset {
self.stopScrolling(at: initialScrollOffset)
os_log(msg, log: devLog, type: .debug, "move -- pinning scroll offset = \(scrollView.contentOffset)")
}
ownerVC.notifyDidMove()
},
completion: { [weak self] in
guard let self = self,
self.ownerVC != nil else { return }
self.updateLayout(to: targetPosition)
let ownerVC = self.ownerVC
else { return }
self.updateLayout(to: state)
// Notify when it has reached the target anchor point. At this point, the surface location is equal to
// the target anchor location.
ownerVC.notifyDidMove()
completion()
})
moveAnimator?.startAnimation()
state = targetPosition
self.state = state
}
private func endAttraction(_ finished: Bool) {
private func endAttraction(_ tryUnlockScroll: Bool) {
self.isAttracting = false
self.moveAnimator = nil
// We need to reset `initialScrollOffset` because the scroll offset can become unexpected
// under the following circumstances:
// 1. The scroll offset changes while the panel does not move.
// 2. The panel is then moved using `move(to:animate:completion:)`.
self.initialScrollOffset = nil
if let vc = ownerVC {
vc.delegate?.floatingPanelDidEndAttracting?(vc)
}
@@ -922,17 +964,31 @@ class Core: NSObject, UIGestureRecognizerDelegate {
os_log(msg, log: devLog, type: .debug, "finishAnimation -- scroll offset = \(scrollView.contentOffset)")
}
stopScrollDeceleration = false
os_log(msg, log: devLog, type: .debug, """
finishAnimation -- state = \(state) \
surface location = \(layoutAdapter.surfaceLocation) \
edge most position = \(layoutAdapter.surfaceLocation(for: layoutAdapter.mostExpandedState))
offset from state position = \(layoutAdapter.offset(from: state))
""")
if finished, state == layoutAdapter.mostExpandedState, 0 == layoutAdapter.offsetFromMostExpandedAnchor {
unlockScrollView()
} else if finished, shouldLooselyLockScrollView {
unlockScrollView()
if tryUnlockScroll {
if (isScrollable(state: state) && 0 == layoutAdapter.offset(from: state))
|| shouldLooselyLockScrollView {
unlockScrollView()
}
}
}
func endWithoutAttraction(_ target: FloatingPanelState) {
// See comments in `endAttraction`
self.initialScrollOffset = nil
self.state = target
self.updateLayout(to: target)
self.unlockScrollView()
// The `floatingPanelDidEndDragging(_:willAttract:)` must be called after the state property changes.
// This allows library users to get the correct state in the delegate method.
if let vc = ownerVC {
vc.delegate?.floatingPanelDidEndDragging?(vc, willAttract: false)
}
}
@@ -959,8 +1015,8 @@ class Core: NSObject, UIGestureRecognizerDelegate {
return (initialVelocity / 1000.0) * decelerationRate / (1.0 - decelerationRate)
}
func targetPosition(from currentY: CGFloat, with velocity: CGFloat) -> (FloatingPanelState) {
os_log(msg, log: devLog, type: .debug, "targetPosition -- currentY = \(currentY), velocity = \(velocity)")
func targetState(from currentY: CGFloat, with velocity: CGFloat) -> FloatingPanelState {
os_log(msg, log: devLog, type: .debug, "targetState -- currentY = \(currentY), velocity = \(velocity)")
let sortedPositions = layoutAdapter.sortedAnchorStatesByCoordinate
@@ -986,7 +1042,7 @@ class Core: NSObject, UIGestureRecognizerDelegate {
(fromPos, toPos) = forwardY ? (lowerPos, upperPos) : (upperPos, lowerPos)
if behaviorAdapter.shouldProjectMomentum(to: toPos) == false {
os_log(msg, log: devLog, type: .debug, "targetPosition -- negate projection: distance = \(distance)")
os_log(msg, log: devLog, type: .debug, "targetState -- negate projection: distance = \(distance)")
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
@@ -1020,7 +1076,7 @@ class Core: NSObject, UIGestureRecognizerDelegate {
return
}
let contentOffset = scrollView.contentOffset.y
guard contentOffset < 0, layoutAdapter.position == .bottom, state == layoutAdapter.mostExpandedState else {
guard contentOffset < 0, layoutAdapter.position == .bottom, isScrollable(state: state) else {
if surfaceView.transform != .identity {
surfaceView.transform = .identity
scrollView.transform = .identity
@@ -1034,14 +1090,11 @@ class Core: NSObject, UIGestureRecognizerDelegate {
private func lockScrollView(strict: Bool = false) {
guard let scrollView = scrollView else { return }
if scrollView.isLocked {
os_log(msg, log: devLog, type: .debug, "Already scroll locked.")
if scrollLocked {
os_log(msg, log: devLog, type: .debug, "Already scroll locked")
return
}
os_log(msg, log: devLog, type: .debug, "lock scroll view")
scrollIndictorVisible = scrollView.showsVerticalScrollIndicator
scrollBounce = scrollView.bounces
if !strict, shouldLooselyLockScrollView {
// Don't change its `bounces` property. If it's changed, it will cause its scroll content offset jump at
// the most expanded anchor position while seamlessly scrolling content. This problem only occurs where its
@@ -1049,26 +1102,49 @@ class Core: NSObject, UIGestureRecognizerDelegate {
// The reason why is because `bounces` prop change leads to the "content frame" change on `.fitToBounds`.
// See also https://github.com/scenee/FloatingPanel/issues/524.
} else {
scrollBounce = scrollView.bounces
scrollView.bounces = false
}
os_log(msg, log: devLog, type: .debug, "lock scroll view")
scrollLocked = true
scrollView.isDirectionalLockEnabled = true
scrollView.showsVerticalScrollIndicator = false
switch layoutAdapter.position {
case .top, .bottom:
scrollIndictorVisible = scrollView.showsVerticalScrollIndicator
scrollView.showsVerticalScrollIndicator = false
case .left, .right:
scrollIndictorVisible = scrollView.showsHorizontalScrollIndicator
scrollView.showsHorizontalScrollIndicator = false
}
}
private func unlockScrollView() {
guard let scrollView = scrollView, scrollView.isLocked else { return }
guard let scrollView = scrollView else { return }
if !scrollLocked {
os_log(msg, log: devLog, type: .debug, "Already scroll unlocked.")
return
}
os_log(msg, log: devLog, type: .debug, "unlock scroll view")
scrollLocked = false
scrollView.bounces = scrollBounce
scrollView.isDirectionalLockEnabled = false
scrollView.showsVerticalScrollIndicator = scrollIndictorVisible
switch layoutAdapter.position {
case .top, .bottom:
scrollView.showsVerticalScrollIndicator = scrollIndictorVisible
case .left, .right:
scrollView.showsHorizontalScrollIndicator = scrollIndictorVisible
}
}
private var shouldLooselyLockScrollView: Bool {
if surfaceView.frame == .zero {
return false
}
var isSmallScrollContentAndFitToBoundsMode: Bool {
if ownerVC?.contentMode == .fitToBounds, let scrollView = scrollView,
value(of: scrollView.contentSize) < value(of: scrollView.bounds.size) - min(layoutAdapter.offsetFromMostExpandedAnchor, 0) {
value(of: scrollView.contentSize) < value(of: scrollView.bounds.size) + max(layoutAdapter.offsetFromMostExpandedAnchor, 0) {
return true
}
return false
@@ -1094,9 +1170,9 @@ class Core: NSObject, UIGestureRecognizerDelegate {
case .left:
return CGPoint(x: scrollView.fp_contentOffsetMax.x, y: 0.0)
case .bottom:
return CGPoint(x: 0.0, y: 0.0 - scrollView.fp_contentInset.top)
return CGPoint(x: 0.0, y: 0.0 - scrollView.adjustedContentInset.top)
case .right:
return CGPoint(x: 0.0 - scrollView.fp_contentInset.left, y: 0.0)
return CGPoint(x: 0.0 - scrollView.adjustedContentInset.left, y: 0.0)
}
}
@@ -1111,31 +1187,78 @@ class Core: NSObject, UIGestureRecognizerDelegate {
return condition(offset)
}
// MARK: - UIPanGestureRecognizer Intermediation
override func responds(to aSelector: Selector!) -> Bool {
return super.responds(to: aSelector) || panGestureRecognizer.delegateProxy?.responds(to: aSelector) == true
func isScrollable(state: FloatingPanelState) -> Bool {
guard let scrollView = scrollView else { return false }
if let fpc = ownerVC,
let scrollable = fpc.delegate?.floatingPanel?(fpc, shouldAllowToScroll: scrollView, in: state)
{
return scrollable
}
return state == layoutAdapter.mostExpandedState
}
override func forwardingTarget(for aSelector: Selector!) -> Any? {
if panGestureRecognizer.delegateProxy?.responds(to: aSelector) == true {
return panGestureRecognizer.delegateProxy
// Adjusts content inset of the tracking scroll view when the following conditions are met:
// - The controller's `contentInsetAdjustmentBehavior` is `.always`
// - Its `contentMode` is `.static`
// - Its content is scrollable
// This ensures that the content remains fully visible in intermediate states like `.half`,
// by using `UIScrollView.safeAreaInsets` and the panel's current position.
// This method must not be invoked in the fully expanded state, as it may lead to unexpected
// behavior under the top safe area (i.e., the status bar).
func adjustScrollContentInsetIfNeeded() {
guard
let fpc = ownerVC,
let scrollView = scrollView,
fpc.contentInsetAdjustmentBehavior == .always,
fpc.state != layoutAdapter.mostExpandedState,
isScrollable(state: fpc.state)
else { return }
switch fpc.contentMode {
case .static:
var inset = scrollView.safeAreaInsets
let offset = layoutAdapter.offsetFromMostExpandedAnchor
if offset > 0 {
switch layoutAdapter.position {
case .top:
inset.top = offset + scrollView.safeAreaInsets.top
case .bottom:
inset.bottom = offset + scrollView.safeAreaInsets.bottom
case .left:
inset.left = offset + scrollView.safeAreaInsets.left
case .right:
inset.left = offset + scrollView.safeAreaInsets.right
}
}
scrollView.contentInset = inset
case .fitToBounds:
scrollView.contentInset = scrollView.safeAreaInsets
}
return super.forwardingTarget(for: aSelector)
}
}
/// A gesture recognizer that looks for panning (dragging) gestures in a panel.
public final class FloatingPanelPanGestureRecognizer: UIPanGestureRecognizer {
fileprivate weak var floatingPanel: Core?
/// The gesture starting location in the surface view which it is attached to.
fileprivate var initialLocation: CGPoint = .zero
private weak var floatingPanel: Core! // Core has this gesture recognizer as non-optional
fileprivate func set(floatingPanel: Core) {
self.floatingPanel = floatingPanel
}
init() {
super.init(target: nil, action: nil)
name = "FloatingPanelPanGestureRecognizer"
}
public override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesBegan(touches, with: event)
initialLocation = touches.first?.location(in: view) ?? .zero
if floatingPanel?.transitionAnimator != 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
@@ -1145,10 +1268,12 @@ public final class FloatingPanelPanGestureRecognizer: UIPanGestureRecognizer {
return super.delegate
}
set {
guard newValue is Core else {
let exception = NSException(name: .invalidArgumentException,
reason: "FloatingPanelController's built-in pan gesture recognizer must have its controller as its delegate. Use 'delegateProxy' property.",
userInfo: nil)
guard newValue is DelegateRouter else {
let exception = NSException(
name: .invalidArgumentException,
reason: "FloatingPanelController's built-in pan gesture recognizer must have its controller as its delegate. Use 'delegateProxy' property.",
userInfo: nil
)
exception.raise()
return
}
@@ -1156,12 +1281,44 @@ public final class FloatingPanelPanGestureRecognizer: UIPanGestureRecognizer {
}
}
/// An object to intercept the delegate of the gesture recognizer.
/// The default object implementing a set methods of the delegate of the gesture recognizer.
///
/// If an object adopting `UIGestureRecognizerDelegate` is set, the delegate methods are proxied to it.
/// Use this property with ``delegateProxy`` when you need to use the default gesture behaviors in a proxy implementation.
public var delegateOrigin: UIGestureRecognizerDelegate {
return floatingPanel
}
/// A proxy object to intercept the default behavior of the gesture recognizer.
///
/// `UIGestureRecognizerDelegate` methods implementing by this object are called instead of the default delegate,
/// ``delegateOrigin``.
public weak var delegateProxy: UIGestureRecognizerDelegate? {
didSet {
self.delegate = floatingPanel // Update the cached IMP
self.delegate = floatingPanel?.panGestureDelegateRouter // Update the cached IMP
}
}
final class DelegateRouter: NSObject, UIGestureRecognizerDelegate {
fileprivate unowned let panGestureRecognizer: FloatingPanelPanGestureRecognizer
init(panGestureRecognizer: FloatingPanelPanGestureRecognizer) {
self.panGestureRecognizer = panGestureRecognizer
super.init()
}
override func responds(to aSelector: Selector!) -> Bool {
return panGestureRecognizer.delegateProxy?.responds(to: aSelector) == true
|| panGestureRecognizer.delegateOrigin.responds(to: aSelector)
}
override func forwardingTarget(for aSelector: Selector!) -> Any? {
if panGestureRecognizer.delegateProxy?.responds(to: aSelector) == true {
return panGestureRecognizer.delegateProxy
}
if panGestureRecognizer.delegateOrigin.responds(to: aSelector) {
return panGestureRecognizer.delegateOrigin
}
return nil
}
}
}
@@ -1232,7 +1389,7 @@ private class NumericSpringAnimator: NSObject {
if isRunning {
return false
}
os_log(msg, log: devLog, type: .debug, "startAnimation --", displayLink)
os_log(msg, log: devLog, type: .debug, "startAnimation -- \(displayLink)")
isRunning = true
displayLink.add(to: RunLoop.main, forMode: .common)
return true
@@ -1244,7 +1401,7 @@ private class NumericSpringAnimator: NSObject {
if locked { lock.unlock() }
}
os_log(msg, log: devLog, type: .debug, "stopAnimation --", displayLink)
os_log(msg, log: devLog, type: .debug, "stopAnimation -- \(displayLink)")
isRunning = false
displayLink.invalidate()
if withoutFinishing {
+10 -67
View File
@@ -7,7 +7,9 @@ import UIKit
extension CGFloat {
/// Returns this value rounded to an logical pixel value by a display scale
func rounded(by displayScale: CGFloat) -> CGFloat {
return (self * displayScale).rounded(.toNearestOrAwayFromZero) / displayScale
let p = CGFloat(1.0e9)
let v = (self * p).rounded(.towardZero) / p
return (v * displayScale).rounded(.toNearestOrAwayFromZero) / displayScale
}
func isEqual(to: CGFloat, on displayScale: CGFloat) -> Bool {
return rounded(by: displayScale) == to.rounded(by: displayScale)
@@ -45,65 +47,16 @@ protocol LayoutGuideProvider {
extension UILayoutGuide: LayoutGuideProvider {}
extension UIView: LayoutGuideProvider {}
private class CustomLayoutGuide: LayoutGuideProvider {
let topAnchor: NSLayoutYAxisAnchor
let leftAnchor: NSLayoutXAxisAnchor
let bottomAnchor: NSLayoutYAxisAnchor
let rightAnchor: NSLayoutXAxisAnchor
let widthAnchor: NSLayoutDimension
let heightAnchor: NSLayoutDimension
init(topAnchor: NSLayoutYAxisAnchor,
leftAnchor: NSLayoutXAxisAnchor,
bottomAnchor: NSLayoutYAxisAnchor,
rightAnchor: NSLayoutXAxisAnchor,
widthAnchor: NSLayoutDimension,
heightAnchor: NSLayoutDimension) {
self.topAnchor = topAnchor
self.leftAnchor = leftAnchor
self.bottomAnchor = bottomAnchor
self.rightAnchor = rightAnchor
self.widthAnchor = widthAnchor
self.heightAnchor = heightAnchor
}
}
extension UIViewController {
/// The proxy property to be used in `LayoutAdapter`
///
/// This property is to allow the safe area inset to change in unit testing
@objc var fp_safeAreaInsets: UIEdgeInsets {
if #available(iOS 11.0, *) {
return view.safeAreaInsets
} else {
return UIEdgeInsets(top: topLayoutGuide.length,
left: 0.0,
bottom: bottomLayoutGuide.length,
right: 0.0)
}
}
var fp_safeAreaLayoutGuide: LayoutGuideProvider {
if #available(iOS 11.0, *) {
return view!.safeAreaLayoutGuide
} else {
return CustomLayoutGuide(topAnchor: topLayoutGuide.bottomAnchor,
leftAnchor: view.leftAnchor,
bottomAnchor: bottomLayoutGuide.topAnchor,
rightAnchor: view.rightAnchor,
widthAnchor: view.widthAnchor,
heightAnchor: topLayoutGuide.bottomAnchor.anchorWithOffset(to: bottomLayoutGuide.topAnchor))
}
return view.safeAreaInsets
}
}
// The reason why UIView has no extensions of safe area insets and top/bottom guides
// is for iOS10 compatibility.
extension UIView {
var fp_safeAreaLayoutGuide: LayoutGuideProvider {
if #available(iOS 11.0, *) {
return safeAreaLayoutGuide
} else {
return self
}
}
var presentationFrame: CGRect {
return layer.presentation()?.frame ?? frame
}
@@ -139,7 +92,7 @@ extension UIView {
}
}
#if __FP_LOG
#if FP_LOG
extension UIGestureRecognizer.State: CustomDebugStringConvertible {
public var debugDescription: String {
switch self {
@@ -156,19 +109,9 @@ extension UIGestureRecognizer.State: CustomDebugStringConvertible {
#endif
extension UIScrollView {
var isLocked: Bool {
return !showsVerticalScrollIndicator && !bounces && isDirectionalLockEnabled
}
var fp_contentInset: UIEdgeInsets {
if #available(iOS 11.0, *) {
return adjustedContentInset
} else {
return contentInset
}
}
var fp_contentOffsetMax: CGPoint {
return CGPoint(x: max((contentSize.width + fp_contentInset.right) - bounds.width, 0.0),
y: max((contentSize.height + fp_contentInset.bottom) - bounds.height, 0.0))
return CGPoint(x: max((contentSize.width + adjustedContentInset.right) - bounds.width, 0.0),
y: max((contentSize.height + adjustedContentInset.bottom) - bounds.height, 0.0))
}
}
+1 -1
View File
@@ -15,7 +15,7 @@
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>2.6.6</string>
<string>2.8.7</string>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
</dict>
+20 -15
View File
@@ -45,8 +45,8 @@ open class FloatingPanelBottomLayout: NSObject, FloatingPanelLayout {
open func prepareLayout(surfaceView: UIView, in view: UIView) -> [NSLayoutConstraint] {
return [
surfaceView.leftAnchor.constraint(equalTo: view.fp_safeAreaLayoutGuide.leftAnchor, constant: 0.0),
surfaceView.rightAnchor.constraint(equalTo: view.fp_safeAreaLayoutGuide.rightAnchor, constant: 0.0),
surfaceView.leftAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leftAnchor, constant: 0.0),
surfaceView.rightAnchor.constraint(equalTo: view.safeAreaLayoutGuide.rightAnchor, constant: 0.0),
]
}
@@ -266,12 +266,23 @@ class LayoutAdapter {
}
var offsetFromMostExpandedAnchor: CGFloat {
return offset(from: mostExpandedState)
}
/// The distance from the given state position to the current surface location.
///
/// If the returned value is positive, it indicates that the surface is moving from
/// the given state position to closer to the `hidden` state position. In other
/// words, the surface is within the given state position. Otherwise, it indicates
/// that the surface is outside this position and is moving away from the `hidden`
/// state position.
func offset(from state: FloatingPanelState) -> CGFloat {
let offset: CGFloat
switch position {
case .top, .left:
offset = edgePosition(surfaceView.presentationFrame) - position(for: mostExpandedState)
offset = position(for: state) - edgePosition(surfaceView.frame)
case .bottom, .right:
offset = position(for: mostExpandedState) - edgePosition(surfaceView.presentationFrame)
offset = edgePosition(surfaceView.frame) - position(for: state)
}
return offset.rounded(by: surfaceView.fp_displayScale)
}
@@ -424,13 +435,13 @@ class LayoutAdapter {
switch position {
case .top, .bottom:
surfaceConstraints = [
surfaceView.leftAnchor.constraint(equalTo: vc.fp_safeAreaLayoutGuide.leftAnchor, constant: 0.0),
surfaceView.rightAnchor.constraint(equalTo: vc.fp_safeAreaLayoutGuide.rightAnchor, constant: 0.0),
surfaceView.leftAnchor.constraint(equalTo: vc.view.safeAreaLayoutGuide.leftAnchor, constant: 0.0),
surfaceView.rightAnchor.constraint(equalTo: vc.view.safeAreaLayoutGuide.rightAnchor, constant: 0.0),
]
case .left, .right:
surfaceConstraints = [
surfaceView.topAnchor.constraint(equalTo: vc.fp_safeAreaLayoutGuide.topAnchor, constant: 0.0),
surfaceView.bottomAnchor.constraint(equalTo: vc.fp_safeAreaLayoutGuide.bottomAnchor, constant: 0.0),
surfaceView.topAnchor.constraint(equalTo: vc.view.safeAreaLayoutGuide.topAnchor, constant: 0.0),
surfaceView.bottomAnchor.constraint(equalTo: vc.view.safeAreaLayoutGuide.bottomAnchor, constant: 0.0),
]
}
}
@@ -538,7 +549,7 @@ class LayoutAdapter {
let layoutGuideProvider: LayoutGuideProvider
switch anchor.referenceGuide {
case .safeArea:
layoutGuideProvider = vc.fp_safeAreaLayoutGuide
layoutGuideProvider = vc.view.safeAreaLayoutGuide
case .superview:
layoutGuideProvider = vc.view
}
@@ -771,12 +782,6 @@ class LayoutAdapter {
NSLayoutConstraint.activate(constraint: self.fitToBoundsConstraint)
}
var state = state
if validStates.contains(state) == false {
state = layout.initialState
}
// 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
+10 -10
View File
@@ -17,7 +17,7 @@ import UIKit
/// positioning.
///
/// - Parameters:
/// - absoluteOffset: An absolute offset to attach the panel from the edge.
/// - absoluteInset: An absolute distance to attach the panel from the specified edge.
/// - edge: Specify the edge of ``FloatingPanelController``'s view. This is the staring point of the offset.
/// - referenceGuide: The rectangular area to lay out the content. If it's set to `.safeArea`, the panel content lays out inside the safe area of its ``FloatingPanelController``'s view.
@objc public init(absoluteInset: CGFloat, edge: FloatingPanelReferenceEdge, referenceGuide: FloatingPanelLayoutReferenceGuide) {
@@ -34,7 +34,7 @@ import UIKit
/// 1.0 represents a distance to the opposite edge.
///
/// - Parameters:
/// - fractionalOffset: A fractional value of the size of ``FloatingPanelController``'s view to attach the panel from the edge.
/// - fractionalInset: A fractional value of the size of ``FloatingPanelController``'s view to attach the panel from the specified edge.
/// - edge: Specify the edge of ``FloatingPanelController``'s view. This is the staring point of the offset.
/// - referenceGuide: The rectangular area to lay out the content. If it's set to `.safeArea`, the panel content lays out inside the safe area of its ``FloatingPanelController``'s view.
@objc public init(fractionalInset: CGFloat, edge: FloatingPanelReferenceEdge, referenceGuide: FloatingPanelLayoutReferenceGuide) {
@@ -115,8 +115,8 @@ public extension FloatingPanelLayoutAnchor {
/// - Parameters:
/// - absoluteOffset: An absolute offset from the content size in the main dimension(i.e. y axis for a bottom panel) to attach the panel.
/// - referenceGuide: The rectangular area to lay out the content. If it's set to `.safeArea`, the panel content lays out inside the safe area of its ``FloatingPanelController``'s view.
@objc public init(absoluteOffset offset: CGFloat, referenceGuide: FloatingPanelLayoutReferenceGuide = .safeArea) {
self.offset = offset
@objc public init(absoluteOffset: CGFloat, referenceGuide: FloatingPanelLayoutReferenceGuide = .safeArea) {
self.offset = absoluteOffset
self.referenceGuide = referenceGuide
self.isAbsolute = true
}
@@ -129,8 +129,8 @@ public extension FloatingPanelLayoutAnchor {
/// - Parameters:
/// - fractionalOffset: A fractional offset of the content size in the main dimension(i.e. y axis for a bottom panel) to attach the panel.
/// - referenceGuide: The rectangular area to lay out the content. If it's set to `.safeArea`, the panel content lays out inside the safe area of its ``FloatingPanelController``'s view.
@objc public init(fractionalOffset offset: CGFloat, referenceGuide: FloatingPanelLayoutReferenceGuide = .safeArea) {
self.offset = offset
@objc public init(fractionalOffset: CGFloat, referenceGuide: FloatingPanelLayoutReferenceGuide = .safeArea) {
self.offset = fractionalOffset
self.referenceGuide = referenceGuide
self.isAbsolute = false
}
@@ -177,12 +177,12 @@ public extension FloatingPanelIntrinsicLayoutAnchor {
///
/// - Warning: If ``contentBoundingGuide`` is set to none, the panel may expand out of the screen size, depending on the intrinsic size of its content.
@objc public init(
absoluteOffset offset: CGFloat,
absoluteOffset: CGFloat,
contentLayout: UILayoutGuide,
referenceGuide: FloatingPanelLayoutReferenceGuide,
contentBoundingGuide: FloatingPanelLayoutContentBoundingGuide = .none
) {
self.offset = offset
self.offset = absoluteOffset
self.contentLayoutGuide = contentLayout
self.referenceGuide = referenceGuide
self.contentBoundingGuide = contentBoundingGuide
@@ -204,12 +204,12 @@ public extension FloatingPanelIntrinsicLayoutAnchor {
///
/// - Warning: If ``contentBoundingGuide`` is set to none, the panel may expand out of the screen size, depending on the intrinsic size of its content.
@objc public init(
fractionalOffset offset: CGFloat,
fractionalOffset: CGFloat,
contentLayout: UILayoutGuide,
referenceGuide: FloatingPanelLayoutReferenceGuide,
contentBoundingGuide: FloatingPanelLayoutContentBoundingGuide = .none
) {
self.offset = offset
self.offset = fractionalOffset
self.contentLayoutGuide = contentLayout
self.referenceGuide = referenceGuide
self.contentBoundingGuide = contentBoundingGuide
+2 -2
View File
@@ -37,7 +37,7 @@ extension FloatingPanelLayoutReferenceGuide {
func layoutGuide(vc: UIViewController) -> LayoutGuideProvider {
switch self {
case .safeArea:
return vc.fp_safeAreaLayoutGuide
return vc.view.safeAreaLayoutGuide
case .superview:
return vc.view
}
@@ -57,7 +57,7 @@ extension FloatingPanelLayoutContentBoundingGuide {
case .superview:
return fpc.view
case .safeArea:
return fpc.fp_safeAreaLayoutGuide
return fpc.view.safeAreaLayoutGuide
case .none:
return nil
}
+11
View File
@@ -15,3 +15,14 @@ struct Logging {
static let category = "FloatingPanel"
private init() {}
}
extension String.StringInterpolation {
mutating func appendInterpolation<T>(optional: T?, defaultValue: String = "nil") {
switch optional {
case let value?:
appendLiteral(String(describing: value))
case nil:
appendLiteral(defaultValue)
}
}
}
+39 -26
View File
@@ -56,11 +56,11 @@ public class SurfaceAppearance: NSObject {
/// Defaults to `.circular`.
@available(iOS 13.0, *)
public var cornerCurve: CALayerCornerCurve {
get { _cornerCurve ?? .circular }
get { _cornerCurve as? CALayerCornerCurve ?? .circular }
set { _cornerCurve = newValue }
}
private var _cornerCurve: CALayerCornerCurve?
private var _cornerCurve: Any?
/// An array of shadows used to create drop shadows underneath a surface view.
public var shadows: [Shadow] = [Shadow()]
@@ -383,29 +383,24 @@ public class SurfaceView: UIView {
}
containerView.layer.masksToBounds = true
if position.inset(containerMargins) != 0 {
if #available(iOS 11, *) {
containerView.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner,
.layerMinXMaxYCorner, .layerMaxXMaxYCorner]
}
containerView.layer.maskedCorners = [
.layerMinXMinYCorner, .layerMaxXMinYCorner,
.layerMinXMaxYCorner, .layerMaxXMaxYCorner
]
return
}
if #available(iOS 11, *) {
// Don't use `contentView.clipToBounds` because it prevents content view from expanding the height of a subview of it
// for the bottom overflow like Auto Layout settings of UIVisualEffectView in Main.storyboard of Example/Maps.
// Because the bottom of contentView must be fit to the bottom of a screen to work the `safeLayoutGuide` of a content VC.
switch position {
case .top:
containerView.layer.maskedCorners = [.layerMinXMaxYCorner, .layerMaxXMaxYCorner]
case .left:
containerView.layer.maskedCorners = [.layerMaxXMinYCorner, .layerMaxXMaxYCorner]
case .bottom:
containerView.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
case .right:
containerView.layer.maskedCorners = [.layerMinXMinYCorner, .layerMinXMaxYCorner]
}
} else {
// Can't use `containerView.layer.mask` because of a UIVisualEffectView issue in iOS 10, https://forums.developer.apple.com/thread/50854
// Instead, a user should display rounding corners appropriately.
// Don't use `contentView.clipToBounds` because it prevents content view from expanding the height of a subview of it
// for the bottom overflow like Auto Layout settings of UIVisualEffectView in Main.storyboard of Example/Maps.
// Because the bottom of contentView must be fit to the bottom of a screen to work the `safeLayoutGuide` of a content VC.
switch position {
case .top:
containerView.layer.maskedCorners = [.layerMinXMaxYCorner, .layerMaxXMaxYCorner]
case .left:
containerView.layer.maskedCorners = [.layerMaxXMinYCorner, .layerMaxXMaxYCorner]
case .bottom:
containerView.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
case .right:
containerView.layer.maskedCorners = [.layerMinXMinYCorner, .layerMinXMaxYCorner]
}
}
@@ -423,12 +418,30 @@ public class SurfaceView: UIView {
let leftConstraint = contentView.leftAnchor.constraint(equalTo: leftAnchor, constant: containerMargins.left + contentPadding.left)
let rightConstraint = rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: containerMargins.right + contentPadding.right)
let bottomConstraint = bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: containerMargins.bottom + contentPadding.bottom)
NSLayoutConstraint.activate([
var constraints = [
topConstraint,
leftConstraint,
rightConstraint,
bottomConstraint,
].map {
bottomConstraint
]
// This constraint is for UICollectionView using UICollectionViewCompositionalLayout.
// It's seemingly obvious, but the UICollectionView doesn't work without setting it. (#628)
switch position {
case .top, .bottom:
constraints += [
heightAnchor.constraint(greaterThanOrEqualToConstant: 1.0)
]
case .left, .right:
constraints += [
widthAnchor.constraint(greaterThanOrEqualToConstant: 1.0)
]
}
NSLayoutConstraint.activate(
constraints
.map {
switch mode {
case .static:
$0.priority = .required
+2 -2
View File
@@ -84,7 +84,7 @@ class ModalPresentTransition: NSObject, UIViewControllerAnimatedTransitioning {
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
guard
let fpc = transitionContext?.viewController(forKey: .to) as? FloatingPanelController
else { fatalError()}
else { return 0.0 }
let animator = fpc.animatorForPresenting(to: fpc.layout.initialState)
return TimeInterval(animator.duration)
@@ -119,7 +119,7 @@ class ModalDismissTransition: NSObject, UIViewControllerAnimatedTransitioning {
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
guard
let fpc = transitionContext?.viewController(forKey: .from) as? FloatingPanelController
else { fatalError()}
else { return 0.0 }
let animator = fpc.animatorForDismissing(with: .zero)
return TimeInterval(animator.duration)
+43 -10
View File
@@ -64,6 +64,7 @@ class ControllerTests: XCTestCase {
}
func test_moveTo() {
let timeout = 3.0
let delegate = FloatingPanelTestDelegate()
let fpc = FloatingPanelController(delegate: delegate)
XCTAssertEqual(delegate.position, .hidden)
@@ -102,7 +103,7 @@ class ControllerTests: XCTestCase {
}
XCTAssertEqual(fpc.state, .full)
XCTAssertEqual(delegate.position, .full)
wait(for: [exp], timeout: 1.0)
wait(for: [exp], timeout: timeout)
}
XCTContext.runActivity(named: "move to half(animated)") { act in
@@ -113,7 +114,7 @@ class ControllerTests: XCTestCase {
}
XCTAssertEqual(fpc.state, .half)
XCTAssertEqual(delegate.position, .half)
wait(for: [exp], timeout: 1.0)
wait(for: [exp], timeout: timeout)
}
XCTContext.runActivity(named: "move to tip(animated)") { act in
@@ -124,7 +125,7 @@ class ControllerTests: XCTestCase {
}
XCTAssertEqual(fpc.state, .tip)
XCTAssertEqual(delegate.position, .tip)
wait(for: [exp], timeout: 1.0)
wait(for: [exp], timeout: timeout)
}
fpc.move(to: .hidden, animated: true)
@@ -137,6 +138,7 @@ class ControllerTests: XCTestCase {
class MyFloatingPanelTop2BottomLayout: FloatingPanelTop2BottomTestLayout {
override var initialState: FloatingPanelState { return .half }
}
let timeout = 3.0
let delegate = FloatingPanelTestDelegate()
let fpc = FloatingPanelController(delegate: delegate)
fpc.layout = MyFloatingPanelTop2BottomLayout()
@@ -175,7 +177,7 @@ class ControllerTests: XCTestCase {
}
XCTAssertEqual(fpc.state, .full)
XCTAssertEqual(delegate.position, .full)
wait(for: [exp], timeout: 1.0)
wait(for: [exp], timeout: timeout)
}
XCTContext.runActivity(named: "move to half(animated)") { act in
@@ -186,7 +188,7 @@ class ControllerTests: XCTestCase {
}
XCTAssertEqual(fpc.state, .half)
XCTAssertEqual(delegate.position, .half)
wait(for: [exp], timeout: 1.0)
wait(for: [exp], timeout: timeout)
}
XCTContext.runActivity(named: "move to tip(animated)") { act in
@@ -197,7 +199,7 @@ class ControllerTests: XCTestCase {
}
XCTAssertEqual(fpc.state, .tip)
XCTAssertEqual(delegate.position, .tip)
wait(for: [exp], timeout: 1.0)
wait(for: [exp], timeout: timeout)
}
fpc.move(to: .hidden, animated: true)
@@ -223,6 +225,7 @@ class ControllerTests: XCTestCase {
}
func test_moveTo_didMoveDelegate() {
let timeout = 3.0
let delegate = FloatingPanelTestDelegate()
let fpc = FloatingPanelController(delegate: delegate)
XCTAssertEqual(delegate.position, .hidden)
@@ -237,7 +240,7 @@ class ControllerTests: XCTestCase {
exp.fulfill()
}
fpc.move(to: .full, animated: false)
wait(for: [exp], timeout: 1.0)
wait(for: [exp], timeout: timeout)
XCTAssertEqual(count, 1)
}
@@ -253,7 +256,7 @@ class ControllerTests: XCTestCase {
fpc.move(to: .half, animated: true) {
exp.fulfill()
}
wait(for: [exp], timeout: 1.0)
wait(for: [exp], timeout: timeout)
XCTAssertGreaterThan(count, 1)
}
@@ -270,7 +273,7 @@ class ControllerTests: XCTestCase {
exp.fulfill()
}
}
wait(for: [exp], timeout: 1.0)
wait(for: [exp], timeout: timeout)
XCTAssertEqual(count, 1)
}
@@ -288,7 +291,7 @@ class ControllerTests: XCTestCase {
exp.fulfill()
}
}
wait(for: [exp], timeout: 1.0)
wait(for: [exp], timeout: timeout)
XCTAssertGreaterThan(count, 1)
}
@@ -334,6 +337,36 @@ class ControllerTests: XCTestCase {
fpc.move(to: .tip, animated: false)
XCTAssertEqual(fpc.surfaceView.frame.height, fpc.view.bounds.height - fpc.surfaceLocation(for: .tip).y)
}
func test_switching_layout() {
final class FirstLayout: FloatingPanelLayout {
let position: FloatingPanelPosition = .bottom
let initialState: FloatingPanelState = .half
let anchors: [FloatingPanelState : FloatingPanelLayoutAnchoring] = [
.full: FloatingPanelLayoutAnchor(absoluteInset: 16.0, edge: .top, referenceGuide: .safeArea),
.half: FloatingPanelLayoutAnchor(absoluteInset: 262, edge: .top, referenceGuide: .safeArea),
.tip: FloatingPanelLayoutAnchor(absoluteInset: 44.0, edge: .bottom, referenceGuide: .safeArea)
]
}
final class SecondLayout: FloatingPanelLayout {
let position: FloatingPanelPosition = .bottom
let initialState: FloatingPanelState = .half
let anchors: [FloatingPanelState : FloatingPanelLayoutAnchoring] = [
.half: FloatingPanelLayoutAnchor(absoluteInset: 262, edge: .top, referenceGuide: .safeArea)
]
}
let fpc = FloatingPanelController()
fpc.layout = FirstLayout()
fpc.showForTest()
fpc.move(to: .tip, animated: false)
// Switch to another layout
fpc.layout = SecondLayout()
fpc.invalidateLayout()
XCTAssertEqual(fpc.state, .half)
}
}
private class MyZombieViewController: UIViewController, FloatingPanelLayout, FloatingPanelBehavior, FloatingPanelControllerDelegate {
+220 -35
View File
@@ -209,6 +209,8 @@ class CoreTests: XCTestCase {
return floor(fpc.backdropView.alpha * 1e+06) / 1e+06
}
let timeout = 3.0
let delegate = TestDelegate()
let fpc = FloatingPanelController(delegate: delegate)
fpc.layout = BackdropTestLayout()
@@ -228,14 +230,14 @@ class CoreTests: XCTestCase {
fpc.move(to: .full, animated: true) {
exp1.fulfill()
}
wait(for: [exp1], timeout: 1.0)
wait(for: [exp1], timeout: timeout)
XCTAssertEqual(_floor(fpc.backdropView.alpha), 0.3)
let exp2 = expectation(description: "move to half with animation")
fpc.move(to: .half, animated: true) {
exp2.fulfill()
}
wait(for: [exp2], timeout: 1.0)
wait(for: [exp2], timeout: timeout)
XCTAssertEqual(fpc.backdropView.alpha, 0.0)
// Test a content mode change of FloatingPanelController
@@ -246,7 +248,7 @@ class CoreTests: XCTestCase {
}
fpc.contentMode = .fitToBounds
XCTAssertEqual(fpc.backdropView.alpha, 0.0) // Must not affect the backdrop alpha by changing the content mode
wait(for: [exp3], timeout: 1.0)
wait(for: [exp3], timeout: timeout)
XCTAssertEqual(_floor(fpc.backdropView.alpha), 0.3)
// Test a size class change of FloatingPanelController.view
@@ -292,7 +294,7 @@ class CoreTests: XCTestCase {
}
}
func test_targetPosition_1positions() {
func test_targetState_1positions() {
class FloatingPanelLayout1Positions: FloatingPanelLayout {
let initialState: FloatingPanelState = .full
let position: FloatingPanelPosition = .bottom
@@ -309,7 +311,7 @@ class CoreTests: XCTestCase {
let fullPos = fpc.surfaceLocation(for: .full).y
fpc.move(to: .full, animated: false)
assertTargetPosition(fpc.floatingPanel, with: [
assertTargetState(fpc.floatingPanel, with: [
(#line, fullPos - 10.0, CGPoint(x: 0.0, y: 1000.0), .full), // redirect
(#line, fullPos, CGPoint(x: 0.0, y: -1000.0), .full), // redirect
(#line, fullPos, CGPoint(x: 0.0, y: -100.0), .full), // redirect
@@ -320,7 +322,7 @@ class CoreTests: XCTestCase {
])
}
func test_targetPosition_2positions() {
func test_targetState_2positions() {
class FloatingPanelLayout2Positions: FloatingPanelLayout {
let initialState: FloatingPanelState = .half
let position: FloatingPanelPosition = .bottom
@@ -340,7 +342,7 @@ class CoreTests: XCTestCase {
let halfPos = fpc.surfaceLocation(for: .half).y
fpc.move(to: .full, animated: false)
assertTargetPosition(fpc.floatingPanel, with: [
assertTargetState(fpc.floatingPanel, with: [
(#line, fullPos - 10.0, CGPoint(x: 0.0, y: 1000.0), .half), // project to half
(#line, fullPos, CGPoint(x: 0.0, y: -1000.0), .full), // redirect
(#line, fullPos, CGPoint(x: 0.0, y: -100.0), .full),
@@ -357,7 +359,7 @@ class CoreTests: XCTestCase {
(#line, halfPos + 10.0, CGPoint(x: 0.0, y: -1000.0), .full), // project to full
])
fpc.move(to: .half, animated: false)
assertTargetPosition(fpc.floatingPanel, with: [
assertTargetState(fpc.floatingPanel, with: [
(#line, fullPos - 10.0, CGPoint(x: 0.0, y: 1000.0), .half), // project to half
(#line, fullPos, CGPoint(x: 0.0, y: -1000.0), .full), // redirect
(#line, fullPos, CGPoint(x: 0.0, y: -100.0), .full),
@@ -375,7 +377,7 @@ class CoreTests: XCTestCase {
])
}
func test_targetPosition_2positionsWithHidden() {
func test_targetState_2positionsWithHidden() {
class FloatingPanelLayout2Positions: FloatingPanelLayout {
let initialState: FloatingPanelState = .hidden
let position: FloatingPanelPosition = .bottom
@@ -395,7 +397,7 @@ class CoreTests: XCTestCase {
let hiddenPos = fpc.surfaceLocation(for: .hidden).y
fpc.move(to: .full, animated: false)
assertTargetPosition(fpc.floatingPanel, with: [
assertTargetState(fpc.floatingPanel, with: [
(#line, fullPos - 10.0, CGPoint(x: 0.0, y: 1000.0), .hidden), // project to hidden
(#line, fullPos, CGPoint(x: 0.0, y: -1000.0), .full), // redirect
(#line, fullPos, CGPoint(x: 0.0, y: -100.0), .full),
@@ -412,7 +414,7 @@ class CoreTests: XCTestCase {
(#line, hiddenPos + 10.0, CGPoint(x: 0.0, y: -1000.0), .full), // project to full
])
fpc.move(to: .hidden, animated: false)
assertTargetPosition(fpc.floatingPanel, with: [
assertTargetState(fpc.floatingPanel, with: [
(#line, fullPos - 10.0, CGPoint(x: 0.0, y: 1000.0), .hidden), // project to hidden
(#line, fullPos, CGPoint(x: 0.0, y: -1000.0), .full), // redirect
(#line, fullPos, CGPoint(x: 0.0, y: -100.0), .full),
@@ -430,7 +432,7 @@ class CoreTests: XCTestCase {
])
}
func test_targetPosition_3positionsFromFull() {
func test_targetState_3positionsFromFull() {
let delegate = FloatingPanelTestDelegate()
let fpc = FloatingPanelController(delegate: delegate)
fpc.layout = FloatingPanelLayout3Positions()
@@ -442,7 +444,7 @@ class CoreTests: XCTestCase {
let tipPos = fpc.surfaceLocation(for: .tip).y
// From .full
fpc.move(to: .full, animated: false)
assertTargetPosition(fpc.floatingPanel, with: [
assertTargetState(fpc.floatingPanel, with: [
(#line, fullPos - 500.0, CGPoint(x: 0.0, y: -100.0), .full), // far from topMostState
(#line, fullPos - 500.0, CGPoint(x: 0.0, y: 0.0), .full), // far from topMostState
(#line, fullPos - 500.0, CGPoint(x: 0.0, y: 100.0), .full), // far from topMostState
@@ -474,7 +476,7 @@ class CoreTests: XCTestCase {
])
}
func test_targetPosition_3positionsFromFull_bottomEdge() {
func test_targetState_3positionsFromFull_bottomEdge() {
let delegate = FloatingPanelTestDelegate()
let fpc = FloatingPanelController(delegate: delegate)
fpc.layout = FloatingPanelLayout3PositionsBottomEdge()
@@ -486,7 +488,7 @@ class CoreTests: XCTestCase {
let tipPos = fpc.surfaceLocation(for: .tip).y
// From .full
fpc.move(to: .full, animated: false)
assertTargetPosition(fpc.floatingPanel, with: [
assertTargetState(fpc.floatingPanel, with: [
(#line, tipPos - 500.0, CGPoint(x: 0.0, y: -100.0), .tip), // far from topMostState
(#line, tipPos - 500.0, CGPoint(x: 0.0, y: 0.0), .tip), // far from topMostState
(#line, tipPos - 500.0, CGPoint(x: 0.0, y: 100.0), .tip), // far from topMostState
@@ -518,7 +520,7 @@ class CoreTests: XCTestCase {
])
}
func test_targetPosition_3positionsFromHalf() {
func test_targetState_3positionsFromHalf() {
let delegate = FloatingPanelTestDelegate()
let fpc = FloatingPanelController(delegate: delegate)
fpc.layout = FloatingPanelLayout3Positions()
@@ -530,7 +532,7 @@ class CoreTests: XCTestCase {
let tipPos = fpc.surfaceLocation(for: .tip).y
// From .half
fpc.move(to: .half, animated: false)
assertTargetPosition(fpc.floatingPanel, with: [
assertTargetState(fpc.floatingPanel, with: [
(#line, fullPos - 500.0, CGPoint(x: 0.0, y: -100.0), .full), // far from topMostState
(#line, fullPos - 500.0, CGPoint(x: 0.0, y: 0.0), .full), // far from topMostState
(#line, fullPos - 500.0, CGPoint(x: 0.0, y: 100.0), .full), // far from topMostState
@@ -560,7 +562,7 @@ class CoreTests: XCTestCase {
])
}
func test_targetPosition_3positionsFromHalf_bottomEdge() {
func test_targetState_3positionsFromHalf_bottomEdge() {
let delegate = FloatingPanelTestDelegate()
let fpc = FloatingPanelController(delegate: delegate)
fpc.layout = FloatingPanelLayout3PositionsBottomEdge()
@@ -572,7 +574,7 @@ class CoreTests: XCTestCase {
let tipPos = fpc.surfaceLocation(for: .tip).y
// From .half
fpc.move(to: .half, animated: false)
assertTargetPosition(fpc.floatingPanel, with: [
assertTargetState(fpc.floatingPanel, with: [
(#line, tipPos - 500.0, CGPoint(x: 0.0, y: -100.0), .tip), // far from topMostState
(#line, tipPos - 500.0, CGPoint(x: 0.0, y: 0.0), .tip), // far from topMostState
(#line, tipPos - 500.0, CGPoint(x: 0.0, y: 100.0), .tip), // far from topMostState
@@ -602,7 +604,7 @@ class CoreTests: XCTestCase {
])
}
func test_targetPosition_3positionsFromTip() {
func test_targetState_3positionsFromTip() {
let delegate = FloatingPanelTestDelegate()
let fpc = FloatingPanelController(delegate: delegate)
fpc.layout = FloatingPanelLayout3Positions()
@@ -615,7 +617,7 @@ class CoreTests: XCTestCase {
// From .tip
fpc.move(to: .tip, animated: false)
assertTargetPosition(fpc.floatingPanel, with: [
assertTargetState(fpc.floatingPanel, with: [
(#line, fullPos - 500.0, CGPoint(x: 0.0, y: -100.0), .full), // far from topMostState
(#line, fullPos - 500.0, CGPoint(x: 0.0, y: 0.0), .full), // far from topMostState
(#line, fullPos - 500.0, CGPoint(x: 0.0, y: 100.0), .full), // far from topMostState
@@ -645,7 +647,7 @@ class CoreTests: XCTestCase {
])
}
func test_targetPosition_3positionsFromTip_bottomEdge() {
func test_targetState_3positionsFromTip_bottomEdge() {
let delegate = FloatingPanelTestDelegate()
let fpc = FloatingPanelController(delegate: delegate)
fpc.layout = FloatingPanelLayout3PositionsBottomEdge()
@@ -658,7 +660,7 @@ class CoreTests: XCTestCase {
// From .tip
fpc.move(to: .tip, animated: false)
assertTargetPosition(fpc.floatingPanel, with: [
assertTargetState(fpc.floatingPanel, with: [
(#line, tipPos - 500.0, CGPoint(x: 0.0, y: -100.0), .tip), // far from topMostState
(#line, tipPos - 500.0, CGPoint(x: 0.0, y: 0.0), .tip), // far from topMostState
(#line, tipPos - 500.0, CGPoint(x: 0.0, y: 100.0), .tip), // far from topMostState
@@ -688,7 +690,7 @@ class CoreTests: XCTestCase {
])
}
func test_targetPosition_3positionsAllProjection() {
func test_targetState_3positionsAllProjection() {
let delegate = FloatingPanelTestDelegate()
let fpc = FloatingPanelController(delegate: delegate)
fpc.layout = FloatingPanelLayout3Positions()
@@ -702,7 +704,7 @@ class CoreTests: XCTestCase {
// From .full
fpc.move(to: .full, animated: false)
assertTargetPosition(fpc.floatingPanel, with: [
assertTargetState(fpc.floatingPanel, with: [
(#line, fullPos - 10.0, CGPoint(x: 0.0, y: 3000.0), .tip),
(#line, fullPos, CGPoint(x: 0.0, y: 1000.0), .tip),
(#line, fullPos, CGPoint(x: 0.0, y: 3000.0), .tip),
@@ -715,7 +717,7 @@ class CoreTests: XCTestCase {
// From .half
fpc.move(to: .tip, animated: false)
assertTargetPosition(fpc.floatingPanel, with: [
assertTargetState(fpc.floatingPanel, with: [
(#line, fullPos, CGPoint(x: 0.0, y: 1000.0), .tip),
(#line, fullPos, CGPoint(x: 0.0, y: 3000.0), .tip),
(#line, tipPos, CGPoint(x: 0.0, y: -3000.0), .full),
@@ -724,7 +726,7 @@ class CoreTests: XCTestCase {
// From .tip
fpc.move(to: .tip, animated: false)
assertTargetPosition(fpc.floatingPanel, with: [
assertTargetState(fpc.floatingPanel, with: [
(#line, fullPos - 10.0, CGPoint(x: 0.0, y: 3000.0), .tip),
(#line, fullPos, CGPoint(x: 0.0, y: 1000.0), .tip),
(#line, fullPos, CGPoint(x: 0.0, y: 3000.0), .tip),
@@ -736,7 +738,7 @@ class CoreTests: XCTestCase {
])
}
func test_targetPosition_3positionsWithHidden() {
func test_targetState_3positionsWithHidden() {
class FloatingPanelLayout3PositionsWithHidden: FloatingPanelLayout {
let initialState: FloatingPanelState = .hidden
let position: FloatingPanelPosition = .bottom
@@ -754,11 +756,11 @@ class CoreTests: XCTestCase {
XCTAssertEqual(fpc.state, .hidden)
fpc.move(to: .full, animated: false)
assertTargetPosition(fpc.floatingPanel, with: [
assertTargetState(fpc.floatingPanel, with: [
(#line, fpc.surfaceView.frame.minY, CGPoint(x: 0.0, y: 1000.0), .half),
])
fpc.move(to: .half, animated: false)
assertTargetPosition(fpc.floatingPanel, with: [
assertTargetState(fpc.floatingPanel, with: [
(#line, fpc.surfaceView.frame.minY, CGPoint(x: 0.0, y: -100.0), .half),
(#line, fpc.surfaceView.frame.minY, CGPoint(x: 0.0, y: -1000.0), .full),
(#line, fpc.surfaceView.frame.minY, CGPoint(x: 0.0, y: 0.0), .half),
@@ -766,7 +768,7 @@ class CoreTests: XCTestCase {
])
}
func test_targetPosition_3positionsWithHiddenWithoutFull() {
func test_targetState_3positionsWithHiddenWithoutFull() {
class FloatingPanelLayout3Positions: FloatingPanelLayout {
let initialState: FloatingPanelState = .hidden
let position: FloatingPanelPosition = .bottom
@@ -790,7 +792,7 @@ class CoreTests: XCTestCase {
//let hiddenPos = fpc.surfaceLocation(for: .hidden)
fpc.move(to: .half, animated: false)
assertTargetPosition(fpc.floatingPanel, with: [
assertTargetState(fpc.floatingPanel, with: [
(#line, halfPos, CGPoint(x: 0.0, y: -100.0), .half),
(#line, halfPos, CGPoint(x: 0.0, y: 0.0), .half),
(#line, halfPos, CGPoint(x: 0.0, y: 385.0), .tip), // projection
@@ -806,7 +808,7 @@ class CoreTests: XCTestCase {
(#line, tipPos - 10.0, CGPoint(x: 0.0, y: 10.0), .tip), // redirection
])
fpc.move(to: .tip, animated: false)
assertTargetPosition(fpc.floatingPanel, with: [
assertTargetState(fpc.floatingPanel, with: [
(#line, tipPos, CGPoint(x: 0.0, y: -100.0), .tip),
(#line, tipPos, CGPoint(x: 0.0, y: -1000.0), .half),
(#line, tipPos, CGPoint(x: 0.0, y: 0.0), .tip),
@@ -820,6 +822,189 @@ class CoreTests: XCTestCase {
fpc.showForTest()
XCTAssertFalse(fpc.panGestureRecognizer.isEnabled)
}
func test_is_scrollable() {
class Delegate: FloatingPanelControllerDelegate {
var shouldScroll = false
func floatingPanel(
_ fpc: FloatingPanelController,
shouldAllowToScroll scrollView: UIScrollView,
in state: FloatingPanelState
) -> Bool {
return shouldScroll
}
}
let fpc = FloatingPanelController()
let scrollView = UIScrollView()
let delegate = Delegate()
fpc.layout = FloatingPanelBottomLayout()
fpc.track(scrollView: scrollView)
fpc.showForTest()
XCTAssertTrue(fpc.floatingPanel.isScrollable(state: .full))
XCTAssertFalse(fpc.floatingPanel.isScrollable(state: .half))
fpc.delegate = delegate
XCTAssertFalse(fpc.floatingPanel.isScrollable(state: .full))
XCTAssertFalse(fpc.floatingPanel.isScrollable(state: .half))
delegate.shouldScroll = true
XCTAssertTrue(fpc.floatingPanel.isScrollable(state: .full))
XCTAssertTrue(fpc.floatingPanel.isScrollable(state: .half))
}
func test_adjustScrollContentInsetIfNeeded() {
class CustomScrollView: UIScrollView {
var customSafeAreaInsets: UIEdgeInsets = .zero
override var safeAreaInsets: UIEdgeInsets {
customSafeAreaInsets
}
}
class PanelDelegate: FloatingPanelControllerDelegate {
func floatingPanel(_ fpc: FloatingPanelController, shouldAllowToScroll scrollView: UIScrollView, in state: FloatingPanelState) -> Bool {
return state == .full || state == .half
}
}
let delegate = PanelDelegate()
do {
let scrollView = CustomScrollView()
scrollView.customSafeAreaInsets = UIEdgeInsets(top: 0, left: 0, bottom: 34, right: 0)
let fpc = FloatingPanelController()
fpc.delegate = delegate
fpc.track(scrollView: scrollView)
fpc.layout = FloatingPanelBottomLayout()
fpc.contentInsetAdjustmentBehavior = .always
fpc.contentMode = .static
fpc.showForTest()
fpc.move(to: .half, animated: false)
fpc.floatingPanel.adjustScrollContentInsetIfNeeded()
let expect = 34 + (fpc.surfaceLocation(for: .half).y - fpc.surfaceLocation(for: .full).y)
XCTAssertEqual(
scrollView.contentInset,
UIEdgeInsets(top: 0, left: 0, bottom: expect, right: 0)
)
fpc.contentMode = .fitToBounds
XCTAssertEqual(
scrollView.contentInset,
scrollView.customSafeAreaInsets
)
}
do {
let scrollView = CustomScrollView()
scrollView.customSafeAreaInsets = UIEdgeInsets(top: 91, left: 0, bottom: 0, right: 0)
let fpc = FloatingPanelController()
fpc.delegate = delegate
fpc.track(scrollView: scrollView)
fpc.layout = FloatingPanelTopPositionedLayout()
fpc.contentInsetAdjustmentBehavior = .always
fpc.contentMode = .static
fpc.showForTest()
fpc.move(to: .half, animated: false)
fpc.floatingPanel.adjustScrollContentInsetIfNeeded()
let expect = 91 + (fpc.surfaceLocation(for: .full).y - fpc.surfaceLocation(for: .half).y)
XCTAssertEqual(
scrollView.contentInset,
UIEdgeInsets(top: expect, left: 0, bottom: 0, right: 0)
)
fpc.contentMode = .fitToBounds
XCTAssertEqual(
scrollView.contentInset,
scrollView.customSafeAreaInsets
)
}
}
func test_adjustScrollContentInsetIfNeeded_normal() {
class CustomScrollView: UIScrollView {
var customSafeAreaInsets: UIEdgeInsets = .zero
override var safeAreaInsets: UIEdgeInsets {
customSafeAreaInsets
}
}
do {
let scrollView = CustomScrollView()
scrollView.customSafeAreaInsets = UIEdgeInsets(top: 42, left: 0, bottom: 34, right: 0)
let fpc = FloatingPanelController()
fpc.track(scrollView: scrollView)
fpc.layout = FloatingPanelBottomLayout()
fpc.contentInsetAdjustmentBehavior = .always
fpc.contentMode = .static
fpc.showForTest()
fpc.move(to: .half, animated: false)
fpc.floatingPanel.adjustScrollContentInsetIfNeeded()
XCTAssertEqual(
scrollView.contentInset,
UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
)
fpc.move(to: .full, animated: false)
fpc.floatingPanel.adjustScrollContentInsetIfNeeded()
XCTAssertEqual(
scrollView.contentInset,
UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
)
}
}
func test_initial_scroll_offset_reset() {
let fpc = FloatingPanelController()
let scrollView = UIScrollView()
fpc.layout = FloatingPanelBottomLayout()
fpc.track(scrollView: scrollView)
fpc.showForTest()
fpc.move(to: .full, animated: false)
fpc.panGestureRecognizer.state = .began
fpc.floatingPanel.handle(panGesture: fpc.panGestureRecognizer)
fpc.panGestureRecognizer.state = .cancelled
fpc.floatingPanel.handle(panGesture: fpc.panGestureRecognizer)
waitRunLoop(secs: 1.0)
let expect = CGPoint(x: 0, y: 100)
scrollView.setContentOffset(expect, animated: false)
fpc.move(to: .half, animated: true)
waitRunLoop(secs: 1.0)
XCTAssertEqual(expect, scrollView.contentOffset)
}
func test_handleGesture_endWithoutAttraction() throws {
class Delegate: FloatingPanelControllerDelegate {
var willAttract: Bool?
func floatingPanelDidEndDragging(_ fpc: FloatingPanelController, willAttract attract: Bool) {
willAttract = attract
}
}
let fpc = FloatingPanelController()
let delegate = Delegate()
fpc.showForTest()
fpc.delegate = delegate
XCTAssertEqual(fpc.state, .half)
fpc.floatingPanel.endWithoutAttraction(.full)
XCTAssertEqual(fpc.state, .full)
XCTAssertEqual(fpc.surfaceLocation(for: .full).y, fpc.surfaceLocation.y)
XCTAssertEqual(delegate.willAttract, false)
}
}
private class FloatingPanelLayout3Positions: FloatingPanelTestLayout {
@@ -835,9 +1020,9 @@ private class FloatingPanelLayout3PositionsBottomEdge: FloatingPanelTop2BottomTe
}
private typealias TestParameter = (UInt, CGFloat, CGPoint, FloatingPanelState)
private func assertTargetPosition(_ floatingPanel: Core, with params: [TestParameter]) {
private func assertTargetState(_ floatingPanel: Core, with params: [TestParameter]) {
params.forEach { (line, pos, velocity, result) in
floatingPanel.surfaceView.frame.origin.y = pos
XCTAssertEqual(floatingPanel.targetPosition(from: pos, with: velocity.y), result, line: line)
XCTAssertEqual(floatingPanel.targetState(from: pos, with: velocity.y), result, line: line)
}
}
+16
View File
@@ -9,4 +9,20 @@ class ExtensionTests: XCTestCase {
XCTAssertNotEqual(CGFloat(333.5).rounded(by: 3), 333.66666666666674)
XCTAssertTrue(CGFloat(333.5).isEqual(to: 333.66666666666674, on: 3.0))
}
func test_roundedByDisplayScale_2() {
XCTAssertEqual(CGFloat(-0.16666666666674246).rounded(by: 3), 0.0)
XCTAssertEqual(CGFloat(0.16666666666674246).rounded(by: 3), 0.0)
XCTAssertEqual(CGFloat(-0.3333333333374246).rounded(by: 3), -0.3333333333333333)
XCTAssertEqual(CGFloat(-0.3333333333074246).rounded(by: 3), -0.3333333333333333)
XCTAssertEqual(CGFloat(0.33333333333374246).rounded(by: 3), 0.3333333333333333)
XCTAssertEqual(CGFloat(0.33333333333074246).rounded(by: 3), 0.3333333333333333)
XCTAssertEqual(CGFloat(-0.16666666666674246).rounded(by: 2), 0.0)
XCTAssertEqual(CGFloat(0.16666666666674246).rounded(by: 2), 0.0)
XCTAssertEqual(CGFloat(-0.16666666666674246).rounded(by: 6), -0.16666666666666666)
XCTAssertEqual(CGFloat(0.16666666666674246).rounded(by: 6), 0.16666666666666666)
}
}
+139
View File
@@ -0,0 +1,139 @@
// Copyright 2018 the FloatingPanel authors. All rights reserved. MIT license.
import XCTest
@testable import FloatingPanel
final class GestureTests: XCTestCase {
func test_delegateProxy_shouldRecognizeSimultaneouslyWith() throws {
class GestureDelegateProxy: NSObject, UIGestureRecognizerDelegate {
var callsOfShouldRecognizeSimultaneouslyWith = 0
func gestureRecognizer(
_ gestureRecognizer: UIGestureRecognizer,
shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer
) -> Bool {
callsOfShouldRecognizeSimultaneouslyWith += 1
return true
}
}
let fpc = FloatingPanelController()
fpc.showForTest()
let delegateProxy = GestureDelegateProxy()
// Set a proxy delegate
fpc.panGestureRecognizer.delegateProxy = delegateProxy
_ = fpc.panGestureRecognizer.delegate!.gestureRecognizer?(
UIGestureRecognizer(),
shouldRecognizeSimultaneouslyWith: UIGestureRecognizer()
)
XCTAssertEqual(delegateProxy.callsOfShouldRecognizeSimultaneouslyWith, 1)
// Check whether the default delegate method is called when the proxy delegate doesn't implement it.
XCTAssertTrue(
fpc.panGestureRecognizer.delegate!.gestureRecognizer!(
fpc.panGestureRecognizer,
shouldRequireFailureOf: FloatingPanelPanGestureRecognizer()
)
)
// Clear the proxy delegate
fpc.panGestureRecognizer.delegateProxy = nil
_ = fpc.panGestureRecognizer.delegate!.gestureRecognizer?(
UIGestureRecognizer(),
shouldRecognizeSimultaneouslyWith: UIGestureRecognizer()
)
XCTAssertEqual(delegateProxy.callsOfShouldRecognizeSimultaneouslyWith, 1)
}
func test_delegateProxy_shouldRequireFailureOf() throws {
class GestureDelegateProxy: NSObject, UIGestureRecognizerDelegate {
var callsOfShouldRequireFailureOf = 0
func gestureRecognizer(
_ gestureRecognizer: UIGestureRecognizer,
shouldRequireFailureOf otherGestureRecognizer: UIGestureRecognizer
) -> Bool {
callsOfShouldRequireFailureOf += 1
return true
}
}
let fpc = FloatingPanelController()
fpc.showForTest()
let delegateProxy = GestureDelegateProxy()
// Set a proxy delegate
fpc.panGestureRecognizer.delegateProxy = delegateProxy
_ = fpc.panGestureRecognizer.delegate!.gestureRecognizer?(
UIGestureRecognizer(),
shouldRequireFailureOf: UIGestureRecognizer()
)
XCTAssertEqual(delegateProxy.callsOfShouldRequireFailureOf, 1)
// Clear the proxy delegate
fpc.panGestureRecognizer.delegateProxy = nil
_ = fpc.panGestureRecognizer.delegate!.gestureRecognizer?(
UIGestureRecognizer(),
shouldRequireFailureOf: UIGestureRecognizer()
)
XCTAssertEqual(delegateProxy.callsOfShouldRequireFailureOf, 1)
}
func test_delegateProxy_shouldBeRequiredToFailBy() throws {
class GestureDelegateProxy: NSObject, UIGestureRecognizerDelegate {
var callsOfShouldBeRequiredToFailBy = 0
func gestureRecognizer(
_ gestureRecognizer: UIGestureRecognizer,
shouldBeRequiredToFailBy otherGestureRecognizer: UIGestureRecognizer
) -> Bool {
callsOfShouldBeRequiredToFailBy += 1
return false
}
}
let fpc = FloatingPanelController()
fpc.showForTest()
let delegateProxy = GestureDelegateProxy()
fpc.panGestureRecognizer.delegateProxy = delegateProxy
_ = fpc.panGestureRecognizer.delegate!.gestureRecognizer?(
UIGestureRecognizer(),
shouldBeRequiredToFailBy: UIGestureRecognizer()
)
XCTAssertEqual(delegateProxy.callsOfShouldBeRequiredToFailBy, 1)
// Check whether the delegate method of the "proxy" object is called.
let otherPanGesture = UIPanGestureRecognizer()
otherPanGesture.name = "_UISheetInteractionBackgroundDismissRecognizer"
XCTAssertFalse(
fpc.panGestureRecognizer.delegate!.gestureRecognizer!(
fpc.panGestureRecognizer,
shouldBeRequiredToFailBy: otherPanGesture
)
)
XCTAssertEqual(delegateProxy.callsOfShouldBeRequiredToFailBy, 2)
fpc.panGestureRecognizer.delegateProxy = nil
// Check whether the delegate method of the "default" object is called.
let otherPanGesture2 = UIPanGestureRecognizer()
otherPanGesture2.name = "_UISheetInteractionBackgroundDismissRecognizer"
XCTAssertTrue(
fpc.panGestureRecognizer.delegate!.gestureRecognizer!(
fpc.panGestureRecognizer,
shouldBeRequiredToFailBy: otherPanGesture2
)
)
XCTAssertEqual(delegateProxy.callsOfShouldBeRequiredToFailBy, 2)
}
}
+59 -33
View File
@@ -411,18 +411,18 @@ class LayoutTests: XCTestCase {
for prop in [
// from top edge
(anchor: FloatingPanelLayoutAnchor(absoluteInset: 0.0, edge: .top, referenceGuide: .safeArea),
result: (#line, constant: 0.0, firstAnchor: fpc.surfaceView.bottomAnchor, secondAnchor: fpc.fp_safeAreaLayoutGuide.topAnchor)),
result: (#line, constant: 0.0, firstAnchor: fpc.surfaceView.bottomAnchor, secondAnchor: fpc.view.safeAreaLayoutGuide.topAnchor)),
(anchor: FloatingPanelLayoutAnchor(absoluteInset: 100.0, edge: .top, referenceGuide: .safeArea),
result: (#line, constant: 100.0, firstAnchor: fpc.surfaceView.bottomAnchor, secondAnchor: fpc.fp_safeAreaLayoutGuide.topAnchor)),
result: (#line, constant: 100.0, firstAnchor: fpc.surfaceView.bottomAnchor, secondAnchor: fpc.view.safeAreaLayoutGuide.topAnchor)),
(anchor: FloatingPanelLayoutAnchor(absoluteInset: 0.0, edge: .top, referenceGuide: .superview),
result: (#line, constant: 0.0, firstAnchor: fpc.surfaceView.bottomAnchor, secondAnchor: fpc.view.topAnchor)),
(anchor: FloatingPanelLayoutAnchor(absoluteInset: 100.0, edge: .top, referenceGuide: .superview),
result: (#line, constant: 100.0, firstAnchor: fpc.surfaceView.bottomAnchor, secondAnchor: fpc.view.topAnchor)),
// from bottom edge
(anchor: FloatingPanelLayoutAnchor(absoluteInset: 0.0, edge: .bottom, referenceGuide: .safeArea),
result: (#line, constant: 0.0, firstAnchor: fpc.fp_safeAreaLayoutGuide.bottomAnchor, secondAnchor: fpc.surfaceView.bottomAnchor)),
result: (#line, constant: 0.0, firstAnchor: fpc.view.safeAreaLayoutGuide.bottomAnchor, secondAnchor: fpc.surfaceView.bottomAnchor)),
(anchor: FloatingPanelLayoutAnchor(absoluteInset: 100.0, edge: .bottom, referenceGuide: .safeArea),
result: (#line, constant: 100.0, firstAnchor: fpc.fp_safeAreaLayoutGuide.bottomAnchor, secondAnchor: fpc.surfaceView.bottomAnchor)),
result: (#line, constant: 100.0, firstAnchor: fpc.view.safeAreaLayoutGuide.bottomAnchor, secondAnchor: fpc.surfaceView.bottomAnchor)),
(anchor: FloatingPanelLayoutAnchor(absoluteInset: 0.0, edge: .bottom, referenceGuide: .superview),
result: (#line, constant: 0.0, firstAnchor: fpc.view.bottomAnchor, secondAnchor: fpc.surfaceView.bottomAnchor)),
(anchor: FloatingPanelLayoutAnchor(absoluteInset: 100.0, edge: .bottom, referenceGuide: .superview),
@@ -440,7 +440,7 @@ class LayoutTests: XCTestCase {
(anchor: FloatingPanelLayoutAnchor(fractionalInset: 0.0, edge: .top, referenceGuide: .safeArea),
result: (#line, multiplier: 1.0, secondAnchor: nil)),
(anchor: FloatingPanelLayoutAnchor(fractionalInset: 0.5, edge: .top, referenceGuide: .safeArea),
result: (#line, multiplier: 0.5, secondAnchor: fpc.fp_safeAreaLayoutGuide.heightAnchor)),
result: (#line, multiplier: 0.5, secondAnchor: fpc.view.safeAreaLayoutGuide.heightAnchor)),
(anchor: FloatingPanelLayoutAnchor(fractionalInset: 0.0, edge: .top, referenceGuide: .superview),
result: (#line, multiplier: 1.0, secondAnchor: nil)),
(anchor: FloatingPanelLayoutAnchor(fractionalInset: 0.5, edge: .top, referenceGuide: .superview),
@@ -450,7 +450,7 @@ class LayoutTests: XCTestCase {
(anchor: FloatingPanelLayoutAnchor(fractionalInset: 0.0, edge: .bottom, referenceGuide: .safeArea),
result: (#line, multiplier: 1.0, secondAnchor: nil)),
(anchor: FloatingPanelLayoutAnchor(fractionalInset: 0.5, edge: .bottom, referenceGuide: .safeArea),
result: (#line, multiplier: 0.5, secondAnchor: fpc.fp_safeAreaLayoutGuide.heightAnchor)),
result: (#line, multiplier: 0.5, secondAnchor: fpc.view.safeAreaLayoutGuide.heightAnchor)),
(anchor: FloatingPanelLayoutAnchor(fractionalInset: 0.0, edge: .bottom, referenceGuide: .superview),
result: (#line, multiplier: 1.0, secondAnchor: nil)),
(anchor: FloatingPanelLayoutAnchor(fractionalInset: 0.5, edge: .bottom, referenceGuide: .superview),
@@ -459,11 +459,7 @@ 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))
// 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))
}
XCTAssertEqual(c.secondAnchor, prop.result.secondAnchor, line: UInt(prop.result.0))
}
}
func test_layoutAnchor_bottomPosition() {
@@ -476,9 +472,9 @@ class LayoutTests: XCTestCase {
for prop in [
// from top edge
(anchor: FloatingPanelLayoutAnchor(absoluteInset: 0.0, edge: .top, referenceGuide: .safeArea),
result: (#line, constant: 0.0, firstAnchor: fpc.surfaceView.topAnchor, secondAnchor: fpc.fp_safeAreaLayoutGuide.topAnchor)),
result: (#line, constant: 0.0, firstAnchor: fpc.surfaceView.topAnchor, secondAnchor: fpc.view.safeAreaLayoutGuide.topAnchor)),
(anchor: FloatingPanelLayoutAnchor(absoluteInset: 100.0, edge: .top, referenceGuide: .safeArea),
result: (#line, constant: 100.0, firstAnchor: fpc.surfaceView.topAnchor, secondAnchor: fpc.fp_safeAreaLayoutGuide.topAnchor)),
result: (#line, constant: 100.0, firstAnchor: fpc.surfaceView.topAnchor, secondAnchor: fpc.view.safeAreaLayoutGuide.topAnchor)),
(anchor: FloatingPanelLayoutAnchor(absoluteInset: 0.0, edge: .top, referenceGuide: .superview),
result: (#line, constant: 0.0, firstAnchor: fpc.surfaceView.topAnchor, secondAnchor: fpc.view.topAnchor)),
(anchor: FloatingPanelLayoutAnchor(absoluteInset: 100.0, edge: .top, referenceGuide: .superview),
@@ -486,9 +482,9 @@ class LayoutTests: XCTestCase {
// from bottom edge
(anchor: FloatingPanelLayoutAnchor(absoluteInset: 0.0, edge: .bottom, referenceGuide: .safeArea),
result: (#line, constant: 0.0, firstAnchor: fpc.fp_safeAreaLayoutGuide.bottomAnchor, secondAnchor: fpc.surfaceView.topAnchor)),
result: (#line, constant: 0.0, firstAnchor: fpc.view.safeAreaLayoutGuide.bottomAnchor, secondAnchor: fpc.surfaceView.topAnchor)),
(anchor: FloatingPanelLayoutAnchor(absoluteInset: 100.0, edge: .bottom, referenceGuide: .safeArea),
result: (#line, constant: 100.0, firstAnchor: fpc.fp_safeAreaLayoutGuide.bottomAnchor, secondAnchor: fpc.surfaceView.topAnchor)),
result: (#line, constant: 100.0, firstAnchor: fpc.view.safeAreaLayoutGuide.bottomAnchor, secondAnchor: fpc.surfaceView.topAnchor)),
(anchor: FloatingPanelLayoutAnchor(absoluteInset: 0.0, edge: .bottom, referenceGuide: .superview),
result: (#line, constant: 0.0, firstAnchor: fpc.view.bottomAnchor, secondAnchor: fpc.surfaceView.topAnchor)),
(anchor: FloatingPanelLayoutAnchor(absoluteInset: 100.0, edge: .bottom, referenceGuide: .superview),
@@ -506,7 +502,7 @@ class LayoutTests: XCTestCase {
(anchor: FloatingPanelLayoutAnchor(fractionalInset: 0.0, edge: .top, referenceGuide: .safeArea),
result: (#line, multiplier: 1.0, secondAnchor: nil)),
(anchor: FloatingPanelLayoutAnchor(fractionalInset: 0.5, edge: .top, referenceGuide: .safeArea),
result: (#line, multiplier: 0.5, secondAnchor: fpc.fp_safeAreaLayoutGuide.heightAnchor)),
result: (#line, multiplier: 0.5, secondAnchor: fpc.view.safeAreaLayoutGuide.heightAnchor)),
(anchor: FloatingPanelLayoutAnchor(fractionalInset: 0.0, edge: .top, referenceGuide: .superview),
result: (#line, multiplier: 1.0, secondAnchor: nil)),
(anchor: FloatingPanelLayoutAnchor(fractionalInset: 0.5, edge: .top, referenceGuide: .superview),
@@ -516,7 +512,7 @@ class LayoutTests: XCTestCase {
(anchor: FloatingPanelLayoutAnchor(fractionalInset: 0.0, edge: .bottom, referenceGuide: .safeArea),
result: (#line, multiplier: 1.0, secondAnchor: nil)),
(anchor: FloatingPanelLayoutAnchor(fractionalInset: 0.5, edge: .bottom, referenceGuide: .safeArea),
result: (#line, multiplier: 0.5, secondAnchor: fpc.fp_safeAreaLayoutGuide.heightAnchor)),
result: (#line, multiplier: 0.5, secondAnchor: fpc.view.safeAreaLayoutGuide.heightAnchor)),
(anchor: FloatingPanelLayoutAnchor(fractionalInset: 0.0, edge: .bottom, referenceGuide: .superview),
result: (#line, multiplier: 1.0, secondAnchor: nil)),
(anchor: FloatingPanelLayoutAnchor(fractionalInset: 0.5, edge: .bottom, referenceGuide: .superview),
@@ -525,12 +521,7 @@ 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))
// 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)
XCTAssertEqual(c.secondAnchor, prop.result.secondAnchor, line: UInt(prop.result.0))
}
}
@@ -556,20 +547,20 @@ class LayoutTests: XCTestCase {
for prop in [
(anchor: FloatingPanelIntrinsicLayoutAnchor(absoluteOffset: 0.0, referenceGuide: .safeArea),
result: (#line, constant: 420, firstAnchor: fpc.surfaceView.bottomAnchor, secondAnchor: fpc.fp_safeAreaLayoutGuide.topAnchor)),
result: (#line, constant: 420, firstAnchor: fpc.surfaceView.bottomAnchor, secondAnchor: fpc.view.safeAreaLayoutGuide.topAnchor)),
(anchor: FloatingPanelIntrinsicLayoutAnchor(absoluteOffset: 42.0, referenceGuide: .safeArea),
result: (#line, constant: 420 - 42, firstAnchor: fpc.surfaceView.bottomAnchor, secondAnchor: fpc.fp_safeAreaLayoutGuide.topAnchor)),
result: (#line, constant: 420 - 42, firstAnchor: fpc.surfaceView.bottomAnchor, secondAnchor: fpc.view.safeAreaLayoutGuide.topAnchor)),
(anchor: FloatingPanelIntrinsicLayoutAnchor(absoluteOffset: 0.0, referenceGuide: .superview),
result: (#line, constant: 420, firstAnchor: fpc.surfaceView.bottomAnchor, secondAnchor: fpc.view.topAnchor)),
(anchor: FloatingPanelIntrinsicLayoutAnchor(absoluteOffset: 42.0, referenceGuide: .superview),
result: (#line, constant: 420 - 42, firstAnchor: fpc.surfaceView.bottomAnchor, secondAnchor: fpc.view.topAnchor)),
(anchor: FloatingPanelIntrinsicLayoutAnchor(fractionalOffset: 0.0, referenceGuide: .safeArea),
result: (#line, constant: 420, firstAnchor: fpc.surfaceView.bottomAnchor, secondAnchor: fpc.fp_safeAreaLayoutGuide.topAnchor)),
result: (#line, constant: 420, firstAnchor: fpc.surfaceView.bottomAnchor, secondAnchor: fpc.view.safeAreaLayoutGuide.topAnchor)),
(anchor: FloatingPanelIntrinsicLayoutAnchor(fractionalOffset: 0.5, referenceGuide: .safeArea),
result: (#line, constant: 210, firstAnchor: fpc.surfaceView.bottomAnchor, secondAnchor: fpc.fp_safeAreaLayoutGuide.topAnchor)),
result: (#line, constant: 210, firstAnchor: fpc.surfaceView.bottomAnchor, secondAnchor: fpc.view.safeAreaLayoutGuide.topAnchor)),
(anchor: FloatingPanelIntrinsicLayoutAnchor(fractionalOffset: 1.0, referenceGuide: .safeArea),
result: (#line, constant: 0, firstAnchor: fpc.surfaceView.bottomAnchor, secondAnchor: fpc.fp_safeAreaLayoutGuide.topAnchor)),
result: (#line, constant: 0, firstAnchor: fpc.surfaceView.bottomAnchor, secondAnchor: fpc.view.safeAreaLayoutGuide.topAnchor)),
(anchor: FloatingPanelIntrinsicLayoutAnchor(fractionalOffset: 0.0, referenceGuide: .superview),
result: (#line, constant: 420, firstAnchor: fpc.surfaceView.bottomAnchor, secondAnchor: fpc.view.topAnchor)),
(anchor: FloatingPanelIntrinsicLayoutAnchor(fractionalOffset: 0.5, referenceGuide: .superview),
@@ -606,20 +597,20 @@ class LayoutTests: XCTestCase {
for prop in [
(anchor: FloatingPanelIntrinsicLayoutAnchor(absoluteOffset: 0.0, referenceGuide: .safeArea),
result: (#line, constant: -420, firstAnchor: fpc.surfaceView.topAnchor, secondAnchor: fpc.fp_safeAreaLayoutGuide.bottomAnchor)),
result: (#line, constant: -420, firstAnchor: fpc.surfaceView.topAnchor, secondAnchor: fpc.view.safeAreaLayoutGuide.bottomAnchor)),
(anchor: FloatingPanelIntrinsicLayoutAnchor(absoluteOffset: 42.0, referenceGuide: .safeArea),
result: (#line, constant: -420 + 42, firstAnchor: fpc.surfaceView.topAnchor, secondAnchor: fpc.fp_safeAreaLayoutGuide.bottomAnchor)),
result: (#line, constant: -420 + 42, firstAnchor: fpc.surfaceView.topAnchor, secondAnchor: fpc.view.safeAreaLayoutGuide.bottomAnchor)),
(anchor: FloatingPanelIntrinsicLayoutAnchor(absoluteOffset: 0.0, referenceGuide: .superview),
result: (#line, constant: -420, firstAnchor: fpc.surfaceView.topAnchor, secondAnchor: fpc.view.bottomAnchor)),
(anchor: FloatingPanelIntrinsicLayoutAnchor(absoluteOffset: 42.0, referenceGuide: .superview),
result: (#line, constant: -420 + 42, firstAnchor: fpc.surfaceView.topAnchor, secondAnchor: fpc.view.bottomAnchor)),
(anchor: FloatingPanelIntrinsicLayoutAnchor(fractionalOffset: 0.0, referenceGuide: .safeArea),
result: (#line, constant: -420, firstAnchor: fpc.surfaceView.topAnchor, secondAnchor: fpc.fp_safeAreaLayoutGuide.bottomAnchor)),
result: (#line, constant: -420, firstAnchor: fpc.surfaceView.topAnchor, secondAnchor: fpc.view.safeAreaLayoutGuide.bottomAnchor)),
(anchor: FloatingPanelIntrinsicLayoutAnchor(fractionalOffset: 0.5, referenceGuide: .safeArea),
result: (#line, constant: -210, firstAnchor: fpc.surfaceView.topAnchor, secondAnchor: fpc.fp_safeAreaLayoutGuide.bottomAnchor)),
result: (#line, constant: -210, firstAnchor: fpc.surfaceView.topAnchor, secondAnchor: fpc.view.safeAreaLayoutGuide.bottomAnchor)),
(anchor: FloatingPanelIntrinsicLayoutAnchor(fractionalOffset: 1.0, referenceGuide: .safeArea),
result: (#line, constant: 0, firstAnchor: fpc.surfaceView.topAnchor, secondAnchor: fpc.fp_safeAreaLayoutGuide.bottomAnchor)),
result: (#line, constant: 0, firstAnchor: fpc.surfaceView.topAnchor, secondAnchor: fpc.view.safeAreaLayoutGuide.bottomAnchor)),
(anchor: FloatingPanelIntrinsicLayoutAnchor(fractionalOffset: 0.0, referenceGuide: .superview),
result: (#line, constant: -420, firstAnchor: fpc.surfaceView.topAnchor, secondAnchor: fpc.view.bottomAnchor)),
(anchor: FloatingPanelIntrinsicLayoutAnchor(fractionalOffset: 0.5, referenceGuide: .superview),
@@ -633,6 +624,41 @@ class LayoutTests: XCTestCase {
XCTAssertEqual(c.secondAnchor, prop.result.secondAnchor, line: UInt(prop.result.0))
}
}
func test_offsetFromMostExpandedAnchor() {
do {
let fpc = FloatingPanelController()
fpc.layout = FloatingPanelBottomLayout()
fpc.showForTest()
fpc.move(to: .full, animated: false)
XCTAssertEqual(fpc.floatingPanel.layoutAdapter.offsetFromMostExpandedAnchor, 0.0)
fpc.move(to: .half, animated: false)
XCTAssertEqual(
fpc.floatingPanel.layoutAdapter.offsetFromMostExpandedAnchor,
(fpc.surfaceLocation(for: .half) - fpc.surfaceLocation(for: .full)).y
)
}
do {
let fpc = FloatingPanelController()
fpc.layout = FloatingPanelTopPositionedLayout()
fpc.showForTest()
fpc.move(to: .full, animated: false)
XCTAssertEqual(fpc.floatingPanel.layoutAdapter.offsetFromMostExpandedAnchor, 0.0)
fpc.move(to: .half, animated: false)
XCTAssertEqual(
fpc.floatingPanel.layoutAdapter.offsetFromMostExpandedAnchor,
-(fpc.surfaceLocation(for: .half) - fpc.surfaceLocation(for: .full)).y
)
}
}
}
private typealias LayoutSegmentTestParameter = (UInt, pos: CGFloat, forwardY: Bool, lower: FloatingPanelState?, upper: FloatingPanelState?)
+11 -1
View File
@@ -72,8 +72,18 @@ class FloatingPanelTop2BottomTestLayout: FloatingPanelLayout {
}
}
class FloatingPanelTopPositionedLayout: FloatingPanelLayout {
let position: FloatingPanelPosition = .top
let initialState: FloatingPanelState = .full
let anchors: [FloatingPanelState : FloatingPanelLayoutAnchoring] = [
.full: FloatingPanelLayoutAnchor(absoluteInset: 88.0, edge: .bottom, referenceGuide: .safeArea),
.half: FloatingPanelLayoutAnchor(absoluteInset: 216.0, edge: .top, referenceGuide: .safeArea),
.tip: FloatingPanelLayoutAnchor(absoluteInset: 44.0, edge: .top, referenceGuide: .safeArea)
]
}
class FloatingPanelProjectableBehavior: FloatingPanelBehavior {
func shouldProjectMomentum(_ fpc: FloatingPanelController, to proposedTargetPosition: FloatingPanelState) -> Bool {
func shouldProjectMomentum(_ fpc: FloatingPanelController, to proposedState: FloatingPanelState) -> Bool {
return true
}
}