Compare commits

...

65 Commits

Author SHA1 Message Date
Shin Yamamoto 29beea0ff9 Version 2.5.0 2021-09-28 08:43:42 +09:00
Shin Yamamoto 1e16d9c1fb Fix a tab bar's appearance in Samples app on iOS 15 2021-09-28 08:43:42 +09:00
milettal 31d7a2a301 Fix a crash in Samples app on iPad (#497)
* Fix a crash when the app shows an action sheet on iPad
* Use the UITableViewCell as the sourceView
2021-09-25 10:11:38 +09:00
Federico Zanetello 5f1d49b0bd Add SwiftUI proof of concept in Maps-SwiftUI example (#481)
Partially solves #281. This adds a new example app which mimics the Maps.app, written in SwiftUI. The code works for iOS 13+, however:
* the project has been created with Xcode 13
* the project uses the SwiftUI lifecycle (iOS 14+)

The source code in Examples/Maps-SwiftUI/Maps/FloatingPanel is ready to move into the library, but there is an issue on SwiftUI’s environment propagation into FloatingPanel. SwiftUI’s environment is propagated to all subviews. However FloatingPanel is not a subview, but a new view controller in the screen (and not a child view controller).

It’s possible to lead behaviors unexpected by SwiftUI users so that this is merged as a sample code until it will be resolved.
2021-09-25 10:06:23 +09:00
Shin Yamamoto ee66f218ca Merge pull request #502 from scenee/release/2.4.1
Release 2.4.1
2021-09-18 12:11:12 +09:00
Shin Yamamoto 3a0831a334 Version 2.4.1 2021-09-18 11:20:52 +09:00
Shin Yamamoto add2f9ba4f Remove .travis.yml 2021-09-18 11:20:52 +09:00
Shin Yamamoto ca6a8d5c16 Update ci workflow 2021-09-18 10:57:03 +09:00
Iran-Q 84eba1aefa fix objc can not obtain dismissalTapGestureRecognizer (#496)
Co-authored-by: CodingIran <codingiran@163.com>
2021-08-27 00:02:02 +09:00
Shin Yamamoto 289ea4c971 Fix a compile error on SampleObjC 2021-08-18 07:29:39 +09:00
Shin Yamamoto 821b03376c Make the pan gesture keep disabled (#486)
Because a panel's pan gesture becomes enabled in showing it even
after it is disabled. This issue was reported in #485.
2021-08-10 11:57:19 +09:00
Shin Yamamoto 19fe1cffef Merge pull request #472 from scenee/release/2.4.0
Release 2.4.0
2021-06-05 15:09:39 +09:00
Shin Yamamoto b8fcb50874 Version 2.4.0 2021-06-05 14:28:00 +09:00
Shin Yamamoto 9c45c31190 Fix a regression for the backdrop behavior (#466,#471)
The backdrop alpha of a panel must be updated only in `Controller.show(animated:completion:)`.
Because that prevents a backdrop flicking just before presenting a panel.

Resolve #466.
2021-06-05 14:24:19 +09:00
Shin Yamamoto 895790f697 Update README for the backdrop tap-to-dismiss action 2021-05-28 13:00:30 +09:00
Shin Yamamoto 43c7e8c2a0 Refactor LayoutAdapter
* Remove `edgeMostY`
* Rename the following properties
    * activeStates -> anchorStates
    * orderedStates -> sortedAnchorStates
    * sortedDirectionalStates -> sortedAnchorStatesByCoordinate
    * directionalLeastState -> leastCoordinateState
    * directionalMostState -> mostCoordinateState
    * edgeMostState -> mostExpandedState
    * edgeLeastState -> leastExpandedState
    * offsetFromEdgeMost -> offsetFromMostExpandedAnchor
2021-05-10 22:57:04 +09:00
Shin Yamamoto 904a66115c Add completion handler in addPanel(toParent:) (#402) 2021-05-10 22:14:01 +09:00
Shin Yamamoto d0932e8e37 Rename floatingPanelDidChangePosition to floatingPanelDidChangeState 2021-05-10 22:14:01 +09:00
Shin Yamamoto 22009013eb Merge pull request #461 from scenee/release/2.3.1
Release v2.3.1
2021-05-10 21:37:17 +09:00
Shin Yamamoto da4668aa2b Version 2.3.1 2021-05-08 13:52:17 +09:00
Shin Yamamoto 00ce232420 Refactor the samples app (#459)
* Add UseCaseController
* Add Layouts.swift
* Add PagePanelController
* Append access modifiers into some objects
* Split view controllers into each file
* Organize the project
* Rename currentMenu with currentUseCase
* Rename SampleListVC with MainVC
* Update UseCase enum
* Update DebugTableViewController to test the scroll (un)tracking functions
2021-05-08 13:17:02 +09:00
Shin Yamamoto 5b176b5cef Tidy up Extensions.swift 2021-04-24 09:38:13 +09:00
Shin Yamamoto 15709f62f8 Remove 'UI' prefix from UIExtensions 2021-04-24 09:38:13 +09:00
Shin Yamamoto 9168236534 Improve the function rounding a dimension by a display scale 2021-04-24 09:37:58 +09:00
Shin Yamamoto 16e709e8ab Fix backdrop flickering (#449)
* Add test_updateBackdropAlpha
* Fix backdrop alpha's flickers in Maps.app

This issue occurs when swinging down a panel with all one's might.
The trigger is here.
```
    func floatingPanelWillEndDragging(_ vc: FloatingPanelController, withVelocity velocity: CGPoint, targetState: UnsafeMutablePointer<FloatingPanelState>) {
        if targetState.pointee != .full {
            owner.searchVC.hideHeader(animated: true)
        }
        if targetState.pointee == .tip {
>>>         vc.contentMode = .static
        }
    }
```
However, any library users expect to affect the backdrop by this code.
And then I reconsidered the reason why the backdrop alpha changes in
activateLayout(for:forceLayout:) and it's because the animation using
CAAnimation on v1.

Therefore I decided to move the point to change the backdrop alpha into
the move animation's completion handler.

And also the responsibility of `setBackdropAlpha(of:)` was moved into
`Core` because `Core` takes on a role of changing the backdrop alpha.
2021-04-13 18:26:22 +09:00
Shin Yamamoto 94c39f0f34 Merge pull request #442 from scenee/release-2.3.0
Release v2.3.0
2021-02-27 09:41:36 +09:00
Shin Yamamoto 16fea625be Version 2.3.0 2021-02-23 13:45:46 +09:00
Shin Yamamoto 6c69694cfe Update readme 2021-02-23 13:45:46 +09:00
Shin Yamamoto 561d783479 Prevent a memory leak in a nested function (#441)
To fix a crash "Layout.swift: Fatal error: Attempted to read an unowned reference but the object was already deallocated"  which is reported at issue #440.
2021-02-23 09:37:35 +09:00
Shin Yamamoto 1bd2e60200 Enable to add custom panel states (#438)
* Support over 3 states in LayoutAdapter
* Allow to inherite FloatingPanelState
* Support a custome FloatingPanelState in ObjC
* Replace Menu enum with UseCase enum in Samples.app
* Rename UIExtensions to Extensions
* Add CustomState use case in Samples app
2021-02-15 21:05:12 +09:00
Shin Yamamoto ec0e1b2dad Remove class keyword
class keyword will be deprecated and in Swift 5 doesn't need it for a
protocol attributed by @objc.
2021-02-12 12:15:58 +09:00
Shin Yamamoto 9958fc5017 Prevent the potential memory leaks in the modal transition (#429)
This dismisses the frame 'FloatingPanel Core.move(from:to:animated:completion:)'
in the following memory leaks

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

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

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

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

Co-authored-by: Shin Yamamoto <shin@scenee.com>
2021-01-05 18:20:58 +09:00
Benjamin Otto a6538b7a2a Add optional removalInteractionVelocityThreshold value to Behavior (#425)
Added an optional variable `removalInteractionVelocityThreshold` to `Behavior`, which let’s the user configure the velocity threshold for the default `floatingPanel:shouldRemoveAt:with` implementation.
2021-01-04 23:09:33 +09:00
Shin Yamamoto 40194d91c0 Add a description for the backdropAlpha(for:) API (#416) 2020-12-24 23:53:42 +09:00
Federico Zanetello c8b2b33de0 fix example typo (#418) 2020-12-22 18:22:19 +09:00
Shin Yamamoto 7114f545ff Merge pull request #414 from SCENEE/release-2.1.0
Prepare version 2.1.0
2020-12-12 12:43:52 +09:00
Shin Yamamoto b4423bcaa2 Version 2.1.0 2020-12-07 19:25:47 +09:00
Shin Yamamoto b34fd41650 Fix Maps samlpe (#411)
* Fix the detail vc layout in Maps sample
* Deactivate search bar when the detail vc shows in Maps sample
2020-12-05 11:36:54 +09:00
Ryan McLeod 199a77182b Work magic numbers out of Adaptive Panel (Custom Layout Guide) example (#412) 2020-12-05 11:36:21 +09:00
Shin Yamamoto 0a0f00172d Add FloatingPanelAdaptiveLayoutAnchor (#390)
* Rename PassThroughView to PassthroughView
* Refactor LayoutAnchor initializer
* Add FloatignPanelAdaptiveLayoutAnchor
* Add samples for FloatingPanelAdaptiveLayoutAnchor
* Revise updateStaticConstraint
2020-12-01 19:33:19 +09:00
Shin Yamamoto 25ca9487fb Open the default behavior class (#406)
* Allow open access to FloatingPanelDefaultBehavior
* Move the default presenting & dismissing animators
* Add the public initializer of FloatingPanelDefaultBehavior
2020-11-30 21:21:21 +09:00
Shin Yamamoto 231ee4c9af Create FUNDING.yml for GitHub Sponsors
According to #400 request.
2020-11-28 10:32:03 +09:00
Ryan McLeod c872218446 Add escape gesture to close modal for VoiceOver users (#383)
* Allow dismissing of panel with Accessibility escape gesture
2020-11-07 11:29:23 +09:00
Takao Horiguchi 8e8c6527d4 update readme url for Transitioning.swift (#398) 2020-10-23 19:24:10 +09:00
Shin Yamamoto 3b4e237eba Merge pull request #397 from SCENEE/release-2.0.1
Release 2.0.1
2020-10-21 20:41:34 +09:00
Shin Yamamoto 9b1cb68f0a Version 2.0.1 2020-10-19 19:22:53 +09:00
Shin Yamamoto 4ba8acaf08 Ease the default velocity for a panel removal (#395)
Because the current threshold(10.0) makes it hard to remove a panel.

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

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

Resolve #392

* Add 2 buttons in DebugTableViewController
2020-10-19 19:17:53 +09:00
Federico Zanetello 94829d2749 add missing commas (#387) 2020-10-07 20:11:04 +09:00
Shin Yamamoto 1522a0990f Merge pull request #372 from SCENEE/v2-dev
Release FloatingPanel 2.0.0
2020-10-03 09:01:19 +09:00
Shin Yamamoto 2e3bbaeec2 Merge pull request #386 from SCENEE/release-1.7.6
Release 1.7.6
2020-09-19 12:10:10 +09:00
73 changed files with 3886 additions and 1740 deletions
+1
View File
@@ -0,0 +1 @@
github: SCENEE
+104
View File
@@ -0,0 +1,104 @@
name: ci
on:
push:
branches:
- master
pull_request:
branches:
- '*'
jobs:
build:
runs-on: macOS-11
steps:
- uses: actions/checkout@v1
- name: "Swift 5.4"
run: xcodebuild -scheme FloatingPanel SWIFT_VERSION=5.4 clean build
env:
DEVELOPER_DIR: /Applications/Xcode_12.5.1.app/Contents/Developer
- name: "Swift 5.5"
run: xcodebuild -scheme FloatingPanel SWIFT_VERSION=5.5 clean build
env:
DEVELOPER_DIR: /Applications/Xcode_13.0.app/Contents/Developer
build_compat:
runs-on: macOS-10.15
steps:
- uses: actions/checkout@v1
- name: "Swift 5.1"
run: xcodebuild -scheme FloatingPanel SWIFT_VERSION=5.1 clean build
env:
DEVELOPER_DIR: /Applications/Xcode_11.3.1.app/Contents/Developer
- name: "Swift 5.2"
run: xcodebuild -scheme FloatingPanel SWIFT_VERSION=5.2 clean build
env:
DEVELOPER_DIR: /Applications/Xcode_11.7.app/Contents/Developer
- name: "Swift 5.3"
run: xcodebuild -scheme FloatingPanel SWIFT_VERSION=5.3 clean build
env:
DEVELOPER_DIR: /Applications/Xcode_12.4.app/Contents/Developer
testing:
runs-on: macOS-11
steps:
- uses: actions/checkout@v1
- name: "Testing in iOS 14.5"
run: xcodebuild clean test -scheme FloatingPanel -workspace FloatingPanel.xcworkspace -destination 'platform=iOS Simulator,OS=14.5,name=iPhone 12 Pro'
env:
DEVELOPER_DIR: /Applications/Xcode_12.5.1.app/Contents/Developer
- name: "Testing in iOS 15.0"
run: xcodebuild clean test -scheme FloatingPanel -workspace FloatingPanel.xcworkspace -destination 'platform=iOS Simulator,OS=15.0,name=iPhone 13 Pro'
env:
DEVELOPER_DIR: /Applications/Xcode_13.0.app/Contents/Developer
testing_compat:
runs-on: macOS-10.15
steps:
- uses: actions/checkout@v1
- name: "Testing in iOS 13.7"
run: xcodebuild clean test -scheme FloatingPanel -workspace FloatingPanel.xcworkspace -destination 'platform=iOS Simulator,OS=13.7,name=iPhone 11 Pro'
env:
DEVELOPER_DIR: /Applications/Xcode_11.7.app/Contents/Developer
- name: "Testing in iOS 14.4"
run: xcodebuild clean test -scheme FloatingPanel -workspace FloatingPanel.xcworkspace -destination 'platform=iOS Simulator,OS=14.4,name=iPhone 12 Pro'
env:
DEVELOPER_DIR: /Applications/Xcode_12.4.app/Contents/Developer
example:
runs-on: macOS-11
env:
DEVELOPER_DIR: /Applications/Xcode_13.0.app/Contents/Developer
steps:
- uses: actions/checkout@v1
- name: "Build Maps"
run: xcodebuild -workspace FloatingPanel.xcworkspace -scheme Maps -sdk iphonesimulator clean build
- name: "Build Stocks"
run: xcodebuild -workspace FloatingPanel.xcworkspace -scheme Stocks -sdk iphonesimulator clean build
- name: "Build Samples"
run: xcodebuild -workspace FloatingPanel.xcworkspace -scheme Samples -sdk iphonesimulator clean build
swiftpm:
runs-on: macOS-11
env:
DEVELOPER_DIR: /Applications/Xcode_13.0.app/Contents/Developer
steps:
- uses: actions/checkout@v1
- name: "Swift Package build"
run: swift build -Xswiftc "-sdk" -Xswiftc "`xcrun --sdk iphonesimulator --show-sdk-path`" -Xswiftc "-target" -Xswiftc "x86_64-apple-ios14.3-simulator"
carthage:
runs-on: macOS-11
steps:
- uses: actions/checkout@v1
- name: "Carthage build"
run: carthage build --use-xcframeworks --no-skip-current
cocoapods:
runs-on: macOS-11
steps:
- uses: actions/checkout@v1
- name: "CocoaPods: pod lib lint"
run: pod lib lint --allow-warnings
- name: "CocoaPods: pod spec lint"
run: pod spec lint --allow-warnings
+1 -1
View File
@@ -1,3 +1,3 @@
--header "// Copyright 2018-Present Shin Yamamoto. All rights reserved. MIT license."
--disable andOperator,anyObjectProtocol,blankLinesAroundMark,blankLinesAtEndOfScope,blankLinesAtStartOfScope,blankLinesBetweenScopes,braces,consecutiveBlankLines,consecutiveSpaces,duplicateImports,elseOnSameLine,emptyBraces,hoistPatternLet,indent,isEmpty,leadingDelimiters,linebreakAtEndOfFile,linebreaks,numberFormatting,ranges,redundantBackticks,redundantBreak,redundantExtensionACL,redundantFileprivate,redundantGet,redundantInit,redundantLet,redundantLetError,redundantNilInit,redundantObjc,redundantParens,redundantPattern,redundantRawValues,redundantReturn,redundantSelf,redundantVoidReturnType,semicolons,sortedImports,spaceAroundBraces,spaceAroundBrackets,spaceAroundComments,spaceAroundGenerics,spaceAroundOperators,spaceAroundParens,spaceInsideBraces,spaceInsideBrackets,spaceInsideComments,spaceInsideGenerics,spaceInsideParens,specifiers,strongOutlets,strongifiedSelf,todos,trailingClosures,trailingCommas,trailingSpace,typeSugar,unusedArguments,void,wrapArguments,yodaConditions
--disable andOperator,anyObjectProtocol,blankLinesAroundMark,blankLinesAtEndOfScope,blankLinesAtStartOfScope,blankLinesBetweenScopes,braces,consecutiveBlankLines,consecutiveSpaces,duplicateImports,elseOnSameLine,emptyBraces,hoistPatternLet,indent,isEmpty,leadingDelimiters,linebreakAtEndOfFile,linebreaks,numberFormatting,redundantBackticks,redundantBreak,redundantExtensionACL,redundantFileprivate,redundantGet,redundantInit,redundantLet,redundantLetError,redundantNilInit,redundantObjc,redundantParens,redundantPattern,redundantRawValues,redundantReturn,redundantSelf,redundantVoidReturnType,semicolons,sortedImports,spaceAroundBraces,spaceAroundBrackets,spaceAroundComments,spaceAroundGenerics,spaceAroundOperators,spaceAroundParens,spaceInsideBraces,spaceInsideBrackets,spaceInsideComments,spaceInsideGenerics,spaceInsideParens,specifiers,strongOutlets,strongifiedSelf,todos,trailingClosures,trailingCommas,trailingSpace,typeSugar,unusedArguments,void,wrapArguments,yodaConditions
-74
View File
@@ -1,74 +0,0 @@
language: objective-c
branches:
only:
- master
env:
global:
- LANG=en_US.UTF-8
- LC_ALL=en_US.UTF-8
jobs:
include:
- stage: "Builds"
script: xcodebuild -scheme FloatingPanel SWIFT_VERSION=5.1 SUPPORTS_MACCATALYST=NO clean build
# SUPPORTS_MACCATALYST=NO because Xcode 11 runs on macOS 10.14 in Travis CI
osx_image: xcode11.3
name: "Swift 5.1"
- script: xcodebuild -scheme FloatingPanel SWIFT_VERSION=5.2 clean build
osx_image: xcode11.6
name: "Swift 5.2"
- script: xcodebuild -scheme FloatingPanel SWIFT_VERSION=5.3 clean build
osx_image: xcode12
name: "Swift 5.3"
- stage: "Tests"
script: xcodebuild clean test -scheme FloatingPanel -workspace FloatingPanel.xcworkspace -destination 'platform=iOS Simulator,OS=10.3.1,name=iPhone SE (1st generation)'
osx_image: xcode11.6
name: "iPhone SE (iOS 10.3)"
- script: xcodebuild clean test -scheme FloatingPanel -workspace FloatingPanel.xcworkspace -destination 'platform=iOS Simulator,OS=11.4,name=iPhone 7'
osx_image: xcode11.6
name: "iPhone 7 (iOS 11.4)"
- script: xcodebuild clean test -scheme FloatingPanel -workspace FloatingPanel.xcworkspace -destination 'platform=iOS Simulator,OS=12.4,name=iPhone X'
osx_image: xcode11.6
name: "iPhone X (iOS 12.4)"
- script: xcodebuild clean test -scheme FloatingPanel -workspace FloatingPanel.xcworkspace -destination 'platform=iOS Simulator,OS=13.6,name=iPhone 11'
osx_image: xcode11.6
name: "iPhone X (iOS 13.6)"
- stage: "Build examples"
osx_image: xcode11.6
script: xcodebuild -workspace FloatingPanel.xcworkspace -scheme Maps -sdk iphonesimulator clean build
name: "Maps"
- script: xcodebuild -workspace FloatingPanel.xcworkspace -scheme Stocks -sdk iphonesimulator clean build
osx_image: xcode11.6
name: "Stocks"
- script: xcodebuild -workspace FloatingPanel.xcworkspace -scheme Samples -sdk iphonesimulator clean build
osx_image: xcode11.6
name: "Samples"
- script: xcodebuild -workspace FloatingPanel.xcworkspace -scheme SamplesObjC -sdk iphonesimulator clean build
osx_image: xcode11.6
name: "SamplesObjC"
- stage: "Swift Package Manager"
script: swift build -Xswiftc "-sdk" -Xswiftc "`xcrun --sdk iphoneos --show-sdk-path`" -Xswiftc "-target" -Xswiftc "arm64-apple-ios13.0"
osx_image: xcode11.6
name: "iPhone OS"
- script: swift build -Xswiftc "-sdk" -Xswiftc "`xcrun --sdk iphonesimulator --show-sdk-path`" -Xswiftc "-target" -Xswiftc "x86_64-apple-ios13.0-simulator"
osx_image: xcode11.6
name: "iPhone Simulator"
- stage: "Carthage"
# Carthage doesn't fix the issue with Xcode 12: https://github.com/Carthage/Carthage/releases/tag/0.36.0
osx_image: xcode11.6
before_install:
- brew update
- brew outdated carthage || brew upgrade carthage
script:
- carthage build --no-skip-current
- stage: "CocoaPods"
osx_image: xcode11.6
before_install:
- gem install cocoapods
script:
- pod spec lint --allow-warnings
- pod lib lint --allow-warnings
@@ -194,8 +194,8 @@ class MyPanelIntrinsicLayout: FloatingPanelLayout {
var initialState: FloatingPanelState { .full }
var anchors: [FloatingPanelState : FloatingPanelLayoutAnchoring] {
return [
.full: FloatingPanelIntrinsicLayoutAnchor(absoluteOffset: 16.0, referenceGuide: .safeArea)
.half: FloatingPanelIntrinsicLayoutAnchor(fractionalOffset: 0.5, referenceGuide: .safeArea)
.full: FloatingPanelIntrinsicLayoutAnchor(absoluteOffset: 16.0, referenceGuide: .safeArea),
.half: FloatingPanelIntrinsicLayoutAnchor(fractionalOffset: 0.5, referenceGuide: .safeArea),
.tip: FloatingPanelLayoutAnchor(absoluteInset: 44.0, edge: .bottom, referenceGuide: .safeArea)
]
}
@@ -0,0 +1,426 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 55;
objects = {
/* Begin PBXBuildFile section */
6467E8642699AC5F00565F4F /* SurfaceAppearance+phone.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6467E8632699AC5F00565F4F /* SurfaceAppearance+phone.swift */; };
6467E86A2699B19D00565F4F /* SearchPanelPhoneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6467E8692699B19D00565F4F /* SearchPanelPhoneDelegate.swift */; };
649A122926C14D0900DAB961 /* UIHostingController+ignoreKeyboard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 649A122826C14D0900DAB961 /* UIHostingController+ignoreKeyboard.swift */; };
649A122D26C168CF00DAB961 /* View+floatingPanelGrabberHandlePadding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 649A122C26C168CF00DAB961 /* View+floatingPanelGrabberHandlePadding.swift */; };
64A5B7232691323900BCAA05 /* MapsApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64A5B7222691323900BCAA05 /* MapsApp.swift */; };
64A5B7252691323900BCAA05 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64A5B7242691323900BCAA05 /* ContentView.swift */; };
64A5B734269133DC00BCAA05 /* FloatingPanel.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 64A5B733269133DC00BCAA05 /* FloatingPanel.framework */; };
64A5B735269133DC00BCAA05 /* FloatingPanel.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 64A5B733269133DC00BCAA05 /* FloatingPanel.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
64A5B738269134CA00BCAA05 /* VisualEffectBlur.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64A5B737269134CA00BCAA05 /* VisualEffectBlur.swift */; };
64A5B73C2691469900BCAA05 /* FloatingPanelContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64A5B73B2691469900BCAA05 /* FloatingPanelContentView.swift */; };
64A5B73E269147DC00BCAA05 /* SearchBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64A5B73D269147DC00BCAA05 /* SearchBar.swift */; };
64A5B7402691532400BCAA05 /* ResultsList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64A5B73F2691532400BCAA05 /* ResultsList.swift */; };
64A5B7422691541A00BCAA05 /* HostingCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64A5B7412691541A00BCAA05 /* HostingCell.swift */; };
64F7E83126AD70EB00A0E0F7 /* View+floatingPanelContentInsetAdjustmentBehavior.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64F7E82C26AD70EB00A0E0F7 /* View+floatingPanelContentInsetAdjustmentBehavior.swift */; };
64F7E83226AD70EB00A0E0F7 /* View+floatingPanelContentMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64F7E82D26AD70EB00A0E0F7 /* View+floatingPanelContentMode.swift */; };
64F7E83326AD70EB00A0E0F7 /* FloatingPanelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64F7E82E26AD70EB00A0E0F7 /* FloatingPanelView.swift */; };
64F7E83426AD70EB00A0E0F7 /* View+floatingPanel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64F7E82F26AD70EB00A0E0F7 /* View+floatingPanel.swift */; };
64F7E83526AD70EB00A0E0F7 /* View+floatingPanelSurfaceAppearance.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64F7E83026AD70EB00A0E0F7 /* View+floatingPanelSurfaceAppearance.swift */; };
/* End PBXBuildFile section */
/* Begin PBXCopyFilesBuildPhase section */
64A5B736269133DC00BCAA05 /* Embed Frameworks */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 10;
files = (
64A5B735269133DC00BCAA05 /* FloatingPanel.framework in Embed Frameworks */,
);
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
6467E8632699AC5F00565F4F /* SurfaceAppearance+phone.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SurfaceAppearance+phone.swift"; sourceTree = "<group>"; };
6467E8692699B19D00565F4F /* SearchPanelPhoneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchPanelPhoneDelegate.swift; sourceTree = "<group>"; };
649A122826C14D0900DAB961 /* UIHostingController+ignoreKeyboard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIHostingController+ignoreKeyboard.swift"; sourceTree = "<group>"; };
649A122C26C168CF00DAB961 /* View+floatingPanelGrabberHandlePadding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+floatingPanelGrabberHandlePadding.swift"; sourceTree = "<group>"; };
64A5B71F2691323900BCAA05 /* Maps-SwiftUI.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Maps-SwiftUI.app"; sourceTree = BUILT_PRODUCTS_DIR; };
64A5B7222691323900BCAA05 /* MapsApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapsApp.swift; sourceTree = "<group>"; };
64A5B7242691323900BCAA05 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
64A5B733269133DC00BCAA05 /* FloatingPanel.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = FloatingPanel.framework; sourceTree = BUILT_PRODUCTS_DIR; };
64A5B737269134CA00BCAA05 /* VisualEffectBlur.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VisualEffectBlur.swift; sourceTree = "<group>"; };
64A5B73B2691469900BCAA05 /* FloatingPanelContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FloatingPanelContentView.swift; sourceTree = "<group>"; };
64A5B73D269147DC00BCAA05 /* SearchBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchBar.swift; sourceTree = "<group>"; };
64A5B73F2691532400BCAA05 /* ResultsList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResultsList.swift; sourceTree = "<group>"; };
64A5B7412691541A00BCAA05 /* HostingCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HostingCell.swift; sourceTree = "<group>"; };
64F7E82C26AD70EB00A0E0F7 /* View+floatingPanelContentInsetAdjustmentBehavior.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "View+floatingPanelContentInsetAdjustmentBehavior.swift"; sourceTree = "<group>"; };
64F7E82D26AD70EB00A0E0F7 /* View+floatingPanelContentMode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "View+floatingPanelContentMode.swift"; sourceTree = "<group>"; };
64F7E82E26AD70EB00A0E0F7 /* FloatingPanelView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FloatingPanelView.swift; sourceTree = "<group>"; };
64F7E82F26AD70EB00A0E0F7 /* View+floatingPanel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "View+floatingPanel.swift"; sourceTree = "<group>"; };
64F7E83026AD70EB00A0E0F7 /* View+floatingPanelSurfaceAppearance.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "View+floatingPanelSurfaceAppearance.swift"; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
64A5B71C2691323900BCAA05 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
64A5B734269133DC00BCAA05 /* FloatingPanel.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
64A5B7162691323900BCAA05 = {
isa = PBXGroup;
children = (
64A5B7212691323900BCAA05 /* Maps */,
64A5B7202691323900BCAA05 /* Products */,
64A5B732269133DC00BCAA05 /* Frameworks */,
);
sourceTree = "<group>";
};
64A5B7202691323900BCAA05 /* Products */ = {
isa = PBXGroup;
children = (
64A5B71F2691323900BCAA05 /* Maps-SwiftUI.app */,
);
name = Products;
sourceTree = "<group>";
};
64A5B7212691323900BCAA05 /* Maps */ = {
isa = PBXGroup;
children = (
64F7E82B26AD70EB00A0E0F7 /* FloatingPanel */,
64BE55702691BBB0006D98BD /* Representable */,
64A5B7222691323900BCAA05 /* MapsApp.swift */,
64A5B7242691323900BCAA05 /* ContentView.swift */,
64A5B73B2691469900BCAA05 /* FloatingPanelContentView.swift */,
6467E8692699B19D00565F4F /* SearchPanelPhoneDelegate.swift */,
6467E8632699AC5F00565F4F /* SurfaceAppearance+phone.swift */,
64A5B7412691541A00BCAA05 /* HostingCell.swift */,
64A5B73F2691532400BCAA05 /* ResultsList.swift */,
);
path = Maps;
sourceTree = "<group>";
};
64A5B732269133DC00BCAA05 /* Frameworks */ = {
isa = PBXGroup;
children = (
64A5B733269133DC00BCAA05 /* FloatingPanel.framework */,
);
name = Frameworks;
sourceTree = "<group>";
};
64BE55702691BBB0006D98BD /* Representable */ = {
isa = PBXGroup;
children = (
64A5B73D269147DC00BCAA05 /* SearchBar.swift */,
64A5B737269134CA00BCAA05 /* VisualEffectBlur.swift */,
);
path = Representable;
sourceTree = "<group>";
};
64F7E82B26AD70EB00A0E0F7 /* FloatingPanel */ = {
isa = PBXGroup;
children = (
64F7E82E26AD70EB00A0E0F7 /* FloatingPanelView.swift */,
64F7E82F26AD70EB00A0E0F7 /* View+floatingPanel.swift */,
64F7E82C26AD70EB00A0E0F7 /* View+floatingPanelContentInsetAdjustmentBehavior.swift */,
64F7E82D26AD70EB00A0E0F7 /* View+floatingPanelContentMode.swift */,
64F7E83026AD70EB00A0E0F7 /* View+floatingPanelSurfaceAppearance.swift */,
649A122C26C168CF00DAB961 /* View+floatingPanelGrabberHandlePadding.swift */,
649A122826C14D0900DAB961 /* UIHostingController+ignoreKeyboard.swift */,
);
path = FloatingPanel;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
64A5B71E2691323900BCAA05 /* Maps-SwiftUI */ = {
isa = PBXNativeTarget;
buildConfigurationList = 64A5B72D2691323A00BCAA05 /* Build configuration list for PBXNativeTarget "Maps-SwiftUI" */;
buildPhases = (
64A5B71B2691323900BCAA05 /* Sources */,
64A5B71C2691323900BCAA05 /* Frameworks */,
64A5B71D2691323900BCAA05 /* Resources */,
64A5B736269133DC00BCAA05 /* Embed Frameworks */,
);
buildRules = (
);
dependencies = (
);
name = "Maps-SwiftUI";
productName = "Maps-SwiftUI";
productReference = 64A5B71F2691323900BCAA05 /* Maps-SwiftUI.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
64A5B7172691323900BCAA05 /* Project object */ = {
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = 1;
LastSwiftUpdateCheck = 1300;
LastUpgradeCheck = 1300;
TargetAttributes = {
64A5B71E2691323900BCAA05 = {
CreatedOnToolsVersion = 13.0;
};
};
};
buildConfigurationList = 64A5B71A2691323900BCAA05 /* Build configuration list for PBXProject "Maps-SwiftUI" */;
compatibilityVersion = "Xcode 13.0";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = 64A5B7162691323900BCAA05;
productRefGroup = 64A5B7202691323900BCAA05 /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
64A5B71E2691323900BCAA05 /* Maps-SwiftUI */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
64A5B71D2691323900BCAA05 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
64A5B71B2691323900BCAA05 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
6467E86A2699B19D00565F4F /* SearchPanelPhoneDelegate.swift in Sources */,
64A5B7422691541A00BCAA05 /* HostingCell.swift in Sources */,
64A5B738269134CA00BCAA05 /* VisualEffectBlur.swift in Sources */,
64A5B7402691532400BCAA05 /* ResultsList.swift in Sources */,
6467E8642699AC5F00565F4F /* SurfaceAppearance+phone.swift in Sources */,
64F7E83226AD70EB00A0E0F7 /* View+floatingPanelContentMode.swift in Sources */,
64A5B7252691323900BCAA05 /* ContentView.swift in Sources */,
64A5B7232691323900BCAA05 /* MapsApp.swift in Sources */,
64A5B73C2691469900BCAA05 /* FloatingPanelContentView.swift in Sources */,
649A122D26C168CF00DAB961 /* View+floatingPanelGrabberHandlePadding.swift in Sources */,
64F7E83326AD70EB00A0E0F7 /* FloatingPanelView.swift in Sources */,
64F7E83426AD70EB00A0E0F7 /* View+floatingPanel.swift in Sources */,
64F7E83526AD70EB00A0E0F7 /* View+floatingPanelSurfaceAppearance.swift in Sources */,
64F7E83126AD70EB00A0E0F7 /* View+floatingPanelContentInsetAdjustmentBehavior.swift in Sources */,
64A5B73E269147DC00BCAA05 /* SearchBar.swift in Sources */,
649A122926C14D0900DAB961 /* UIHostingController+ignoreKeyboard.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin XCBuildConfiguration section */
64A5B72B2691323A00BCAA05 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
};
name = Debug;
};
64A5B72C2691323A00BCAA05 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
SDKROOT = iphoneos;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O";
VALIDATE_PRODUCT = YES;
};
name = Release;
};
64A5B72E2691323A00BCAA05 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = "com.scenee.Maps-SwiftUI";
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
64A5B72F2691323A00BCAA05 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = "com.scenee.Maps-SwiftUI";
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
64A5B71A2691323900BCAA05 /* Build configuration list for PBXProject "Maps-SwiftUI" */ = {
isa = XCConfigurationList;
buildConfigurations = (
64A5B72B2691323A00BCAA05 /* Debug */,
64A5B72C2691323A00BCAA05 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
64A5B72D2691323A00BCAA05 /* Build configuration list for PBXNativeTarget "Maps-SwiftUI" */ = {
isa = XCConfigurationList;
buildConfigurations = (
64A5B72E2691323A00BCAA05 /* Debug */,
64A5B72F2691323A00BCAA05 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = 64A5B7172691323900BCAA05 /* Project object */;
}
@@ -0,0 +1,33 @@
// Copyright 2021 the FloatingPanel authors. All rights reserved. MIT license.
import SwiftUI
import MapKit
struct ContentView: View {
@State private var region = MKCoordinateRegion(
center: CLLocationCoordinate2D(latitude: 37.623198015869235, longitude: -122.43066818432008),
span: MKCoordinateSpan(latitudeDelta: 0.4425100023575723, longitudeDelta: 0.28543697435880233)
)
var body: some View {
ZStack {
Map(coordinateRegion: $region)
.ignoresSafeArea()
statusBarBlur
}
}
private var statusBarBlur: some View {
GeometryReader { geometry in
VisualEffectBlur()
.frame(height: geometry.safeAreaInsets.top)
.ignoresSafeArea()
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
@@ -0,0 +1,121 @@
// Copyright 2021 the FloatingPanel authors. All rights reserved. MIT license.
import FloatingPanel
import SwiftUI
/// A proxy for exposing the methods of the floating panel controller.
public struct FloatingPanelProxy {
/// The associated floating panel controller.
public weak var fpc: FloatingPanelController?
/// Tracks the specified scroll view to correspond with the scroll.
///
/// - Parameter scrollView: Specify a scroll view to continuously and
/// seamlessly work in concert with interactions of the surface view.
public func track(scrollView: UIScrollView) {
fpc?.track(scrollView: scrollView)
}
/// Moves the floating panel to the specified position.
///
/// - Parameters:
/// - floatingPanelState: The state to move to.
/// - animated: `true` to animate the transition to the new state; `false`
/// otherwise.
public func move(
to floatingPanelState: FloatingPanelState,
animated: Bool,
completion: (() -> Void)? = nil
) {
fpc?.move(to: floatingPanelState, animated: animated, completion: completion)
}
}
/// A view with an associated floating panel.
struct FloatingPanelView<Content: View, FloatingPanelContent: View>: UIViewControllerRepresentable {
/// A type that conforms to the `FloatingPanelControllerDelegate` protocol.
var delegate: FloatingPanelControllerDelegate?
/// The behavior for determining the adjusted content offsets.
@Environment(\.contentInsetAdjustmentBehavior) var contentInsetAdjustmentBehavior
/// Constants that define how a panel content fills in the surface.
@Environment(\.contentMode) var contentMode
/// The floating panel grabber handle offset.
@Environment(\.grabberHandlePadding) var grabberHandlePadding
/// The floating panel `surfaceView` appearance.
@Environment(\.surfaceAppearance) var surfaceAppearance
/// The view builder that creates the floating panel parent view content.
@ViewBuilder var content: Content
/// The view builder that creates the floating panel content.
@ViewBuilder var floatingPanelContent: (FloatingPanelProxy) -> FloatingPanelContent
public func makeUIViewController(context: Context) -> UIHostingController<Content> {
let hostingController = UIHostingController(rootView: content)
hostingController.view.backgroundColor = nil
// We need to wait for the current runloop cycle to complete before our
// view is actually added (into the view hierarchy), otherwise the
// environment is not ready yet.
DispatchQueue.main.async {
context.coordinator.setupFloatingPanel(hostingController)
}
return hostingController
}
public func updateUIViewController(
_ uiViewController: UIHostingController<Content>,
context: Context
) {
context.coordinator.updateIfNeeded()
}
public func makeCoordinator() -> Coordinator {
Coordinator(parent: self)
}
/// `FloatingPanelView` coordinator.
///
/// Responsible to setup the view hierarchy and floating panel.
final class Coordinator {
private let parent: FloatingPanelView<Content, FloatingPanelContent>
private lazy var fpc = FloatingPanelController()
init(parent: FloatingPanelView<Content, FloatingPanelContent>) {
self.parent = parent
}
func setupFloatingPanel(_ parentViewController: UIViewController) {
updateIfNeeded()
let panelContent = parent.floatingPanelContent(FloatingPanelProxy(fpc: fpc))
let hostingViewController = UIHostingController(
rootView: panelContent,
ignoresKeyboard: true
)
hostingViewController.view.backgroundColor = nil
fpc.set(contentViewController: hostingViewController)
fpc.addPanel(toParent: parentViewController, at: 1, animated: false)
}
func updateIfNeeded() {
if fpc.contentInsetAdjustmentBehavior != parent.contentInsetAdjustmentBehavior {
fpc.contentInsetAdjustmentBehavior = parent.contentInsetAdjustmentBehavior
}
if fpc.contentMode != parent.contentMode {
fpc.contentMode = parent.contentMode
}
if fpc.delegate !== parent.delegate {
fpc.delegate = parent.delegate
}
if fpc.surfaceView.grabberHandlePadding != parent.grabberHandlePadding {
fpc.surfaceView.grabberHandlePadding = parent.grabberHandlePadding
}
if fpc.surfaceView.appearance != parent.surfaceAppearance {
fpc.surfaceView.appearance = parent.surfaceAppearance
}
}
}
}
@@ -0,0 +1,44 @@
// Copyright 2021 the FloatingPanel authors. All rights reserved. MIT license.
import SwiftUI
/// This extension makes sure SwiftUI views are not affected by iOS keyboard.
///
/// Credits to https://steipete.me/posts/disabling-keyboard-avoidance-in-swiftui-uihostingcontroller/
extension UIHostingController {
public convenience init(rootView: Content, ignoresKeyboard: Bool) {
self.init(rootView: rootView)
if ignoresKeyboard {
guard let viewClass = object_getClass(view) else { return }
let viewSubclassName = String(
cString: class_getName(viewClass)
).appending("_IgnoresKeyboard")
if let viewSubclass = NSClassFromString(viewSubclassName) {
object_setClass(view, viewSubclass)
} else {
guard
let viewClassNameUtf8 = (viewSubclassName as NSString).utf8String,
let viewSubclass = objc_allocateClassPair(viewClass, viewClassNameUtf8, 0)
else { return }
if let method = class_getInstanceMethod(
viewClass,
NSSelectorFromString("keyboardWillShowWithNotification:")
) {
let keyboardWillShow: @convention(block) (AnyObject, AnyObject) -> Void = { _, _ in }
class_addMethod(
viewSubclass,
NSSelectorFromString("keyboardWillShowWithNotification:"),
imp_implementationWithBlock(keyboardWillShow),
method_getTypeEncoding(method)
)
}
objc_registerClassPair(viewSubclass)
object_setClass(view, viewSubclass)
}
}
}
}
@@ -0,0 +1,31 @@
// Copyright 2021 the FloatingPanel authors. All rights reserved. MIT license.
import FloatingPanel
import SwiftUI
extension View {
/// Presents a floating panel using the given closure as its content.
///
/// The modifier's content view builder receives a `FloatingPanelProxy`
/// instance; you use the proxy's methods to interact with the associated
/// `FloatingPanelController`.
///
/// - Parameters:
/// - delegate: A type that conforms to the
/// `FloatingPanelControllerDelegate` protocol. You have comprehensive
/// control over the floating panel behavior when you use a delegate.
/// - floatingPanelContent: The floating panel content. This view builder
/// receives a `FloatingPanelProxy` instance that you use to interact
/// with the `FloatingPanelController`.
public func floatingPanel<FloatingPanelContent: View>(
delegate: FloatingPanelControllerDelegate? = nil,
@ViewBuilder _ floatingPanelContent: @escaping (_: FloatingPanelProxy) -> FloatingPanelContent
) -> some View {
FloatingPanelView(
delegate: delegate,
content: { self },
floatingPanelContent: floatingPanelContent
)
.ignoresSafeArea()
}
}
@@ -0,0 +1,38 @@
// Copyright 2021 the FloatingPanel authors. All rights reserved. MIT license.
import FloatingPanel
import SwiftUI
struct ContentInsetKey: EnvironmentKey {
static var defaultValue: FloatingPanelController.ContentInsetAdjustmentBehavior = .always
}
extension EnvironmentValues {
/// The behavior for determining the adjusted content offsets.
var contentInsetAdjustmentBehavior: FloatingPanelController.ContentInsetAdjustmentBehavior {
get { self[ContentInsetKey.self] }
set { self[ContentInsetKey.self] = newValue }
}
}
extension View {
/// Sets the content inset adjustment behavior for floating panels within
/// this view.
///
/// Use this modifier to set a specific content inset adjustment behavior
/// for floating panel instances within a view:
///
/// MainView()
/// .floatingPanel { _ in
/// FloatingPanelContent()
/// }
/// .floatingPanelContentInsetAdjustmentBehavior(.never)
///
/// - Parameter contentInsetAdjustmentBehavior: The content inset adjustment
/// behavior to set.
public func floatingPanelContentInsetAdjustmentBehavior(
_ contentInsetAdjustmentBehavior: FloatingPanelController.ContentInsetAdjustmentBehavior
) -> some View {
environment(\.contentInsetAdjustmentBehavior, contentInsetAdjustmentBehavior)
}
}
@@ -0,0 +1,37 @@
// Copyright 2021 the FloatingPanel authors. All rights reserved. MIT license.
import FloatingPanel
import SwiftUI
struct ContentModeKey: EnvironmentKey {
static var defaultValue: FloatingPanelController.ContentMode = .static
}
extension EnvironmentValues {
/// Used to determine how the floating panel controller lays out the content
/// view when the surface position changes.
var contentMode: FloatingPanelController.ContentMode {
get { self[ContentModeKey.self] }
set { self[ContentModeKey.self] = newValue }
}
}
extension View {
/// Sets the content mode for floating panels within this view.
///
/// Use this modifier to set a specific content mode for floating panel
/// instances within a view:
///
/// MainView()
/// .floatingPanel { _ in
/// FloatingPanelContent()
/// }
/// .floatingPanelContentMode(.static)
///
/// - Parameter contentMode: The content mode to set.
public func floatingPanelContentMode(
_ contentMode: FloatingPanelController.ContentMode
) -> some View {
environment(\.contentMode, contentMode)
}
}
@@ -0,0 +1,36 @@
// Copyright 2021 the FloatingPanel authors. All rights reserved. MIT license.
import FloatingPanel
import SwiftUI
struct GrabberHandlePaddingKey: EnvironmentKey {
static var defaultValue: CGFloat = 6.0
}
extension EnvironmentValues {
/// The offset of the grabber handle from the interactive edge.
var grabberHandlePadding: CGFloat {
get { self[GrabberHandlePaddingKey.self] }
set { self[GrabberHandlePaddingKey.self] = newValue }
}
}
extension View {
/// Sets the grabber handle padding for floating panels within this view.
///
/// Use this modifier to set a specific padding to floating panel instances
/// within a view:
///
/// MainView()
/// .floatingPanel { _ in
/// FloatingPanelContent()
/// }
/// .floatingPanelGrabberHandlePadding(16)
///
/// - Parameter padding: The grabber handle padding to set.
public func floatingPanelGrabberHandlePadding(
_ padding: CGFloat
) -> some View {
environment(\.grabberHandlePadding, padding)
}
}
@@ -0,0 +1,44 @@
// Copyright 2021 the FloatingPanel authors. All rights reserved. MIT license.
import FloatingPanel
import SwiftUI
struct SurfaceAppearanceKey: EnvironmentKey {
static var defaultValue = SurfaceAppearance()
}
extension EnvironmentValues {
/// The appearance of a surface view.
var surfaceAppearance: SurfaceAppearance {
get { self[SurfaceAppearanceKey.self] }
set { self[SurfaceAppearanceKey.self] = newValue }
}
}
extension View {
/// Sets the surface appearance for floating panels within this view.
///
/// Use this modifier to set a specific surface appearance for floating
/// panel instances within a view:
///
/// MainView()
/// .floatingPanel { _ in
/// FloatingPanelContent()
/// }
/// .floatingPanelSurfaceAppearance(.transparent)
///
/// extension SurfaceAppearance {
/// static var transparent: SurfaceAppearance {
/// let appearance = SurfaceAppearance()
/// appearance.backgroundColor = .clear
/// return appearance
/// }
/// }
///
/// - Parameter surfaceAppearance: The surface appearance to set.
public func floatingPanelSurfaceAppearance(
_ surfaceAppearance: SurfaceAppearance
) -> some View {
environment(\.surfaceAppearance, surfaceAppearance)
}
}
@@ -0,0 +1,43 @@
// Copyright 2021 the FloatingPanel authors. All rights reserved. MIT license.
import SwiftUI
struct FloatingPanelContentView: View {
@State private var searchText = ""
@State private var isShowingCancelButton = false
var proxy: FloatingPanelProxy
var body: some View {
VStack(spacing: 0) {
searchBar
resultsList
}
// 👇🏻 for the floating panel grabber handle.
.padding(.top, 6)
.background(
VisualEffectBlur(blurStyle: .systemMaterial)
// If the `VisualEffectBlur` view receives taps, it's going
// to mess up with the whole panel and render it
// non-interactive, make sure it never receives any taps.
.allowsHitTesting(false)
)
.ignoresSafeArea()
}
var searchBar: some View {
SearchBar(
"Search for a place or address",
text: $searchText,
isShowingCancelButton: $isShowingCancelButton
) { isFocused in
proxy.move(to: isFocused ? .full : .half, animated: true)
isShowingCancelButton = isFocused
} onCancel: {
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
}
}
var resultsList: some View {
ResultsList(onScrollViewCreated: proxy.track(scrollView:))
}
}
@@ -0,0 +1,49 @@
// Copyright 2021 the FloatingPanel authors. All rights reserved. MIT license.
import SwiftUI
/// A `UITableViewCell` that accepts a SwiftUI view as its content.
///
/// Credits to https://noahgilmore.com/blog/swiftui-self-sizing-cells/ .
public final class HostingCell<Content: View>: UITableViewCell {
private let hostingController = UIHostingController<Content?>(
rootView: nil,
ignoresKeyboard: true
)
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
hostingController.view.backgroundColor = nil
backgroundColor = .clear
}
@available(*, unavailable)
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
public func set(rootView: Content, parentController: UIViewController) {
hostingController.rootView = rootView
hostingController.view.invalidateIntrinsicContentSize()
let requiresControllerMove = hostingController.parent != parentController
if requiresControllerMove {
parentController.addChild(hostingController)
}
if !contentView.subviews.contains(hostingController.view) {
contentView.addSubview(hostingController.view)
hostingController.view.translatesAutoresizingMaskIntoConstraints = false
contentView.addConstraints([
hostingController.view.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
hostingController.view.topAnchor.constraint(equalTo: contentView.topAnchor),
hostingController.view.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
hostingController.view.bottomAnchor.constraint(equalTo: contentView.bottomAnchor)
])
}
if requiresControllerMove {
hostingController.didMove(toParent: parentController)
}
}
}
+18
View File
@@ -0,0 +1,18 @@
// Copyright 2021 the FloatingPanel authors. All rights reserved. MIT license.
import SwiftUI
@main
struct MapsApp: App {
var body: some Scene {
WindowGroup {
ContentView()
.floatingPanel(delegate: SearchPanelPhoneDelegate()) { proxy in
FloatingPanelContentView(proxy: proxy)
}
.floatingPanelSurfaceAppearance(.phone)
.floatingPanelContentMode(.fitToBounds)
.floatingPanelContentInsetAdjustmentBehavior(.never)
}
}
}
@@ -0,0 +1,74 @@
// Copyright 2021 the FloatingPanel authors. All rights reserved. MIT license.
import SwiftUI
/// UIKit's `UISearchBar`brought to SwiftUI.
public struct SearchBar: UIViewRepresentable {
var title: String
@Binding var text: String
@Binding var isShowingCancelButton: Bool
var onEditingChanged: (Bool) -> Void
var onCancel: () -> Void
public init(
_ title: String = "",
text: Binding<String>,
isShowingCancelButton: Binding<Bool>,
onEditingChanged: @escaping (Bool) -> Void = { _ in },
onCancel: @escaping () -> Void
) {
self.title = title
self._text = text
self._isShowingCancelButton = isShowingCancelButton
self.onEditingChanged = onEditingChanged
self.onCancel = onCancel
}
public func makeUIView(context: UIViewRepresentableContext<SearchBar>) -> UISearchBar {
let searchBar = UISearchBar(frame: .zero)
searchBar.searchBarStyle = .minimal
searchBar.isTranslucent = true
searchBar.placeholder = title
searchBar.delegate = context.coordinator
searchBar.autocapitalizationType = .none
return searchBar
}
public func updateUIView(_ uiView: UISearchBar, context: UIViewRepresentableContext<SearchBar>) {
uiView.text = text
uiView.placeholder = title
uiView.setShowsCancelButton(isShowingCancelButton, animated: true)
}
public func makeCoordinator() -> SearchBar.Coordinator {
Coordinator(parent: self)
}
public class Coordinator: NSObject, UISearchBarDelegate {
var parent: SearchBar
init(parent: SearchBar) {
self.parent = parent
}
public func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
parent.text = searchText
}
public func searchBarTextDidBeginEditing(_ searchBar: UISearchBar) {
parent.onEditingChanged(true)
}
public func searchBarSearchButtonClicked(_ searchBar: UISearchBar) {
searchBar.resignFirstResponder()
}
public func searchBarTextDidEndEditing(_ searchBar: UISearchBar) {
parent.onEditingChanged(false)
}
public func searchBarCancelButtonClicked(_ searchBar: UISearchBar) {
parent.onCancel()
}
}
}
@@ -0,0 +1,69 @@
// Copyright 2021 the FloatingPanel authors. All rights reserved. MIT license.
import SwiftUI
@available(iOS, introduced: 13, deprecated: 15, message: "Use iOS 15 material API.")
public struct VisualEffectBlur<Content: View>: UIViewRepresentable {
var blurStyle: UIBlurEffect.Style = .systemMaterial
var vibrancyStyle: UIVibrancyEffectStyle? = nil
@ViewBuilder var content: Content
public func makeUIView(context: Context) -> UIVisualEffectView {
context.coordinator.blurView
}
public func updateUIView(_ view: UIVisualEffectView, context: Context) {
context.coordinator.update(
content: content,
blurStyle: blurStyle,
vibrancyStyle: vibrancyStyle
)
}
public func makeCoordinator() -> Coordinator {
Coordinator(content: content)
}
public class Coordinator {
let blurView = UIVisualEffectView()
let vibrancyView = UIVisualEffectView()
let hostingController: UIHostingController<Content>
init(content: Content) {
hostingController = UIHostingController(rootView: content)
hostingController.view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
hostingController.view.backgroundColor = nil
blurView.contentView.addSubview(vibrancyView)
blurView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
vibrancyView.contentView.addSubview(hostingController.view)
vibrancyView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
}
func update(content: Content, blurStyle: UIBlurEffect.Style, vibrancyStyle: UIVibrancyEffectStyle?) {
hostingController.rootView = content
let blurEffect = UIBlurEffect(style: blurStyle)
blurView.effect = blurEffect
if let vibrancyStyle = vibrancyStyle {
vibrancyView.effect = UIVibrancyEffect(blurEffect: blurEffect, style: vibrancyStyle)
} else {
vibrancyView.effect = nil
}
hostingController.view.setNeedsDisplay()
}
}
}
public extension VisualEffectBlur where Content == EmptyView {
init(
blurStyle: UIBlurEffect.Style = .systemMaterial,
vibrancyStyle: UIVibrancyEffectStyle? = nil
) {
self.init(blurStyle: blurStyle, vibrancyStyle: vibrancyStyle) {
EmptyView()
}
}
}
@@ -0,0 +1,138 @@
// Copyright 2021 the FloatingPanel authors. All rights reserved. MIT license.
import SwiftUI
struct ResultsList: UIViewControllerRepresentable {
var onScrollViewCreated: (_ scrollView: UIScrollView) -> Void
func makeUIViewController(
context: Context
) -> ResultsTableViewController {
let rtvc = ResultsTableViewController()
onScrollViewCreated(rtvc.tableView)
return rtvc
}
func updateUIViewController(
_ uiViewController: ResultsTableViewController,
context: Context
) {
}
}
final class ResultsTableViewController: UITableViewController {
private let reuseIdentifier = "HostingCell<ResultListCell>"
private enum Section: CaseIterable {
case main
}
private struct TableViewItem: Hashable {
let color: Color
let symbolName: String
let title: String
let description: String
}
private var dataSource: UITableViewDiffableDataSource<Section, TableViewItem>?
// MARK: Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
tableView.backgroundColor = nil
tableView.register(HostingCell<ResultListCell>.self, forCellReuseIdentifier: reuseIdentifier)
configureDataSource()
}
// MARK: UITableViewDataSource
private func configureDataSource() {
dataSource = UITableViewDiffableDataSource
<Section, TableViewItem>(tableView: tableView) { [weak self] tableView, _, tableItem -> UITableViewCell? in
self?.tableView(tableView, cellForTableViewItem: tableItem)
}
tableView.dataSource = dataSource
var snapshot = NSDiffableDataSourceSnapshot<Section, TableViewItem>()
snapshot.appendSections([.main])
let results: [TableViewItem] = (1...100).map {
TableViewItem(
color: Color(red: 255 / 255.0, green: 94 / 255.0 , blue: 94 / 255.0),
symbolName: "heart.fill",
title: "Favorites",
description: "\($0) Places"
)
}
snapshot.appendItems(results, toSection: .main)
dataSource?.apply(snapshot, animatingDifferences: false)
}
private func tableView(
_ tableView: UITableView,
cellForTableViewItem tableViewItem: TableViewItem
) -> UITableViewCell {
let cell: HostingCell<ResultListCell> = tableView.dequeueReusableCell(withIdentifier: reuseIdentifier) as! HostingCell<ResultListCell>
setupResultTableViewCell(
cell,
color: tableViewItem.color,
symbolName: tableViewItem.symbolName,
title: tableViewItem.title,
description: tableViewItem.description
)
return cell
}
private func setupResultTableViewCell(
_ cell: HostingCell<ResultListCell>,
color: Color,
symbolName: String,
title: String,
description: String
) {
cell.set(
rootView: ResultListCell(
color: color,
symbolName: symbolName,
title: title,
description: description
),
parentController: self
)
}
// MARK: UITableViewDelegate
override func tableView(
_ tableView: UITableView,
didSelectRowAt indexPath: IndexPath
) {
tableView.deselectRow(at: indexPath, animated: true)
}
}
struct ResultListCell: View {
let color: Color
let symbolName: String
let title: String
let description: String
var body: some View {
HStack {
Image(systemName: symbolName)
.foregroundColor(.white)
.font(.headline)
.padding(8)
.background(Circle().fill(color))
VStack(alignment: .leading, spacing: 8) {
Text(title)
.font(.system(size: 20, weight: .bold))
.frame(maxWidth: .infinity, alignment: .leading)
Text(description)
.font(.system(size: 13))
.foregroundColor(Color(.secondaryLabel))
}
}
.padding()
}
}
@@ -0,0 +1,11 @@
// Copyright 2021 the FloatingPanel authors. All rights reserved. MIT license.
import FloatingPanel
final class SearchPanelPhoneDelegate: FloatingPanelControllerDelegate {
func floatingPanelWillBeginDragging(_ vc: FloatingPanelController) {
if vc.state == .full {
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
}
}
}
@@ -0,0 +1,13 @@
// Copyright 2021 the FloatingPanel authors. All rights reserved. MIT license.
import FloatingPanel
extension FloatingPanel.SurfaceAppearance {
static var phone: SurfaceAppearance {
let appearance = SurfaceAppearance()
appearance.cornerCurve = .continuous
appearance.cornerRadius = 8.0
appearance.backgroundColor = .clear
return appearance
}
}
@@ -1,9 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="16096" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="BYZ-38-t0r">
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="17506" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="BYZ-38-t0r">
<device id="retina5_9" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="16087"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="17505"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
@@ -28,6 +28,7 @@
<blurEffect style="prominent"/>
</visualEffectView>
</subviews>
<viewLayoutGuide key="safeArea" id="6Tk-OE-BBY"/>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstItem="5Jw-n2-Cpw" firstAttribute="trailing" secondItem="8bC-Xf-vdC" secondAttribute="trailing" id="1Fg-Be-qfh"/>
@@ -39,7 +40,6 @@
<constraint firstItem="6Tk-OE-BBY" firstAttribute="top" secondItem="d9i-3g-8Ja" secondAttribute="bottom" id="Y1G-hr-aEX"/>
<constraint firstItem="d9i-3g-8Ja" firstAttribute="leading" secondItem="6Tk-OE-BBY" secondAttribute="leading" id="diB-Ij-HN3"/>
</constraints>
<viewLayoutGuide key="safeArea" id="6Tk-OE-BBY"/>
</view>
<connections>
<outlet property="mapView" destination="5Jw-n2-Cpw" id="WVC-rU-mLe"/>
@@ -233,6 +233,7 @@
<blurEffect style="prominent"/>
</visualEffectView>
</subviews>
<viewLayoutGuide key="safeArea" id="G74-X7-Za8"/>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstItem="Ye3-uU-bq3" firstAttribute="leading" secondItem="G74-X7-Za8" secondAttribute="leading" id="Kr2-sU-ZWZ"/>
@@ -240,7 +241,6 @@
<constraint firstItem="Ye3-uU-bq3" firstAttribute="trailing" secondItem="G74-X7-Za8" secondAttribute="trailing" id="fEL-8y-Acc"/>
<constraint firstItem="Ye3-uU-bq3" firstAttribute="top" secondItem="Ncl-E9-yRn" secondAttribute="top" id="w77-ba-FrJ"/>
</constraints>
<viewLayoutGuide key="safeArea" id="G74-X7-Za8"/>
</view>
<connections>
<outlet property="searchBar" destination="Zcj-SE-gb8" id="BH7-Gy-RG5"/>
@@ -262,7 +262,7 @@
<subviews>
<visualEffectView opaque="NO" contentMode="scaleToFill" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="c3d-2e-0b1">
<rect key="frame" x="0.0" y="0.0" width="375" height="812"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" id="9fL-a5-0LS">
<rect key="frame" x="0.0" y="0.0" width="375" height="812"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
@@ -282,8 +282,8 @@
<blurEffect style="extraLight"/>
</visualEffectView>
</subviews>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<viewLayoutGuide key="safeArea" id="ctv-Dd-JUc"/>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="EDp-D2-xcT" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
@@ -21,6 +21,9 @@ extension ViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
deactivate(searchBar: searchVC.searchBar)
// Show a detail panel
switch indexPath.row {
case 0:
+19 -8
View File
@@ -81,16 +81,19 @@ class ViewController: UIViewController {
}
func layoutPanelForPhone() {
fpc.track(scrollView: searchVC.tableView) // Only track the tabvle view on iPhone
fpc.track(scrollView: searchVC.tableView) // Only track the table view on iPhone
fpc.addPanel(toParent: self, animated: true)
fpc.setApearanceForPhone()
detailFpc.setApearanceForPhone()
fpc.setAppearanceForPhone()
detailFpc.setAppearanceForPhone()
}
}
extension FloatingPanelController {
func setApearanceForPhone() {
func setAppearanceForPhone() {
let appearance = SurfaceAppearance()
if #available(iOS 13.0, *) {
appearance.cornerCurve = .continuous
}
appearance.cornerRadius = 8.0
appearance.backgroundColor = .clear
surfaceView.appearance = appearance
@@ -114,19 +117,27 @@ extension FloatingPanelController {
// MARK: - UISearchBarDelegate
extension ViewController: UISearchBarDelegate {
func searchBarCancelButtonClicked(_ searchBar: UISearchBar) {
func activate(searchBar: UISearchBar) {
searchBar.showsCancelButton = true
searchVC.showHeader(animated: true)
searchVC.tableView.alpha = 1.0
detailVC.dismiss(animated: true, completion: nil)
}
func deactivate(searchBar: UISearchBar) {
searchBar.resignFirstResponder()
searchBar.showsCancelButton = false
searchVC.hideHeader(animated: true)
}
func searchBarCancelButtonClicked(_ searchBar: UISearchBar) {
deactivate(searchBar: searchBar)
UIView.animate(withDuration: 0.25) {
self.fpc.move(to: .half, animated: false)
}
}
func searchBarTextDidBeginEditing(_ searchBar: UISearchBar) {
searchBar.showsCancelButton = true
searchVC.showHeader(animated: true)
searchVC.tableView.alpha = 1.0
activate(searchBar: searchBar)
UIView.animate(withDuration: 0.25) { [weak self] in
self?.fpc.move(to: .full, animated: false)
}
@@ -7,17 +7,32 @@
objects = {
/* Begin PBXBuildFile section */
5442E22425FC51AF00A26F43 /* ImageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5442E22325FC51AF00A26F43 /* ImageViewController.swift */; };
5442E22825FC51E200A26F43 /* MultiPanelController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5442E22725FC51E200A26F43 /* MultiPanelController.swift */; };
5442E22C25FC521F00A26F43 /* SettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5442E22B25FC521F00A26F43 /* SettingsViewController.swift */; };
5442E23025FC525200A26F43 /* TabBarViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5442E22F25FC525200A26F43 /* TabBarViewController.swift */; };
5442E23425FC528400A26F43 /* DetailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5442E23325FC528400A26F43 /* DetailViewController.swift */; };
5442E23A25FC52CD00A26F43 /* ModalViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5442E23925FC52CD00A26F43 /* ModalViewController.swift */; };
5442E24025FC533800A26F43 /* DebugTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5442E23F25FC533800A26F43 /* DebugTableViewController.swift */; };
5442E24425FC538200A26F43 /* InspectorViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5442E24325FC538200A26F43 /* InspectorViewController.swift */; };
5442E24A25FC53C100A26F43 /* DebugTextViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5442E24925FC53C100A26F43 /* DebugTextViewController.swift */; };
5442E25225FC541700A26F43 /* NestedScrollViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5442E25125FC541700A26F43 /* NestedScrollViewController.swift */; };
54496C59263A7E5A0031E0C8 /* UseCaseController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54496C58263A7E5A0031E0C8 /* UseCaseController.swift */; };
545DB9EE21511E6300CA77B8 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 545DB9ED21511E6300CA77B8 /* AppDelegate.swift */; };
545DB9F021511E6300CA77B8 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 545DB9EF21511E6300CA77B8 /* ViewController.swift */; };
545DB9F021511E6300CA77B8 /* MainViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 545DB9EF21511E6300CA77B8 /* MainViewController.swift */; };
545DB9F321511E6300CA77B8 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 545DB9F121511E6300CA77B8 /* Main.storyboard */; };
545DB9F521511E6400CA77B8 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 545DB9F421511E6400CA77B8 /* Assets.xcassets */; };
545DB9F821511E6400CA77B8 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 545DB9F621511E6400CA77B8 /* LaunchScreen.storyboard */; };
545DBA0321511E6400CA77B8 /* SampleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 545DBA0221511E6400CA77B8 /* SampleTests.swift */; };
545DBA0E21511E6400CA77B8 /* SampleUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 545DBA0D21511E6400CA77B8 /* SampleUITests.swift */; };
546341A125C6415100CA0596 /* UseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 546341A025C6415100CA0596 /* UseCase.swift */; };
546341AC25C6426500CA0596 /* CustomState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 546341AB25C6426500CA0596 /* CustomState.swift */; };
549D23CB233C7779008EF4D7 /* FloatingPanel.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 549D23CA233C7779008EF4D7 /* FloatingPanel.framework */; };
549D23CC233C7779008EF4D7 /* FloatingPanel.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 549D23CA233C7779008EF4D7 /* FloatingPanel.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
54B51116216AFE5F0033A6F3 /* UIExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54B51115216AFE5F0033A6F3 /* UIExtensions.swift */; };
54CDC5D8215BBE23007D205C /* UIComponents.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CDC5D7215BBE23007D205C /* UIComponents.swift */; };
54B51116216AFE5F0033A6F3 /* Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54B51115216AFE5F0033A6F3 /* Extensions.swift */; };
54CDC5D8215BBE23007D205C /* Components.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CDC5D7215BBE23007D205C /* Components.swift */; };
54EAD35B263A75EB006A36EA /* Layouts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54EAD35A263A75EB006A36EA /* Layouts.swift */; };
54EAD365263A765F006A36EA /* PagePanelController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54EAD364263A765F006A36EA /* PagePanelController.swift */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@@ -52,9 +67,20 @@
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
5442E22325FC51AF00A26F43 /* ImageViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageViewController.swift; sourceTree = "<group>"; };
5442E22725FC51E200A26F43 /* MultiPanelController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultiPanelController.swift; sourceTree = "<group>"; };
5442E22B25FC521F00A26F43 /* SettingsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewController.swift; sourceTree = "<group>"; };
5442E22F25FC525200A26F43 /* TabBarViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabBarViewController.swift; sourceTree = "<group>"; };
5442E23325FC528400A26F43 /* DetailViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailViewController.swift; sourceTree = "<group>"; };
5442E23925FC52CD00A26F43 /* ModalViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModalViewController.swift; sourceTree = "<group>"; };
5442E23F25FC533800A26F43 /* DebugTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebugTableViewController.swift; sourceTree = "<group>"; };
5442E24325FC538200A26F43 /* InspectorViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InspectorViewController.swift; sourceTree = "<group>"; };
5442E24925FC53C100A26F43 /* DebugTextViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebugTextViewController.swift; sourceTree = "<group>"; };
5442E25125FC541700A26F43 /* NestedScrollViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NestedScrollViewController.swift; sourceTree = "<group>"; };
54496C58263A7E5A0031E0C8 /* UseCaseController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UseCaseController.swift; sourceTree = "<group>"; };
545DB9EA21511E6300CA77B8 /* Samples.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Samples.app; sourceTree = BUILT_PRODUCTS_DIR; };
545DB9ED21511E6300CA77B8 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
545DB9EF21511E6300CA77B8 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = "<group>"; };
545DB9EF21511E6300CA77B8 /* MainViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainViewController.swift; sourceTree = "<group>"; };
545DB9F221511E6300CA77B8 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
545DB9F421511E6400CA77B8 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
545DB9F721511E6400CA77B8 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
@@ -65,9 +91,13 @@
545DBA0921511E6400CA77B8 /* SamplesUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SamplesUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
545DBA0D21511E6400CA77B8 /* SampleUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SampleUITests.swift; sourceTree = "<group>"; };
545DBA0F21511E6400CA77B8 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
546341A025C6415100CA0596 /* UseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UseCase.swift; sourceTree = "<group>"; };
546341AB25C6426500CA0596 /* CustomState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomState.swift; sourceTree = "<group>"; };
549D23CA233C7779008EF4D7 /* FloatingPanel.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = FloatingPanel.framework; sourceTree = BUILT_PRODUCTS_DIR; };
54B51115216AFE5F0033A6F3 /* UIExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIExtensions.swift; sourceTree = "<group>"; };
54CDC5D7215BBE23007D205C /* UIComponents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIComponents.swift; sourceTree = "<group>"; };
54B51115216AFE5F0033A6F3 /* Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Extensions.swift; sourceTree = "<group>"; };
54CDC5D7215BBE23007D205C /* Components.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Components.swift; sourceTree = "<group>"; };
54EAD35A263A75EB006A36EA /* Layouts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Layouts.swift; sourceTree = "<group>"; };
54EAD364263A765F006A36EA /* PagePanelController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PagePanelController.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@@ -96,6 +126,23 @@
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
5442E22225FC519700A26F43 /* ViewControllers */ = {
isa = PBXGroup;
children = (
5442E23F25FC533800A26F43 /* DebugTableViewController.swift */,
5442E24925FC53C100A26F43 /* DebugTextViewController.swift */,
5442E23325FC528400A26F43 /* DetailViewController.swift */,
5442E24325FC538200A26F43 /* InspectorViewController.swift */,
5442E22325FC51AF00A26F43 /* ImageViewController.swift */,
5442E25125FC541700A26F43 /* NestedScrollViewController.swift */,
5442E23925FC52CD00A26F43 /* ModalViewController.swift */,
5442E22725FC51E200A26F43 /* MultiPanelController.swift */,
5442E22B25FC521F00A26F43 /* SettingsViewController.swift */,
5442E22F25FC525200A26F43 /* TabBarViewController.swift */,
);
path = ViewControllers;
sourceTree = "<group>";
};
545DB9E121511E6300CA77B8 = {
isa = PBXGroup;
children = (
@@ -124,9 +171,13 @@
545DB9F621511E6400CA77B8 /* LaunchScreen.storyboard */,
545DB9F121511E6300CA77B8 /* Main.storyboard */,
545DB9ED21511E6300CA77B8 /* AppDelegate.swift */,
545DB9EF21511E6300CA77B8 /* ViewController.swift */,
54B51115216AFE5F0033A6F3 /* UIExtensions.swift */,
54CDC5D7215BBE23007D205C /* UIComponents.swift */,
545DB9EF21511E6300CA77B8 /* MainViewController.swift */,
54496C58263A7E5A0031E0C8 /* UseCaseController.swift */,
546341AA25C6421000CA0596 /* UseCases */,
5442E22225FC519700A26F43 /* ViewControllers */,
54EAD35A263A75EB006A36EA /* Layouts.swift */,
54B51115216AFE5F0033A6F3 /* Extensions.swift */,
54CDC5D7215BBE23007D205C /* Components.swift */,
545DB9F921511E6400CA77B8 /* Info.plist */,
);
path = Sources;
@@ -150,6 +201,16 @@
path = UITests;
sourceTree = "<group>";
};
546341AA25C6421000CA0596 /* UseCases */ = {
isa = PBXGroup;
children = (
546341A025C6415100CA0596 /* UseCase.swift */,
546341AB25C6426500CA0596 /* CustomState.swift */,
54EAD364263A765F006A36EA /* PagePanelController.swift */,
);
path = UseCases;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
@@ -303,10 +364,25 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
54CDC5D8215BBE23007D205C /* UIComponents.swift in Sources */,
54B51116216AFE5F0033A6F3 /* UIExtensions.swift in Sources */,
545DB9F021511E6300CA77B8 /* ViewController.swift in Sources */,
5442E23425FC528400A26F43 /* DetailViewController.swift in Sources */,
54496C59263A7E5A0031E0C8 /* UseCaseController.swift in Sources */,
54CDC5D8215BBE23007D205C /* Components.swift in Sources */,
54B51116216AFE5F0033A6F3 /* Extensions.swift in Sources */,
5442E24A25FC53C100A26F43 /* DebugTextViewController.swift in Sources */,
546341AC25C6426500CA0596 /* CustomState.swift in Sources */,
5442E23A25FC52CD00A26F43 /* ModalViewController.swift in Sources */,
5442E22425FC51AF00A26F43 /* ImageViewController.swift in Sources */,
5442E24025FC533800A26F43 /* DebugTableViewController.swift in Sources */,
5442E25225FC541700A26F43 /* NestedScrollViewController.swift in Sources */,
5442E22825FC51E200A26F43 /* MultiPanelController.swift in Sources */,
546341A125C6415100CA0596 /* UseCase.swift in Sources */,
545DB9F021511E6300CA77B8 /* MainViewController.swift in Sources */,
545DB9EE21511E6300CA77B8 /* AppDelegate.swift in Sources */,
5442E23025FC525200A26F43 /* TabBarViewController.swift in Sources */,
54EAD35B263A75EB006A36EA /* Layouts.swift in Sources */,
54EAD365263A765F006A36EA /* PagePanelController.swift in Sources */,
5442E24425FC538200A26F43 /* InspectorViewController.swift in Sources */,
5442E22C25FC521F00A26F43 /* SettingsViewController.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -1,6 +1,6 @@
{
"info" : {
"version" : 1,
"author" : "xcode"
"author" : "xcode",
"version" : 1
}
}
}
@@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "IMG_0003.jpg",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 202 KiB

@@ -1,11 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="17156" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="RoN-h0-uBD">
<device id="retina5_9" orientation="portrait" appearance="light"/>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="17701" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="RoN-h0-uBD">
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="17125"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="17703"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="Stack View standard spacing" minToolsVersion="9.0"/>
<capability name="System colors in document resources" minToolsVersion="11.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
@@ -28,24 +27,24 @@
<!--Samples-->
<scene sceneID="35L-Gs-Vts">
<objects>
<viewController id="jF4-A0-Eq6" customClass="SampleListViewController" customModule="Samples" customModuleProvider="target" sceneMemberID="viewController">
<viewController id="jF4-A0-Eq6" customClass="MainViewController" customModule="Samples" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="Smh-Bd-AAc">
<rect key="frame" x="0.0" y="0.0" width="375" height="812"/>
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<tableView clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="plain" separatorStyle="default" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="28" sectionFooterHeight="28" translatesAutoresizingMaskIntoConstraints="NO" id="7IS-PU-x0P">
<rect key="frame" x="0.0" y="0.0" width="375" height="812"/>
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<prototypes>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" reuseIdentifier="Cell" textLabel="M0G-C8-hAO" style="IBUITableViewCellStyleDefault" id="ySY-oA-g81">
<rect key="frame" x="0.0" y="28" width="375" height="43.666667938232422"/>
<rect key="frame" x="0.0" y="28" width="600" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="ySY-oA-g81" id="sXB-nH-2g2">
<rect key="frame" x="0.0" y="0.0" width="375" height="43.666667938232422"/>
<rect key="frame" x="0.0" y="0.0" width="600" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Title" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="M0G-C8-hAO">
<rect key="frame" x="16" y="0.0" width="343" height="43.666667938232422"/>
<rect key="frame" x="16" y="0.0" width="568" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/>
@@ -86,23 +85,23 @@
<objects>
<viewController storyboardIdentifier="SettingsViewController" id="C1X-9Z-TyQ" customClass="SettingsViewController" customModule="Samples" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" id="af9-Zr-Ppc">
<rect key="frame" x="0.0" y="0.0" width="375" height="197.33333333333334"/>
<rect key="frame" x="0.0" y="0.0" width="375" height="197.33000000000001"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" distribution="fillProportionally" spacing="16" translatesAutoresizingMaskIntoConstraints="NO" id="n93-ZL-fmC">
<rect key="frame" x="32" y="16" width="311" height="149.33333333333334"/>
<rect key="frame" x="32" y="16" width="311" height="149.5"/>
<subviews>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" distribution="fillProportionally" alignment="center" spacing="8" translatesAutoresizingMaskIntoConstraints="NO" id="DCi-Iv-o6d">
<rect key="frame" x="0.0" y="0.0" width="311" height="52.666666666666664"/>
<rect key="frame" x="0.0" y="0.0" width="311" height="53"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Version: 1.0" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="WmC-Tq-NDN">
<rect key="frame" x="118.33333333333334" y="0.0" width="74.333333333333343" height="17.333333333333332"/>
<rect key="frame" x="118.5" y="0.0" width="74.5" height="17.5"/>
<fontDescription key="fontDescription" type="system" pointSize="14"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="UINavigationBar" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Ulg-gS-ah0">
<rect key="frame" x="78.333333333333329" y="25.333333333333336" width="154.66666666666669" height="27.333333333333336"/>
<rect key="frame" x="78.5" y="25.5" width="154.5" height="27.5"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleTitle2"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
@@ -110,19 +109,19 @@
</subviews>
</stackView>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" distribution="fillProportionally" spacing="16" translatesAutoresizingMaskIntoConstraints="NO" id="J8j-7w-yCZ">
<rect key="frame" x="0.0" y="68.666666666666657" width="311" height="80.666666666666657"/>
<rect key="frame" x="0.0" y="69" width="311" height="80.5"/>
<subviews>
<stackView opaque="NO" contentMode="scaleToFill" alignment="center" spacingType="standard" translatesAutoresizingMaskIntoConstraints="NO" id="uEf-g4-CeU">
<rect key="frame" x="0.0" y="0.0" width="311" height="32"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Large Titles" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="ogl-S5-4tJ">
<rect key="frame" x="0.0" y="5.9999999999999982" width="254" height="20.333333333333329"/>
<rect key="frame" x="0.0" y="5.5" width="254" height="20.5"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleHeadline"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<switch opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" contentHorizontalAlignment="center" contentVerticalAlignment="center" translatesAutoresizingMaskIntoConstraints="NO" id="js8-Qv-lUC">
<rect key="frame" x="262" y="0.66666666666665719" width="51" height="31"/>
<rect key="frame" x="262" y="0.5" width="51" height="31"/>
<connections>
<action selector="toggleLargeTitle:" destination="C1X-9Z-TyQ" eventType="valueChanged" id="FJS-Ty-mCY"/>
</connections>
@@ -130,16 +129,16 @@
</subviews>
</stackView>
<stackView opaque="NO" contentMode="scaleToFill" alignment="center" spacingType="standard" translatesAutoresizingMaskIntoConstraints="NO" id="ZtZ-Dz-4cC">
<rect key="frame" x="0.0" y="47.999999999999986" width="311" height="32.666666666666671"/>
<rect key="frame" x="0.0" y="48" width="311" height="32.5"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Translucent" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Z5i-rm-QgL">
<rect key="frame" x="0.0" y="6.333333333333341" width="254" height="20.333333333333329"/>
<rect key="frame" x="0.0" y="6" width="254" height="20.5"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleHeadline"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<switch opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" contentHorizontalAlignment="center" contentVerticalAlignment="center" translatesAutoresizingMaskIntoConstraints="NO" id="s6b-j9-8Kw">
<rect key="frame" x="262" y="1" width="51" height="31"/>
<rect key="frame" x="262" y="0.5" width="51" height="31"/>
<connections>
<action selector="toggleTranslucent:" destination="C1X-9Z-TyQ" eventType="valueChanged" id="nL4-3L-9hh"/>
</connections>
@@ -176,11 +175,11 @@
<objects>
<viewController id="RpE-lI-27a" customClass="TabBarContentViewController" customModule="Samples" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="JER-jz-KSq">
<rect key="frame" x="0.0" y="0.0" width="375" height="812"/>
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="IvG-yp-yzI">
<rect key="frame" x="20" y="44" width="39" height="30"/>
<rect key="frame" x="20" y="0.0" width="39" height="30"/>
<state key="normal" title="Close"/>
<connections>
<action selector="closeWithSender:" destination="RpE-lI-27a" eventType="touchUpInside" id="hj3-Xv-6Gq"/>
@@ -207,11 +206,11 @@
<objects>
<viewController id="pOk-Zm-vD9" customClass="TabBarContentViewController" customModule="Samples" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="85d-ub-G8k">
<rect key="frame" x="0.0" y="0.0" width="375" height="812"/>
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="NbG-e8-HdI">
<rect key="frame" x="20" y="44" width="39" height="30"/>
<rect key="frame" x="20" y="0.0" width="39" height="30"/>
<state key="normal" title="Close"/>
<connections>
<action selector="closeWithSender:" destination="pOk-Zm-vD9" eventType="touchUpInside" id="111-PD-Pop"/>
@@ -238,11 +237,11 @@
<objects>
<viewController id="lto-Zc-Vtp" customClass="TabBarContentViewController" customModule="Samples" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="ji9-Ez-N7i">
<rect key="frame" x="0.0" y="0.0" width="375" height="812"/>
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="eFN-tN-4Ct">
<rect key="frame" x="20" y="44" width="39" height="30"/>
<rect key="frame" x="20" y="0.0" width="39" height="30"/>
<state key="normal" title="Close"/>
<connections>
<action selector="closeWithSender:" destination="bYI-y3-Rzb" eventType="touchUpInside" id="YL4-GP-ZEZ"/>
@@ -273,7 +272,7 @@
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="1000" verticalHuggingPriority="1000" horizontalCompressionResistancePriority="1000" verticalCompressionResistancePriority="1000" text="Change this text" textAlignment="center" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="ge4-RW-Gmz">
<rect key="frame" x="125.66666666666669" y="24" width="124" height="20.333333333333329"/>
<rect key="frame" x="125.5" y="24" width="124" height="20.5"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
@@ -293,6 +292,71 @@
</objects>
<point key="canvasLocation" x="2753" y="734"/>
</scene>
<!--Image View Controller-->
<scene sceneID="NAI-Rh-ZQ6">
<objects>
<viewController storyboardIdentifier="ImageViewController" id="VWY-cF-RoY" customClass="ImageViewController" customModule="Samples" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="dAf-gD-ghB">
<rect key="frame" x="0.0" y="0.0" width="375" height="778"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" translatesAutoresizingMaskIntoConstraints="NO" id="Gs4-S6-Goh">
<rect key="frame" x="0.0" y="0.0" width="375" height="778"/>
<subviews>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="t7x-eG-MKh">
<rect key="frame" x="0.0" y="0.0" width="375" height="49"/>
<color key="backgroundColor" systemColor="systemOrangeColor"/>
<constraints>
<constraint firstAttribute="height" constant="49" id="DmG-pt-gij"/>
</constraints>
</view>
<scrollView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" alwaysBounceHorizontal="YES" showsHorizontalScrollIndicator="NO" showsVerticalScrollIndicator="NO" translatesAutoresizingMaskIntoConstraints="NO" id="kRA-qy-GpJ">
<rect key="frame" x="0.0" y="49" width="375" height="680"/>
<subviews>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFill" horizontalHuggingPriority="1000" verticalHuggingPriority="1000" image="IMG_0003" translatesAutoresizingMaskIntoConstraints="NO" id="rGf-jW-WNf">
<rect key="frame" x="0.0" y="0.0" width="750" height="501"/>
<color key="backgroundColor" systemColor="systemPurpleColor"/>
</imageView>
</subviews>
<color key="backgroundColor" red="0.0078431372550000003" green="0.72156862749999995" blue="0.45882352939999999" alpha="1" colorSpace="calibratedRGB"/>
<constraints>
<constraint firstAttribute="trailing" secondItem="rGf-jW-WNf" secondAttribute="trailing" id="472-Be-cUl"/>
<constraint firstAttribute="bottom" secondItem="rGf-jW-WNf" secondAttribute="bottom" id="ncs-tN-3Wx"/>
<constraint firstItem="rGf-jW-WNf" firstAttribute="leading" secondItem="kRA-qy-GpJ" secondAttribute="leading" id="rlv-5Y-utR"/>
<constraint firstItem="rGf-jW-WNf" firstAttribute="top" secondItem="kRA-qy-GpJ" secondAttribute="top" id="zum-Zl-Wzz"/>
</constraints>
</scrollView>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="SEa-7Y-wa9">
<rect key="frame" x="0.0" y="729" width="375" height="49"/>
<color key="backgroundColor" systemColor="systemTealColor"/>
<constraints>
<constraint firstAttribute="height" constant="49" id="3mS-zi-8BP"/>
</constraints>
</view>
</subviews>
</stackView>
</subviews>
<viewLayoutGuide key="safeArea" id="hCg-v5-nJs"/>
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
<constraints>
<constraint firstAttribute="trailing" secondItem="Gs4-S6-Goh" secondAttribute="trailing" id="6z7-Md-pxr"/>
<constraint firstAttribute="bottom" secondItem="Gs4-S6-Goh" secondAttribute="bottom" id="PcQ-bu-yT3"/>
<constraint firstItem="Gs4-S6-Goh" firstAttribute="top" secondItem="dAf-gD-ghB" secondAttribute="top" id="zGx-Wd-hjz"/>
<constraint firstItem="Gs4-S6-Goh" firstAttribute="leading" secondItem="dAf-gD-ghB" secondAttribute="leading" id="zxi-Ty-U7L"/>
</constraints>
</view>
<size key="freeformSize" width="375" height="778"/>
<connections>
<outlet property="footerView" destination="SEa-7Y-wa9" id="Gzj-dP-YXl"/>
<outlet property="headerView" destination="t7x-eG-MKh" id="njM-un-U8q"/>
<outlet property="scrollView" destination="kRA-qy-GpJ" id="iWC-o4-APi"/>
<outlet property="stackView" destination="Gs4-S6-Goh" id="f1D-bO-mjr"/>
</connections>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="pnR-69-Ek4" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="3388" y="734.48275862068965"/>
</scene>
<!--Tab Bar View Controller-->
<scene sceneID="nQ5-PV-qFw">
<objects>
@@ -317,11 +381,11 @@
<objects>
<viewController storyboardIdentifier="ModalViewController" id="bYI-y3-Rzb" customClass="ModalViewController" customModule="Samples" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="qwo-GK-p1U">
<rect key="frame" x="0.0" y="0.0" width="375" height="724"/>
<rect key="frame" x="0.0" y="0.0" width="375" height="778"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="vut-mK-Y4t" customClass="SafeAreaView" customModule="Samples" customModuleProvider="target">
<rect key="frame" x="0.0" y="724" width="375" height="0.0"/>
<rect key="frame" x="0.0" y="758" width="375" height="0.0"/>
<color key="backgroundColor" white="0.66666666666666663" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</view>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="sbF-Az-7sy">
@@ -332,7 +396,7 @@
</connections>
</button>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" alignment="top" spacing="44" translatesAutoresizingMaskIntoConstraints="NO" id="9p4-06-y2T">
<rect key="frame" x="134.66666666666666" y="88" width="106" height="326"/>
<rect key="frame" x="134.5" y="88" width="106" height="326"/>
<subviews>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="i9x-x5-n1q">
<rect key="frame" x="0.0" y="0.0" width="80" height="30"/>
@@ -532,11 +596,11 @@
</constraints>
</view>
<view alpha="0.5" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="Kva-Z7-0qY" customClass="OnSafeAreaView" customModule="Samples" customModuleProvider="target">
<rect key="frame" x="0.0" y="44" width="375" height="734"/>
<rect key="frame" x="0.0" y="0.0" width="375" height="778"/>
<color key="backgroundColor" red="0.0078431372550000003" green="0.72156862749999995" blue="0.45882352939999999" alpha="1" colorSpace="calibratedRGB"/>
</view>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="noi-1a-5bZ" customClass="CloseButton" customModule="Samples" customModuleProvider="target">
<rect key="frame" x="319" y="44" width="44" height="44"/>
<rect key="frame" x="319" y="0.0" width="44" height="44"/>
<constraints>
<constraint firstAttribute="height" constant="44" id="0jg-5D-A1F"/>
<constraint firstAttribute="width" constant="44" id="1Cq-PA-wgW"/>
@@ -546,10 +610,10 @@
</connections>
</button>
<stackView opaque="NO" contentMode="scaleToFill" distribution="fillProportionally" alignment="center" spacing="8" translatesAutoresizingMaskIntoConstraints="NO" id="qux-uG-4o2">
<rect key="frame" x="8" y="52" width="148" height="31"/>
<rect key="frame" x="8" y="8" width="148" height="31"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="fitToBounds" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="7lq-d3-PKi">
<rect key="frame" x="0.0" y="5.3333333333333357" width="91" height="20.333333333333332"/>
<rect key="frame" x="0.0" y="5.5" width="91" height="20.5"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
@@ -563,7 +627,7 @@
</subviews>
</stackView>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="22" translatesAutoresizingMaskIntoConstraints="NO" id="tP3-oJ-4EB">
<rect key="frame" x="130.66666666666666" y="88" width="114" height="134"/>
<rect key="frame" x="130.5" y="88" width="114" height="134"/>
<subviews>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="c5r-jU-haj">
<rect key="frame" x="0.0" y="0.0" width="114" height="30"/>
@@ -717,12 +781,22 @@ Section 1.10.33 of "de Finibus Bonorum et Malorum", written by Cicero in 45 BC
<point key="canvasLocation" x="-1" y="734"/>
</scene>
</scenes>
<designables>
<designable name="noi-1a-5bZ">
<size key="intrinsicContentSize" width="30" height="30"/>
</designable>
</designables>
<inferredMetricsTieBreakers>
<segue reference="r1P-2i-NDe"/>
</inferredMetricsTieBreakers>
<resources>
<image name="IMG_0003" width="750" height="501"/>
<systemColor name="systemBackgroundColor">
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</systemColor>
<systemColor name="systemOrangeColor">
<color red="1" green="0.58431372549019611" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</systemColor>
<systemColor name="systemPurpleColor">
<color red="0.68627450980392157" green="0.32156862745098042" blue="0.87058823529411766" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</systemColor>
<systemColor name="systemTealColor">
<color red="0.35294117647058826" green="0.78431372549019607" blue="0.98039215686274506" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</systemColor>
</resources>
</document>
@@ -3,7 +3,7 @@
import UIKit
@IBDesignable
class CloseButton: UIButton {
final class CloseButton: UIButton {
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
render()
@@ -71,7 +71,7 @@ class CloseButton: UIButton {
}
@IBDesignable
class SafeAreaView: UIView {
final class SafeAreaView: UIView {
override func prepareForInterfaceBuilder() {
let label = UILabel()
label.text = "Safe Area"
@@ -86,7 +86,7 @@ class SafeAreaView: UIView {
@IBDesignable
class OnSafeAreaView: UIView {
final class OnSafeAreaView: UIView {
override func prepareForInterfaceBuilder() {
let label = UILabel()
label.text = "On Safe Area"
+90
View File
@@ -0,0 +1,90 @@
// Copyright 2018 the FloatingPanel authors. All rights reserved. MIT license.
import UIKit
import FloatingPanel
/**
- Attention: `FloatingPanelLayout` must not be applied by the parent view
controller of a panel. But here `MainViewController` adopts it
purposely to check if the library prints an appropriate warning.
*/
extension MainViewController: FloatingPanelLayout {
var position: FloatingPanelPosition { .bottom }
var initialState: FloatingPanelState { .half }
var anchors: [FloatingPanelState: FloatingPanelLayoutAnchoring] {
return [
.full: FloatingPanelLayoutAnchor(absoluteInset: UIScreen.main.bounds.height == 667.0 ? 18.0 : 16.0, edge: .top, referenceGuide: .safeArea),
.half: FloatingPanelLayoutAnchor(absoluteInset: 262.0, edge: .bottom, referenceGuide: .safeArea),
.tip: FloatingPanelLayoutAnchor(absoluteInset: 69.0, edge: .bottom, referenceGuide: .safeArea)
]
}
}
class TopPositionedPanelLayout: FloatingPanelLayout {
let position: FloatingPanelPosition = .top
let initialState: FloatingPanelState = .full
var anchors: [FloatingPanelState : FloatingPanelLayoutAnchoring] {
return [
.full: FloatingPanelLayoutAnchor(absoluteInset: 88.0, edge: .bottom, referenceGuide: .safeArea),
.half: FloatingPanelLayoutAnchor(absoluteInset: 216.0, edge: .top, referenceGuide: .safeArea),
.tip: FloatingPanelLayoutAnchor(absoluteInset: 44.0, edge: .top, referenceGuide: .safeArea)
]
}
}
class IntrinsicPanelLayout: FloatingPanelBottomLayout {
override var initialState: FloatingPanelState { .full }
override var anchors: [FloatingPanelState : FloatingPanelLayoutAnchoring] {
return [
.full: FloatingPanelIntrinsicLayoutAnchor(fractionalOffset: 0.0, referenceGuide: .safeArea)
]
}
}
class RemovablePanelLayout: FloatingPanelLayout {
let position: FloatingPanelPosition = .bottom
let initialState: FloatingPanelState = .half
var anchors: [FloatingPanelState : FloatingPanelLayoutAnchoring] {
return [
.full: FloatingPanelIntrinsicLayoutAnchor(fractionalOffset: 0.0, referenceGuide: .safeArea),
.half: FloatingPanelLayoutAnchor(absoluteInset: 130.0, edge: .bottom, referenceGuide: .safeArea)
]
}
func backdropAlpha(for state: FloatingPanelState) -> CGFloat {
return 0.3
}
}
class RemovablePanelLandscapeLayout: FloatingPanelLayout {
let position: FloatingPanelPosition = .bottom
let initialState: FloatingPanelState = .full
var anchors: [FloatingPanelState : FloatingPanelLayoutAnchoring] {
return [
.full: FloatingPanelIntrinsicLayoutAnchor(fractionalOffset: 0.0, referenceGuide: .safeArea),
.half: FloatingPanelLayoutAnchor(absoluteInset: 216.0, edge: .bottom, referenceGuide: .safeArea)
]
}
func backdropAlpha(for state: FloatingPanelState) -> CGFloat {
return 0.3
}
}
class ModalPanelLayout: FloatingPanelLayout {
let position: FloatingPanelPosition = .bottom
let initialState: FloatingPanelState = .full
var anchors: [FloatingPanelState : FloatingPanelLayoutAnchoring] {
return [
.full: FloatingPanelIntrinsicLayoutAnchor(absoluteOffset: 0.0, referenceGuide: .safeArea),
]
}
func backdropAlpha(for state: FloatingPanelState) -> CGFloat {
return 0.3
}
}
@@ -0,0 +1,93 @@
// Copyright 2018 the FloatingPanel authors. All rights reserved. MIT license.
import UIKit
import FloatingPanel
final class MainViewController: UIViewController {
@IBOutlet weak var tableView: UITableView!
private var observations: [NSKeyValueObservation] = []
private lazy var useCaseController = UseCaseController(mainVC: self)
override func viewDidLoad() {
super.viewDidLoad()
tableView.dataSource = self
tableView.delegate = self
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "Cell")
automaticallyAdjustsScrollViewInsets = false
let searchController = UISearchController(searchResultsController: nil)
if #available(iOS 11.0, *) {
navigationItem.searchController = searchController
navigationItem.hidesSearchBarWhenScrolling = false
navigationItem.largeTitleDisplayMode = .automatic
} else {
// Fallback on earlier versions
}
var insets = UIEdgeInsets.zero
insets.bottom += 69.0
tableView.contentInset = insets
// Show the initial panel
useCaseController.set(useCase: .trackingTableView)
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
if #available(iOS 11.0, *) {
if let observation = navigationController?.navigationBar.observe(\.prefersLargeTitles, changeHandler: { (bar, _) in
self.tableView.reloadData()
}) {
observations.append(observation)
}
}
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
observations.removeAll()
}
// MARK:- Actions
@IBAction func showDebugMenu(_ sender: UIBarButtonItem) {
useCaseController.setUpSettingsPanel(for: self)
}
}
extension MainViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if #available(iOS 11.0, *) {
if navigationController?.navigationBar.prefersLargeTitles == true {
return UseCase.allCases.count + 30
} else {
return UseCase.allCases.count
}
} else {
return UseCase.allCases.count
}
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
if UseCase.allCases.count > indexPath.row {
let menu = UseCase.allCases[indexPath.row]
cell.textLabel?.text = menu.name
} else {
cell.textLabel?.text = "\(indexPath.row) row"
}
return cell
}
}
extension MainViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
guard UseCase.allCases.count > indexPath.row else { return }
// Change panels
useCaseController.set(useCase: UseCase.allCases[indexPath.row])
}
@objc func dismissPresentedVC() {
self.presentedViewController?.dismiss(animated: true, completion: nil)
}
}
@@ -0,0 +1,329 @@
// Copyright 2018-Present Shin Yamamoto. All rights reserved. MIT license.
import UIKit
import FloatingPanel
final class UseCaseController: NSObject {
unowned let mainVC: MainViewController
private(set) var useCase: UseCase = .trackingTableView
fileprivate var mainPanelVC: FloatingPanelController!
private var detailPanelVC: FloatingPanelController!
private var settingsPanelVC: FloatingPanelController!
private lazy var pagePanelController = PagePanelController()
private var mainPanelObserves: [NSKeyValueObservation] = []
init(mainVC: MainViewController) {
self.mainVC = mainVC
}
func set(useCase: UseCase) {
self.useCase = useCase
let contentVC = useCase.makeContentViewController(with: mainVC.storyboard!)
detailPanelVC?.removePanelFromParent(animated: true, completion: nil)
detailPanelVC = nil
switch useCase {
case .showDetail:
detailPanelVC?.removePanelFromParent(animated: false)
// Initialize FloatingPanelController
detailPanelVC = FloatingPanelController()
detailPanelVC.delegate = self
let appearance = SurfaceAppearance()
appearance.cornerRadius = 6.0
detailPanelVC.surfaceView.appearance = appearance
// Set a content view controller
detailPanelVC.set(contentViewController: contentVC)
detailPanelVC.contentMode = .fitToBounds
(contentVC as? DetailViewController)?.intrinsicHeightConstraint.isActive = false
// Add FloatingPanel to self.view
detailPanelVC.addPanel(toParent: mainVC, animated: true)
case .showModal, .showTabBar:
let modalVC = contentVC
modalVC.modalPresentationStyle = .fullScreen
mainVC.present(modalVC, animated: true, completion: nil)
case .showPageView:
let pageVC = pagePanelController.makePageViewController(for: mainVC)
mainVC.present(pageVC, animated: true, completion: nil)
case .showPageContentView:
let pageVC = pagePanelController.makePageViewControllerForContent()
self.addMainPanel(with: pageVC)
case .showPanelModal:
let fpc = FloatingPanelController()
let contentVC = mainVC.storyboard!.instantiateViewController(withIdentifier: "DetailViewController")
contentVC.loadViewIfNeeded()
(contentVC as? DetailViewController)?.modeChangeView.isHidden = true
fpc.set(contentViewController: contentVC)
fpc.delegate = self
let appearance = SurfaceAppearance()
appearance.cornerRadius = 38.5
fpc.surfaceView.appearance = appearance
fpc.backdropView.dismissalTapGestureRecognizer.isEnabled = true
fpc.isRemovalInteractionEnabled = true
mainVC.present(fpc, animated: true, completion: nil)
case .showMultiPanelModal:
let fpc = MultiPanelController()
mainVC.present(fpc, animated: true, completion: nil)
case .showPanelInSheetModal:
let fpc = FloatingPanelController()
let contentVC = UIViewController()
fpc.set(contentViewController: contentVC)
fpc.delegate = self
let apprearance = SurfaceAppearance()
apprearance.cornerRadius = 38.5
apprearance.shadows = []
fpc.surfaceView.appearance = apprearance
fpc.isRemovalInteractionEnabled = true
let mvc = UIViewController()
mvc.view.backgroundColor = UIColor(displayP3Red: 2/255, green: 184/255, blue: 117/255, alpha: 1.0)
fpc.addPanel(toParent: mvc)
mainVC.present(mvc, animated: true, completion: nil)
case .showContentInset:
let contentViewController = UIViewController()
contentViewController.view.backgroundColor = .green
let fpc = FloatingPanelController()
fpc.set(contentViewController: contentViewController)
fpc.surfaceView.contentPadding = .init(top: 20, left: 20, bottom: 20, right: 20)
fpc.delegate = self
fpc.isRemovalInteractionEnabled = true
mainVC.present(fpc, animated: true, completion: nil)
case .showContainerMargins:
let fpc = FloatingPanelController()
let appearance = SurfaceAppearance()
appearance.cornerRadius = 38.5
fpc.surfaceView.appearance = appearance
fpc.surfaceView.backgroundColor = .red
fpc.surfaceView.containerMargins = .init(top: 24.0, left: 8.0, bottom: max(mainVC.layoutInsets.bottom, 8.0), right: 8.0)
#if swift(>=5.1) // Actually Xcode 11 or later
if #available(iOS 13.0, *) {
fpc.surfaceView.layer.cornerCurve = .continuous
}
#endif
fpc.delegate = self
fpc.isRemovalInteractionEnabled = true
mainVC.present(fpc, animated: true, completion: nil)
default:
self.addMainPanel(with: contentVC)
}
}
private func addMainPanel(with contentVC: UIViewController) {
mainPanelObserves.removeAll()
let oldMainPanelVC = mainPanelVC
mainPanelVC = FloatingPanelController()
mainPanelVC.delegate = self
mainPanelVC.contentInsetAdjustmentBehavior = .always
let appearance = SurfaceAppearance()
appearance.cornerRadius = 6.0
mainPanelVC.surfaceView.appearance = appearance
set(contentViewController: contentVC)
useCase.setUpInteraction(for: self)
// Add FloatingPanel to self.view
if let oldMainPanelVC = oldMainPanelVC {
oldMainPanelVC.removePanelFromParent(animated: true, completion: {
self.mainPanelVC.addPanel(toParent: self.mainVC, animated: true)
})
} else {
mainPanelVC.addPanel(toParent: mainVC, animated: true)
}
}
private func set(contentViewController contentVC: UIViewController) {
mainPanelVC.set(contentViewController: contentVC)
// Track a scroll view
switch contentVC {
case let consoleVC as DebugTextViewController:
mainPanelVC.track(scrollView: consoleVC.textView)
case let contentVC as DebugTableViewController:
let ob = contentVC.tableView.observe(\.isEditing) { (tableView, _) in
self.mainPanelVC.panGestureRecognizer.isEnabled = !tableView.isEditing
}
mainPanelObserves.append(ob)
mainPanelVC.track(scrollView: contentVC.tableView)
case let contentVC as NestedScrollViewController:
mainPanelVC.track(scrollView: contentVC.scrollView)
case let navVC as UINavigationController:
if let rootVC = (navVC.topViewController as? MainViewController) {
rootVC.loadViewIfNeeded()
mainPanelVC.track(scrollView: rootVC.tableView)
}
case let contentVC as ImageViewController:
if #available(iOS 11.0, *) {
let mode: ImageViewController.Mode = (useCase == .showAdaptivePanelWithCustomGuide) ? .withHeaderFooter : .onlyImage
let layoutGuide = contentVC.layoutGuideFor(mode: mode)
mainPanelVC.layout = ImageViewController.PanelLayout(targetGuide: layoutGuide)
} else {
mainPanelVC.layout = ImageViewController.PanelLayout(targetGuide: nil)
}
mainPanelVC.delegate = nil
mainPanelVC.isRemovalInteractionEnabled = true
mainPanelVC.track(scrollView: contentVC.scrollView)
default:
break
}
}
@objc
fileprivate func handleSurface(tapGesture: UITapGestureRecognizer) {
switch mainPanelVC.state {
case .full:
mainPanelVC.move(to: .half, animated: true)
default:
mainPanelVC.move(to: .full, animated: true)
}
}
func setUpSettingsPanel(for mainVC: MainViewController) {
guard settingsPanelVC == nil else { return }
// Initialize FloatingPanelController
settingsPanelVC = FloatingPanelController()
let appearance = SurfaceAppearance()
appearance.cornerRadius = 6.0
settingsPanelVC.surfaceView.appearance = appearance
settingsPanelVC.isRemovalInteractionEnabled = true
settingsPanelVC.backdropView.dismissalTapGestureRecognizer.isEnabled = true
settingsPanelVC.delegate = self
let contentVC = mainVC.storyboard?.instantiateViewController(withIdentifier: "SettingsViewController")
// Set a content view controller
settingsPanelVC.set(contentViewController: contentVC)
// Add FloatingPanel to self.view
settingsPanelVC.addPanel(toParent: mainVC, animated: true)
}
}
extension UseCaseController: FloatingPanelControllerDelegate {
func floatingPanel(_ vc: FloatingPanelController, contentOffsetForPinning trackingScrollView: UIScrollView) -> CGPoint {
if useCase == .showNavigationController, #available(iOS 11.0, *) {
// 148.0 is the SafeArea's top value for a navigation bar with a large title.
return CGPoint(x: 0.0, y: 0.0 - trackingScrollView.contentInset.top - 148.0)
}
return CGPoint(x: 0.0, y: 0.0 - trackingScrollView.contentInset.top)
}
func floatingPanel(_ vc: FloatingPanelController, layoutFor newCollection: UITraitCollection) -> FloatingPanelLayout {
if vc == settingsPanelVC {
return IntrinsicPanelLayout()
}
switch useCase {
case .showTopPositionedPanel:
return TopPositionedPanelLayout()
case .showRemovablePanel:
return newCollection.verticalSizeClass == .compact ? RemovablePanelLandscapeLayout() : RemovablePanelLayout()
case .showIntrinsicView:
return IntrinsicPanelLayout()
case .showPanelModal:
if vc != mainPanelVC && vc != detailPanelVC {
return ModalPanelLayout()
}
fallthrough
case .showContentInset:
return FloatingPanelBottomLayout()
case .showCustomStatePanel:
return FloatingPanelLayoutWithCustomState()
default:
return (newCollection.verticalSizeClass == .compact) ? FloatingPanelBottomLayout() : mainVC
}
}
func floatingPanelDidRemove(_ vc: FloatingPanelController) {
switch vc {
case settingsPanelVC:
settingsPanelVC = nil
default:
break
}
}
}
extension UseCaseController: UIGestureRecognizerDelegate {
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
switch useCase {
case .showNestedScrollView:
return true
default:
return false
}
}
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRequireFailureOf otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return false
}
}
extension UseCase {
func makeContentViewController(with storyboard: UIStoryboard) -> UIViewController {
guard let storyboardID = self.storyboardID else { return DebugTableViewController() }
return storyboard.instantiateViewController(withIdentifier: storyboardID)
}
func setUpInteraction(for useCaseController: UseCaseController) {
let mainVC = useCaseController.mainVC
let mainPanelVC = useCaseController.mainPanelVC!
// Enable tap-to-hide and removal interaction
switch self {
case .trackingTableView:
let tapGesture = UITapGestureRecognizer(target: useCaseController, action: #selector(UseCaseController.handleSurface(tapGesture:)))
tapGesture.cancelsTouchesInView = false
tapGesture.numberOfTapsRequired = 2
// Prevents a delay to response a tap in menus of DebugTableViewController.
tapGesture.delaysTouchesEnded = false
mainPanelVC.surfaceView.addGestureRecognizer(tapGesture)
case .showNestedScrollView:
mainPanelVC.panGestureRecognizer.delegateProxy = useCaseController
case .showPageContentView:
if let page = (mainPanelVC.contentViewController as? UIPageViewController)?.viewControllers?.first {
mainPanelVC.track(scrollView: (page as! DebugTableViewController).tableView)
}
case .showRemovablePanel, .showIntrinsicView:
mainPanelVC.isRemovalInteractionEnabled = true
mainPanelVC.backdropView.dismissalTapGestureRecognizer.isEnabled = true
case .showNavigationController:
mainPanelVC.contentInsetAdjustmentBehavior = .never
case .showTopPositionedPanel: // For debug
let contentVC = UIViewController()
contentVC.view.backgroundColor = .red
mainPanelVC.set(contentViewController: contentVC)
mainPanelVC.addPanel(toParent: mainVC, animated: true)
return
default:
break
}
}
}
@@ -0,0 +1,22 @@
// Copyright 2018-Present Shin Yamamoto. All rights reserved. MIT license.
import Foundation
import FloatingPanel
extension FloatingPanelState {
static let lastQuart: FloatingPanelState = FloatingPanelState(rawValue: "lastQuart", order: 750)
static let firstQuart: FloatingPanelState = FloatingPanelState(rawValue: "firstQuart", order: 250)
}
class FloatingPanelLayoutWithCustomState: FloatingPanelBottomLayout {
override var anchors: [FloatingPanelState: FloatingPanelLayoutAnchoring] {
return [
.full: FloatingPanelLayoutAnchor(absoluteInset: 0.0, edge: .top, referenceGuide: .safeArea),
.lastQuart: FloatingPanelLayoutAnchor(fractionalInset: 0.75, edge: .bottom, referenceGuide: .safeArea),
.half: FloatingPanelLayoutAnchor(fractionalInset: 0.5, edge: .bottom, referenceGuide: .safeArea),
.firstQuart: FloatingPanelLayoutAnchor(fractionalInset: 0.25, edge: .bottom, referenceGuide: .safeArea),
.tip: FloatingPanelLayoutAnchor(absoluteInset: 20.0, edge: .bottom, referenceGuide: .safeArea),
]
}
}
@@ -0,0 +1,82 @@
// Copyright 2018 the FloatingPanel authors. All rights reserved. MIT license.
import UIKit
import FloatingPanel
final class PagePanelController: NSObject {
lazy var pages = [UIColor.blue, .red, .green].compactMap({ (color) -> UIViewController in
let page = FloatingPanelController(delegate: self)
page.view.backgroundColor = color
page.panGestureRecognizer.delegateProxy = self
page.show()
return page
})
func makePageViewControllerForContent() -> UIPageViewController {
pages = [DebugTableViewController(), DebugTableViewController(), DebugTableViewController()]
let pageVC = UIPageViewController(transitionStyle: .scroll, navigationOrientation: .horizontal, options: [:])
pageVC.dataSource = self
pageVC.delegate = self
pageVC.setViewControllers([pages[0]], direction: .forward, animated: false, completion: nil)
return pageVC
}
func makePageViewController(for vc: MainViewController) -> UIPageViewController {
let pageVC = UIPageViewController(transitionStyle: .scroll, navigationOrientation: .horizontal, options: [:])
let closeButton = UIButton(type: .custom)
pageVC.view.addSubview(closeButton)
closeButton.setTitle("Close", for: .normal)
closeButton.translatesAutoresizingMaskIntoConstraints = false
closeButton.addTarget(vc, action: #selector(MainViewController.dismissPresentedVC), for: .touchUpInside)
NSLayoutConstraint.activate([
closeButton.topAnchor.constraint(equalTo: pageVC.layoutGuide.topAnchor, constant: 16.0),
closeButton.leftAnchor.constraint(equalTo: pageVC.view.leftAnchor, constant: 16.0),
])
pageVC.dataSource = self
pageVC.setViewControllers([pages[0]], direction: .forward, animated: false, completion: nil)
pageVC.modalPresentationStyle = .fullScreen
return pageVC
}
}
extension PagePanelController: FloatingPanelControllerDelegate {
func floatingPanel(_ vc: FloatingPanelController, layoutFor newCollection: UITraitCollection) -> FloatingPanelLayout {
return FloatingPanelBottomLayout()
}
}
extension PagePanelController: UIGestureRecognizerDelegate {
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return true
}
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRequireFailureOf otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return false
}
}
extension PagePanelController: UIPageViewControllerDataSource {
func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
guard
let index = pages.firstIndex(of: viewController),
index + 1 < pages.count
else { return nil }
return pages[index + 1]
}
func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
guard
let index = pages.firstIndex(of: viewController),
index - 1 >= 0
else { return nil }
return pages[index - 1]
}
}
extension PagePanelController: UIPageViewControllerDelegate {
// For showPageContent
func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) {
if completed, let page = pageViewController.viewControllers?.first {
(pageViewController.parent as! FloatingPanelController).track(scrollView: (page as! DebugTableViewController).tableView)
}
}
}
@@ -0,0 +1,78 @@
// Copyright 2018-Present Shin Yamamoto. All rights reserved. MIT license.
import Foundation
enum UseCase: Int, CaseIterable {
case trackingTableView
case trackingTextView
case showDetail
case showModal
case showPanelModal
case showMultiPanelModal
case showPanelInSheetModal
case showTabBar
case showPageView
case showPageContentView
case showNestedScrollView
case showRemovablePanel
case showIntrinsicView
case showContentInset
case showContainerMargins
case showNavigationController
case showTopPositionedPanel
case showAdaptivePanel
case showAdaptivePanelWithCustomGuide
case showCustomStatePanel
var name: String {
switch self {
case .trackingTableView: return "Scroll tracking(TableView)"
case .trackingTextView: return "Scroll tracking(TextView)"
case .showDetail: return "Show Detail Panel"
case .showModal: return "Show Modal"
case .showPanelModal: return "Show Panel Modal"
case .showMultiPanelModal: return "Show Multi Panel Modal"
case .showPanelInSheetModal: return "Show Panel in Sheet Modal"
case .showTabBar: return "Show Tab Bar"
case .showPageView: return "Show Page View"
case .showPageContentView: return "Show Page Content View"
case .showNestedScrollView: return "Show Nested ScrollView"
case .showRemovablePanel: return "Show Removable Panel"
case .showIntrinsicView: return "Show Intrinsic View"
case .showContentInset: return "Show with ContentInset"
case .showContainerMargins: return "Show with ContainerMargins"
case .showNavigationController: return "Show Navigation Controller"
case .showTopPositionedPanel: return "Show Top Positioned Panel"
case .showAdaptivePanel: return "Show Adaptive Panel"
case .showAdaptivePanelWithCustomGuide: return "Show Adaptive Panel (Custom Layout Guide)"
case .showCustomStatePanel: return "Show Panel with Custom state"
}
}
var storyboardID: String? {
switch self {
case .trackingTableView: return nil
case .trackingTextView: return "ConsoleViewController" // Storyboard only
case .showDetail: return String(describing: DetailViewController.self)
case .showModal: return String(describing: ModalViewController.self)
case .showMultiPanelModal: return nil
case .showPanelInSheetModal: return nil
case .showPanelModal: return nil
case .showTabBar: return String(describing: TabBarViewController.self)
case .showPageView: return nil
case .showPageContentView: return nil
case .showNestedScrollView: return String(describing: NestedScrollViewController.self)
case .showRemovablePanel: return String(describing: DetailViewController.self)
case .showIntrinsicView: return "IntrinsicViewController" // Storyboard only
case .showContentInset: return nil
case .showContainerMargins: return nil
case .showNavigationController: return "RootNavigationController" // Storyboard only
case .showTopPositionedPanel: return nil
case .showAdaptivePanel,
.showAdaptivePanelWithCustomGuide:
return String(describing: ImageViewController.self)
case .showCustomStatePanel:
return nil
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,278 @@
// Copyright 2018 the FloatingPanel authors. All rights reserved. MIT license.
import UIKit
import FloatingPanel
class DebugTableViewController: InspectableViewController {
// MARK: - Views
lazy var tableView: UITableView = {
let tableView = UITableView(frame: .zero, style: .plain)
tableView.dataSource = self
tableView.delegate = self
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "Cell")
return tableView
}()
lazy var buttonStackView: UIStackView = {
let stackView = UIStackView()
stackView.axis = .vertical
stackView.distribution = .fillEqually
stackView.alignment = .trailing
stackView.spacing = 10.0
return stackView
}()
private lazy var reorderButton: UIButton = {
let button = UIButton()
button.setTitle(Menu.reorder.rawValue, for: .normal)
button.setTitleColor(view.tintColor, for: .normal)
button.addTarget(self, action: #selector(reorderItems), for: .touchUpInside)
return button
}()
private lazy var trackingSwitchWrapper: UIStackView = {
let stackView = UIStackView()
stackView.axis = .horizontal
stackView.distribution = .fillProportionally
stackView.alignment = .center
stackView.spacing = 8.0
stackView.addArrangedSubview(trackingLabel)
stackView.addArrangedSubview(trackingSwitch)
return stackView
}()
private lazy var trackingLabel: UILabel = {
let label = UILabel()
label.text = "Tracking"
label.font = UIFont.systemFont(ofSize: 17.0, weight: .regular)
return label
}()
private lazy var trackingSwitch: UISwitch = {
let trackingSwitch = UISwitch()
trackingSwitch.isOn = true
trackingSwitch.addTarget(self, action: #selector(turnTrackingOn), for: .touchUpInside)
return trackingSwitch
}()
// MARK: - Properties
private lazy var items: [String] = {
let items = (0..<100).map { "Items \($0)" }
return Command.replace(items: items)
}()
private var itemHeight: CGFloat = 66.0
enum Menu: String, CaseIterable {
case turnOffTracking = "Tracking"
case reorder = "Reorder"
}
enum Command: Int, CaseIterable {
case animateScroll
case changeContentSize
case moveToFull
case moveToHalf
var text: String {
switch self {
case .animateScroll: return "Scroll in the middle"
case .changeContentSize: return "Change content size"
case .moveToFull: return "Move to Full"
case.moveToHalf: return "Move to Half"
}
}
static func replace(items: [String]) -> [String] {
return items.enumerated().map { (index, text) -> String in
if let action = Command(rawValue: index) {
return "\(index). \(action.text)"
}
return text
}
}
func execute(for vc: DebugTableViewController, sourceView: UIView) {
switch self {
case .animateScroll:
vc.animateScroll()
case .changeContentSize:
vc.changeContentSize(sourceView: sourceView)
case .moveToFull:
vc.moveToFull()
case .moveToHalf:
vc.moveToHalf()
}
}
}
// MARK: - Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
layoutTableView()
layoutMenuStackView()
setUpMenu()
}
private func layoutTableView() {
view.addSubview(tableView)
tableView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
tableView.topAnchor.constraint(equalTo: view.topAnchor),
tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
tableView.leftAnchor.constraint(equalTo: view.leftAnchor),
tableView.rightAnchor.constraint(equalTo: view.rightAnchor)
])
}
private func layoutMenuStackView() {
view.addSubview(buttonStackView)
buttonStackView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
buttonStackView.topAnchor.constraint(equalTo: view.topAnchor, constant: 22.0),
buttonStackView.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -22.0),
])
}
private func setUpMenu() {
for menu in Menu.allCases {
switch menu {
case .reorder:
buttonStackView.addArrangedSubview(reorderButton)
case .turnOffTracking:
buttonStackView.addArrangedSubview(trackingSwitchWrapper)
}
}
}
// MARK: - Menu
@objc
private func reorderItems() {
if reorderButton.titleLabel?.text == Menu.reorder.rawValue {
tableView.isEditing = true
reorderButton.setTitle("Cancel", for: .normal)
} else {
tableView.isEditing = false
reorderButton.setTitle(Menu.reorder.rawValue, for: .normal)
}
}
@objc
private func turnTrackingOn(_ sender: UISwitch) {
guard let fpc = self.parent as? FloatingPanelController else { return }
if sender.isOn {
fpc.track(scrollView: tableView)
} else {
fpc.untrack(scrollView: tableView)
}
}
// MARK: - Actions
private func execute(command: Command, sourceView: UIView) {
command.execute(for: self, sourceView: sourceView)
}
@objc
private func animateScroll() {
tableView.scrollToRow(at: IndexPath(row: lround(Double(items.count) / 2.0),
section: 0),
at: .top, animated: true)
}
@objc
private func changeContentSize(sourceView: UIView) {
let actionSheet = UIAlertController(title: "Change content size", message: "", preferredStyle: .actionSheet)
actionSheet.addAction(UIAlertAction(title: "Large", style: .default, handler: { (_) in
self.itemHeight = 66.0
self.changeItems(100)
}))
actionSheet.addAction(UIAlertAction(title: "Match", style: .default, handler: { (_) in
switch self.tableView.bounds.height {
case 585: // iPhone 6,7,8
self.itemHeight = self.tableView.bounds.height / 13.0
self.changeItems(13)
case 656: // iPhone {6,7,8} Plus
self.itemHeight = self.tableView.bounds.height / 16.0
self.changeItems(16)
default: // iPhone X family
self.itemHeight = self.tableView.bounds.height / 12.0
self.changeItems(12)
}
}))
actionSheet.addAction(UIAlertAction(title: "Short", style: .default, handler: { (_) in
self.itemHeight = 66.0
self.changeItems(3)
}))
if let popoverController = actionSheet.popoverPresentationController {
popoverController.sourceView = sourceView
popoverController.sourceRect = sourceView.bounds
}
self.present(actionSheet, animated: true, completion: nil)
}
private func changeItems(_ count: Int) {
items = Command.replace(items: (0..<count).map{ "\($0). No action" })
tableView.reloadData()
}
@objc
private func moveToFull() {
(self.parent as! FloatingPanelController).move(to: .full, animated: true)
}
@objc
private func moveToHalf() {
(self.parent as! FloatingPanelController).move(to: .half, animated: true)
}
@objc
private func close(sender: UIButton) {
// Remove FloatingPanel from a view
(self.parent as! FloatingPanelController).removePanelFromParent(animated: true, completion: nil)
}
}
extension DebugTableViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return items.count
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return itemHeight
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
cell.textLabel?.text = items[indexPath.row]
return cell
}
}
extension DebugTableViewController: UITableViewDelegate {
func scrollViewDidScroll(_ scrollView: UIScrollView) {
print("TableView --- ", scrollView.contentOffset, scrollView.contentInset)
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
print("DebugTableViewController -- select row \(indexPath.row)")
guard let action = Command(rawValue: indexPath.row) else { return }
let cell = tableView.cellForRow(at: indexPath)
execute(command: action, sourceView: cell ?? tableView)
}
func tableView(_ tableView: UITableView, editActionsForRowAt indexPath: IndexPath) -> [UITableViewRowAction]? {
return [
UITableViewRowAction(style: .destructive, title: "Delete", handler: { (action, path) in
self.items.remove(at: path.row)
tableView.deleteRows(at: [path], with: .automatic)
}),
]
}
func tableView(_ tableView: UITableView, canMoveRowAt indexPath: IndexPath) -> Bool {
return true
}
func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {
items.insert(items.remove(at: sourceIndexPath.row), at: destinationIndexPath.row)
}
}
@@ -0,0 +1,44 @@
// Copyright 2018 the FloatingPanel authors. All rights reserved. MIT license.
import UIKit
final class DebugTextViewController: UIViewController, UITextViewDelegate {
@IBOutlet weak var textView: UITextView!
@IBOutlet weak var textViewTopConstraint: NSLayoutConstraint!
override func viewDidLoad() {
super.viewDidLoad()
textView.delegate = self
print("viewDidLoad: TextView --- ", textView.contentOffset, textView.contentInset)
if #available(iOS 11.0, *) {
textView.contentInsetAdjustmentBehavior = .never
}
}
override func viewWillLayoutSubviews() {
print("viewWillLayoutSubviews: TextView --- ", textView.contentOffset, textView.contentInset, textView.frame)
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
print("viewDidLayoutSubviews: TextView --- ", textView.contentOffset, textView.contentInset, textView.frame)
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
print("TextView --- ", textView.contentOffset, textView.contentInset, textView.frame)
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
print("TextView --- ", scrollView.contentOffset, scrollView.contentInset)
if #available(iOS 11.0, *) {
print("TextView --- ", scrollView.adjustedContentInset)
}
}
@IBAction func close(sender: UIButton) {
// (self.parent as? FloatingPanelController)?.removePanelFromParent(animated: true, completion: nil)
dismiss(animated: true, completion: nil)
}
}
@@ -0,0 +1,39 @@
// Copyright 2018-Present Shin Yamamoto. All rights reserved. MIT license.
import UIKit
import FloatingPanel
final class DetailViewController: InspectableViewController {
@IBOutlet weak var modeChangeView: UIStackView!
@IBOutlet weak var intrinsicHeightConstraint: NSLayoutConstraint!
@IBOutlet weak var closeButton: UIButton!
@IBAction func close(sender: UIButton) {
// (self.parent as? FloatingPanelController)?.removePanelFromParent(animated: true, completion: nil)
dismiss(animated: true, completion: nil)
}
@IBAction func buttonPressed(_ sender: UIButton) {
switch sender.titleLabel?.text {
case "Show":
performSegue(withIdentifier: "ShowSegue", sender: self)
case "Present Modally":
performSegue(withIdentifier: "PresentModallySegue", sender: self)
default:
break
}
}
@IBAction func modeChanged(_ sender: Any) {
guard let fpc = parent as? FloatingPanelController else { return }
fpc.contentMode = (fpc.contentMode == .static) ? .fitToBounds : .static
}
@IBAction func tapped(_ sender: Any) {
print("Detail panel is tapped!")
}
@IBAction func swipped(_ sender: Any) {
print("Detail panel is swipped!")
}
@IBAction func longPressed(_ sender: Any) {
print("Detail panel is longPressed!")
}
}
@@ -0,0 +1,69 @@
// Copyright 2018-Present Shin Yamamoto. All rights reserved. MIT license.
import UIKit
import FloatingPanel
final class ImageViewController: UIViewController {
class PanelLayout: FloatingPanelLayout {
weak var targetGuide: UILayoutGuide?
init(targetGuide: UILayoutGuide?) {
self.targetGuide = targetGuide
}
let position: FloatingPanelPosition = .bottom
let initialState: FloatingPanelState = .full
var anchors: [FloatingPanelState : FloatingPanelLayoutAnchoring] {
if #available(iOS 11.0, *), let targetGuide = targetGuide {
return [
.full: FloatingPanelAdaptiveLayoutAnchor(absoluteOffset: 0,
contentLayout: targetGuide,
referenceGuide: .superview),
.half: FloatingPanelAdaptiveLayoutAnchor(fractionalOffset: 0.5,
contentLayout: targetGuide,
referenceGuide: .superview)
]
} else {
return [
.full: FloatingPanelLayoutAnchor(absoluteInset: 500,
edge: .bottom,
referenceGuide: .superview)
]
}
}
}
@IBOutlet weak var headerView: UIView!
@IBOutlet weak var footerView: UIView!
@IBOutlet weak var scrollView: UIScrollView!
@IBOutlet weak var stackView: UIStackView!
enum Mode {
case onlyImage
case withHeaderFooter
}
@available(iOS 11.0, *)
func layoutGuideFor(mode: Mode) -> UILayoutGuide {
switch mode {
case .onlyImage:
self.headerView.isHidden = true
self.footerView.isHidden = true
return scrollView.contentLayoutGuide
case .withHeaderFooter:
self.headerView.isHidden = false
self.footerView.isHidden = false
let guide = UILayoutGuide()
view.addLayoutGuide(guide)
NSLayoutConstraint.activate([
scrollView.heightAnchor.constraint(equalTo: scrollView.contentLayoutGuide.heightAnchor),
guide.topAnchor.constraint(equalTo: stackView.topAnchor),
guide.leftAnchor.constraint(equalTo: stackView.leftAnchor),
guide.bottomAnchor.constraint(equalTo: stackView.bottomAnchor),
guide.rightAnchor.constraint(equalTo: stackView.rightAnchor),
])
return guide
}
}
}
@@ -0,0 +1,48 @@
// Copyright 2018 the FloatingPanel authors. All rights reserved. MIT license.
import UIKit
class InspectableViewController: UIViewController {
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
print(">>> Content View: viewWillLayoutSubviews", layoutInsets)
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
print(">>> Content View: viewDidLayoutSubviews", layoutInsets)
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
print(">>> Content View: viewWillAppear", layoutInsets)
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
print(">>> Content View: viewDidAppear", view.bounds, layoutInsets)
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
print(">>> Content View: viewWillDisappear")
}
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
print(">>> Content View: viewDidDisappear")
}
override func willMove(toParent parent: UIViewController?) {
super.willMove(toParent: parent)
print(">>> Content View: willMove(toParent: \(String(describing: parent))")
}
override func didMove(toParent parent: UIViewController?) {
super.didMove(toParent: parent)
print(">>> Content View: didMove(toParent: \(String(describing: parent))")
}
public override func willTransition(to newCollection: UITraitCollection, with coordinator: UIViewControllerTransitionCoordinator) {
print(">>> Content View: willTransition(to: \(newCollection), with: \(coordinator))", layoutInsets)
}
}
@@ -0,0 +1,79 @@
// Copyright 2018 the FloatingPanel authors. All rights reserved. MIT license.
import UIKit
import FloatingPanel
final class ModalViewController: UIViewController, FloatingPanelControllerDelegate {
var fpc: FloatingPanelController!
var consoleVC: DebugTextViewController!
@IBOutlet weak var safeAreaView: UIView!
var isNewlayout: Bool = false
override func viewDidLoad() {
// Initialize FloatingPanelController
fpc = FloatingPanelController()
fpc.delegate = self
let appearance = SurfaceAppearance()
appearance.cornerRadius = 6.0
fpc.surfaceView.appearance = appearance
// Set a content view controller and track the scroll view
let consoleVC = storyboard?.instantiateViewController(withIdentifier: "ConsoleViewController") as! DebugTextViewController
fpc.set(contentViewController: consoleVC)
fpc.track(scrollView: consoleVC.textView)
self.consoleVC = consoleVC
// Add FloatingPanel to self.view
fpc.addPanel(toParent: self, at: view.subviews.firstIndex(of: safeAreaView) ?? -1)
}
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
// Remove FloatingPanel from a view
fpc.removePanelFromParent(animated: false)
}
@IBAction func close(sender: UIButton) {
dismiss(animated: true, completion: nil)
}
@IBAction func moveToFull(sender: UIButton) {
fpc.move(to: .full, animated: true)
}
@IBAction func moveToHalf(sender: UIButton) {
fpc.move(to: .half, animated: true)
}
@IBAction func moveToTip(sender: UIButton) {
fpc.move(to: .tip, animated: true)
}
@IBAction func moveToHidden(sender: UIButton) {
fpc.move(to: .hidden, animated: true)
}
@IBAction func updateLayout(_ sender: Any) {
isNewlayout = !isNewlayout
UIView.animate(withDuration: 0.5) {
self.fpc.layout = (self.isNewlayout) ? ModalSecondLayout() : FloatingPanelBottomLayout()
self.fpc.invalidateLayout()
}
}
func floatingPanel(_ vc: FloatingPanelController, layoutFor newCollection: UITraitCollection) -> FloatingPanelLayout {
return (isNewlayout) ? ModalSecondLayout() : FloatingPanelBottomLayout()
}
class ModalSecondLayout: FloatingPanelLayout {
var position: FloatingPanelPosition = .bottom
var initialState: FloatingPanelState { .half }
var anchors: [FloatingPanelState : FloatingPanelLayoutAnchoring] {
return [
.full: FloatingPanelLayoutAnchor(absoluteInset: 16.0, edge: .top, referenceGuide: .safeArea),
.half: FloatingPanelLayoutAnchor(absoluteInset: 262, edge: .top, referenceGuide: .safeArea),
.tip: FloatingPanelLayoutAnchor(absoluteInset: 44.0, edge: .bottom, referenceGuide: .safeArea)
]
}
}
}
@@ -0,0 +1,67 @@
// Copyright 2018-Present Shin Yamamoto. All rights reserved. MIT license.
import UIKit
import FloatingPanel
import WebKit
final class MultiPanelController: FloatingPanelController, FloatingPanelControllerDelegate {
private final class FirstPanelContentViewController: UIViewController {
lazy var webView: WKWebView = WKWebView()
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(webView)
webView.frame = view.bounds
webView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
webView.load(URLRequest(url: URL(string: "https://www.apple.com")!))
let vc = MultiSecondPanelController()
vc.setUpContent()
vc.addPanel(toParent: self)
}
}
private final class MultiSecondPanelController: FloatingPanelController {
private final class SecondPanelContentViewController: DebugTableViewController {}
func setUpContent() {
contentInsetAdjustmentBehavior = .never
let vc = SecondPanelContentViewController()
vc.loadViewIfNeeded()
vc.title = "Second Panel"
vc.buttonStackView.isHidden = true
let navigationController = UINavigationController(rootViewController: vc)
navigationController.navigationBar.barTintColor = .white
navigationController.navigationBar.titleTextAttributes = [
.foregroundColor: UIColor.black
]
set(contentViewController: navigationController)
self.track(scrollView: vc.tableView)
surfaceView.containerMargins = .init(top: 24.0, left: 0.0, bottom: layoutInsets.bottom, right: 0.0)
}
}
override func viewDidLoad() {
super.viewDidLoad()
layout = FirstViewLayout()
isRemovalInteractionEnabled = true
let vc = FirstPanelContentViewController()
set(contentViewController: vc)
track(scrollView: vc.webView.scrollView)
}
private final class FirstViewLayout: FloatingPanelLayout {
let position: FloatingPanelPosition = .bottom
let initialState: FloatingPanelState = .full
var anchors: [FloatingPanelState : FloatingPanelLayoutAnchoring] {
return [
.full: FloatingPanelLayoutAnchor(absoluteInset: 40.0, edge: .top, referenceGuide: .superview)
]
}
}
}
@@ -0,0 +1,18 @@
// Copyright 2018 the FloatingPanel authors. All rights reserved. MIT license.
import UIKit
final class NestedScrollViewController: UIViewController {
@IBOutlet weak var scrollView: UIScrollView!
@IBOutlet weak var nestedScrollView: UIScrollView!
@IBAction func longPressed(_ sender: Any) {
print("LongPressed!")
}
@IBAction func swipped(_ sender: Any) {
print("Swipped!")
}
@IBAction func tapped(_ sender: Any) {
print("Tapped!")
}
}
@@ -0,0 +1,36 @@
// Copyright 2018-Present Shin Yamamoto. All rights reserved. MIT license.
import UIKit
import FloatingPanel
final class SettingsViewController: InspectableViewController {
@IBOutlet weak var largeTitlesSwicth: UISwitch!
@IBOutlet weak var translucentSwicth: UISwitch!
@IBOutlet weak var versionLabel: UILabel!
override func viewDidLoad() {
versionLabel.text = "Version: \(Bundle.main.infoDictionary?["CFBundleVersion"] ?? "--")"
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
if #available(iOS 11.0, *) {
let prefersLargeTitles = navigationController!.navigationBar.prefersLargeTitles
largeTitlesSwicth.setOn(prefersLargeTitles, animated: false)
} else {
largeTitlesSwicth.isEnabled = false
}
let isTranslucent = navigationController!.navigationBar.isTranslucent
translucentSwicth.setOn(isTranslucent, animated: false)
}
@IBAction func toggleLargeTitle(_ sender: UISwitch) {
if #available(iOS 11.0, *) {
navigationController?.navigationBar.prefersLargeTitles = sender.isOn
}
}
@IBAction func toggleTranslucent(_ sender: UISwitch) {
navigationController?.navigationBar.isTranslucent = sender.isOn
}
}
@@ -0,0 +1,252 @@
// Copyright 2018-Present Shin Yamamoto. All rights reserved. MIT license.
import UIKit
import FloatingPanel
final class TabBarViewController: UITabBarController {}
final class TabBarContentViewController: UIViewController {
enum Tab3Mode {
case changeOffset
case changeAutoLayout
var label: String {
switch self {
case .changeAutoLayout: return "Use AutoLayout(OK)"
case .changeOffset: return "Use ContentOffset(NG)"
}
}
}
lazy var fpc = FloatingPanelController()
var consoleVC: DebugTextViewController!
var threeLayout: ThreeTabBarPanelLayout!
var tab3Mode: Tab3Mode = .changeAutoLayout
var switcherLabel: UILabel!
override func viewDidLoad() {
fpc.delegate = self
let appearance = SurfaceAppearance()
appearance.cornerRadius = 6.0
fpc.surfaceView.appearance = appearance
// Set a content view controller and track the scroll view
let consoleVC = storyboard?.instantiateViewController(withIdentifier: "ConsoleViewController") as! DebugTextViewController
fpc.set(contentViewController: consoleVC)
consoleVC.textView.delegate = self // MUST call it before fpc.track(scrollView:)
fpc.track(scrollView: consoleVC.textView)
self.consoleVC = consoleVC
// Add FloatingPanel to self.view
fpc.addPanel(toParent: self)
if #available(iOS 15, *) {
tabBarController?.tabBar.scrollEdgeAppearance = UITabBarAppearance()
}
switch tabBarItem.tag {
case 1:
fpc.behavior = TwoTabBarPanelBehavior()
case 2:
let switcher = UISwitch()
fpc.view.addSubview(switcher)
switcher.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
switcher.bottomAnchor.constraint(equalTo: fpc.surfaceView.topAnchor, constant: -16.0),
switcher.rightAnchor.constraint(equalTo: fpc.surfaceView.rightAnchor, constant: -16.0),
])
switcher.isOn = true
switcher.tintColor = .white
switcher.backgroundColor = .white
switcher.layer.cornerRadius = 16.0
switcher.addTarget(self,
action: #selector(changeTab3Mode(_:)),
for: .valueChanged)
let label = UILabel()
label.text = tab3Mode.label
fpc.view.addSubview(label)
switcherLabel = label
label.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
label.centerYAnchor.constraint(equalTo: switcher.centerYAnchor, constant: 0.0),
label.rightAnchor.constraint(equalTo: switcher.leftAnchor, constant: -16.0),
])
// Turn off the mask instead of content inset change
consoleVC.textView.clipsToBounds = false
default:
break
}
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
fpc.invalidateLayout()
}
// MARK: - Action
@IBAction func close(sender: UIButton) {
dismiss(animated: true, completion: nil)
}
// MARK: - Private
@objc
private func changeTab3Mode(_ sender: UISwitch) {
if sender.isOn {
tab3Mode = .changeAutoLayout
} else {
tab3Mode = .changeOffset
}
switcherLabel.text = tab3Mode.label
}
}
extension TabBarContentViewController: UITextViewDelegate {
func scrollViewDidScroll(_ scrollView: UIScrollView) {
guard self.tabBarItem.tag == 2 else { return }
// Reset an invalid content offset by a user after updating the layout
// of `consoleVC.textView`.
// NOTE: FloatingPanel doesn't implicitly reset the offset(i.e.
// Using KVO of `scrollView.contentOffset`). Because it can lead to an
// infinite loop if a user also resets a content offset as below and,
// in the situation, a user has to modify the library.
if fpc.state != .full, fpc.surfaceLocation.y > fpc.surfaceLocation(for: .full).y {
scrollView.contentOffset = .zero
}
}
}
extension TabBarContentViewController: FloatingPanelControllerDelegate {
// MARK: - FloatingPanel
func floatingPanel(_ vc: FloatingPanelController, layoutFor newCollection: UITraitCollection) -> FloatingPanelLayout {
switch self.tabBarItem.tag {
case 0:
return OneTabBarPanelLayout()
case 1:
return TwoTabBarPanelLayout()
case 2:
threeLayout = ThreeTabBarPanelLayout(parent: self)
return threeLayout
default:
return FloatingPanelBottomLayout()
}
}
func floatingPanelDidMove(_ vc: FloatingPanelController) {
guard self.tabBarItem.tag == 2 else { return }
switch tab3Mode {
case .changeAutoLayout:
/* Good solution: Manipulate top constraint */
assert(consoleVC.textViewTopConstraint != nil)
let safeAreaTop = vc.layoutInsets.top
if vc.surfaceLocation.y + threeLayout.topPadding < safeAreaTop {
consoleVC.textViewTopConstraint?.constant = min(safeAreaTop - vc.surfaceLocation.y,
safeAreaTop)
} else {
consoleVC.textViewTopConstraint?.constant = threeLayout.topPadding
}
case .changeOffset:
/*
Bad solution: Manipulate scroll content inset
FloatingPanelController keeps a content offset in moving a panel
so that changing content inset or offset causes a buggy behavior.
*/
guard let scrollView = consoleVC.textView else { return }
var insets = vc.adjustedContentInsets
if vc.surfaceView.frame.minY < vc.layoutInsets.top {
insets.top = vc.layoutInsets.top - vc.surfaceView.frame.minY
} else {
insets.top = 0.0
}
scrollView.contentInset = insets
if vc.surfaceView.frame.minY > 0 {
scrollView.contentOffset = CGPoint(x: 0.0,
y: 0.0 - scrollView.contentInset.top)
}
}
if vc.surfaceLocation.y > vc.surfaceLocation(for: .half).y {
let progress = (vc.surfaceLocation.y - vc.surfaceLocation(for: .half).y)
/ (vc.surfaceLocation(for: .tip).y - vc.surfaceLocation(for: .half).y)
threeLayout.leftConstraint.constant = max(min(progress, 1.0), 0.0) * threeLayout.sideMargin
threeLayout.rightConstraint.constant = -max(min(progress, 1.0), 0.0) * threeLayout.sideMargin
} else {
threeLayout.leftConstraint.constant = 0.0
threeLayout.rightConstraint.constant = 0.0
}
}
}
class OneTabBarPanelLayout: FloatingPanelLayout {
var initialState: FloatingPanelState { .tip }
var position: FloatingPanelPosition { .bottom }
var anchors: [FloatingPanelState : FloatingPanelLayoutAnchoring] {
return [
.full: FloatingPanelLayoutAnchor(absoluteInset: 16.0, edge: .top, referenceGuide: .safeArea),
.tip: FloatingPanelLayoutAnchor(absoluteInset: 22.0, edge: .bottom, referenceGuide: .safeArea)
]
}
}
class TwoTabBarPanelLayout: FloatingPanelLayout {
var initialState: FloatingPanelState { .half }
var position: FloatingPanelPosition { .bottom }
var anchors: [FloatingPanelState : FloatingPanelLayoutAnchoring] {
return [
.full: FloatingPanelLayoutAnchor(absoluteInset: 100.0, edge: .top, referenceGuide: .safeArea),
.half: FloatingPanelLayoutAnchor(absoluteInset: 261.0, edge: .bottom, referenceGuide: .safeArea)
]
}
}
class TwoTabBarPanelBehavior: FloatingPanelBehavior {
func allowsRubberBanding(for edges: UIRectEdge) -> Bool {
return [UIRectEdge.top, UIRectEdge.bottom].contains(edges)
}
}
class ThreeTabBarPanelLayout: FloatingPanelLayout {
weak var parentVC: UIViewController!
var leftConstraint: NSLayoutConstraint!
var rightConstraint: NSLayoutConstraint!
let topPadding: CGFloat = 17.0
let sideMargin: CGFloat = 16.0
init(parent: UIViewController) {
parentVC = parent
}
var initialState: FloatingPanelState { .half }
var position: FloatingPanelPosition { .bottom }
var anchors: [FloatingPanelState : FloatingPanelLayoutAnchoring] {
return [
.full: FloatingPanelLayoutAnchor(absoluteInset: 0.0, edge: .top, referenceGuide: .superview),
.half: FloatingPanelLayoutAnchor(absoluteInset: 261.0 + parentVC.layoutInsets.bottom, edge: .bottom, referenceGuide: .superview),
.tip: FloatingPanelLayoutAnchor(absoluteInset: 88.0 + parentVC.layoutInsets.bottom, edge: .bottom, referenceGuide: .superview),
]
}
func backdropAlpha(for state: FloatingPanelState) -> CGFloat {
return 0.3
}
func prepareLayout(surfaceView: UIView, in view: UIView) -> [NSLayoutConstraint] {
if #available(iOS 11.0, *) {
leftConstraint = surfaceView.leftAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leftAnchor, constant: 0.0)
rightConstraint = surfaceView.rightAnchor.constraint(equalTo: view.safeAreaLayoutGuide.rightAnchor, constant: 0.0)
} else {
leftConstraint = surfaceView.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 0.0)
rightConstraint = surfaceView.rightAnchor.constraint(equalTo: view.rightAnchor, constant: 0.0)
}
return [ leftConstraint, rightConstraint ]
}
}
@@ -1,6 +1,6 @@
{
"info" : {
"version" : 1,
"author" : "xcode"
"author" : "xcode",
"version" : 1
}
}
}
@@ -3,6 +3,22 @@
#import "ViewController.h"
@import FloatingPanel;
// Defining a custom FloatingPanelState
@interface FloatingPanelState(Extended)
+ (FloatingPanelState *)LastQuart;
@end
@implementation FloatingPanelState(Extended)
static FloatingPanelState *_lastQuart;
+ (FloatingPanelState *)LastQuart {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
_lastQuart = [[FloatingPanelState alloc] initWithRawValue:@"lastquart" order:750];
});
return _lastQuart;
}
@end
@interface ViewController()<FloatingPanelControllerDelegate>
@end
@@ -20,7 +36,7 @@
[fpc setBehavior:[MyFloatingPanelBehavior new]];
[fpc setRemovalInteractionEnabled:NO];
[fpc addPanelToParent:self at:self.view.subviews.count animated:NO];
[fpc addPanelToParent:self at:self.view.subviews.count animated:NO completion:nil];
[fpc moveToState:FloatingPanelState.Tip animated:true completion:nil];
[self updateAppearance: fpc];
@@ -59,12 +75,15 @@
}
- (NSDictionary<FloatingPanelState *, id<FloatingPanelLayoutAnchoring>> *)anchors {
return @{
FloatingPanelState.LastQuart: [[FloatingPanelLayoutAnchor alloc] initWithFractionalInset:0.25
edge:FloatingPanelReferenceEdgeTop
referenceGuide:FloatingPanelLayoutReferenceGuideSafeArea],
FloatingPanelState.Half: [[FloatingPanelLayoutAnchor alloc] initWithFractionalInset:0.5
edge:FloatingPanelReferenceEdgeTop
referenceGuide:FloatingPanelLayoutReferenceGuideSafeArea],
referenceGuide:FloatingPanelLayoutReferenceGuideSafeArea],
FloatingPanelState.Tip: [[FloatingPanelLayoutAnchor alloc] initWithAbsoluteInset:44.0
edge:FloatingPanelReferenceEdgeBottom
referenceGuide:FloatingPanelLayoutReferenceGuideSafeArea],
referenceGuide:FloatingPanelLayoutReferenceGuideSafeArea],
};
}
+1 -1
View File
@@ -1,7 +1,7 @@
Pod::Spec.new do |s|
s.name = "FloatingPanel"
s.version = "2.0.0"
s.version = "2.5.0"
s.summary = "FloatingPanel is a clean and easy-to-use UI component of a floating panel interface."
s.description = <<-DESC
FloatingPanel is a clean and easy-to-use UI component for a new interface introduced in Apple Maps, Shortcuts and Stocks app.
+12 -12
View File
@@ -9,12 +9,11 @@
/* Begin PBXBuildFile section */
542753C622C49A6E00D17955 /* LayoutTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 542753C522C49A6E00D17955 /* LayoutTests.swift */; };
54352E9621A51A2500CBCA08 /* Transitioning.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54352E9521A51A2500CBCA08 /* Transitioning.swift */; };
54352E9821A521CA00CBCA08 /* PassThroughView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54352E9721A521CA00CBCA08 /* PassThroughView.swift */; };
54352E9821A521CA00CBCA08 /* PassthroughView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54352E9721A521CA00CBCA08 /* PassthroughView.swift */; };
5450EEE421646DF500135936 /* Behavior.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5450EEE321646DF500135936 /* Behavior.swift */; };
545DB9CB2151169500CA77B8 /* FloatingPanel.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 545DB9C12151169500CA77B8 /* FloatingPanel.framework */; };
545DB9D02151169500CA77B8 /* ControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 545DB9CF2151169500CA77B8 /* ControllerTests.swift */; };
545DB9D22151169500CA77B8 /* FloatingPanel.h in Headers */ = {isa = PBXBuildFile; fileRef = 545DB9C42151169500CA77B8 /* FloatingPanel.h */; settings = {ATTRIBUTES = (Public, ); }; };
545DB9DE215118C800CA77B8 /* UIExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 545DB9DD215118C800CA77B8 /* UIExtensions.swift */; };
545DB9E021511AC100CA77B8 /* Controller.swift in Sources */ = {isa = PBXBuildFile; fileRef = 545DB9DF21511AC100CA77B8 /* Controller.swift */; };
545DBA2B2152383100CA77B8 /* GrabberView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 545DBA2A2152383100CA77B8 /* GrabberView.swift */; };
546055BF2333C4740069F400 /* TestSupports.swift in Sources */ = {isa = PBXBuildFile; fileRef = 542753C722C49A8F00D17955 /* TestSupports.swift */; };
@@ -24,7 +23,7 @@
5469F4B024B30E1500537F8A /* LayoutAnchoring.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5469F4AF24B30E1500537F8A /* LayoutAnchoring.swift */; };
5469F4B224B30F1100537F8A /* Position.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5469F4B124B30F1100537F8A /* Position.swift */; };
5469F4B424B30F3500537F8A /* LayoutReferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5469F4B324B30F3500537F8A /* LayoutReferences.swift */; };
549C371F2361E15E007D8058 /* UtilTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 549C371E2361E15D007D8058 /* UtilTests.swift */; };
549C371F2361E15E007D8058 /* ExtensionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 549C371E2361E15D007D8058 /* ExtensionTests.swift */; };
549E944522CF295D0050AECF /* StateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 549E944422CF295D0050AECF /* StateTests.swift */; };
54A6B6B122968B530077F348 /* CoreTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54A6B6B022968B530077F348 /* CoreTests.swift */; };
54A6B6B82296A8520077F348 /* SurfaceViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54A6B6B72296A8520077F348 /* SurfaceViewTests.swift */; };
@@ -33,6 +32,7 @@
54CDC5D5215B6D8D007D205C /* BackdropView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CDC5D4215B6D8D007D205C /* BackdropView.swift */; };
54CFBFC3215CD045006B5735 /* Layout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CFBFC2215CD045006B5735 /* Layout.swift */; };
54CFBFC5215CD09C006B5735 /* Core.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CFBFC4215CD09C006B5735 /* Core.swift */; };
54DBA3DC262E938500D75969 /* Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54DBA3DB262E938500D75969 /* Extensions.swift */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@@ -56,7 +56,7 @@
542753C522C49A6E00D17955 /* LayoutTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LayoutTests.swift; sourceTree = "<group>"; };
542753C722C49A8F00D17955 /* TestSupports.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestSupports.swift; sourceTree = "<group>"; };
54352E9521A51A2500CBCA08 /* Transitioning.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Transitioning.swift; sourceTree = "<group>"; };
54352E9721A521CA00CBCA08 /* PassThroughView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PassThroughView.swift; sourceTree = "<group>"; };
54352E9721A521CA00CBCA08 /* PassthroughView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PassthroughView.swift; sourceTree = "<group>"; };
5450EEE321646DF500135936 /* Behavior.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Behavior.swift; sourceTree = "<group>"; };
545DB9C12151169500CA77B8 /* FloatingPanel.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = FloatingPanel.framework; sourceTree = BUILT_PRODUCTS_DIR; };
545DB9C42151169500CA77B8 /* FloatingPanel.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FloatingPanel.h; sourceTree = "<group>"; };
@@ -64,7 +64,6 @@
545DB9CA2151169500CA77B8 /* FloatingPanelTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = FloatingPanelTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
545DB9CF2151169500CA77B8 /* ControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ControllerTests.swift; sourceTree = "<group>"; };
545DB9D12151169500CA77B8 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
545DB9DD215118C800CA77B8 /* UIExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIExtensions.swift; sourceTree = "<group>"; };
545DB9DF21511AC100CA77B8 /* Controller.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Controller.swift; sourceTree = "<group>"; };
545DBA2A2152383100CA77B8 /* GrabberView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GrabberView.swift; sourceTree = "<group>"; };
5469F49F24B003EF00537F8A /* LaunchScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = LaunchScreen.storyboard; sourceTree = "<group>"; };
@@ -74,7 +73,7 @@
5469F4AF24B30E1500537F8A /* LayoutAnchoring.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LayoutAnchoring.swift; sourceTree = "<group>"; };
5469F4B124B30F1100537F8A /* Position.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Position.swift; sourceTree = "<group>"; };
5469F4B324B30F3500537F8A /* LayoutReferences.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LayoutReferences.swift; sourceTree = "<group>"; };
549C371E2361E15D007D8058 /* UtilTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UtilTests.swift; sourceTree = "<group>"; };
549C371E2361E15D007D8058 /* ExtensionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExtensionTests.swift; sourceTree = "<group>"; };
549E944422CF295D0050AECF /* StateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StateTests.swift; sourceTree = "<group>"; };
54A6B6B022968B530077F348 /* CoreTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreTests.swift; sourceTree = "<group>"; };
54A6B6B72296A8520077F348 /* SurfaceViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SurfaceViewTests.swift; sourceTree = "<group>"; };
@@ -83,6 +82,7 @@
54CDC5D4215B6D8D007D205C /* BackdropView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackdropView.swift; sourceTree = "<group>"; };
54CFBFC2215CD045006B5735 /* Layout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Layout.swift; sourceTree = "<group>"; };
54CFBFC4215CD09C006B5735 /* Core.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Core.swift; sourceTree = "<group>"; };
54DBA3DB262E938500D75969 /* Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Extensions.swift; sourceTree = "<group>"; };
54E740CA218AFD67005C1A34 /* FloatingPanelTesting.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = FloatingPanelTesting.app; sourceTree = BUILT_PRODUCTS_DIR; };
/* End PBXFileReference section */
@@ -142,12 +142,12 @@
5469F4B324B30F3500537F8A /* LayoutReferences.swift */,
5469F4AF24B30E1500537F8A /* LayoutAnchoring.swift */,
5450EEE321646DF500135936 /* Behavior.swift */,
54352E9721A521CA00CBCA08 /* PassThroughView.swift */,
54352E9721A521CA00CBCA08 /* PassthroughView.swift */,
54CDC5D2215B6D5A007D205C /* SurfaceView.swift */,
54CDC5D4215B6D8D007D205C /* BackdropView.swift */,
545DBA2A2152383100CA77B8 /* GrabberView.swift */,
54352E9521A51A2500CBCA08 /* Transitioning.swift */,
545DB9DD215118C800CA77B8 /* UIExtensions.swift */,
54DBA3DB262E938500D75969 /* Extensions.swift */,
54ABD7AE216CCFF7002E6C13 /* Logger.swift */,
545DB9C42151169500CA77B8 /* FloatingPanel.h */,
545DB9C52151169500CA77B8 /* Info.plist */,
@@ -164,7 +164,7 @@
542753C522C49A6E00D17955 /* LayoutTests.swift */,
54A6B6B72296A8520077F348 /* SurfaceViewTests.swift */,
549E944422CF295D0050AECF /* StateTests.swift */,
549C371E2361E15D007D8058 /* UtilTests.swift */,
549C371E2361E15D007D8058 /* ExtensionTests.swift */,
542753C722C49A8F00D17955 /* TestSupports.swift */,
545DB9D12151169500CA77B8 /* Info.plist */,
);
@@ -328,14 +328,14 @@
54CFBFC3215CD045006B5735 /* Layout.swift in Sources */,
5469F4B424B30F3500537F8A /* LayoutReferences.swift in Sources */,
54CDC5D5215B6D8D007D205C /* BackdropView.swift in Sources */,
54352E9821A521CA00CBCA08 /* PassThroughView.swift in Sources */,
54352E9821A521CA00CBCA08 /* PassthroughView.swift in Sources */,
54CFBFC5215CD09C006B5735 /* Core.swift in Sources */,
54ABD7AF216CCFF7002E6C13 /* Logger.swift in Sources */,
545DB9E021511AC100CA77B8 /* Controller.swift in Sources */,
54DBA3DC262E938500D75969 /* Extensions.swift in Sources */,
5450EEE421646DF500135936 /* Behavior.swift in Sources */,
545DBA2B2152383100CA77B8 /* GrabberView.swift in Sources */,
54352E9621A51A2500CBCA08 /* Transitioning.swift in Sources */,
545DB9DE215118C800CA77B8 /* UIExtensions.swift in Sources */,
5469F4AE24B30D7E00537F8A /* State.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
@@ -345,7 +345,7 @@
buildActionMask = 2147483647;
files = (
54A6B6B122968B530077F348 /* CoreTests.swift in Sources */,
549C371F2361E15E007D8058 /* UtilTests.swift in Sources */,
549C371F2361E15E007D8058 /* ExtensionTests.swift in Sources */,
545DB9D02151169500CA77B8 /* ControllerTests.swift in Sources */,
549E944522CF295D0050AECF /* StateTests.swift in Sources */,
542753C622C49A6E00D17955 /* LayoutTests.swift in Sources */,
+3
View File
@@ -10,6 +10,9 @@
<Group
location = "group:Examples"
name = "Examples">
<FileRef
location = "group:Maps-SwiftUI/Maps-SwiftUI.xcodeproj">
</FileRef>
<FileRef
location = "group:Maps/Maps.xcodeproj">
</FileRef>
+55 -4
View File
@@ -35,6 +35,8 @@ The new interface displays the related contents and utilities in parallel as a u
- [Support your landscape layout](#support-your-landscape-layout)
- [Use the intrinsic size of a content in your panel layout](#use-the-intrinsic-size-of-a-content-in-your-panel-layout)
- [Specify an anchor for each state by an inset of the `FloatingPanelController.view` frame](#specify-an-anchor-for-each-state-by-an-inset-of-the-floatingpanelcontrollerview-frame)
- [Change the backdrop alpha](#change-the-backdrop-alpha)
- [Using custome panel states](#using-custome-panel-states)
- [Customize the behavior with `FloatingPanelBehavior` protocol](#customize-the-behavior-with-floatingpanelbehavior-protocol)
- [Modify your floating panel's interaction](#modify-your-floating-panels-interaction)
- [Activate the rubber-band effect on panel edges](#activate-the-rubber-band-effect-on-panel-edges)
@@ -53,6 +55,7 @@ The new interface displays the related contents and utilities in parallel as a u
- [Create an additional floating panel for a detail](#create-an-additional-floating-panel-for-a-detail)
- [Move a position with an animation](#move-a-position-with-an-animation)
- [Work your contents together with a floating panel behavior](#work-your-contents-together-with-a-floating-panel-behavior)
- [Enabling the tap-to-dismiss action of the backdrop view](#enabling-the-tap-to-dismiss-action-of-the-backdrop-view)
- [Notes](#notes)
- ['Show' or 'Show Detail' Segues from `FloatingPanelController`'s content view controller](#show-or-show-detail-segues-from-floatingpanelcontrollers-content-view-controller)
- [UISearchController issue](#uisearchcontroller-issue)
@@ -71,7 +74,7 @@ The new interface displays the related contents and utilities in parallel as a u
- [x] Multi panel support
- [x] Modal presentation
- [x] 4 positioning support(top, left, bottom, right)
- [x] 1~3 magnetic anchors(full, half, tip)
- [x] 1 or more magnetic anchors(full, half, tip and more)
- [x] Layout support for all trait environments(i.e. Landscape orientation)
- [x] Common UI elements: surface, backdrop and grabber handle
- [x] Free from common issues of Auto Layout and gesture handling
@@ -163,7 +166,7 @@ self.present(fpc, animated: true, completion: nil)
You can show a floating panel over UINavigationController from the container view controllers as a modality of `.overCurrentContext` style.
:pencil2: FloatingPanelController has the custom presentation controller. If you would like to customize the presentation/dismissal, please see [FloatingPanelTransitioning](https://github.com/SCENEE/FloatingPanel/blob/master/Framework/Sources/FloatingPanelTransitioning.swift).
:pencil2: FloatingPanelController has the custom presentation controller. If you would like to customize the presentation/dismissal, please see [Transitioning](https://github.com/SCENEE/FloatingPanel/blob/master/Sources/Transitioning.swift).
## View hierarchy
@@ -370,6 +373,46 @@ class MyFullScreenLayout: FloatingPanelLayout {
:pencil2: `FloatingPanelFullScreenLayout` is deprecated on v1.
#### Change the backdrop alpha
You can change the backdrop alpha by `FloatingPanelLayout.backdropAlpha(for:)` for each state(`.full`, `.half` and `.tip`).
For instance, if a panel seems like the backdrop view isn't there on `.half` state, it's time to implement the backdropAlpha API and return a value for the state as below.
```swift
class MyPanelLayout: FloatingPanelLayout {
func backdropAlpha(for state: FloatingPanelState) -> CGFloat {
switch state {
case .full, .half: return 0.3
default: return 0.0
}
}
}
```
#### Using custome panel states
You're able to define custom panel states and use them as the following example.
```swift
extension FloatingPanelState {
static let lastQuart: FloatingPanelState = FloatingPanelState(rawValue: "lastQuart", order: 750)
static let firstQuart: FloatingPanelState = FloatingPanelState(rawValue: "firstQuart", order: 250)
}
class FloatingPanelLayoutWithCustomState: FloatingPanelBottomLayout {
override var anchors: [FloatingPanelState: FloatingPanelLayoutAnchoring] {
return [
.full: FloatingPanelLayoutAnchor(absoluteInset: 0.0, edge: .top, referenceGuide: .safeArea),
.lastQuart: FloatingPanelLayoutAnchor(fractionalInset: 0.75, edge: .bottom, referenceGuide: .safeArea),
.half: FloatingPanelLayoutAnchor(fractionalInset: 0.5, edge: .bottom, referenceGuide: .safeArea),
.firstQuart: FloatingPanelLayoutAnchor(fractionalInset: 0.25, edge: .bottom, referenceGuide: .safeArea),
.tip: FloatingPanelLayoutAnchor(absoluteInset: 20.0, edge: .bottom, referenceGuide: .safeArea),
]
}
}
```
### Customize the behavior with `FloatingPanelBehavior` protocol
#### Modify your floating panel's interaction
@@ -522,8 +565,8 @@ override func viewDidLoad() {
surfaceTapGesture.isEnabled = (fpc.position == .tip)
}
// Enable `surfaceTapGesture` only at `tip` position
func floatingPanelDidChangePosition(_ vc: FloatingPanelController) {
// Enable `surfaceTapGesture` only at `tip` state
func floatingPanelDidChangeState(_ vc: FloatingPanelController) {
surfaceTapGesture.isEnabled = (vc.position == .tip)
}
```
@@ -616,6 +659,14 @@ class ViewController: UIViewController, FloatingPanelControllerDelegate {
}
```
### Enabling the tap-to-dismiss action of the backdrop view
The tap-to-dismiss action is disabled by default. So it needs to be enabled as below.
```swift
fpc.backdropView.dismissalTapGestureRecognizer.isEnabled = true
```
## Notes
### 'Show' or 'Show Detail' Segues from `FloatingPanelController`'s content view controller
+1 -1
View File
@@ -7,5 +7,5 @@ import UIKit
public class BackdropView: UIView {
/// The gesture recognizer for tap gestures to dismiss a panel.
public var dismissalTapGestureRecognizer: UITapGestureRecognizer!
@objc public var dismissalTapGestureRecognizer: UITapGestureRecognizer!
}
+20 -19
View File
@@ -43,44 +43,41 @@ public protocol FloatingPanelBehavior {
/// This method allows a panel to activate the rubber band effect to a given edge of the surface view. By default, the effect is disabled.
@objc optional
func allowsRubberBanding(for edge: UIRectEdge) -> Bool
/// Returns the velocity threshold for the default interactive removal gesture.
///
/// In case `floatingPanel:shouldRemoveAt:with` is implemented, this value will not be used. The default value of `FloatingPanelDefaultBehavior` is 5.5
@objc optional
var removalInteractionVelocityThreshold: CGFloat { get }
}
/// The default behavior object for a panel
///
/// This behavior object is fine-tuned to behave as a search panel(card) in Apple Maps on iPhone portrait orientation.
public class FloatingPanelDefaultBehavior: FloatingPanelBehavior {
public var springDecelerationRate: CGFloat {
open class FloatingPanelDefaultBehavior: FloatingPanelBehavior {
public init() {}
open var springDecelerationRate: CGFloat {
return UIScrollView.DecelerationRate.fast.rawValue + 0.001
}
public var springResponseTime: CGFloat {
open var springResponseTime: CGFloat {
return 0.4
}
public var momentumProjectionRate: CGFloat {
open var momentumProjectionRate: CGFloat {
return UIScrollView.DecelerationRate.normal.rawValue
}
public func redirectionalProgress(_ fpc: FloatingPanelController, from: FloatingPanelState, to: FloatingPanelState) -> CGFloat {
open func redirectionalProgress(_ fpc: FloatingPanelController, from: FloatingPanelState, to: FloatingPanelState) -> CGFloat {
return 0.5
}
func addPanelAnimator(_ fpc: FloatingPanelController, to: FloatingPanelState) -> UIViewPropertyAnimator {
return UIViewPropertyAnimator(duration: 0.0,
timingParameters: UISpringTimingParameters(decelerationRate: UIScrollView.DecelerationRate.fast.rawValue,
frequencyResponse: 0.25))
}
func removePanelAnimator(_ fpc: FloatingPanelController, from: FloatingPanelState, with velocity: CGVector) -> UIViewPropertyAnimator {
return UIViewPropertyAnimator(duration: 0.0,
timingParameters: UISpringTimingParameters(decelerationRate: UIScrollView.DecelerationRate.fast.rawValue,
frequencyResponse: 0.25,
initialVelocity: velocity))
}
public func allowsRubberBanding(for edge: UIRectEdge) -> Bool {
open func allowsRubberBanding(for edge: UIRectEdge) -> Bool {
return false
}
open var removalInteractionVelocityThreshold: CGFloat = 5.5
}
class BehaviorAdapter {
@@ -103,6 +100,10 @@ class BehaviorAdapter {
var momentumProjectionRate: CGFloat {
behavior.momentumProjectionRate ?? FloatingPanelDefaultBehavior().momentumProjectionRate
}
var removalInteractionVelocityThreshold: CGFloat {
behavior.removalInteractionVelocityThreshold ?? FloatingPanelDefaultBehavior().removalInteractionVelocityThreshold
}
func redirectionalProgress(from: FloatingPanelState, to: FloatingPanelState) -> CGFloat {
behavior.redirectionalProgress?(vc, from: from, to: to) ?? FloatingPanelDefaultBehavior().redirectionalProgress(vc,from: from, to: to)
+49 -27
View File
@@ -5,7 +5,7 @@ import UIKit
/// A set of methods implemented by the delegate of a panel controller allows the adopting delegate to respond to
/// messages from the FloatingPanelController class and thus respond to, and in some affect, operations such as
/// dragging, attracting a panel, layout of a panel and the content, and transition animations.
@objc public protocol FloatingPanelControllerDelegate: class {
@objc public protocol FloatingPanelControllerDelegate {
/// Returns a FloatingPanelLayout object. If you use the default one, you can use a `FloatingPanelBottomLayout` object.
@objc(floatingPanel:layoutForTraitCollection:) optional
func floatingPanel(_ fpc: FloatingPanelController, layoutFor newCollection: UITraitCollection) -> FloatingPanelLayout
@@ -26,10 +26,12 @@ import UIKit
@objc(floatingPanel:animatorForDismissingWithVelocity:) optional
func floatingPanel(_ fpc: FloatingPanelController, animatorForDismissingWith velocity: CGVector) -> UIViewPropertyAnimator
/// Called when a panel has changed to a new position. Can be called inside an animation block, so any
/// view properties set inside this function will be automatically animated alongside a panel.
/// Called when a panel has changed to a new state.
///
/// This can be called inside an animation block for presenting, dismissing a panel or moving a panel with your
/// animation. So any view properties set inside this function will be automatically animated alongside a panel.
@objc optional
func floatingPanelDidChangePosition(_ fpc: FloatingPanelController)
func floatingPanelDidChangeState(_ fpc: FloatingPanelController)
/// Asks the delegate if dragging should begin by the pan gesture recognizer.
@objc optional
@@ -274,7 +276,7 @@ open class FloatingPanelController: UIViewController {
open override func loadView() {
assert(self.storyboard == nil, "Storyboard isn't supported")
let view = PassThroughView()
let view = PassthroughView()
view.backgroundColor = .clear
backdropView.frame = view.bounds
@@ -396,20 +398,8 @@ open class FloatingPanelController: UIViewController {
}
private func activateLayout(forceLayout: Bool = false) {
floatingPanel.layoutAdapter.prepareLayout()
// preserve the current content offset if contentInsetAdjustmentBehavior is `.always`
var contentOffset: CGPoint?
if contentInsetAdjustmentBehavior == .always {
contentOffset = trackingScrollView?.contentOffset
}
floatingPanel.layoutAdapter.updateStaticConstraint()
floatingPanel.layoutAdapter.activateLayout(for: floatingPanel.state, forceLayout: forceLayout)
if let contentOffset = contentOffset {
trackingScrollView?.contentOffset = contentOffset
}
floatingPanel.activateLayout(forceLayout: forceLayout,
contentInsetAdjustmentBehavior: contentInsetAdjustmentBehavior)
}
func remove() {
@@ -427,6 +417,9 @@ open class FloatingPanelController: UIViewController {
// MARK: - Container view controller interface
/// Shows the surface view at the initial position defined by the current layout
/// - Parameters:
/// - animated: Pass true to animate the presentation; otherwise, pass false.
/// - completion: The block to execute after the presentation finishes. This block has no return value and takes no parameters. You may specify nil for this parameter.
@objc(show:completion:)
public func show(animated: Bool = false, completion: (() -> Void)? = nil) {
// Must apply the current layout here
@@ -468,8 +461,9 @@ open class FloatingPanelController: UIViewController {
/// - parent: A parent view controller object that displays FloatingPanelController's view. A container view controller object isn't applicable.
/// - viewIndex: Insert the surface view managed by the controller below the specified view index. By default, the surface view will be added to the end of the parent list of subviews.
/// - animated: Pass true to animate the presentation; otherwise, pass false.
@objc(addPanelToParent:at:animated:)
public func addPanel(toParent parent: UIViewController, at viewIndex: Int = -1, animated: Bool = false) {
/// - completion: The block to execute after the presentation finishes. This block has no return value and takes no parameters. You may specify nil for this parameter.
@objc(addPanelToParent:at:animated:completion:)
public func addPanel(toParent parent: UIViewController, at viewIndex: Int = -1, animated: Bool = false, completion: (() -> Void)? = nil) {
guard self.parent == nil else {
log.warning("Already added to a parent(\(parent))")
return
@@ -498,8 +492,9 @@ open class FloatingPanelController: UIViewController {
])
show(animated: animated) { [weak self] in
guard let `self` = self else { return }
guard let self = self else { return }
self.didMove(toParent: parent)
completion?()
}
}
@@ -517,7 +512,7 @@ open class FloatingPanelController: UIViewController {
delegate?.floatingPanelWillRemove?(self)
hide(animated: animated) { [weak self] in
guard let `self` = self else { return }
guard let self = self else { return }
self.willMove(toParent: nil)
@@ -531,13 +526,13 @@ open class FloatingPanelController: UIViewController {
}
/// Moves the position to the specified position.
///
/// - Parameters:
/// - to: Pass a FloatingPanelPosition value to move the surface view to the position.
/// - animated: Pass true to animate the presentation; otherwise, pass false.
/// - completion: The block to execute after the view controller has finished moving. This block has no return value and takes no parameters. You may specify nil for this parameter.
@objc(moveToState:animated:completion:)
public func move(to: FloatingPanelState, animated: Bool, completion: (() -> Void)? = nil) {
assert(floatingPanel.layoutAdapter.vc != nil, "Use show(animated:completion)")
floatingPanel.move(to: to, animated: animated, completion: completion)
}
@@ -598,6 +593,14 @@ open class FloatingPanelController: UIViewController {
}
}
// MARK: - Accessibility
open override func accessibilityPerformEscape() -> Bool {
guard isRemovalInteractionEnabled else { return false }
dismiss(animated: true, completion: nil)
return true
}
// MARK: - Utilities
/// Updates the layout object from the delegate and lays out the views managed
@@ -641,10 +644,31 @@ extension FloatingPanelController {
#endif
delegate?.floatingPanelDidMove?(self)
}
func animatorForPresenting(to: FloatingPanelState) -> UIViewPropertyAnimator {
if let animator = delegate?.floatingPanel?(self, animatorForPresentingTo: to) {
return animator
}
let timingParameters = UISpringTimingParameters(decelerationRate: UIScrollView.DecelerationRate.fast.rawValue,
frequencyResponse: 0.25)
return UIViewPropertyAnimator(duration: 0.0,
timingParameters: timingParameters)
}
func animatorForDismissing(with velocity: CGVector) -> UIViewPropertyAnimator {
if let animator = delegate?.floatingPanel?(self, animatorForDismissingWith: velocity) {
return animator
}
let timingParameters = UISpringTimingParameters(decelerationRate: UIScrollView.DecelerationRate.fast.rawValue,
frequencyResponse: 0.25,
initialVelocity: velocity)
return UIViewPropertyAnimator(duration: 0.0,
timingParameters: timingParameters)
}
}
extension FloatingPanelController {
private static let dismissSwizzling: Any? = {
private static let dismissSwizzling: Void = {
let aClass: AnyClass! = UIViewController.self //object_getClass(vc)
if let imp = class_getMethodImplementation(aClass, #selector(dismiss(animated:completion:))),
let originalAltMethod = class_getInstanceMethod(aClass, #selector(fp_original_dismiss(animated:completion:))) {
@@ -653,10 +677,8 @@ extension FloatingPanelController {
let originalMethod = class_getInstanceMethod(aClass, #selector(dismiss(animated:completion:)))
let swizzledMethod = class_getInstanceMethod(aClass, #selector(fp_dismiss(animated:completion:)))
if let originalMethod = originalMethod, let swizzledMethod = swizzledMethod {
// switch implementation..
method_exchangeImplementations(originalMethod, swizzledMethod)
}
return nil
}()
}
+121 -61
View File
@@ -24,7 +24,7 @@ class Core: NSObject, UIGestureRecognizerDelegate {
didSet {
log.debug("state changed: \(oldValue) -> \(state)")
if let vc = ownerVC {
vc.delegate?.floatingPanelDidChangePosition?(vc)
vc.delegate?.floatingPanelDidChangeState?(vc)
}
}
}
@@ -32,7 +32,8 @@ class Core: NSObject, UIGestureRecognizerDelegate {
let panGestureRecognizer: FloatingPanelPanGestureRecognizer
var isRemovalInteractionEnabled: Bool = false
fileprivate var animator: UIViewPropertyAnimator?
fileprivate var isSuspended: Bool = false // Prevent a memory leak in the modal transition
fileprivate var transitionAnimator: UIViewPropertyAnimator?
fileprivate var moveAnimator: NumericSpringAnimator?
private var initialSurfaceLocation: CGPoint = .zero
@@ -68,11 +69,8 @@ class Core: NSObject, UIGestureRecognizerDelegate {
backdropView.backgroundColor = .black
backdropView.alpha = 0.0
self.layoutAdapter = LayoutAdapter(vc: vc,
surfaceView: surfaceView,
backdropView: backdropView,
layout: layout)
self.behaviorAdapter = BehaviorAdapter(vc: vc, behavior: behavior)
layoutAdapter = LayoutAdapter(vc: vc, layout: layout)
behaviorAdapter = BehaviorAdapter(vc: vc, behavior: behavior)
panGestureRecognizer = FloatingPanelPanGestureRecognizer()
@@ -94,6 +92,11 @@ class Core: NSObject, UIGestureRecognizerDelegate {
backdropView.addGestureRecognizer(tapGesture)
}
deinit {
// Release `NumericSpringAnimator.displayLink` from the run loop.
self.moveAnimator?.stopAnimation(false)
}
func move(to: FloatingPanelState, animated: Bool, completion: (() -> Void)? = nil) {
move(from: state, to: to, animated: animated, completion: completion)
}
@@ -104,7 +107,7 @@ class Core: NSObject, UIGestureRecognizerDelegate {
completion?()
return
}
if state != layoutAdapter.edgeMostState {
if state != layoutAdapter.mostExpandedState {
lockScrollView()
}
tearDownActiveInteraction()
@@ -112,8 +115,9 @@ class Core: NSObject, UIGestureRecognizerDelegate {
interruptAnimationIfNeeded()
if animated {
func updateScrollView() {
if self.state == self.layoutAdapter.edgeMostState, abs(self.layoutAdapter.offsetFromEdgeMost) <= 1.0 {
let updateScrollView: () -> Void = { [weak self] in
guard let self = self else { return }
if self.state == self.layoutAdapter.mostExpandedState, abs(self.layoutAdapter.offsetFromMostExpandedAnchor) <= 1.0 {
self.unlockScrollView()
} else {
self.lockScrollView()
@@ -123,14 +127,15 @@ class Core: NSObject, UIGestureRecognizerDelegate {
let animator: UIViewPropertyAnimator
switch (from, to) {
case (.hidden, let to):
animator = vc.delegate?.floatingPanel?(vc, animatorForPresentingTo: to)
?? FloatingPanelDefaultBehavior().addPanelAnimator(vc, to: to)
case (let from, .hidden):
animator = vc.animatorForPresenting(to: to)
case (_, .hidden):
let animationVector = CGVector(dx: abs(removalVector.dx), dy: abs(removalVector.dy))
animator = vc.delegate?.floatingPanel?(vc, animatorForDismissingWith: .zero)
?? FloatingPanelDefaultBehavior().removePanelAnimator(vc, from: from, with: animationVector)
animator = vc.animatorForDismissing(with: animationVector)
default:
move(to: to, with: 0) {
move(to: to, with: 0) { [weak self] in
guard let self = self else { return }
self.moveAnimator = nil
updateScrollView()
completion?()
}
@@ -142,7 +147,7 @@ class Core: NSObject, UIGestureRecognizerDelegate {
&& layoutAdapter.isIntrinsicAnchor(state: to)
animator.addAnimations { [weak self] in
guard let `self` = self else { return }
guard let self = self else { return }
self.state = to
self.updateLayout(to: to)
@@ -153,18 +158,22 @@ class Core: NSObject, UIGestureRecognizerDelegate {
}
}
animator.addCompletion { [weak self] _ in
guard let `self` = self else { return }
self.animator = nil
guard let self = self else { return }
self.transitionAnimator = nil
updateScrollView()
self.ownerVC?.notifyDidMove()
completion?()
}
self.animator = animator
self.transitionAnimator = animator
if isSuspended {
return
}
animator.startAnimation()
} else {
self.state = to
self.updateLayout(to: to)
if self.state == self.layoutAdapter.edgeMostState {
if self.state == self.layoutAdapter.mostExpandedState {
self.unlockScrollView()
} else {
self.lockScrollView()
@@ -177,8 +186,37 @@ class Core: NSObject, UIGestureRecognizerDelegate {
// MARK: - Layout update
func activateLayout(forceLayout: Bool = false,
contentInsetAdjustmentBehavior: FloatingPanelController.ContentInsetAdjustmentBehavior) {
layoutAdapter.prepareLayout()
// preserve the current content offset if contentInsetAdjustmentBehavior is `.always`
var contentOffset: CGPoint?
if contentInsetAdjustmentBehavior == .always {
contentOffset = scrollView?.contentOffset
}
layoutAdapter.updateStaticConstraint()
layoutAdapter.activateLayout(for: state, forceLayout: true)
// Update the backdrop alpha only when called in `Controller.show(animated:completion:)`
// Because that prevents a backdrop flicking just before presenting a panel(#466).
if forceLayout {
backdropView.alpha = getBackdropAlpha(for: state)
}
if let contentOffset = contentOffset {
scrollView?.contentOffset = contentOffset
}
}
private func updateLayout(to target: FloatingPanelState) {
self.layoutAdapter.activateLayout(for: state, forceLayout: true)
self.layoutAdapter.activateLayout(for: target, forceLayout: true)
self.backdropView.alpha = self.getBackdropAlpha(for: target)
}
private func getBackdropAlpha(for target: FloatingPanelState) -> CGFloat {
return target == .hidden ? 0.0 : layoutAdapter.backdropAlpha(for: target)
}
func getBackdropAlpha(at cur: CGFloat, with translation: CGFloat) -> CGFloat {
@@ -187,8 +225,8 @@ class Core: NSObject, UIGestureRecognizerDelegate {
let segment = layoutAdapter.segment(at: cur, forward: forwardY)
let lowerState = segment.lower ?? layoutAdapter.edgeMostState
let upperState = segment.upper ?? layoutAdapter.edgeLeastState
let lowerState = segment.lower ?? layoutAdapter.mostExpandedState
let upperState = segment.upper ?? layoutAdapter.leastExpandedState
let preState = forwardY ? lowerState : upperState
let nextState = forwardY ? upperState : lowerState
@@ -201,9 +239,8 @@ class Core: NSObject, UIGestureRecognizerDelegate {
if pre == next {
return preAlpha
} else {
return preAlpha + max(min(1.0, 1.0 - (next - cur) / (next - pre) ), 0.0) * (nextAlpha - preAlpha)
}
return preAlpha + max(min(1.0, 1.0 - (next - cur) / (next - pre) ), 0.0) * (nextAlpha - preAlpha)
}
// MARK: - UIGestureRecognizerDelegate
@@ -340,7 +377,7 @@ class Core: NSObject, UIGestureRecognizerDelegate {
let velocity = value(of: panGesture.velocity(in: panGesture.view))
let location = panGesture.location(in: surfaceView)
let belowEdgeMost = 0 > layoutAdapter.offsetFromEdgeMost + (1.0 / surfaceView.fp_displayScale)
let belowEdgeMost = 0 > layoutAdapter.offsetFromMostExpandedAnchor + (1.0 / surfaceView.fp_displayScale)
log.debug("""
scroll gesture(\(state):\(panGesture.state)) -- \
@@ -354,7 +391,7 @@ class Core: NSObject, UIGestureRecognizerDelegate {
if belowEdgeMost {
// Scroll offset pinning
if state == layoutAdapter.edgeMostState {
if state == layoutAdapter.mostExpandedState {
if interactionInProgress {
log.debug("settle offset --", value(of: initialScrollOffset))
stopScrolling(at: initialScrollOffset)
@@ -372,7 +409,7 @@ class Core: NSObject, UIGestureRecognizerDelegate {
if interactionInProgress {
lockScrollView()
} else {
if state == layoutAdapter.edgeMostState, self.animator == nil {
if state == layoutAdapter.mostExpandedState, self.transitionAnimator == nil {
switch layoutAdapter.position {
case .top, .left:
if offsetDiff < 0 && velocity > 0 {
@@ -400,14 +437,14 @@ class Core: NSObject, UIGestureRecognizerDelegate {
return
}
}
if state == layoutAdapter.edgeMostState {
if state == layoutAdapter.mostExpandedState {
// Adjust a small gap of the scroll offset just after swiping down starts in the grabber area.
if grabberAreaFrame.contains(location), grabberAreaFrame.contains(initialLocation) {
stopScrolling(at: initialScrollOffset)
}
}
} else {
if state == layoutAdapter.edgeMostState {
if state == layoutAdapter.mostExpandedState {
switch layoutAdapter.position {
case .top, .left:
if velocity < 0, !allowScrollPanGesture(for: scrollView) {
@@ -492,16 +529,16 @@ class Core: NSObject, UIGestureRecognizerDelegate {
animator.stopAnimation(true)
endAttraction(false)
}
if let animator = self.animator {
guard 0 >= layoutAdapter.offsetFromEdgeMost else { return }
if let animator = self.transitionAnimator {
guard 0 >= layoutAdapter.offsetFromMostExpandedAnchor else { return }
log.debug("a panel animation(interruptible: \(animator.isInterruptible)) interrupted!!!")
if animator.isInterruptible {
animator.stopAnimation(false)
// A user can stop a panel at the nearest Y of a target position so this fine-tunes
// the a small gap between the presentation layer frame and model layer frame
// to unlock scroll view properly at finishAnimation(at:)
if abs(layoutAdapter.offsetFromEdgeMost) <= 1.0 {
layoutAdapter.surfaceLocation = layoutAdapter.surfaceLocation(for: layoutAdapter.edgeMostState)
if abs(layoutAdapter.offsetFromMostExpandedAnchor) <= 1.0 {
layoutAdapter.surfaceLocation = layoutAdapter.surfaceLocation(for: layoutAdapter.mostExpandedState)
}
animator.finishAnimation(at: .current)
} else {
@@ -527,22 +564,27 @@ class Core: NSObject, UIGestureRecognizerDelegate {
}
guard
state == layoutAdapter.edgeMostState, // When not top most(i.e. .full), don't scroll.
state == layoutAdapter.mostExpandedState, // When not top most(i.e. .full), don't scroll.
interactionInProgress == false, // When interaction already in progress, don't scroll.
0 == layoutAdapter.offsetFromEdgeMost
0 == layoutAdapter.offsetFromMostExpandedAnchor
else {
return false
}
// When the current and initial point within grabber area, do scroll.
// When the current point is within grabber area but the initial point is not, do scroll.
if grabberAreaFrame.contains(point), !grabberAreaFrame.contains(initialLocation) {
return true
}
// When the initial point is within grabber area and the current point is out of surface, don't scroll.
if grabberAreaFrame.contains(initialLocation), !surfaceView.frame.contains(point) {
return false
}
let scrollViewFrame = scrollView.convert(scrollView.bounds, to: surfaceView)
guard
scrollViewFrame.contains(initialLocation), // When initialLocation not in scrollView, don't scroll.
!grabberAreaFrame.contains(point) // When point within grabber area, don't scroll.
scrollViewFrame.contains(initialLocation), // When the initial point not in scrollView, don't scroll.
!grabberAreaFrame.contains(point) // When point within grabber area, don't scroll.
else {
return false
}
@@ -570,6 +612,9 @@ class Core: NSObject, UIGestureRecognizerDelegate {
if scrollView.isDecelerating {
return true
}
if let tableView = (scrollView as? UITableView), tableView.isEditing {
return true
}
return false
}
@@ -581,7 +626,7 @@ class Core: NSObject, UIGestureRecognizerDelegate {
log.debug("panningBegan -- location = \(value(of: location))")
guard let scrollView = scrollView else { return }
if state == layoutAdapter.edgeMostState {
if state == layoutAdapter.mostExpandedState {
if grabberAreaFrame.contains(location) {
initialScrollOffset = scrollView.contentOffset
}
@@ -648,10 +693,11 @@ class Core: NSObject, UIGestureRecognizerDelegate {
return
}
stopScrollDeceleration = (0 > layoutAdapter.offsetFromEdgeMost + (1.0 / surfaceView.fp_displayScale)) // Projecting the dragging to the scroll dragging or not
stopScrollDeceleration = (0 > layoutAdapter.offsetFromMostExpandedAnchor + (1.0 / surfaceView.fp_displayScale)) // Projecting the dragging to the scroll dragging or not
if stopScrollDeceleration {
DispatchQueue.main.async { [weak self] in
guard let `self` = self else { return }
guard let self = self else { return }
self.stopScrolling(at: self.initialScrollOffset)
}
}
@@ -697,14 +743,14 @@ class Core: NSObject, UIGestureRecognizerDelegate {
// Workaround: Disable a tracking scroll to prevent bouncing a scroll content in a panel animating
let isScrollEnabled = scrollView?.isScrollEnabled
if let scrollView = scrollView, targetPosition != .full {
if let scrollView = scrollView, targetPosition != layoutAdapter.mostExpandedState {
scrollView.isScrollEnabled = false
}
startAttraction(to: targetPosition, with: velocity)
// Workaround: Reset `self.scrollView.isScrollEnabled`
if let scrollView = scrollView, targetPosition != .full,
if let scrollView = scrollView, targetPosition != layoutAdapter.mostExpandedState,
let isScrollEnabled = isScrollEnabled {
scrollView.isScrollEnabled = isScrollEnabled
}
@@ -717,15 +763,16 @@ class Core: NSObject, UIGestureRecognizerDelegate {
if let result = vc.delegate?.floatingPanel?(vc, shouldRemoveAt: vc.surfaceLocation, with: velocityVector) {
return result
}
let threshold = behaviorAdapter.removalInteractionVelocityThreshold
switch layoutAdapter.position {
case .top:
return (velocityVector.dy <= -10.0)
return (velocityVector.dy <= -threshold)
case .left:
return (velocityVector.dx <= -10.0)
return (velocityVector.dx <= -threshold)
case .bottom:
return (velocityVector.dy >= 10.0)
return (velocityVector.dy >= threshold)
case .right:
return (velocityVector.dx >= 10.0)
return (velocityVector.dx >= threshold)
}
}
@@ -737,7 +784,7 @@ class Core: NSObject, UIGestureRecognizerDelegate {
var offset: CGPoint = .zero
initialSurfaceLocation = layoutAdapter.surfaceLocation
if state == layoutAdapter.edgeMostState, let scrollView = scrollView {
if state == layoutAdapter.mostExpandedState, let scrollView = scrollView {
if grabberAreaFrame.contains(location) {
initialScrollOffset = scrollView.contentOffset
} else {
@@ -782,7 +829,7 @@ class Core: NSObject, UIGestureRecognizerDelegate {
interactionInProgress = false
// Prevent to keep a scroll view indicator visible at the half/tip position
if targetPosition != layoutAdapter.edgeMostState {
if targetPosition != layoutAdapter.mostExpandedState {
lockScrollView()
}
@@ -790,6 +837,7 @@ class Core: NSObject, UIGestureRecognizerDelegate {
}
private func tearDownActiveInteraction() {
guard panGestureRecognizer.isEnabled else { return }
// Cancel the pan gesture so that panningEnd(with:velocity:) is called
panGestureRecognizer.isEnabled = false
panGestureRecognizer.isEnabled = true
@@ -823,16 +871,19 @@ class Core: NSObject, UIGestureRecognizerDelegate {
decelerationRate: behaviorAdapter.springDecelerationRate,
responseTime: behaviorAdapter.springResponseTime,
update: { [weak self] data in
guard let self = self else { return }
guard let self = self,
let ownerVC = self.ownerVC // Ensure the owner vc is existing for `layoutAdapter.surfaceLocation`
else { return }
animationConstraint.constant = data.value
let current = self.value(of: self.layoutAdapter.surfaceLocation)
let translation = data.value - initialData.value
self.backdropView.alpha = self.getBackdropAlpha(at: current, with: translation)
self.ownerVC?.notifyDidMove()
ownerVC.notifyDidMove()
},
completion: { [weak self] in
guard let self = self else { return }
self.layoutAdapter.activateLayout(for: targetPosition, forceLayout: true)
guard let self = self,
self.ownerVC != nil else { return }
self.updateLayout(to: targetPosition)
completion()
})
moveAnimator?.startAnimation()
@@ -856,9 +907,9 @@ class Core: NSObject, UIGestureRecognizerDelegate {
log.debug("""
finishAnimation -- state = \(state) \
surface location = \(layoutAdapter.surfaceLocation) \
edge most position = \(layoutAdapter.surfaceLocation(for: layoutAdapter.edgeMostState))
edge most position = \(layoutAdapter.surfaceLocation(for: layoutAdapter.mostExpandedState))
""")
if finished, state == layoutAdapter.edgeMostState, abs(layoutAdapter.offsetFromEdgeMost) <= 1.0 {
if finished, state == layoutAdapter.mostExpandedState, abs(layoutAdapter.offsetFromMostExpandedAnchor) <= 1.0 {
unlockScrollView()
}
}
@@ -885,7 +936,7 @@ class Core: NSObject, UIGestureRecognizerDelegate {
func targetPosition(from currentY: CGFloat, with velocity: CGFloat) -> (FloatingPanelState) {
log.debug("targetPosition -- currentY = \(currentY), velocity = \(velocity)")
let sortedPositions = layoutAdapter.sortedDirectionalStates
let sortedPositions = layoutAdapter.sortedAnchorStatesByCoordinate
guard sortedPositions.count > 1 else {
return state
@@ -893,7 +944,7 @@ class Core: NSObject, UIGestureRecognizerDelegate {
// Projection
let decelerationRate = behaviorAdapter.momentumProjectionRate
let baseY = abs(layoutAdapter.position(for: layoutAdapter.edgeLeastState) - layoutAdapter.position(for: layoutAdapter.edgeMostState))
let baseY = abs(layoutAdapter.position(for: layoutAdapter.leastExpandedState) - layoutAdapter.position(for: layoutAdapter.mostExpandedState))
let vecY = velocity / baseY
var pY = project(initialVelocity: vecY, decelerationRate: decelerationRate) * baseY + currentY
@@ -989,7 +1040,7 @@ class Core: NSObject, UIGestureRecognizerDelegate {
}
private func allowScrollPanGesture(for scrollView: UIScrollView) -> Bool {
guard state == layoutAdapter.edgeMostState else { return false }
guard state == layoutAdapter.mostExpandedState else { return false }
var offsetY: CGFloat = 0
switch layoutAdapter.position {
case .top, .left:
@@ -1021,7 +1072,7 @@ public final class FloatingPanelPanGestureRecognizer: UIPanGestureRecognizer {
public override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesBegan(touches, with: event)
initialLocation = touches.first?.location(in: view) ?? .zero
if floatingPanel?.animator != nil || floatingPanel?.moveAnimator != nil {
if floatingPanel?.transitionAnimator != nil || floatingPanel?.moveAnimator != nil {
self.state = .began
}
}
@@ -1182,3 +1233,12 @@ private class NumericSpringAnimator: NSObject {
v = (v + h * o2 * (xt - x)) / det
}
}
extension FloatingPanelController {
func suspendTransitionAnimator(_ suspended: Bool) {
self.floatingPanel.isSuspended = suspended
}
var transitionAnimator: UIViewPropertyAnimator? {
return self.floatingPanel.transitionAnimator
}
}
@@ -2,16 +2,38 @@
import UIKit
internal func displayTrunc(_ v: CGFloat, by s: CGFloat) -> CGFloat {
let base = (1 / s)
let t = v.rounded(.down)
return t + ((v - t) / base).rounded(.toNearestOrAwayFromZero) * base
// MARK: - CoreGraphics
extension CGFloat {
/// Returns this value rounded to an logical pixel value by a display scale
func rounded(by displayScale: CGFloat) -> CGFloat {
return (self * displayScale).rounded(.toNearestOrAwayFromZero) / displayScale
}
func isEqual(to: CGFloat, on displayScale: CGFloat) -> Bool {
return self.rounded(by: displayScale) == to.rounded(by: displayScale)
}
}
internal func displayEqual(_ lhs: CGFloat, _ rhs: CGFloat, by displayScale: CGFloat) -> Bool {
return displayTrunc(lhs, by: displayScale) == displayTrunc(rhs, by: displayScale)
extension CGPoint {
static var leastNonzeroMagnitude: CGPoint {
return CGPoint(x: CGFloat.leastNonzeroMagnitude, y: CGFloat.leastNonzeroMagnitude)
}
static func + (lhs: CGPoint, rhs: CGPoint) -> CGPoint {
return CGPoint(x: lhs.x + rhs.x, y: lhs.y + rhs.y)
}
static func - (lhs: CGPoint, rhs: CGPoint) -> CGPoint {
return CGPoint(x: lhs.x - rhs.x, y: lhs.y - rhs.y)
}
static prefix func - (point: CGPoint) -> CGPoint {
return CGPoint(x: -point.x, y: -point.y)
}
}
// MARK: - UIKit
protocol LayoutGuideProvider {
var topAnchor: NSLayoutYAxisAnchor { get }
var leftAnchor: NSLayoutXAxisAnchor { get }
@@ -164,24 +186,6 @@ extension UISpringTimingParameters {
}
}
extension CGPoint {
static var leastNonzeroMagnitude: CGPoint {
return CGPoint(x: CGFloat.leastNonzeroMagnitude, y: CGFloat.leastNonzeroMagnitude)
}
static func + (lhs: CGPoint, rhs: CGPoint) -> CGPoint {
return CGPoint(x: lhs.x + rhs.x, y: lhs.y + rhs.y)
}
static func - (lhs: CGPoint, rhs: CGPoint) -> CGPoint {
return CGPoint(x: lhs.x - rhs.x, y: lhs.y - rhs.y)
}
static prefix func - (point: CGPoint) -> CGPoint {
return CGPoint(x: -point.x, y: -point.y)
}
}
extension NSLayoutConstraint {
static func activate(constraint: NSLayoutConstraint?) {
guard let constraint = constraint else { return }
@@ -201,3 +205,70 @@ extension UIEdgeInsets {
return self.top + self.bottom
}
}
extension UIBezierPath {
static func path(roundedRect rect: CGRect, appearance: SurfaceAppearance) -> UIBezierPath {
let cornerRadius = appearance.cornerRadius;
if #available(iOS 13.0, *) {
if appearance.cornerCurve == .circular {
let path = UIBezierPath()
let start = CGPoint(x: rect.minX + cornerRadius, y: rect.minY)
path.move(to: start)
path.addLine(to: CGPoint(x: rect.maxX - cornerRadius, y: rect.minY))
if cornerRadius > 0 {
path .addArc(withCenter: CGPoint(x: rect.maxX - cornerRadius,
y: rect.minY + cornerRadius),
radius: cornerRadius,
startAngle: -0.5 * .pi,
endAngle: 0,
clockwise: true)
}
path.addLine(to: CGPoint(x: rect.maxX, y: rect.maxY - cornerRadius))
if cornerRadius > 0 {
path.addArc(withCenter: CGPoint(x: rect.maxX - cornerRadius,
y: rect.maxY - cornerRadius),
radius: cornerRadius,
startAngle: 0,
endAngle: .pi * 0.5,
clockwise: true)
}
path.addLine(to: CGPoint(x: rect.minX + cornerRadius, y: rect.maxY))
if cornerRadius > 0 {
path.addArc(withCenter: CGPoint(x: rect.minX + cornerRadius,
y: rect.maxY - cornerRadius),
radius: cornerRadius,
startAngle: .pi * 0.5,
endAngle: .pi,
clockwise: true)
}
path.addLine(to: CGPoint(x: rect.minX, y: rect.minY + cornerRadius))
if cornerRadius > 0 {
path.addArc(withCenter: CGPoint(x: rect.minX + cornerRadius,
y: rect.minY + cornerRadius),
radius: cornerRadius,
startAngle: .pi,
endAngle: .pi * 1.5,
clockwise: true)
}
path.addLine(to: start)
path.close()
return path
}
}
return UIBezierPath(roundedRect: rect,
byRoundingCorners: [.allCorners],
cornerRadii: CGSize(width: cornerRadius,
height: cornerRadius))
}
}
+1 -1
View File
@@ -15,7 +15,7 @@
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>2.0.0</string>
<string>2.5.0</string>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
</dict>
+95 -96
View File
@@ -60,9 +60,7 @@ struct LayoutSegment {
}
class LayoutAdapter {
weak var vc: FloatingPanelController!
private weak var surfaceView: SurfaceView!
private weak var backdropView: BackdropView!
private unowned var vc: FloatingPanelController
private let defaultLayout = FloatingPanelBottomLayout()
fileprivate var layout: FloatingPanelLayout {
@@ -71,16 +69,21 @@ class LayoutAdapter {
}
}
private var surfaceView: SurfaceView {
return vc.surfaceView
}
private var backdropView: BackdropView {
return vc.backdropView
}
private var safeAreaInsets: UIEdgeInsets {
return vc?.fp_safeAreaInsets ?? .zero
return vc.fp_safeAreaInsets
}
private var initialConst: CGFloat = 0.0
private var fixedConstraints: [NSLayoutConstraint] = []
private var fullConstraints: [NSLayoutConstraint] = []
private var halfConstraints: [NSLayoutConstraint] = []
private var tipConstraints: [NSLayoutConstraint] = []
private var stateConstraints: [FloatingPanelState: [NSLayoutConstraint]] = [:]
private var offConstraints: [NSLayoutConstraint] = []
private var fitToBoundsConstraint: NSLayoutConstraint?
@@ -89,10 +92,16 @@ class LayoutAdapter {
private var staticConstraint: NSLayoutConstraint?
private var activeStates: Set<FloatingPanelState> {
private var anchorStates: Set<FloatingPanelState> {
return Set(layout.anchors.keys)
}
private var sortedAnchorStates: [FloatingPanelState] {
return anchorStates.sorted(by: {
return $0.order < $1.order
})
}
var initialState: FloatingPanelState {
layout.initialState
}
@@ -101,18 +110,12 @@ class LayoutAdapter {
layout.position
}
var orderedStates: [FloatingPanelState] {
return activeStates.sorted(by: {
return $0.order < $1.order
})
}
var validStates: Set<FloatingPanelState> {
return activeStates.union([.hidden])
return anchorStates.union([.hidden])
}
var sortedDirectionalStates: [FloatingPanelState] {
return activeStates.sorted(by: {
var sortedAnchorStatesByCoordinate: [FloatingPanelState] {
return anchorStates.sorted(by: {
switch position {
case .top, .left:
return $0.order < $1.order
@@ -122,30 +125,26 @@ class LayoutAdapter {
})
}
private var directionalLeastState: FloatingPanelState {
return sortedDirectionalStates.first ?? .hidden
private var leastCoordinateState: FloatingPanelState {
return sortedAnchorStatesByCoordinate.first ?? .hidden
}
private var directionalMostState: FloatingPanelState {
return sortedDirectionalStates.last ?? .hidden
private var mostCoordinateState: FloatingPanelState {
return sortedAnchorStatesByCoordinate.last ?? .hidden
}
var edgeLeastState: FloatingPanelState {
if orderedStates.count == 1 {
var leastExpandedState: FloatingPanelState {
if sortedAnchorStates.count == 1 {
return .hidden
}
return orderedStates.first ?? .hidden
return sortedAnchorStates.first ?? .hidden
}
var edgeMostState: FloatingPanelState {
if orderedStates.count == 1 {
return orderedStates[0]
var mostExpandedState: FloatingPanelState {
if sortedAnchorStates.count == 1 {
return sortedAnchorStates[0]
}
return orderedStates.last ?? .hidden
}
var edgeMostY: CGFloat {
return position(for: edgeMostState)
return sortedAnchorStates.last ?? .hidden
}
var adjustedContentInsets: UIEdgeInsets {
@@ -220,7 +219,7 @@ class LayoutAdapter {
}
}
} else {
pos = displayTrunc(edgePosition(surfaceView.frame), by: surfaceView.fp_displayScale)
pos = edgePosition(surfaceView.frame).rounded(by: surfaceView.fp_displayScale)
}
switch position {
case .top, .bottom:
@@ -262,12 +261,12 @@ class LayoutAdapter {
}
}
var offsetFromEdgeMost: CGFloat {
var offsetFromMostExpandedAnchor: CGFloat {
switch position {
case .top, .left:
return edgePosition(surfaceView.presentationFrame) - position(for: directionalMostState)
return edgePosition(surfaceView.presentationFrame) - position(for: mostExpandedState)
case .bottom, .right:
return position(for: directionalLeastState) - edgePosition(surfaceView.presentationFrame)
return position(for: mostExpandedState) - edgePosition(surfaceView.presentationFrame)
}
}
@@ -284,18 +283,13 @@ class LayoutAdapter {
}
}
init(vc: FloatingPanelController,
surfaceView: SurfaceView,
backdropView: BackdropView,
layout: FloatingPanelLayout) {
init(vc: FloatingPanelController, layout: FloatingPanelLayout) {
self.vc = vc
self.layout = layout
self.surfaceView = surfaceView
self.backdropView = backdropView
}
func surfaceLocation(for state: FloatingPanelState) -> CGPoint {
let pos = displayTrunc(position(for: state), by: surfaceView.fp_displayScale)
let pos = position(for: state).rounded(by: surfaceView.fp_displayScale)
switch layout.position {
case .top, .bottom:
return CGPoint(x: 0.0, y: pos)
@@ -327,6 +321,22 @@ class LayoutAdapter {
}
return base - intrinsicLength + diff
}
case let anchor as FloatingPanelAdaptiveLayoutAnchor:
let dimension = layout.position.mainDimension(anchor.contentLayoutGuide.layoutFrame.size)
let diff = anchor.distance(from: dimension)
var referenceBoundsLength = layout.position.mainDimension(bounds.size)
switch layout.position {
case .top, .left:
if anchor.referenceGuide == .safeArea {
referenceBoundsLength += position.inset(safeAreaInsets)
}
return dimension - diff
case .bottom, .right:
if anchor.referenceGuide == .safeArea {
referenceBoundsLength -= position.inset(safeAreaInsets)
}
return referenceBoundsLength - dimension + diff
}
case let anchor as FloatingPanelLayoutAnchor:
let referenceBounds = anchor.referenceGuide == .safeArea ? bounds.inset(by: safeAreaInsets) : bounds
let diff = anchor.isAbsolute ? anchor.inset : position.mainDimension(referenceBounds.size) * anchor.inset
@@ -364,7 +374,8 @@ class LayoutAdapter {
private func referenceEdge(of anchor: FloatingPanelLayoutAnchoring) -> FloatingPanelReferenceEdge {
switch anchor {
case is FloatingPanelIntrinsicLayoutAnchor:
case is FloatingPanelIntrinsicLayoutAnchor,
is FloatingPanelAdaptiveLayoutAnchor:
switch position {
case .top: return .top
case .left: return .left
@@ -436,25 +447,13 @@ class LayoutAdapter {
}
private func updateStateConstraints() {
NSLayoutConstraint.deactivate(fullConstraints + halfConstraints + tipConstraints + offConstraints)
if let fullAnchor = layout.anchors[.full] {
fullConstraints = fullAnchor.layoutConstraints(vc, for: position)
fullConstraints.forEach {
$0.identifier = "FloatingPanel-full-constraint"
}
}
if let halfAnchor = layout.anchors[.half] {
halfConstraints = halfAnchor.layoutConstraints(vc, for: position)
halfConstraints.forEach {
$0.identifier = "FloatingPanel-half-constraint"
}
}
if let tipAnchors = layout.anchors[.tip] {
tipConstraints = tipAnchors.layoutConstraints(vc, for: position)
tipConstraints.forEach {
$0.identifier = "FloatingPanel-tip-constraint"
}
let allStateConstraints = stateConstraints.flatMap { $1 }
NSLayoutConstraint.deactivate(allStateConstraints + offConstraints)
stateConstraints.removeAll()
for state in layout.anchors.keys {
stateConstraints[state] = layout.anchors[state]?
.layoutConstraints(vc, for: position)
.map{ $0.identifier = "FloatingPanel-\(state)-constraint"; return $0 }
}
let hiddenAnchor = layout.anchors[.hidden] ?? self.hiddenAnchor
offConstraints = hiddenAnchor.layoutConstraints(vc, for: position)
@@ -471,7 +470,7 @@ class LayoutAdapter {
tearDownAttraction()
NSLayoutConstraint.deactivate(fullConstraints + halfConstraints + tipConstraints + offConstraints)
NSLayoutConstraint.deactivate(stateConstraints.flatMap { $1 } + offConstraints)
initialConst = edgePosition(surfaceView.frame) + offset.y
@@ -487,7 +486,7 @@ class LayoutAdapter {
constraint = surfaceView.leftAnchor.constraint(equalTo: vc.view.leftAnchor, constant: initialConst)
}
constraint.priority = .defaultHigh
constraint.priority = .required
constraint.identifier = "FloatingPanel-interaction"
NSLayoutConstraint.activate([constraint])
@@ -511,7 +510,7 @@ class LayoutAdapter {
let anchor = layout.anchors[state] ?? self.hiddenAnchor
NSLayoutConstraint.deactivate(fullConstraints + halfConstraints + tipConstraints + offConstraints)
NSLayoutConstraint.deactivate(stateConstraints.flatMap { $1 } + offConstraints)
NSLayoutConstraint.deactivate(constraint: interactionConstraint)
interactionConstraint = nil
@@ -630,7 +629,6 @@ class LayoutAdapter {
// The method is separated from prepareLayout(to:) for the rotation support
// It must be called in FloatingPanelController.traitCollectionDidChange(_:)
func updateStaticConstraint() {
guard let vc = vc else { return }
NSLayoutConstraint.deactivate(constraint: staticConstraint)
staticConstraint = nil
@@ -639,20 +637,32 @@ class LayoutAdapter {
return
}
let anchor = layout.anchors[self.edgeMostState]!
if anchor is FloatingPanelIntrinsicLayoutAnchor {
var constant = layout.position.mainDimension(surfaceView.intrinsicContentSize)
let anchor = layout.anchors[self.mostExpandedState]!
let surfaceAnchor = position.mainDimensionAnchor(surfaceView)
switch anchor {
case let anchor as FloatingPanelIntrinsicLayoutAnchor:
var constant = position.mainDimension(surfaceView.intrinsicContentSize)
if anchor.referenceGuide == .safeArea {
constant += position.inset(safeAreaInsets)
}
staticConstraint = position.mainDimensionAnchor(surfaceView).constraint(equalToConstant: constant)
} else {
staticConstraint = surfaceAnchor.constraint(equalToConstant: constant)
case let anchor as FloatingPanelAdaptiveLayoutAnchor:
let constant: CGFloat
if anchor.referenceGuide == .safeArea {
constant = position.inset(safeAreaInsets)
} else {
constant = 0.0
}
let baseAnchor = position.mainDimensionAnchor(anchor.contentLayoutGuide)
staticConstraint = surfaceAnchor.constraint(equalTo: baseAnchor, constant: constant)
default:
switch position {
case .top, .left:
staticConstraint = position.mainDimensionAnchor(surfaceView).constraint(equalToConstant: position(for: self.directionalMostState))
staticConstraint = surfaceAnchor.constraint(equalToConstant: position(for: self.mostCoordinateState))
case .bottom, .right:
staticConstraint = position.mainDimensionAnchor(vc.view).constraint(equalTo: position.mainDimensionAnchor(surfaceView),
constant: position(for: self.directionalLeastState))
let rootViewAnchor = position.mainDimensionAnchor(vc.view)
staticConstraint = rootViewAnchor.constraint(equalTo: surfaceAnchor,
constant: position(for: self.leastCoordinateState))
}
}
@@ -673,8 +683,8 @@ class LayoutAdapter {
log.debug("update surface location = \(surfaceLocation)")
}
let minConst: CGFloat = position(for: directionalLeastState)
let maxConst: CGFloat = position(for: directionalMostState)
let minConst: CGFloat = position(for: leastCoordinateState)
let maxConst: CGFloat = position(for: mostCoordinateState)
var const = initialConst + diff
@@ -730,8 +740,6 @@ class LayoutAdapter {
var state = state
setBackdropAlpha(of: state)
if validStates.contains(state) == false {
state = layout.initialState
}
@@ -741,17 +749,16 @@ class LayoutAdapter {
// on-screen and off-screen view which includes
// UIStackView(i.e. Settings view in Samples.app)
updateStateConstraints()
switch state {
case .full:
NSLayoutConstraint.activate(fullConstraints)
case .half:
NSLayoutConstraint.activate(halfConstraints)
case .tip:
NSLayoutConstraint.activate(tipConstraints)
case .hidden:
NSLayoutConstraint.activate(offConstraints)
default:
break
if let constraints = stateConstraints[state] {
NSLayoutConstraint.activate(constraints)
} else {
log.error("Couldn't find any constraints for \(state)")
}
}
}
@@ -762,21 +769,13 @@ class LayoutAdapter {
surfaceView.superview?.layoutIfNeeded()
}
private func setBackdropAlpha(of target: FloatingPanelState) {
if target == .hidden {
self.backdropView.alpha = 0.0
} else {
self.backdropView.alpha = backdropAlpha(for: target)
}
}
func backdropAlpha(for state: FloatingPanelState) -> CGFloat {
return layout.backdropAlpha?(for: state) ?? defaultLayout.backdropAlpha(for: state)
}
fileprivate func checkLayout() {
// Verify layout configurations
assert(activeStates.count > 0)
assert(anchorStates.count > 0)
assert(validStates.contains(layout.initialState),
"Does not include an initial state (\(layout.initialState)) in (\(validStates))")
/* This assertion does not work in a device rotating
@@ -796,7 +795,7 @@ extension LayoutAdapter {
/// |-------|-------|===o===| |-------|===o===|-------|
/// pos: o/x, segment: =
let sortedStates = sortedDirectionalStates
let sortedStates = sortedAnchorStatesByCoordinate
let upperIndex: Int?
if forward {
+75 -12
View File
@@ -8,22 +8,25 @@ import UIKit
func layoutConstraints(_ fpc: FloatingPanelController, for position: FloatingPanelPosition) -> [NSLayoutConstraint]
}
/// A layout anchor object that anchors a panel in a state.
/// An object that defines how to settles a panel with insets from an edge of a reference rectangle.
@objc final public class FloatingPanelLayoutAnchor: NSObject, FloatingPanelLayoutAnchoring /*, NSCopying */ {
/// Initializes and returns a layout anchor object to specify an absolute inset value for the position of a panel.
/// Returns a layout anchor with the specified inset by an absolute value, edge and reference guide for a panel.
///
/// The inset is a distance from the edge of the specified layout guide.
/// The inset is an amount to inset a panel from an edge of the reference guide. The edge refers to a panel
/// positioning.
@objc public init(absoluteInset: CGFloat, edge: FloatingPanelReferenceEdge, referenceGuide: FloatingPanelLayoutReferenceGuide) {
self.inset = absoluteInset
self.referenceGuide = referenceGuide
self.referenceEdge = edge
self.isAbsolute = true
}
/// Initializes and returns a layout anchor object to specify a fractional inset value for the position of a panel.
/// Returns a layout anchor with the specified inset by a fractional value, edge and reference guide for a panel.
///
/// The inset is a distance from the edge of the specified layout guide. The value is a floating-point number
/// in the range 0.0 to 1.0, where 0.0 represents zero distance from the edge and 1.0 represents a distance
/// to the opposite edge.
/// The inset is an amount to inset a panel from the edge of the specified reference guide. The value is
/// a floating-point number in the range 0.0 to 1.0, where 0.0 represents zero distance from the edge and
/// 1.0 represents a distance to the opposite edge.
@objc public init(fractionalInset: CGFloat, edge: FloatingPanelReferenceEdge, referenceGuide: FloatingPanelLayoutReferenceGuide) {
self.inset = fractionalInset
self.referenceGuide = referenceGuide
@@ -91,19 +94,20 @@ public extension FloatingPanelLayoutAnchor {
}
}
/// A layout anchor object that anchors a panel in a state using the intrinsic size for a content.
/// An object that defines how to settles a panel with the intrinsic size for a content.
@objc final public class FloatingPanelIntrinsicLayoutAnchor: NSObject, FloatingPanelLayoutAnchoring /*, NSCopying */ {
/// Initializes and returns a layout anchor object to specify an absolute offset value for the position of a panel.
/// Returns a layout anchor with the specified offset by an absolute value and reference guide for a panel.
///
/// The offset is a distance from a position at which a panel displays the entire content.
/// The offset is an amount to offset a position of panel that displays the entire content from an edge of
/// the reference guide. The edge refers to a panel positioning.
@objc public init(absoluteOffset offset: CGFloat, referenceGuide: FloatingPanelLayoutReferenceGuide = .safeArea) {
self.offset = offset
self.referenceGuide = referenceGuide
self.isAbsolute = true
}
/// Initializes and returns a layout anchor object to specify a fractional offset value for the position of a panel.
/// Returns a layout anchor with the specified offset by a fractional value and reference guide for a panel.
///
/// The offset value is a floating-point number in the range 0.0 to 1.0, where 0.0 represents the full content
/// is displayed and 0.5 represents the half of content is displayed.
@@ -136,3 +140,62 @@ public extension FloatingPanelIntrinsicLayoutAnchor {
}
}
}
/// An object that defines how to settles a panel with a layout guide of a content view.
@objc final public class FloatingPanelAdaptiveLayoutAnchor: NSObject, FloatingPanelLayoutAnchoring /*, NSCopying */ {
/// Returns a layout anchor with the specified offset by an absolute value, layout guide to display content and reference guide for a panel.
///
/// The offset is an amount to offset a position of panel that displays the entire content of the specified guide from an edge of
/// the reference guide. The edge refers to a panel positioning.
@objc public init(absoluteOffset offset: CGFloat, contentLayout: UILayoutGuide, referenceGuide: FloatingPanelLayoutReferenceGuide = .safeArea) {
self.offset = offset
self.contentLayoutGuide = contentLayout
self.referenceGuide = referenceGuide
self.isAbsolute = true
}
/// Returns a layout anchor with the specified offset by a fractional value, layout guide to display content and reference guide for a panel.
///
/// The offset value is a floating-point number in the range 0.0 to 1.0, where 0.0 represents the full content
/// is displayed and 0.5 represents the half of content is displayed.
@objc public init(fractionalOffset offset: CGFloat, contentLayout: UILayoutGuide, referenceGuide: FloatingPanelLayoutReferenceGuide = .safeArea) {
self.offset = offset
self.contentLayoutGuide = contentLayout
self.referenceGuide = referenceGuide
self.isAbsolute = false
}
fileprivate let offset: CGFloat
fileprivate let isAbsolute: Bool
let contentLayoutGuide: UILayoutGuide
@objc public let referenceGuide: FloatingPanelLayoutReferenceGuide
}
public extension FloatingPanelAdaptiveLayoutAnchor {
func layoutConstraints(_ vc: FloatingPanelController, for position: FloatingPanelPosition) -> [NSLayoutConstraint] {
let layoutGuide = referenceGuide.layoutGuide(vc: vc)
let offsetAnchor: NSLayoutDimension
switch position {
case .top:
offsetAnchor = layoutGuide.topAnchor.anchorWithOffset(to: vc.surfaceView.bottomAnchor)
case .left:
offsetAnchor = layoutGuide.leftAnchor.anchorWithOffset(to: vc.surfaceView.rightAnchor)
case .bottom:
offsetAnchor = vc.surfaceView.topAnchor.anchorWithOffset(to: layoutGuide.bottomAnchor)
case .right:
offsetAnchor = vc.surfaceView.leftAnchor.anchorWithOffset(to: layoutGuide.rightAnchor)
}
if isAbsolute {
return [offsetAnchor.constraint(equalTo: position.mainDimensionAnchor(contentLayoutGuide), constant: -offset)]
} else {
return [offsetAnchor.constraint(equalTo: position.mainDimensionAnchor(contentLayoutGuide), multiplier: (1 - offset))]
}
}
}
extension FloatingPanelAdaptiveLayoutAnchor {
func distance(from dimension: CGFloat) -> CGFloat {
return isAbsolute ? offset : dimension * offset
}
}
+2 -1
View File
@@ -3,6 +3,7 @@
import Foundation
import os.log
// Must be a variable to use `hook` property in testing
var log = {
return Logger()
}()
@@ -68,7 +69,7 @@ struct Logger {
hook?(log, level)
os_log("%@", log: osLog, type: level.osLogType, log)
os_log("%{public}@", log: osLog, type: level.osLogType, log)
}
private func getPrettyFunction(_ function: String, _ file: String) -> String {
@@ -2,8 +2,8 @@
import UIKit
@objc(FloatingPanelPassThroughView)
class PassThroughView: UIView {
@objc(FloatingPanelPassthroughView)
class PassthroughView: UIView {
public weak var eventForwardingView: UIView?
public override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
let hitView = super.hitTest(point, with: event)
+3 -2
View File
@@ -4,7 +4,7 @@ import Foundation
/// An object that represents the display state of a panel in a screen.
@objc
public class FloatingPanelState: NSObject, NSCopying, RawRepresentable {
open class FloatingPanelState: NSObject, NSCopying, RawRepresentable {
public typealias RawValue = String
required public init?(rawValue: RawValue) {
@@ -13,6 +13,7 @@ public class FloatingPanelState: NSObject, NSCopying, RawRepresentable {
super.init()
}
@objc
public init(rawValue: RawValue, order: Int) {
self.rawValue = rawValue
self.order = order
@@ -33,7 +34,7 @@ public class FloatingPanelState: NSObject, NSCopying, RawRepresentable {
}
public override var debugDescription: String {
return description
return "<FloatingPanel.FloatingPanelState: \(Unmanaged.passUnretained(self).toOpaque())>"
}
/// A panel state indicates the entire panel is shown.
+14 -9
View File
@@ -50,6 +50,12 @@ public class SurfaceAppearance: NSObject {
/// On iOS 10, they are not automatically masked because of a UIVisualEffectView issue. See https://forums.developer.apple.com/thread/50854
public var cornerRadius: CGFloat = 0.0
/// Defines the curve used for rendering the rounded corners of the layer.
///
/// Defaults to `.circular`.
@available(iOS 13.0, *)
public lazy var cornerCurve: CALayerCornerCurve = .circular
/// An array of shadows used to create drop shadows underneath a surface view.
public var shadows: [Shadow] = [Shadow()]
@@ -311,8 +317,8 @@ public class SurfaceView: UIView {
containerView.backgroundColor = appearance.backgroundColor
updateShadow()
updateCornerRadius()
updateShadow()
updateBorder()
grabberHandle.layer.cornerRadius = grabberHandleSize.height / 2
@@ -336,10 +342,9 @@ public class SurfaceView: UIView {
shadowLayer.frame = layer.bounds
let spread = shadow.spread
let shadowPath = UIBezierPath(roundedRect: containerView.frame.insetBy(dx: -spread,
dy: -spread),
byRoundingCorners: [.allCorners],
cornerRadii: CGSize(width: appearance.cornerRadius, height: 0))
let shadowRect = containerView.frame.insetBy(dx: -spread, dy: -spread)
let shadowPath = UIBezierPath.path(roundedRect: shadowRect,
appearance: appearance)
shadowLayer.shadowPath = shadowPath.cgPath
shadowLayer.shadowColor = shadow.color.cgColor
shadowLayer.shadowOffset = shadow.offset
@@ -348,16 +353,16 @@ public class SurfaceView: UIView {
shadowLayer.shadowOpacity = shadow.opacity
let mask = CAShapeLayer()
let path = UIBezierPath(roundedRect: containerView.frame,
byRoundingCorners: [.allCorners],
cornerRadii: CGSize(width: appearance.cornerRadius, height: 0))
let path = UIBezierPath.path(roundedRect: containerView.frame,
appearance: appearance)
let size = window?.bounds.size ?? CGSize(width: 1000.0, height: 1000.0)
path.append(UIBezierPath(rect: layer.bounds.insetBy(dx: -size.width,
dy: -size.height)))
mask.fillRule = .evenOdd
mask.path = path.cgPath
if #available(iOS 13.0, *) {
mask.cornerCurve = containerView.layer.cornerCurve
containerView.layer.cornerCurve = appearance.cornerCurve
mask.cornerCurve = appearance.cornerCurve
}
shadowLayer.mask = mask
}
+29 -9
View File
@@ -61,7 +61,7 @@ class PresentationController: UIPresentationController {
addFloatingPanel()
// Forward touch events to the presenting view controller
(fpc.view as? PassThroughView)?.eventForwardingView = presentingViewController.view
(fpc.view as? PassthroughView)?.eventForwardingView = presentingViewController.view
}
@objc func handleBackdrop(tapGesture: UITapGestureRecognizer) {
@@ -86,19 +86,29 @@ class ModalPresentTransition: NSObject, UIViewControllerAnimatedTransitioning {
let fpc = transitionContext?.viewController(forKey: .to) as? FloatingPanelController
else { fatalError()}
let animator = fpc.delegate?.floatingPanel?(fpc, animatorForPresentingTo: fpc.layout.initialState)
?? FloatingPanelDefaultBehavior().addPanelAnimator(fpc, to: fpc.layout.initialState)
let animator = fpc.animatorForPresenting(to: fpc.layout.initialState)
return TimeInterval(animator.duration)
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
func interruptibleAnimator(using transitionContext: UIViewControllerContextTransitioning) -> UIViewImplicitlyAnimating {
guard
let fpc = transitionContext.viewController(forKey: .to) as? FloatingPanelController
else { fatalError() }
fpc.show(animated: true) {
if let animator = fpc.transitionAnimator {
return animator
}
fpc.suspendTransitionAnimator(true)
fpc.show(animated: true) { [weak fpc] in
fpc?.suspendTransitionAnimator(false)
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
}
return fpc.transitionAnimator!
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
self.interruptibleAnimator(using: transitionContext).startAnimation()
}
}
@@ -108,19 +118,29 @@ class ModalDismissTransition: NSObject, UIViewControllerAnimatedTransitioning {
let fpc = transitionContext?.viewController(forKey: .from) as? FloatingPanelController
else { fatalError()}
let animator = fpc.delegate?.floatingPanel?(fpc, animatorForDismissingWith: .zero)
?? FloatingPanelDefaultBehavior().removePanelAnimator(fpc, from: fpc.state, with: .zero)
let animator = fpc.animatorForDismissing(with: .zero)
return TimeInterval(animator.duration)
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
func interruptibleAnimator(using transitionContext: UIViewControllerContextTransitioning) -> UIViewImplicitlyAnimating {
guard
let fpc = transitionContext.viewController(forKey: .from) as? FloatingPanelController
else { fatalError() }
fpc.hide(animated: true) {
if let animator = fpc.transitionAnimator {
return animator
}
fpc.suspendTransitionAnimator(true)
fpc.hide(animated: true) { [weak fpc] in
fpc?.suspendTransitionAnimator(false)
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
}
return fpc.transitionAnimator!
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
self.interruptibleAnimator(using: transitionContext).startAnimation()
}
}
+58
View File
@@ -185,6 +185,57 @@ class CoreTests: XCTestCase {
XCTAssertEqual(fpc.floatingPanel.getBackdropAlpha(at: halfPos, with: -1 * distance2), 0.0)
}
func test_updateBackdropAlpha() {
class BackdropTestLayout: FloatingPanelTestLayout {
override var initialState: FloatingPanelState { .hidden }
func backdropAlpha(for state: FloatingPanelState) -> CGFloat {
switch state {
case .full: return 0.3
case .half: return 0.0
case .tip: return 0.3
default: return 0.0
}
}
}
let fpc = FloatingPanelController()
fpc.layout = BackdropTestLayout()
fpc.showForTest()
fpc.move(to: .full, animated: false)
XCTAssertEqual(floor(fpc.backdropView.alpha * 1000_000) / 1000_000, 0.3)
fpc.move(to: .half, animated: false)
XCTAssertEqual(fpc.backdropView.alpha, 0.0)
fpc.move(to: .tip, animated: false)
XCTAssertEqual(floor(fpc.backdropView.alpha * 1000_000) / 1000_000, 0.3)
let exp1 = expectation(description: "move to full with animation")
fpc.move(to: .full, animated: true) {
exp1.fulfill()
}
wait(for: [exp1], timeout: 1.0)
XCTAssertEqual(floor(fpc.backdropView.alpha * 1000_000) / 1000_000, 0.3)
let exp2 = expectation(description: "move to half with animation")
fpc.move(to: .half, animated: true) {
exp2.fulfill()
}
wait(for: [exp2], timeout: 1.0)
XCTAssertEqual(fpc.backdropView.alpha, 0.0)
let exp3 = expectation(description: "move to tip with animation")
fpc.move(to: .tip, animated: true) {
exp3.fulfill()
}
fpc.contentMode = .fitToBounds
XCTAssertEqual(fpc.backdropView.alpha, 0.0) // Must not affect the backdrop alpha by changing the content mode
wait(for: [exp3], timeout: 1.0)
XCTAssertEqual(floor(fpc.backdropView.alpha * 1000_000) / 1000_000, 0.3)
}
func test_targetPosition_1positions() {
class FloatingPanelLayout1Positions: FloatingPanelLayout {
let initialState: FloatingPanelState = .full
@@ -715,6 +766,13 @@ class CoreTests: XCTestCase {
(#line, tipPos, CGPoint(x: 0.0, y: 1000.0), .hidden),
])
}
func test_keep_pan_gesture_disabled() {
let fpc = FloatingPanelController()
fpc.panGestureRecognizer.isEnabled = false
fpc.showForTest()
XCTAssertFalse(fpc.panGestureRecognizer.isEnabled)
}
}
private class FloatingPanelLayout3Positions: FloatingPanelTestLayout {
+12
View File
@@ -0,0 +1,12 @@
// Copyright 2018 the FloatingPanel authors. All rights reserved. MIT license.
import XCTest
@testable import FloatingPanel
class ExtensionTests: XCTestCase {
func test_roundedByDisplayScale() {
XCTAssertEqual(CGFloat(333.222).rounded(by: 3), 333.3333333333333)
XCTAssertNotEqual(CGFloat(333.5).rounded(by: 3), 333.66666666666674)
XCTAssertTrue(CGFloat(333.5).isEqual(to: 333.66666666666674, on: 3.0))
}
}
+6 -6
View File
@@ -13,8 +13,8 @@ class LayoutTests: XCTestCase {
override func tearDown() {}
func test_layoutAdapter_topAndBottomMostState() {
XCTAssertEqual(fpc.floatingPanel.layoutAdapter.edgeMostState, .full)
XCTAssertEqual(fpc.floatingPanel.layoutAdapter.edgeLeastState, .tip)
XCTAssertEqual(fpc.floatingPanel.layoutAdapter.mostExpandedState, .full)
XCTAssertEqual(fpc.floatingPanel.layoutAdapter.leastExpandedState, .tip)
class FloatingPanelLayoutWithHidden: FloatingPanelLayout {
var anchors: [FloatingPanelState: FloatingPanelLayoutAnchoring] {
@@ -38,12 +38,12 @@ class LayoutTests: XCTestCase {
let position: FloatingPanelPosition = .bottom
}
fpc.layout = FloatingPanelLayoutWithHidden()
XCTAssertEqual(fpc.floatingPanel.layoutAdapter.edgeMostState, .full)
XCTAssertEqual(fpc.floatingPanel.layoutAdapter.edgeLeastState, .hidden)
XCTAssertEqual(fpc.floatingPanel.layoutAdapter.mostExpandedState, .full)
XCTAssertEqual(fpc.floatingPanel.layoutAdapter.leastExpandedState, .hidden)
fpc.layout = FloatingPanelLayout2Positions()
XCTAssertEqual(fpc.floatingPanel.layoutAdapter.edgeMostState, .half)
XCTAssertEqual(fpc.floatingPanel.layoutAdapter.edgeLeastState, .tip)
XCTAssertEqual(fpc.floatingPanel.layoutAdapter.mostExpandedState, .half)
XCTAssertEqual(fpc.floatingPanel.layoutAdapter.leastExpandedState, .tip)
}
func test_layoutSegment_3position() {
+1 -1
View File
@@ -18,7 +18,7 @@ extension FloatingPanelController {
class FloatingPanelTestDelegate: FloatingPanelControllerDelegate {
var position: FloatingPanelState = .hidden
var didMoveCallback: ((FloatingPanelController) -> Void)?
func floatingPanelDidChangePosition(_ vc: FloatingPanelController) {
func floatingPanelDidChangeState(_ vc: FloatingPanelController) {
position = vc.state
}
func floatingPanelDidMove(_ vc: FloatingPanelController) {
-12
View File
@@ -1,12 +0,0 @@
// Copyright 2018 the FloatingPanel authors. All rights reserved. MIT license.
import XCTest
@testable import FloatingPanel
class UtilTests: XCTestCase {
func test_displayTrunc() {
XCTAssertEqual(displayTrunc(333.222, by: 3), 333.3333333333333)
XCTAssertNotEqual(displayTrunc(333.5, by: 3), 333.66666666666674)
XCTAssertTrue(displayEqual(333.5, 333.66666666666674, by: 3))
}
}