Compare commits
96 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3a0831a334 | |||
| add2f9ba4f | |||
| ca6a8d5c16 | |||
| 84eba1aefa | |||
| 289ea4c971 | |||
| 821b03376c | |||
| 19fe1cffef | |||
| b8fcb50874 | |||
| 9c45c31190 | |||
| 895790f697 | |||
| 43c7e8c2a0 | |||
| 904a66115c | |||
| d0932e8e37 | |||
| 22009013eb | |||
| da4668aa2b | |||
| 00ce232420 | |||
| 5b176b5cef | |||
| 15709f62f8 | |||
| 9168236534 | |||
| 16e709e8ab | |||
| 94c39f0f34 | |||
| 16fea625be | |||
| 6c69694cfe | |||
| 561d783479 | |||
| 1bd2e60200 | |||
| ec0e1b2dad | |||
| 9958fc5017 | |||
| 11dfc0d2f3 | |||
| 34246d1f37 | |||
| 3d6c0220e0 | |||
| ecbd318186 | |||
| 34809bd8ea | |||
| aa5fbe9e94 | |||
| 9fbb7df15a | |||
| d55f9a0abf | |||
| a932f3b782 | |||
| 9ea95d69a1 | |||
| e783b92905 | |||
| d380fdeb13 | |||
| 987bf79121 | |||
| be0ebd0fae | |||
| a6538b7a2a | |||
| 40194d91c0 | |||
| c8b2b33de0 | |||
| 7114f545ff | |||
| b4423bcaa2 | |||
| b34fd41650 | |||
| 199a77182b | |||
| 0a0f00172d | |||
| 25ca9487fb | |||
| 231ee4c9af | |||
| c872218446 | |||
| 8e8c6527d4 | |||
| 3b4e237eba | |||
| 9b1cb68f0a | |||
| 4ba8acaf08 | |||
| 02d8d4516c | |||
| 94829d2749 | |||
| 1522a0990f | |||
| 0a613a7339 | |||
| b4a518fe3e | |||
| 538e046368 | |||
| bb44d14370 | |||
| 230ec689d4 | |||
| c7605a94b9 | |||
| 864104de3e | |||
| a8e1cfa2bc | |||
| 207dd27113 | |||
| 802b6f9b6b | |||
| 0ff9d5dd9b | |||
| 6e509481bb | |||
| 054a21f3f8 | |||
| 2e3bbaeec2 | |||
| 18e739fc7b | |||
| ca7596e1ca | |||
| 38103da2eb | |||
| 0c2275def1 | |||
| a0da1b99c0 | |||
| 427814839a | |||
| 007f9af3eb | |||
| da4e1d26d3 | |||
| 2ce1375ce7 | |||
| 016bf3e185 | |||
| 214942f918 | |||
| af53ac1964 | |||
| 991166534f | |||
| 39feab437d | |||
| ba8d37e8e0 | |||
| 885493f2e5 | |||
| 65fd04975d | |||
| 1a45f43232 | |||
| 28c384aa0d | |||
| 99c922bf3a | |||
| 009033be79 | |||
| 5d2e6dd417 | |||
| 9c71a47d9b |
@@ -0,0 +1 @@
|
||||
github: SCENEE
|
||||
@@ -0,0 +1,104 @@
|
||||
name: ci
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
branches:
|
||||
- '*'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: macOS-11
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- name: "Swift 5.4"
|
||||
run: xcodebuild -scheme FloatingPanel SWIFT_VERSION=5.4 clean build
|
||||
env:
|
||||
DEVELOPER_DIR: /Applications/Xcode_12.5.1.app/Contents/Developer
|
||||
- name: "Swift 5.5"
|
||||
run: xcodebuild -scheme FloatingPanel SWIFT_VERSION=5.5 clean build
|
||||
env:
|
||||
DEVELOPER_DIR: /Applications/Xcode_13.0.app/Contents/Developer
|
||||
|
||||
build_compat:
|
||||
runs-on: macOS-10.15
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- name: "Swift 5.1"
|
||||
run: xcodebuild -scheme FloatingPanel SWIFT_VERSION=5.1 clean build
|
||||
env:
|
||||
DEVELOPER_DIR: /Applications/Xcode_11.3.1.app/Contents/Developer
|
||||
- name: "Swift 5.2"
|
||||
run: xcodebuild -scheme FloatingPanel SWIFT_VERSION=5.2 clean build
|
||||
env:
|
||||
DEVELOPER_DIR: /Applications/Xcode_11.7.app/Contents/Developer
|
||||
- name: "Swift 5.3"
|
||||
run: xcodebuild -scheme FloatingPanel SWIFT_VERSION=5.3 clean build
|
||||
env:
|
||||
DEVELOPER_DIR: /Applications/Xcode_12.4.app/Contents/Developer
|
||||
|
||||
testing:
|
||||
runs-on: macOS-11
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- name: "Testing in iOS 14.5"
|
||||
run: xcodebuild clean test -scheme FloatingPanel -workspace FloatingPanel.xcworkspace -destination 'platform=iOS Simulator,OS=14.5,name=iPhone 12 Pro'
|
||||
env:
|
||||
DEVELOPER_DIR: /Applications/Xcode_12.5.1.app/Contents/Developer
|
||||
- name: "Testing in iOS 15.0"
|
||||
run: xcodebuild clean test -scheme FloatingPanel -workspace FloatingPanel.xcworkspace -destination 'platform=iOS Simulator,OS=15.0,name=iPhone 13 Pro'
|
||||
env:
|
||||
DEVELOPER_DIR: /Applications/Xcode_13.0.app/Contents/Developer
|
||||
|
||||
testing_compat:
|
||||
runs-on: macOS-10.15
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- name: "Testing in iOS 13.7"
|
||||
run: xcodebuild clean test -scheme FloatingPanel -workspace FloatingPanel.xcworkspace -destination 'platform=iOS Simulator,OS=13.7,name=iPhone 11 Pro'
|
||||
env:
|
||||
DEVELOPER_DIR: /Applications/Xcode_11.7.app/Contents/Developer
|
||||
- name: "Testing in iOS 14.4"
|
||||
run: xcodebuild clean test -scheme FloatingPanel -workspace FloatingPanel.xcworkspace -destination 'platform=iOS Simulator,OS=14.4,name=iPhone 12 Pro'
|
||||
env:
|
||||
DEVELOPER_DIR: /Applications/Xcode_12.4.app/Contents/Developer
|
||||
|
||||
example:
|
||||
runs-on: macOS-11
|
||||
env:
|
||||
DEVELOPER_DIR: /Applications/Xcode_13.0.app/Contents/Developer
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- name: "Build Maps"
|
||||
run: xcodebuild -workspace FloatingPanel.xcworkspace -scheme Maps -sdk iphonesimulator clean build
|
||||
- name: "Build Stocks"
|
||||
run: xcodebuild -workspace FloatingPanel.xcworkspace -scheme Stocks -sdk iphonesimulator clean build
|
||||
- name: "Build Samples"
|
||||
run: xcodebuild -workspace FloatingPanel.xcworkspace -scheme Samples -sdk iphonesimulator clean build
|
||||
|
||||
swiftpm:
|
||||
runs-on: macOS-11
|
||||
env:
|
||||
DEVELOPER_DIR: /Applications/Xcode_13.0.app/Contents/Developer
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- name: "Swift Package build"
|
||||
run: swift build -Xswiftc "-sdk" -Xswiftc "`xcrun --sdk iphonesimulator --show-sdk-path`" -Xswiftc "-target" -Xswiftc "x86_64-apple-ios14.3-simulator"
|
||||
|
||||
carthage:
|
||||
runs-on: macOS-11
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- name: "Carthage build"
|
||||
run: carthage build --use-xcframeworks --no-skip-current
|
||||
|
||||
cocoapods:
|
||||
runs-on: macOS-11
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- name: "CocoaPods: pod lib lint"
|
||||
run: pod lib lint --allow-warnings
|
||||
- name: "CocoaPods: pod spec lint"
|
||||
run: pod spec lint --allow-warnings
|
||||
@@ -17,6 +17,7 @@ DerivedData/
|
||||
*.perspectivev3
|
||||
!default.perspectivev3
|
||||
xcuserdata/
|
||||
IDEWorkspaceChecks.plis
|
||||
|
||||
## Other
|
||||
*.moved-aside
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
--header "// Copyright 2018 the FloatingPanel authors. All rights reserved. MIT license."
|
||||
--disable andOperator,anyObjectProtocol,blankLinesAroundMark,blankLinesAtEndOfScope,blankLinesAtStartOfScope,blankLinesBetweenScopes,braces,consecutiveBlankLines,consecutiveSpaces,duplicateImports,elseOnSameLine,emptyBraces,hoistPatternLet,indent,isEmpty,leadingDelimiters,linebreakAtEndOfFile,linebreaks,numberFormatting,ranges,redundantBackticks,redundantBreak,redundantExtensionACL,redundantFileprivate,redundantGet,redundantInit,redundantLet,redundantLetError,redundantNilInit,redundantObjc,redundantParens,redundantPattern,redundantRawValues,redundantReturn,redundantSelf,redundantVoidReturnType,semicolons,sortedImports,spaceAroundBraces,spaceAroundBrackets,spaceAroundComments,spaceAroundGenerics,spaceAroundOperators,spaceAroundParens,spaceInsideBraces,spaceInsideBrackets,spaceInsideComments,spaceInsideGenerics,spaceInsideParens,specifiers,strongOutlets,strongifiedSelf,todos,trailingClosures,trailingCommas,trailingSpace,typeSugar,unusedArguments,void,wrapArguments,yodaConditions
|
||||
--header "// Copyright 2018-Present Shin Yamamoto. All rights reserved. MIT license."
|
||||
--disable andOperator,anyObjectProtocol,blankLinesAroundMark,blankLinesAtEndOfScope,blankLinesAtStartOfScope,blankLinesBetweenScopes,braces,consecutiveBlankLines,consecutiveSpaces,duplicateImports,elseOnSameLine,emptyBraces,hoistPatternLet,indent,isEmpty,leadingDelimiters,linebreakAtEndOfFile,linebreaks,numberFormatting,redundantBackticks,redundantBreak,redundantExtensionACL,redundantFileprivate,redundantGet,redundantInit,redundantLet,redundantLetError,redundantNilInit,redundantObjc,redundantParens,redundantPattern,redundantRawValues,redundantReturn,redundantSelf,redundantVoidReturnType,semicolons,sortedImports,spaceAroundBraces,spaceAroundBrackets,spaceAroundComments,spaceAroundGenerics,spaceAroundOperators,spaceAroundParens,spaceInsideBraces,spaceInsideBrackets,spaceInsideComments,spaceInsideGenerics,spaceInsideParens,specifiers,strongOutlets,strongifiedSelf,todos,trailingClosures,trailingCommas,trailingSpace,typeSugar,unusedArguments,void,wrapArguments,yodaConditions
|
||||
|
||||
|
||||
-74
@@ -1,74 +0,0 @@
|
||||
language: objective-c
|
||||
branches:
|
||||
only:
|
||||
- master
|
||||
cache:
|
||||
directories:
|
||||
- /usr/local/Homebrew
|
||||
- $HOME/Library/Caches/Homebrew
|
||||
before_cache:
|
||||
- brew cleanup
|
||||
env:
|
||||
global:
|
||||
- LANG=en_US.UTF-8
|
||||
- LC_ALL=en_US.UTF-8
|
||||
jobs:
|
||||
include:
|
||||
- stage: "Builds"
|
||||
osx_image: xcode9.4
|
||||
script: xcodebuild -scheme FloatingPanel SWIFT_VERSION=4.1 clean build
|
||||
name: "Swift 4.1"
|
||||
- script: xcodebuild -scheme FloatingPanel SWIFT_VERSION=4.2 clean build
|
||||
osx_image: xcode10
|
||||
name: "Swift 4.2"
|
||||
- script: xcodebuild -scheme FloatingPanel SWIFT_VERSION=5.0 clean build
|
||||
osx_image: xcode10.3
|
||||
name: "Swift 5.0"
|
||||
- script: xcodebuild -scheme FloatingPanel SWIFT_VERSION=5.1 SUPPORTS_MACCATALYST=NO clean build
|
||||
# SUPPORTS_MACCATALYST=NO because Xcode 11 runs on macOS 10.14 in Travis CI
|
||||
osx_image: xcode11
|
||||
name: "Swift 5.1"
|
||||
|
||||
- stage: "Tests"
|
||||
osx_image: xcode10.3
|
||||
script: xcodebuild clean test -scheme FloatingPanel -workspace FloatingPanel.xcworkspace -destination 'platform=iOS Simulator,OS=10.3.1,name=iPhone SE'
|
||||
name: "iPhone SE (iOS 10.3)"
|
||||
- script: xcodebuild clean test -scheme FloatingPanel -workspace FloatingPanel.xcworkspace -destination 'platform=iOS Simulator,OS=11.4,name=iPhone 7'
|
||||
osx_image: xcode10.3
|
||||
name: "iPhone 7 (iOS 11.4)"
|
||||
- script: xcodebuild clean test -scheme FloatingPanel -workspace FloatingPanel.xcworkspace -destination 'platform=iOS Simulator,OS=12.2,name=iPhone X'
|
||||
osx_image: xcode10.3
|
||||
name: "iPhone X (iOS 12.2)"
|
||||
- script: xcodebuild clean test -scheme FloatingPanel -workspace FloatingPanel.xcworkspace -destination 'platform=iOS Simulator,OS=13.0,name=iPhone 11'
|
||||
osx_image: xcode11
|
||||
name: "iPhone X (iOS 13.0)"
|
||||
|
||||
- stage: Build examples
|
||||
osx_image: xcode11
|
||||
script: xcodebuild -scheme Maps -sdk iphonesimulator clean build
|
||||
name: "Maps"
|
||||
- script: xcodebuild -scheme Stocks -sdk iphonesimulator clean build
|
||||
osx_image: xcode11
|
||||
name: "Stocks"
|
||||
- script: xcodebuild -scheme Samples -sdk iphonesimulator clean build
|
||||
osx_image: xcode11
|
||||
name: "Samples"
|
||||
- script: xcodebuild -scheme SamplesObjC -sdk iphonesimulator clean build
|
||||
osx_image: xcode11
|
||||
name: "SamplesObjC"
|
||||
|
||||
- stage: Carthage
|
||||
osx_image: xcode11
|
||||
before_install:
|
||||
- brew update
|
||||
- brew outdated carthage || brew upgrade carthage
|
||||
script:
|
||||
- carthage build --no-skip-current
|
||||
|
||||
- stage: CocoaPods
|
||||
osx_image: xcode11
|
||||
before_install:
|
||||
- gem install cocoapods
|
||||
script:
|
||||
- pod spec lint --allow-warnings
|
||||
- pod lib lint --allow-warnings
|
||||
@@ -0,0 +1,294 @@
|
||||
# FloatingPanel 2.0 Migration Guide
|
||||
|
||||
FloatingPanel 2.0 is the latest major release of FloatingPanel. As a major release, following Semantic Versioning conventions, 2.0 introduces API-breaking changes.
|
||||
|
||||
This guide is provided in order to ease the transition of existing applications using FloatingPanel 1.x to the latest APIs, as well as explain the design and structure of new and updated functionality.
|
||||
|
||||
## Updated Minimum Requirements
|
||||
|
||||
* Swift 5.0
|
||||
* iOS 11 (iOS 10 is still the deployment target, but not tested well)
|
||||
* Xcode 11.0
|
||||
|
||||
## Benefits of Upgrading
|
||||
|
||||
* __Top, left and right positioned panel__
|
||||
* FloatingPanel is not just a library for a bottom positioned panel, but also top, left and right positioned ones.
|
||||
* __Objective-C compatibility__
|
||||
* The entire APIs are exposed in Objective-C. So you can use them in Objective-C directly.
|
||||
* __Flexible and explicit layout customization__
|
||||
* `FloatingPanelLayout` is redesigned. There is no implicit rules to lay out a panel anymore.
|
||||
* __New spring animation without UIViewPropertyAnimator__
|
||||
* The new spring animation uses [Numeric springing](http://allenchou.net/2015/04/game-math-precise-control-over-numeric-springing/) which is a very powerful tool for procedural animation. Therefore a library consumer is easy to modify a panel behavior by 2 paramters of the deceleration rate and response time.
|
||||
* __Handle the panel position anytime__
|
||||
* `floatingPanelDidMove(_:)` delegate method is also called while a panel is moving. The method behavior becomes same as `scrollViewDidScroll(_:)` in `UIScrollViewDelegate`. And in the method a library consumer is able to change a panel location.
|
||||
* __Update the removal interaction's invocation__
|
||||
* Now you can invoke the removal interaction at any time where you want. There is no restrictions in the library.
|
||||
* __Fix many issues depending on API design__
|
||||
* See the following sections for details.
|
||||
|
||||
## API Name Changes
|
||||
|
||||
* `FloatingPanelPosition` is now `FloatingPanelState`.
|
||||
* `FloatingPanelPosition` in v2 is used to specify a panel position(top, left, bottom and right) in a screen.
|
||||
* `FloatingPanelSurfaceView` is `SurfaceView` only in Swift.
|
||||
* `FloatingPanelBackdropView` is `BackdropView` only in Swift.
|
||||
* `FloatingPanelGrabberHandleView` is `GrabberView` only in Swift.
|
||||
* "decelerate" term is replaced with "attract" because the panel's behavior is not unidirectional, but going back and forth so that it is settled to a location.
|
||||
|
||||
## `FloatingPanelController`
|
||||
|
||||
* `layout` and `behavior` properties can be changed directly without using the delegate methods.
|
||||
|
||||
```swift
|
||||
fpc.behavior = SearchPaneliPadBehavior()
|
||||
fpc.layout = SearchPaneliPadLayout()
|
||||
fpc.invalidateLayout() // If needed
|
||||
```
|
||||
|
||||
* The second argument of `addPanel(toParent:)` changes to specify an index of subviews of a view in which a panel is added.
|
||||
|
||||
```diff
|
||||
- public func addPanel(toParent parent: UIViewController, belowView: UIView? = nil, animated: Bool = false) {
|
||||
+ public func addPanel(toParent parent: UIViewController, at viewIndex: Int = -1, animated: Bool = false) {
|
||||
```
|
||||
|
||||
* `surfaceOriginY` is now `surfaceLocation`.
|
||||
* `updateLayout` is now `invalidateLayout`.
|
||||
* The scroll tracking API is changed a bit to support multiple scroll view tracking in the future.
|
||||
* Now `untrack(scrollView:)` is used to disable the scroll tracking.
|
||||
|
||||
## `FloatingPanelControllerDelegate`
|
||||
|
||||
* `floatingPanelDidEndDragging(_ vc:willAttract:)` is added to check whether a panel will continue to move after dragging.
|
||||
* `floatingPanelDidMove(_:)` behavior changes. The method is also called in the spring animation.
|
||||
* The removal interaction delegate is updated.
|
||||
* `floatingPanel(_:shouldRemoveAt:with:)` is added to determine whether it invokes the removal interaction in any state.
|
||||
* `floatingPanelWillRemove(_:)` is added.
|
||||
* `floatingPanel(_: FloatingPanelController, layoutFor size: CGSize)` is added to respond to a layout change in regular size classes on iPad.
|
||||
|
||||
```swift
|
||||
func floatingPanel(_ fpc: FloatingPanelController, layoutFor size: CGSize) -> FloatingPanelLayout {
|
||||
if aCondition(for: size) {
|
||||
return SearchPanelLayout()
|
||||
}
|
||||
return SearchPanel2Layout()
|
||||
}
|
||||
```
|
||||
|
||||
* The `targetState` argument type of `floatingPanelWillEndDragging(_:withVelocity:targetState:)` is changed from `FloatingPanelState` to `UnsafeMutablePointer<FloatingPanelState>` to modify a target state on demand.
|
||||
|
||||
```swift
|
||||
func floatingPanelWillEndDragging(_ vc: FloatingPanelController, withVelocity velocity: CGPoint, targetState: UnsafeMutablePointer<FloatingPanelState>) {
|
||||
switch targetState.pointee {
|
||||
case .full:
|
||||
// do something...
|
||||
case .half:
|
||||
if aCondition {
|
||||
targetState.pointee = .tip
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Deprecated APIs
|
||||
|
||||
* `floatingPanel(_:behaviorFor:)`
|
||||
* Please update `FloatingPanelController.behavior` directly.
|
||||
* `floatingPanel(_:shouldRecognizeSimultaneouslyWith:)`
|
||||
* Please use `FloatingPanelController.panGestureRecognizer.delegateProxy`.
|
||||
|
||||
## `FloatingPanelLayout`
|
||||
|
||||
* `position` property is added to determine a panel position.
|
||||
* `initialPosition` is now `initialState`.
|
||||
* `supportedPositions` and `insetFor(position:)` are replaced with `anchors` property.
|
||||
* `backdropAlphaFor(position:)` is now `backdropAlpha(for:)`.
|
||||
|
||||
```swift
|
||||
class SearchPanelPadLayout: FloatingPanelLayout {
|
||||
let position: FloatingPanelPosition = .top
|
||||
let initialState: FloatingPanelState = .tip
|
||||
var anchors: [FloatingPanelState : FloatingPanelLayoutAnchoring] {
|
||||
return [
|
||||
...
|
||||
]
|
||||
}
|
||||
func backdropAlpha(for state: FloatingPanelState) -> CGFloat {
|
||||
return 0.3
|
||||
}
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
### New `FloatingPanelLayoutAnchoring` classes
|
||||
|
||||
The following objects adopting `FloatingPanelLayoutAnchoring` protocol are added to configure the flexible and explicit layout.
|
||||
|
||||
#### `FloatingPanelLayoutAnchor`
|
||||
|
||||
This class is used to specify a panel layout using insets from a rectangle area of the superview or safe area.
|
||||
|
||||
* `FloatingPanelFullScreenLayout` is replaced with anchors using `.superview` reference guide.
|
||||
* `FloatingPanelLayoutAnchor(fractionalInset:edge:referenceGuide:)` lets you lay out a panel at a relative position in a reference rectangle area.
|
||||
|
||||
```swift
|
||||
// Before:
|
||||
class MyPanelLayout: FloatingPanelLayout {
|
||||
var initialPosition: FloatingPanelPosition {
|
||||
return .half
|
||||
}
|
||||
|
||||
func insetFor(position: FloatingPanelPosition) -> CGFloat? {
|
||||
switch position {
|
||||
case .full: return 16.0
|
||||
case .half: return 262.0
|
||||
case .tip: return 44.0
|
||||
case .hidden: return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// After:
|
||||
class MyPanelLayout: FloatingPanelLayout {
|
||||
var position: FloatingPanelPosition = .bottom
|
||||
var initialState: FloatingPanelState { .half }
|
||||
var anchors: [FloatingPanelState : FloatingPanelLayoutAnchoring] {
|
||||
return [
|
||||
.full: FloatingPanelLayoutAnchor(absoluteInset: 16.0, edge: .top, referenceGuide: .safeArea),
|
||||
.half: FloatingPanelLayoutAnchor(fractionalInset: 0.5, edge: .bottom, referenceGuide: .superview),
|
||||
.tip: FloatingPanelLayoutAnchor(absoluteInset: 44.0, edge: .bottom, referenceGuide: .safeArea)
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### `FloatingPanelIntrinsicLayoutAnchor`
|
||||
|
||||
This class is used to specify a panel layout using offsets from the intrinsic size layout.
|
||||
|
||||
* This replaces `FloatingPanelIntrinsicLayout`.
|
||||
* This is also able to configure a fractional layout in the intrinsic size.
|
||||
|
||||
```swift
|
||||
// Before:
|
||||
class MyPanelIntrinsicLayout: FloatingPanelIntrinsicLayout {
|
||||
var initialPosition: FloatingPanelPosition {
|
||||
return .half
|
||||
}
|
||||
func insetFor(position: FloatingPanelPosition) -> CGFloat? {
|
||||
switch position {
|
||||
case .full: return 16.0
|
||||
case .half: return 262.0
|
||||
case .tip: return 44.0
|
||||
case .hidden: return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// After:
|
||||
class MyPanelIntrinsicLayout: FloatingPanelLayout {
|
||||
var position: FloatingPanelPosition = .bottom
|
||||
var initialState: FloatingPanelState { .full }
|
||||
var anchors: [FloatingPanelState : FloatingPanelLayoutAnchoring] {
|
||||
return [
|
||||
.full: FloatingPanelIntrinsicLayoutAnchor(absoluteOffset: 16.0, referenceGuide: .safeArea),
|
||||
.half: FloatingPanelIntrinsicLayoutAnchor(fractionalOffset: 0.5, referenceGuide: .safeArea),
|
||||
.tip: FloatingPanelLayoutAnchor(absoluteInset: 44.0, edge: .bottom, referenceGuide: .safeArea)
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Deprecated APIs
|
||||
|
||||
* `.topInteractionBuffer` and `.bottomInteractionBuffer`.
|
||||
* Please control the max/min range of the motion in `floatingPanelDidMove(_:)` delegate method as below.
|
||||
|
||||
```swift
|
||||
func floatingPanelDidMove(_ fpc: FloatingPanelController) {
|
||||
if fpc.isAttracting == false {
|
||||
let loc = fpc.surfaceLocation
|
||||
let minY = fpc.surfaceLocation(for: .full).y - 6.0
|
||||
let maxY = fpc.surfaceLocation(for: .tip).y + 6.0
|
||||
fpc.surfaceLocation = CGPoint(x: loc.x, y: min(max(loc.y, minY), maxY))
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## `FloatingPanelBehavior`
|
||||
|
||||
* `.springDecelerationRate` and `.springResponseTime` properties are added to control the new spring effect of Numeric springing.
|
||||
|
||||
### Deprecated APIs
|
||||
|
||||
* `addAnimator(_:to:)`, `removeAnimator(_:from:)`
|
||||
* They are moved into `floatingPanel(_:animatorForPresentingTo:)` and `floatingPanel(_:animatorForDismissingWith:)` of `FloatingPanelControllerDelegate` because they are used for view transitions.
|
||||
* `interactionAnimator(_:to:with:)`, `moveAnimator(_:from:to:)`
|
||||
* They are removed because the animators are replaced with the new spring effect.
|
||||
* `removalVelocity`, `removalProgress`
|
||||
* They are replaced with `floatingPanel(_:shouldRemoveAt:with:)` of `FloatingPanelControllerDelegate`
|
||||
* `removalInteractionAnimator(_:with:)`
|
||||
* It is integrated with `floatingPanel(_:animatorForDismissingWith:)` of `FloatingPanelControllerDelegate`.
|
||||
|
||||
## `SurfaceView`
|
||||
|
||||
* `SurfaceAppearance` class and `SurfaceView.appearance` property are added to specify the rounding corners, shadows and background color.
|
||||
* `SurfaceView.appearance` property avoids `Ambiguous use of 'cornerRadius'` error, for instance.
|
||||
* `SurfaceAppearance` enables to apply layered box shadows into a surface to materialize it.
|
||||
|
||||
```swift
|
||||
// Before:
|
||||
fpc.surfaceView.cornerRadius = 6.0
|
||||
fpc.surfaceView.backgroundColor = .clear
|
||||
fpc.surfaceView.shadowHidden = false
|
||||
fpc.surfaceView.shadowColor = .black
|
||||
fpc.surfaceView.shadowOffset = CGSize(width: 0, height: 16)
|
||||
fpc.surfaceView.shadowRadius = 16.0
|
||||
|
||||
// After:
|
||||
let appearance = SurfaceAppearance()
|
||||
appearance.cornerRadius = 8.0
|
||||
appearance.backgroundColor = .clear
|
||||
|
||||
let shadow = SurfaceAppearance.Shadow()
|
||||
shadow.color = .black
|
||||
shadow.offset = CGSize(width: 0, height: 16)
|
||||
shadow.radius = 16
|
||||
shadow.spread = 8
|
||||
appearance.shadows = [shadow]
|
||||
|
||||
fpc.surfaceView.appearance = appearance
|
||||
```
|
||||
|
||||
* These properties are changed for the top, left and right positioned panel.
|
||||
* `grabberTopPadding` is now `grabberHandlePadding`.
|
||||
* `topGrabberBarHeight` is now `grabberAreaOffset`.
|
||||
* `grabberHandleWidth` and `grabberHandleHeight` are replaced with `grabberHandleSize`.
|
||||
|
||||
## `BackdropView`
|
||||
|
||||
* The dismissal action of the backdrop is disabled by default.
|
||||
* You can enable it to set `BackdropView.dismissalTapGestureRecognizer.isEnabled` to `true`.
|
||||
|
||||
## `FloatingPanelPanGestureRecognizer`
|
||||
|
||||
* `delegateProxy` property is added to intercept the gesture recognizer delegate.
|
||||
|
||||
```swift
|
||||
func layoutPanelForPad() {
|
||||
fpc.behavior = SearchPaneliPadBehavior()
|
||||
fpc.panGestureRecognizer.delegateProxy = self
|
||||
}
|
||||
|
||||
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRequireFailureOf otherGestureRecognizer: UIGestureRecognizer) -> Bool {
|
||||
return false
|
||||
}
|
||||
```
|
||||
|
||||
## Miscellaneous
|
||||
|
||||
* `UISpringTimingParameters(decelerationRate:frequencyResponse:initialVelocity:)` initializer is added.
|
||||
* The directory structure and file names in the Xcode project changes.
|
||||
@@ -1,9 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="16096" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="BYZ-38-t0r">
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="17506" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="BYZ-38-t0r">
|
||||
<device id="retina5_9" orientation="portrait" appearance="light"/>
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="16087"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="17505"/>
|
||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
@@ -25,9 +25,10 @@
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
</view>
|
||||
<blurEffect style="light"/>
|
||||
<blurEffect style="prominent"/>
|
||||
</visualEffectView>
|
||||
</subviews>
|
||||
<viewLayoutGuide key="safeArea" id="6Tk-OE-BBY"/>
|
||||
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<constraints>
|
||||
<constraint firstItem="5Jw-n2-Cpw" firstAttribute="trailing" secondItem="8bC-Xf-vdC" secondAttribute="trailing" id="1Fg-Be-qfh"/>
|
||||
@@ -39,7 +40,6 @@
|
||||
<constraint firstItem="6Tk-OE-BBY" firstAttribute="top" secondItem="d9i-3g-8Ja" secondAttribute="bottom" id="Y1G-hr-aEX"/>
|
||||
<constraint firstItem="d9i-3g-8Ja" firstAttribute="leading" secondItem="6Tk-OE-BBY" secondAttribute="leading" id="diB-Ij-HN3"/>
|
||||
</constraints>
|
||||
<viewLayoutGuide key="safeArea" id="6Tk-OE-BBY"/>
|
||||
</view>
|
||||
<connections>
|
||||
<outlet property="mapView" destination="5Jw-n2-Cpw" id="WVC-rU-mLe"/>
|
||||
@@ -230,9 +230,10 @@
|
||||
<constraint firstItem="Zcj-SE-gb8" firstAttribute="leading" secondItem="ED1-gT-FBj" secondAttribute="leading" id="wMb-L2-Z0W"/>
|
||||
</constraints>
|
||||
</view>
|
||||
<blurEffect style="extraLight"/>
|
||||
<blurEffect style="prominent"/>
|
||||
</visualEffectView>
|
||||
</subviews>
|
||||
<viewLayoutGuide key="safeArea" id="G74-X7-Za8"/>
|
||||
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<constraints>
|
||||
<constraint firstItem="Ye3-uU-bq3" firstAttribute="leading" secondItem="G74-X7-Za8" secondAttribute="leading" id="Kr2-sU-ZWZ"/>
|
||||
@@ -240,7 +241,6 @@
|
||||
<constraint firstItem="Ye3-uU-bq3" firstAttribute="trailing" secondItem="G74-X7-Za8" secondAttribute="trailing" id="fEL-8y-Acc"/>
|
||||
<constraint firstItem="Ye3-uU-bq3" firstAttribute="top" secondItem="Ncl-E9-yRn" secondAttribute="top" id="w77-ba-FrJ"/>
|
||||
</constraints>
|
||||
<viewLayoutGuide key="safeArea" id="G74-X7-Za8"/>
|
||||
</view>
|
||||
<connections>
|
||||
<outlet property="searchBar" destination="Zcj-SE-gb8" id="BH7-Gy-RG5"/>
|
||||
@@ -262,7 +262,7 @@
|
||||
<subviews>
|
||||
<visualEffectView opaque="NO" contentMode="scaleToFill" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="c3d-2e-0b1">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="812"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" id="9fL-a5-0LS">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="812"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
@@ -282,8 +282,8 @@
|
||||
<blurEffect style="extraLight"/>
|
||||
</visualEffectView>
|
||||
</subviews>
|
||||
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<viewLayoutGuide key="safeArea" id="ctv-Dd-JUc"/>
|
||||
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
</view>
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="EDp-D2-xcT" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
|
||||
|
||||
@@ -21,6 +21,9 @@ extension ViewController: UITableViewDelegate {
|
||||
|
||||
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
tableView.deselectRow(at: indexPath, animated: true)
|
||||
|
||||
deactivate(searchBar: searchVC.searchBar)
|
||||
|
||||
// Show a detail panel
|
||||
switch indexPath.row {
|
||||
case 0:
|
||||
|
||||
@@ -83,14 +83,17 @@ class ViewController: UIViewController {
|
||||
func layoutPanelForPhone() {
|
||||
fpc.track(scrollView: searchVC.tableView) // Only track the tabvle view on iPhone
|
||||
fpc.addPanel(toParent: self, animated: true)
|
||||
fpc.setApearanceForPhone()
|
||||
detailFpc.setApearanceForPhone()
|
||||
fpc.setAppearanceForPhone()
|
||||
detailFpc.setAppearanceForPhone()
|
||||
}
|
||||
}
|
||||
|
||||
extension FloatingPanelController {
|
||||
func setApearanceForPhone() {
|
||||
func setAppearanceForPhone() {
|
||||
let appearance = SurfaceAppearance()
|
||||
if #available(iOS 13.0, *) {
|
||||
appearance.cornerCurve = .continuous
|
||||
}
|
||||
appearance.cornerRadius = 8.0
|
||||
appearance.backgroundColor = .clear
|
||||
surfaceView.appearance = appearance
|
||||
@@ -114,19 +117,27 @@ extension FloatingPanelController {
|
||||
// MARK: - UISearchBarDelegate
|
||||
|
||||
extension ViewController: UISearchBarDelegate {
|
||||
func searchBarCancelButtonClicked(_ searchBar: UISearchBar) {
|
||||
func activate(searchBar: UISearchBar) {
|
||||
searchBar.showsCancelButton = true
|
||||
searchVC.showHeader(animated: true)
|
||||
searchVC.tableView.alpha = 1.0
|
||||
detailVC.dismiss(animated: true, completion: nil)
|
||||
}
|
||||
func deactivate(searchBar: UISearchBar) {
|
||||
searchBar.resignFirstResponder()
|
||||
searchBar.showsCancelButton = false
|
||||
searchVC.hideHeader(animated: true)
|
||||
UIView.animate(withDuration: 0.25) { [weak self] in
|
||||
self?.fpc.move(to: .half, animated: false)
|
||||
}
|
||||
|
||||
func searchBarCancelButtonClicked(_ searchBar: UISearchBar) {
|
||||
deactivate(searchBar: searchBar)
|
||||
UIView.animate(withDuration: 0.25) {
|
||||
self.fpc.move(to: .half, animated: false)
|
||||
}
|
||||
}
|
||||
|
||||
func searchBarTextDidBeginEditing(_ searchBar: UISearchBar) {
|
||||
searchBar.showsCancelButton = true
|
||||
searchVC.showHeader(animated: true)
|
||||
searchVC.tableView.alpha = 1.0
|
||||
activate(searchBar: searchBar)
|
||||
UIView.animate(withDuration: 0.25) { [weak self] in
|
||||
self?.fpc.move(to: .full, animated: false)
|
||||
}
|
||||
|
||||
@@ -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,10 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="16096" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="RoN-h0-uBD">
|
||||
<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="16087"/>
|
||||
<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>
|
||||
@@ -27,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="15" y="0.0" width="345" 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"/>
|
||||
@@ -56,6 +56,7 @@
|
||||
</prototypes>
|
||||
</tableView>
|
||||
</subviews>
|
||||
<viewLayoutGuide key="safeArea" id="39L-Nq-qfp"/>
|
||||
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<constraints>
|
||||
<constraint firstItem="7IS-PU-x0P" firstAttribute="top" secondItem="Smh-Bd-AAc" secondAttribute="top" id="6yd-jv-ey3"/>
|
||||
@@ -63,7 +64,6 @@
|
||||
<constraint firstItem="7IS-PU-x0P" firstAttribute="bottom" secondItem="Smh-Bd-AAc" secondAttribute="bottom" id="fNW-DP-lhV"/>
|
||||
<constraint firstItem="7IS-PU-x0P" firstAttribute="trailing" secondItem="39L-Nq-qfp" secondAttribute="trailing" id="vfY-Rc-FOI"/>
|
||||
</constraints>
|
||||
<viewLayoutGuide key="safeArea" id="39L-Nq-qfp"/>
|
||||
</view>
|
||||
<navigationItem key="navigationItem" title="Samples" id="wCF-su-7up">
|
||||
<barButtonItem key="rightBarButtonItem" title="Settings" id="rbH-U3-XyA">
|
||||
@@ -85,69 +85,79 @@
|
||||
<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" alignment="center" spacing="16" translatesAutoresizingMaskIntoConstraints="NO" id="n93-ZL-fmC">
|
||||
<rect key="frame" x="32" y="16" width="311" height="181.33333333333334"/>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" distribution="fillProportionally" spacing="16" translatesAutoresizingMaskIntoConstraints="NO" id="n93-ZL-fmC">
|
||||
<rect key="frame" x="32" y="16" width="311" height="149.5"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Version: 1.0" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="WmC-Tq-NDN">
|
||||
<rect key="frame" x="118.33333333333334" y="0.0" width="74.333333333333343" height="21"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="14"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="UINavigationBar" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Ulg-gS-ah0">
|
||||
<rect key="frame" x="90.666666666666686" y="37" width="130" height="25"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleHeadline"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" alignment="center" spacing="126" translatesAutoresizingMaskIntoConstraints="NO" id="uEf-g4-CeU">
|
||||
<rect key="frame" x="23.333333333333343" y="78" width="264.66666666666663" height="38"/>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" distribution="fillProportionally" alignment="center" spacing="8" translatesAutoresizingMaskIntoConstraints="NO" id="DCi-Iv-o6d">
|
||||
<rect key="frame" x="0.0" y="0.0" width="311" height="53"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Large Titles" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="ogl-S5-4tJ">
|
||||
<rect key="frame" x="0.0" y="8.9999999999999982" width="89.666666666666671" height="20.333333333333329"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Version: 1.0" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="WmC-Tq-NDN">
|
||||
<rect key="frame" x="118.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.5" y="25.5" width="154.5" height="27.5"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleTitle2"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<switch opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" contentHorizontalAlignment="center" contentVerticalAlignment="center" translatesAutoresizingMaskIntoConstraints="NO" id="js8-Qv-lUC">
|
||||
<rect key="frame" x="215.66666666666666" y="3.6666666666666714" width="50.999999999999972" height="31"/>
|
||||
<connections>
|
||||
<action selector="toggleLargeTitle:" destination="C1X-9Z-TyQ" eventType="valueChanged" id="FJS-Ty-mCY"/>
|
||||
</connections>
|
||||
</switch>
|
||||
</subviews>
|
||||
</stackView>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" spacing="126" translatesAutoresizingMaskIntoConstraints="NO" id="ZtZ-Dz-4cC">
|
||||
<rect key="frame" x="23.333333333333343" y="132" width="264.66666666666663" height="49.333333333333343"/>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" distribution="fillProportionally" spacing="16" translatesAutoresizingMaskIntoConstraints="NO" id="J8j-7w-yCZ">
|
||||
<rect key="frame" x="0.0" y="69" width="311" height="80.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="0.0" width="89.666666666666671" height="49.333333333333336"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<switch opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" contentHorizontalAlignment="center" contentVerticalAlignment="center" translatesAutoresizingMaskIntoConstraints="NO" id="s6b-j9-8Kw">
|
||||
<rect key="frame" x="215.66666666666666" y="0.0" width="50.999999999999972" height="49.333333333333336"/>
|
||||
<connections>
|
||||
<action selector="toggleTranslucent:" destination="C1X-9Z-TyQ" eventType="valueChanged" id="nL4-3L-9hh"/>
|
||||
</connections>
|
||||
</switch>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" alignment="center" spacingType="standard" translatesAutoresizingMaskIntoConstraints="NO" id="uEf-g4-CeU">
|
||||
<rect key="frame" x="0.0" y="0.0" width="311" height="32"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Large Titles" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="ogl-S5-4tJ">
|
||||
<rect key="frame" x="0.0" y="5.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.5" width="51" height="31"/>
|
||||
<connections>
|
||||
<action selector="toggleLargeTitle:" destination="C1X-9Z-TyQ" eventType="valueChanged" id="FJS-Ty-mCY"/>
|
||||
</connections>
|
||||
</switch>
|
||||
</subviews>
|
||||
</stackView>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" alignment="center" spacingType="standard" translatesAutoresizingMaskIntoConstraints="NO" id="ZtZ-Dz-4cC">
|
||||
<rect key="frame" x="0.0" y="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" 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="0.5" width="51" height="31"/>
|
||||
<connections>
|
||||
<action selector="toggleTranslucent:" destination="C1X-9Z-TyQ" eventType="valueChanged" id="nL4-3L-9hh"/>
|
||||
</connections>
|
||||
</switch>
|
||||
</subviews>
|
||||
</stackView>
|
||||
</subviews>
|
||||
</stackView>
|
||||
</subviews>
|
||||
</stackView>
|
||||
</subviews>
|
||||
<viewLayoutGuide key="safeArea" id="0hr-ty-yWm"/>
|
||||
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<constraints>
|
||||
<constraint firstItem="0hr-ty-yWm" firstAttribute="bottom" secondItem="n93-ZL-fmC" secondAttribute="bottom" id="2Ey-ou-E1M"/>
|
||||
<constraint firstAttribute="bottom" secondItem="n93-ZL-fmC" secondAttribute="bottom" constant="32" id="2Ey-ou-E1M"/>
|
||||
<constraint firstAttribute="trailing" secondItem="n93-ZL-fmC" secondAttribute="trailing" constant="32" id="DdZ-eB-F5s"/>
|
||||
<constraint firstItem="n93-ZL-fmC" firstAttribute="leading" secondItem="af9-Zr-Ppc" secondAttribute="leading" constant="32" id="TyK-GP-Ari"/>
|
||||
<constraint firstItem="n93-ZL-fmC" firstAttribute="top" secondItem="af9-Zr-Ppc" secondAttribute="topMargin" constant="16" id="mbC-6H-z9M"/>
|
||||
</constraints>
|
||||
<viewLayoutGuide key="safeArea" id="0hr-ty-yWm"/>
|
||||
</view>
|
||||
<size key="freeformSize" width="375" height="197.33000000000001"/>
|
||||
<connections>
|
||||
@@ -165,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="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="IvG-yp-yzI">
|
||||
<rect key="frame" x="20" y="44" width="39" height="30"/>
|
||||
<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="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"/>
|
||||
@@ -177,13 +187,13 @@
|
||||
</connections>
|
||||
</button>
|
||||
</subviews>
|
||||
<viewLayoutGuide key="safeArea" id="954-Dk-zvc"/>
|
||||
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<constraints>
|
||||
<constraint firstItem="IvG-yp-yzI" firstAttribute="top" secondItem="954-Dk-zvc" secondAttribute="top" id="18k-sV-PgT"/>
|
||||
<constraint firstItem="954-Dk-zvc" firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="IvG-yp-yzI" secondAttribute="trailing" id="mpr-u5-MZu"/>
|
||||
<constraint firstItem="IvG-yp-yzI" firstAttribute="leading" secondItem="954-Dk-zvc" secondAttribute="leading" constant="20" id="pYt-jE-CTF"/>
|
||||
</constraints>
|
||||
<viewLayoutGuide key="safeArea" id="954-Dk-zvc"/>
|
||||
</view>
|
||||
<tabBarItem key="tabBarItem" tag="1" title="Layout 2" id="qb3-RB-B28"/>
|
||||
</viewController>
|
||||
@@ -196,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="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="NbG-e8-HdI">
|
||||
<rect key="frame" x="20" y="44" width="39" height="30"/>
|
||||
<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="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"/>
|
||||
@@ -208,13 +218,13 @@
|
||||
</connections>
|
||||
</button>
|
||||
</subviews>
|
||||
<viewLayoutGuide key="safeArea" id="0ao-SI-QZW"/>
|
||||
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<constraints>
|
||||
<constraint firstItem="0ao-SI-QZW" firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="NbG-e8-HdI" secondAttribute="trailing" id="K9F-6x-KWn"/>
|
||||
<constraint firstItem="NbG-e8-HdI" firstAttribute="top" secondItem="0ao-SI-QZW" secondAttribute="top" id="nsE-so-rTl"/>
|
||||
<constraint firstItem="NbG-e8-HdI" firstAttribute="leading" secondItem="0ao-SI-QZW" secondAttribute="leading" constant="20" id="sF4-Dm-aoY"/>
|
||||
</constraints>
|
||||
<viewLayoutGuide key="safeArea" id="0ao-SI-QZW"/>
|
||||
</view>
|
||||
<tabBarItem key="tabBarItem" tag="2" title="Layout 3" id="RJD-TF-Sdh"/>
|
||||
</viewController>
|
||||
@@ -227,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="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="eFN-tN-4Ct">
|
||||
<rect key="frame" x="20" y="44" width="39" height="30"/>
|
||||
<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="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"/>
|
||||
@@ -239,13 +249,13 @@
|
||||
</connections>
|
||||
</button>
|
||||
</subviews>
|
||||
<viewLayoutGuide key="safeArea" id="5Ns-4l-Ufg"/>
|
||||
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<constraints>
|
||||
<constraint firstItem="eFN-tN-4Ct" firstAttribute="leading" secondItem="5Ns-4l-Ufg" secondAttribute="leading" constant="20" id="5BT-yZ-EKe"/>
|
||||
<constraint firstItem="5Ns-4l-Ufg" firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="eFN-tN-4Ct" secondAttribute="trailing" id="OzZ-Dz-RNF"/>
|
||||
<constraint firstItem="eFN-tN-4Ct" firstAttribute="top" secondItem="5Ns-4l-Ufg" secondAttribute="top" id="hUV-3a-XkY"/>
|
||||
</constraints>
|
||||
<viewLayoutGuide key="safeArea" id="5Ns-4l-Ufg"/>
|
||||
</view>
|
||||
<tabBarItem key="tabBarItem" title="Layout 1" id="HEV-kf-jxH"/>
|
||||
</viewController>
|
||||
@@ -262,20 +272,19 @@
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="1000" verticalHuggingPriority="1000" horizontalCompressionResistancePriority="1000" verticalCompressionResistancePriority="1000" text="Change this text" textAlignment="center" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="ge4-RW-Gmz">
|
||||
<rect key="frame" x="24" y="24" width="327" height="20.333333333333329"/>
|
||||
<rect key="frame" x="125.5" y="24" width="124" height="20.5"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
<viewLayoutGuide key="safeArea" id="ouu-g9-OiX"/>
|
||||
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="trailing" secondItem="ge4-RW-Gmz" secondAttribute="trailing" constant="24" id="V59-MD-Lcg"/>
|
||||
<constraint firstItem="ge4-RW-Gmz" firstAttribute="leading" secondItem="eLM-xc-d9e" secondAttribute="leading" constant="24" id="hAO-P0-7Kw"/>
|
||||
<constraint firstItem="ge4-RW-Gmz" firstAttribute="top" secondItem="eLM-xc-d9e" secondAttribute="top" constant="24" id="j0s-fd-MYj"/>
|
||||
<constraint firstAttribute="bottom" relation="greaterThanOrEqual" secondItem="ge4-RW-Gmz" secondAttribute="bottom" constant="24" id="tEn-PO-nVD"/>
|
||||
<constraint firstItem="ge4-RW-Gmz" firstAttribute="centerX" secondItem="eLM-xc-d9e" secondAttribute="centerX" id="vh3-l7-uY8"/>
|
||||
</constraints>
|
||||
<viewLayoutGuide key="safeArea" id="ouu-g9-OiX"/>
|
||||
</view>
|
||||
<size key="freeformSize" width="375" height="778"/>
|
||||
</viewController>
|
||||
@@ -283,6 +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>
|
||||
@@ -307,14 +381,14 @@
|
||||
<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="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="sbF-Az-7sy">
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="sbF-Az-7sy">
|
||||
<rect key="frame" x="20" y="0.0" width="39" height="30"/>
|
||||
<state key="normal" title="Close"/>
|
||||
<connections>
|
||||
@@ -322,37 +396,37 @@
|
||||
</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="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="i9x-x5-n1q">
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="i9x-x5-n1q">
|
||||
<rect key="frame" x="0.0" y="0.0" width="80" height="30"/>
|
||||
<state key="normal" title="Move to full"/>
|
||||
<connections>
|
||||
<action selector="moveToFullWithSender:" destination="bYI-y3-Rzb" eventType="touchUpInside" id="TDe-3J-gIR"/>
|
||||
</connections>
|
||||
</button>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="2u5-cH-RAN">
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="2u5-cH-RAN">
|
||||
<rect key="frame" x="0.0" y="74" width="85" height="30"/>
|
||||
<state key="normal" title="Move to half"/>
|
||||
<connections>
|
||||
<action selector="moveToHalfWithSender:" destination="bYI-y3-Rzb" eventType="touchUpInside" id="12s-o7-Et5"/>
|
||||
</connections>
|
||||
</button>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="M4A-iO-RIE">
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="M4A-iO-RIE">
|
||||
<rect key="frame" x="0.0" y="148" width="77" height="30"/>
|
||||
<state key="normal" title="Move to tip"/>
|
||||
<connections>
|
||||
<action selector="moveToTipWithSender:" destination="bYI-y3-Rzb" eventType="touchUpInside" id="BmL-91-9ai"/>
|
||||
</connections>
|
||||
</button>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="swr-XM-GzZ">
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="swr-XM-GzZ">
|
||||
<rect key="frame" x="0.0" y="222" width="106" height="30"/>
|
||||
<state key="normal" title="Move to hidden"/>
|
||||
<connections>
|
||||
<action selector="moveToHiddenWithSender:" destination="bYI-y3-Rzb" eventType="touchUpInside" id="jfJ-0f-fdk"/>
|
||||
</connections>
|
||||
</button>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="szf-HE-QTk">
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="szf-HE-QTk">
|
||||
<rect key="frame" x="0.0" y="296" width="96" height="30"/>
|
||||
<state key="normal" title="Update layout"/>
|
||||
<connections>
|
||||
@@ -362,6 +436,7 @@
|
||||
</subviews>
|
||||
</stackView>
|
||||
</subviews>
|
||||
<viewLayoutGuide key="safeArea" id="kjr-TP-fcM"/>
|
||||
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<constraints>
|
||||
<constraint firstItem="sbF-Az-7sy" firstAttribute="top" secondItem="kjr-TP-fcM" secondAttribute="top" id="3VR-hj-zeQ"/>
|
||||
@@ -373,7 +448,6 @@
|
||||
<constraint firstItem="9p4-06-y2T" firstAttribute="centerX" secondItem="kjr-TP-fcM" secondAttribute="centerX" id="l8t-p3-ETf"/>
|
||||
<constraint firstItem="vut-mK-Y4t" firstAttribute="top" secondItem="kjr-TP-fcM" secondAttribute="bottom" id="rMy-JT-t4B"/>
|
||||
</constraints>
|
||||
<viewLayoutGuide key="safeArea" id="kjr-TP-fcM"/>
|
||||
</view>
|
||||
<size key="freeformSize" width="375" height="778"/>
|
||||
<connections>
|
||||
@@ -466,6 +540,7 @@
|
||||
</constraints>
|
||||
</scrollView>
|
||||
</subviews>
|
||||
<viewLayoutGuide key="safeArea" id="ufS-Rf-F2F"/>
|
||||
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<gestureRecognizers/>
|
||||
<constraints>
|
||||
@@ -474,7 +549,6 @@
|
||||
<constraint firstAttribute="bottom" secondItem="sBe-tN-uMi" secondAttribute="bottom" id="jzB-47-P7e"/>
|
||||
<constraint firstItem="ufS-Rf-F2F" firstAttribute="trailing" secondItem="sBe-tN-uMi" secondAttribute="trailing" id="nHG-wg-pLP"/>
|
||||
</constraints>
|
||||
<viewLayoutGuide key="safeArea" id="ufS-Rf-F2F"/>
|
||||
<connections>
|
||||
<outletCollection property="gestureRecognizers" destination="tOa-bf-zGz" appends="YES" id="zle-Sz-M3U"/>
|
||||
<outletCollection property="gestureRecognizers" destination="SCk-hG-weZ" appends="YES" id="OcK-FK-Lac"/>
|
||||
@@ -522,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="roundedRect" 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"/>
|
||||
<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="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"/>
|
||||
@@ -536,16 +610,16 @@
|
||||
</connections>
|
||||
</button>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" distribution="fillProportionally" alignment="center" spacing="8" translatesAutoresizingMaskIntoConstraints="NO" id="qux-uG-4o2">
|
||||
<rect key="frame" x="8" y="52" width="148.33333333333334" height="31"/>
|
||||
<rect key="frame" x="8" y="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.333333333333329" 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"/>
|
||||
</label>
|
||||
<switch opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" contentHorizontalAlignment="center" contentVerticalAlignment="center" on="YES" translatesAutoresizingMaskIntoConstraints="NO" id="0MA-lV-KjS">
|
||||
<rect key="frame" x="99.333333333333329" y="0.0" width="50.999999999999986" height="31"/>
|
||||
<rect key="frame" x="99" y="0.0" width="51" height="31"/>
|
||||
<connections>
|
||||
<action selector="modeChanged:" destination="YC8-ae-15L" eventType="valueChanged" id="IQ8-u2-Rib"/>
|
||||
</connections>
|
||||
@@ -553,16 +627,16 @@
|
||||
</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="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="c5r-jU-haj">
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="c5r-jU-haj">
|
||||
<rect key="frame" x="0.0" y="0.0" width="114" height="30"/>
|
||||
<state key="normal" title="Show"/>
|
||||
<connections>
|
||||
<action selector="buttonPressed:" destination="YC8-ae-15L" eventType="touchUpInside" id="Mi1-o6-TWt"/>
|
||||
</connections>
|
||||
</button>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="wmd-ab-Nz3">
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="wmd-ab-Nz3">
|
||||
<rect key="frame" x="0.0" y="52" width="114" height="30"/>
|
||||
<state key="normal" title="Present Modallly"/>
|
||||
<connections>
|
||||
@@ -570,7 +644,7 @@
|
||||
<segue destination="bYI-y3-Rzb" kind="presentation" identifier="PresentModallySegue" id="3yq-HE-Tgn"/>
|
||||
</connections>
|
||||
</button>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="01L-lp-oy6">
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="01L-lp-oy6">
|
||||
<rect key="frame" x="0.0" y="104" width="114" height="30"/>
|
||||
<state key="normal" title="Update Layout"/>
|
||||
<connections>
|
||||
@@ -580,6 +654,7 @@
|
||||
</subviews>
|
||||
</stackView>
|
||||
</subviews>
|
||||
<viewLayoutGuide key="safeArea" id="aOK-7l-cA6"/>
|
||||
<color key="backgroundColor" red="1" green="0.83234566450000003" blue="0.47320586440000001" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<gestureRecognizers/>
|
||||
<constraints>
|
||||
@@ -598,7 +673,6 @@
|
||||
<constraint firstItem="8yw-OC-Ubk" firstAttribute="top" relation="greaterThanOrEqual" secondItem="tP3-oJ-4EB" secondAttribute="bottom" constant="88" id="vKQ-h9-uKt"/>
|
||||
<constraint firstItem="qux-uG-4o2" firstAttribute="leading" secondItem="g7l-kO-y7q" secondAttribute="leading" constant="8" id="zXb-R9-bMO"/>
|
||||
</constraints>
|
||||
<viewLayoutGuide key="safeArea" id="aOK-7l-cA6"/>
|
||||
<connections>
|
||||
<outletCollection property="gestureRecognizers" destination="6Ca-p8-7uF" appends="YES" id="xOy-f1-NZE"/>
|
||||
<outletCollection property="gestureRecognizers" destination="SPY-Vr-XDT" appends="YES" id="vgS-Am-jhQ"/>
|
||||
@@ -686,6 +760,7 @@ Section 1.10.33 of "de Finibus Bonorum et Malorum", written by Cicero in 45 BC
|
||||
<textInputTraits key="textInputTraits" autocapitalizationType="sentences"/>
|
||||
</textView>
|
||||
</subviews>
|
||||
<viewLayoutGuide key="safeArea" id="5ET-zC-lCb"/>
|
||||
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<constraints>
|
||||
<constraint firstItem="rN1-HL-YHv" firstAttribute="leading" secondItem="5ET-zC-lCb" secondAttribute="leading" id="7V3-KL-vXd"/>
|
||||
@@ -693,7 +768,6 @@ Section 1.10.33 of "de Finibus Bonorum et Malorum", written by Cicero in 45 BC
|
||||
<constraint firstItem="rN1-HL-YHv" firstAttribute="top" secondItem="9YG-0j-Zzg" secondAttribute="top" constant="17" id="fiO-LL-nSC"/>
|
||||
<constraint firstItem="rN1-HL-YHv" firstAttribute="trailing" secondItem="5ET-zC-lCb" secondAttribute="trailing" id="lfg-EE-euw"/>
|
||||
</constraints>
|
||||
<viewLayoutGuide key="safeArea" id="5ET-zC-lCb"/>
|
||||
</view>
|
||||
<size key="freeformSize" width="375" height="778"/>
|
||||
<connections>
|
||||
@@ -710,4 +784,19 @@ Section 1.10.33 of "de Finibus Bonorum et Malorum", written by Cicero in 45 BC
|
||||
<inferredMetricsTieBreakers>
|
||||
<segue reference="r1P-2i-NDe"/>
|
||||
</inferredMetricsTieBreakers>
|
||||
<resources>
|
||||
<image name="IMG_0003" width="750" height="501"/>
|
||||
<systemColor name="systemBackgroundColor">
|
||||
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
</systemColor>
|
||||
<systemColor name="systemOrangeColor">
|
||||
<color red="1" green="0.58431372549019611" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
</systemColor>
|
||||
<systemColor name="systemPurpleColor">
|
||||
<color red="0.68627450980392157" green="0.32156862745098042" blue="0.87058823529411766" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
</systemColor>
|
||||
<systemColor name="systemTealColor">
|
||||
<color red="0.35294117647058826" green="0.78431372549019607" blue="0.98039215686274506" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
</systemColor>
|
||||
</resources>
|
||||
</document>
|
||||
|
||||
+3
-3
@@ -3,7 +3,7 @@
|
||||
import UIKit
|
||||
|
||||
@IBDesignable
|
||||
class CloseButton: UIButton {
|
||||
final class CloseButton: UIButton {
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
super.init(coder: aDecoder)
|
||||
render()
|
||||
@@ -71,7 +71,7 @@ class CloseButton: UIButton {
|
||||
}
|
||||
|
||||
@IBDesignable
|
||||
class SafeAreaView: UIView {
|
||||
final class SafeAreaView: UIView {
|
||||
override func prepareForInterfaceBuilder() {
|
||||
let label = UILabel()
|
||||
label.text = "Safe Area"
|
||||
@@ -86,7 +86,7 @@ class SafeAreaView: UIView {
|
||||
|
||||
|
||||
@IBDesignable
|
||||
class OnSafeAreaView: UIView {
|
||||
final class OnSafeAreaView: UIView {
|
||||
override func prepareForInterfaceBuilder() {
|
||||
let label = UILabel()
|
||||
label.text = "On Safe Area"
|
||||
@@ -0,0 +1,90 @@
|
||||
// Copyright 2018 the FloatingPanel authors. All rights reserved. MIT license.
|
||||
|
||||
import UIKit
|
||||
import FloatingPanel
|
||||
|
||||
/**
|
||||
- Attention: `FloatingPanelLayout` must not be applied by the parent view
|
||||
controller of a panel. But here `MainViewController` adopts it
|
||||
purposely to check if the library prints an appropriate warning.
|
||||
*/
|
||||
extension MainViewController: FloatingPanelLayout {
|
||||
var position: FloatingPanelPosition { .bottom }
|
||||
var initialState: FloatingPanelState { .half }
|
||||
var anchors: [FloatingPanelState: FloatingPanelLayoutAnchoring] {
|
||||
return [
|
||||
.full: FloatingPanelLayoutAnchor(absoluteInset: UIScreen.main.bounds.height == 667.0 ? 18.0 : 16.0, edge: .top, referenceGuide: .safeArea),
|
||||
.half: FloatingPanelLayoutAnchor(absoluteInset: 262.0, edge: .bottom, referenceGuide: .safeArea),
|
||||
.tip: FloatingPanelLayoutAnchor(absoluteInset: 69.0, edge: .bottom, referenceGuide: .safeArea)
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
class TopPositionedPanelLayout: FloatingPanelLayout {
|
||||
let position: FloatingPanelPosition = .top
|
||||
let initialState: FloatingPanelState = .full
|
||||
|
||||
var anchors: [FloatingPanelState : FloatingPanelLayoutAnchoring] {
|
||||
return [
|
||||
.full: FloatingPanelLayoutAnchor(absoluteInset: 88.0, edge: .bottom, referenceGuide: .safeArea),
|
||||
.half: FloatingPanelLayoutAnchor(absoluteInset: 216.0, edge: .top, referenceGuide: .safeArea),
|
||||
.tip: FloatingPanelLayoutAnchor(absoluteInset: 44.0, edge: .top, referenceGuide: .safeArea)
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
class IntrinsicPanelLayout: FloatingPanelBottomLayout {
|
||||
override var initialState: FloatingPanelState { .full }
|
||||
override var anchors: [FloatingPanelState : FloatingPanelLayoutAnchoring] {
|
||||
return [
|
||||
.full: FloatingPanelIntrinsicLayoutAnchor(fractionalOffset: 0.0, referenceGuide: .safeArea)
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
class RemovablePanelLayout: FloatingPanelLayout {
|
||||
let position: FloatingPanelPosition = .bottom
|
||||
let initialState: FloatingPanelState = .half
|
||||
|
||||
var anchors: [FloatingPanelState : FloatingPanelLayoutAnchoring] {
|
||||
return [
|
||||
.full: FloatingPanelIntrinsicLayoutAnchor(fractionalOffset: 0.0, referenceGuide: .safeArea),
|
||||
.half: FloatingPanelLayoutAnchor(absoluteInset: 130.0, edge: .bottom, referenceGuide: .safeArea)
|
||||
]
|
||||
}
|
||||
|
||||
func backdropAlpha(for state: FloatingPanelState) -> CGFloat {
|
||||
return 0.3
|
||||
}
|
||||
}
|
||||
|
||||
class RemovablePanelLandscapeLayout: FloatingPanelLayout {
|
||||
let position: FloatingPanelPosition = .bottom
|
||||
let initialState: FloatingPanelState = .full
|
||||
|
||||
var anchors: [FloatingPanelState : FloatingPanelLayoutAnchoring] {
|
||||
return [
|
||||
.full: FloatingPanelIntrinsicLayoutAnchor(fractionalOffset: 0.0, referenceGuide: .safeArea),
|
||||
.half: FloatingPanelLayoutAnchor(absoluteInset: 216.0, edge: .bottom, referenceGuide: .safeArea)
|
||||
]
|
||||
}
|
||||
|
||||
func backdropAlpha(for state: FloatingPanelState) -> CGFloat {
|
||||
return 0.3
|
||||
}
|
||||
}
|
||||
|
||||
class ModalPanelLayout: FloatingPanelLayout {
|
||||
let position: FloatingPanelPosition = .bottom
|
||||
let initialState: FloatingPanelState = .full
|
||||
|
||||
var anchors: [FloatingPanelState : FloatingPanelLayoutAnchoring] {
|
||||
return [
|
||||
.full: FloatingPanelIntrinsicLayoutAnchor(absoluteOffset: 0.0, referenceGuide: .safeArea),
|
||||
]
|
||||
}
|
||||
|
||||
func backdropAlpha(for state: FloatingPanelState) -> CGFloat {
|
||||
return 0.3
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
// Copyright 2018 the FloatingPanel authors. All rights reserved. MIT license.
|
||||
|
||||
import UIKit
|
||||
import FloatingPanel
|
||||
|
||||
final class MainViewController: UIViewController {
|
||||
@IBOutlet weak var tableView: UITableView!
|
||||
private var observations: [NSKeyValueObservation] = []
|
||||
|
||||
private lazy var useCaseController = UseCaseController(mainVC: self)
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
tableView.dataSource = self
|
||||
tableView.delegate = self
|
||||
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "Cell")
|
||||
automaticallyAdjustsScrollViewInsets = false
|
||||
|
||||
let searchController = UISearchController(searchResultsController: nil)
|
||||
if #available(iOS 11.0, *) {
|
||||
navigationItem.searchController = searchController
|
||||
navigationItem.hidesSearchBarWhenScrolling = false
|
||||
navigationItem.largeTitleDisplayMode = .automatic
|
||||
} else {
|
||||
// Fallback on earlier versions
|
||||
}
|
||||
var insets = UIEdgeInsets.zero
|
||||
insets.bottom += 69.0
|
||||
tableView.contentInset = insets
|
||||
|
||||
// Show the initial panel
|
||||
useCaseController.set(useCase: .trackingTableView)
|
||||
}
|
||||
|
||||
override func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
if #available(iOS 11.0, *) {
|
||||
if let observation = navigationController?.navigationBar.observe(\.prefersLargeTitles, changeHandler: { (bar, _) in
|
||||
self.tableView.reloadData()
|
||||
}) {
|
||||
observations.append(observation)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override func viewWillDisappear(_ animated: Bool) {
|
||||
super.viewWillDisappear(animated)
|
||||
observations.removeAll()
|
||||
}
|
||||
|
||||
// MARK:- Actions
|
||||
@IBAction func showDebugMenu(_ sender: UIBarButtonItem) {
|
||||
useCaseController.setUpSettingsPanel(for: self)
|
||||
}
|
||||
}
|
||||
|
||||
extension MainViewController: UITableViewDataSource {
|
||||
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
if #available(iOS 11.0, *) {
|
||||
if navigationController?.navigationBar.prefersLargeTitles == true {
|
||||
return UseCase.allCases.count + 30
|
||||
} else {
|
||||
return UseCase.allCases.count
|
||||
}
|
||||
} else {
|
||||
return UseCase.allCases.count
|
||||
}
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
|
||||
if UseCase.allCases.count > indexPath.row {
|
||||
let menu = UseCase.allCases[indexPath.row]
|
||||
cell.textLabel?.text = menu.name
|
||||
} else {
|
||||
cell.textLabel?.text = "\(indexPath.row) row"
|
||||
}
|
||||
return cell
|
||||
}
|
||||
}
|
||||
|
||||
extension MainViewController: UITableViewDelegate {
|
||||
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
guard UseCase.allCases.count > indexPath.row else { return }
|
||||
|
||||
// Change panels
|
||||
useCaseController.set(useCase: UseCase.allCases[indexPath.row])
|
||||
}
|
||||
|
||||
@objc func dismissPresentedVC() {
|
||||
self.presentedViewController?.dismiss(animated: true, completion: nil)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,329 @@
|
||||
// Copyright 2018-Present Shin Yamamoto. All rights reserved. MIT license.
|
||||
|
||||
import UIKit
|
||||
import FloatingPanel
|
||||
|
||||
final class UseCaseController: NSObject {
|
||||
unowned let mainVC: MainViewController
|
||||
private(set) var useCase: UseCase = .trackingTableView
|
||||
|
||||
fileprivate var mainPanelVC: FloatingPanelController!
|
||||
private var detailPanelVC: FloatingPanelController!
|
||||
private var settingsPanelVC: FloatingPanelController!
|
||||
|
||||
private lazy var pagePanelController = PagePanelController()
|
||||
|
||||
private var mainPanelObserves: [NSKeyValueObservation] = []
|
||||
|
||||
init(mainVC: MainViewController) {
|
||||
self.mainVC = mainVC
|
||||
}
|
||||
|
||||
func set(useCase: UseCase) {
|
||||
self.useCase = useCase
|
||||
|
||||
let contentVC = useCase.makeContentViewController(with: mainVC.storyboard!)
|
||||
|
||||
detailPanelVC?.removePanelFromParent(animated: true, completion: nil)
|
||||
detailPanelVC = nil
|
||||
|
||||
switch useCase {
|
||||
case .showDetail:
|
||||
detailPanelVC?.removePanelFromParent(animated: false)
|
||||
|
||||
// Initialize FloatingPanelController
|
||||
detailPanelVC = FloatingPanelController()
|
||||
detailPanelVC.delegate = self
|
||||
|
||||
let appearance = SurfaceAppearance()
|
||||
appearance.cornerRadius = 6.0
|
||||
detailPanelVC.surfaceView.appearance = appearance
|
||||
|
||||
// Set a content view controller
|
||||
detailPanelVC.set(contentViewController: contentVC)
|
||||
|
||||
detailPanelVC.contentMode = .fitToBounds
|
||||
(contentVC as? DetailViewController)?.intrinsicHeightConstraint.isActive = false
|
||||
|
||||
// Add FloatingPanel to self.view
|
||||
detailPanelVC.addPanel(toParent: mainVC, animated: true)
|
||||
case .showModal, .showTabBar:
|
||||
let modalVC = contentVC
|
||||
modalVC.modalPresentationStyle = .fullScreen
|
||||
mainVC.present(modalVC, animated: true, completion: nil)
|
||||
|
||||
case .showPageView:
|
||||
let pageVC = pagePanelController.makePageViewController(for: mainVC)
|
||||
mainVC.present(pageVC, animated: true, completion: nil)
|
||||
|
||||
case .showPageContentView:
|
||||
let pageVC = pagePanelController.makePageViewControllerForContent()
|
||||
self.addMainPanel(with: pageVC)
|
||||
case .showPanelModal:
|
||||
let fpc = FloatingPanelController()
|
||||
let contentVC = mainVC.storyboard!.instantiateViewController(withIdentifier: "DetailViewController")
|
||||
contentVC.loadViewIfNeeded()
|
||||
(contentVC as? DetailViewController)?.modeChangeView.isHidden = true
|
||||
fpc.set(contentViewController: contentVC)
|
||||
fpc.delegate = self
|
||||
|
||||
let appearance = SurfaceAppearance()
|
||||
appearance.cornerRadius = 38.5
|
||||
fpc.surfaceView.appearance = appearance
|
||||
fpc.backdropView.dismissalTapGestureRecognizer.isEnabled = true
|
||||
|
||||
fpc.isRemovalInteractionEnabled = true
|
||||
|
||||
mainVC.present(fpc, animated: true, completion: nil)
|
||||
|
||||
case .showMultiPanelModal:
|
||||
let fpc = MultiPanelController()
|
||||
mainVC.present(fpc, animated: true, completion: nil)
|
||||
|
||||
case .showPanelInSheetModal:
|
||||
let fpc = FloatingPanelController()
|
||||
let contentVC = UIViewController()
|
||||
fpc.set(contentViewController: contentVC)
|
||||
fpc.delegate = self
|
||||
|
||||
let apprearance = SurfaceAppearance()
|
||||
apprearance.cornerRadius = 38.5
|
||||
apprearance.shadows = []
|
||||
fpc.surfaceView.appearance = apprearance
|
||||
fpc.isRemovalInteractionEnabled = true
|
||||
|
||||
let mvc = UIViewController()
|
||||
mvc.view.backgroundColor = UIColor(displayP3Red: 2/255, green: 184/255, blue: 117/255, alpha: 1.0)
|
||||
fpc.addPanel(toParent: mvc)
|
||||
mainVC.present(mvc, animated: true, completion: nil)
|
||||
case .showContentInset:
|
||||
let contentViewController = UIViewController()
|
||||
contentViewController.view.backgroundColor = .green
|
||||
|
||||
let fpc = FloatingPanelController()
|
||||
fpc.set(contentViewController: contentViewController)
|
||||
fpc.surfaceView.contentPadding = .init(top: 20, left: 20, bottom: 20, right: 20)
|
||||
|
||||
fpc.delegate = self
|
||||
fpc.isRemovalInteractionEnabled = true
|
||||
mainVC.present(fpc, animated: true, completion: nil)
|
||||
|
||||
case .showContainerMargins:
|
||||
let fpc = FloatingPanelController()
|
||||
|
||||
let appearance = SurfaceAppearance()
|
||||
appearance.cornerRadius = 38.5
|
||||
fpc.surfaceView.appearance = appearance
|
||||
|
||||
fpc.surfaceView.backgroundColor = .red
|
||||
fpc.surfaceView.containerMargins = .init(top: 24.0, left: 8.0, bottom: max(mainVC.layoutInsets.bottom, 8.0), right: 8.0)
|
||||
#if swift(>=5.1) // Actually Xcode 11 or later
|
||||
if #available(iOS 13.0, *) {
|
||||
fpc.surfaceView.layer.cornerCurve = .continuous
|
||||
}
|
||||
#endif
|
||||
|
||||
fpc.delegate = self
|
||||
fpc.isRemovalInteractionEnabled = true
|
||||
mainVC.present(fpc, animated: true, completion: nil)
|
||||
default:
|
||||
self.addMainPanel(with: contentVC)
|
||||
}
|
||||
}
|
||||
|
||||
private func addMainPanel(with contentVC: UIViewController) {
|
||||
mainPanelObserves.removeAll()
|
||||
|
||||
let oldMainPanelVC = mainPanelVC
|
||||
|
||||
mainPanelVC = FloatingPanelController()
|
||||
mainPanelVC.delegate = self
|
||||
mainPanelVC.contentInsetAdjustmentBehavior = .always
|
||||
|
||||
let appearance = SurfaceAppearance()
|
||||
appearance.cornerRadius = 6.0
|
||||
mainPanelVC.surfaceView.appearance = appearance
|
||||
|
||||
set(contentViewController: contentVC)
|
||||
|
||||
useCase.setUpInteraction(for: self)
|
||||
|
||||
// Add FloatingPanel to self.view
|
||||
if let oldMainPanelVC = oldMainPanelVC {
|
||||
oldMainPanelVC.removePanelFromParent(animated: true, completion: {
|
||||
self.mainPanelVC.addPanel(toParent: self.mainVC, animated: true)
|
||||
})
|
||||
} else {
|
||||
mainPanelVC.addPanel(toParent: mainVC, animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
private func set(contentViewController contentVC: UIViewController) {
|
||||
mainPanelVC.set(contentViewController: contentVC)
|
||||
// Track a scroll view
|
||||
switch contentVC {
|
||||
case let consoleVC as DebugTextViewController:
|
||||
mainPanelVC.track(scrollView: consoleVC.textView)
|
||||
|
||||
case let contentVC as DebugTableViewController:
|
||||
let ob = contentVC.tableView.observe(\.isEditing) { (tableView, _) in
|
||||
self.mainPanelVC.panGestureRecognizer.isEnabled = !tableView.isEditing
|
||||
}
|
||||
mainPanelObserves.append(ob)
|
||||
mainPanelVC.track(scrollView: contentVC.tableView)
|
||||
case let contentVC as NestedScrollViewController:
|
||||
mainPanelVC.track(scrollView: contentVC.scrollView)
|
||||
case let navVC as UINavigationController:
|
||||
if let rootVC = (navVC.topViewController as? MainViewController) {
|
||||
rootVC.loadViewIfNeeded()
|
||||
mainPanelVC.track(scrollView: rootVC.tableView)
|
||||
}
|
||||
case let contentVC as ImageViewController:
|
||||
if #available(iOS 11.0, *) {
|
||||
let mode: ImageViewController.Mode = (useCase == .showAdaptivePanelWithCustomGuide) ? .withHeaderFooter : .onlyImage
|
||||
let layoutGuide = contentVC.layoutGuideFor(mode: mode)
|
||||
mainPanelVC.layout = ImageViewController.PanelLayout(targetGuide: layoutGuide)
|
||||
} else {
|
||||
mainPanelVC.layout = ImageViewController.PanelLayout(targetGuide: nil)
|
||||
}
|
||||
mainPanelVC.delegate = nil
|
||||
mainPanelVC.isRemovalInteractionEnabled = true
|
||||
mainPanelVC.track(scrollView: contentVC.scrollView)
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
@objc
|
||||
fileprivate func handleSurface(tapGesture: UITapGestureRecognizer) {
|
||||
switch mainPanelVC.state {
|
||||
case .full:
|
||||
mainPanelVC.move(to: .half, animated: true)
|
||||
default:
|
||||
mainPanelVC.move(to: .full, animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
func setUpSettingsPanel(for mainVC: MainViewController) {
|
||||
guard settingsPanelVC == nil else { return }
|
||||
// Initialize FloatingPanelController
|
||||
settingsPanelVC = FloatingPanelController()
|
||||
|
||||
let appearance = SurfaceAppearance()
|
||||
appearance.cornerRadius = 6.0
|
||||
settingsPanelVC.surfaceView.appearance = appearance
|
||||
|
||||
settingsPanelVC.isRemovalInteractionEnabled = true
|
||||
settingsPanelVC.backdropView.dismissalTapGestureRecognizer.isEnabled = true
|
||||
settingsPanelVC.delegate = self
|
||||
|
||||
let contentVC = mainVC.storyboard?.instantiateViewController(withIdentifier: "SettingsViewController")
|
||||
|
||||
// Set a content view controller
|
||||
settingsPanelVC.set(contentViewController: contentVC)
|
||||
|
||||
// Add FloatingPanel to self.view
|
||||
settingsPanelVC.addPanel(toParent: mainVC, animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
extension UseCaseController: FloatingPanelControllerDelegate {
|
||||
func floatingPanel(_ vc: FloatingPanelController, contentOffsetForPinning trackingScrollView: UIScrollView) -> CGPoint {
|
||||
if useCase == .showNavigationController, #available(iOS 11.0, *) {
|
||||
// 148.0 is the SafeArea's top value for a navigation bar with a large title.
|
||||
return CGPoint(x: 0.0, y: 0.0 - trackingScrollView.contentInset.top - 148.0)
|
||||
}
|
||||
return CGPoint(x: 0.0, y: 0.0 - trackingScrollView.contentInset.top)
|
||||
}
|
||||
|
||||
func floatingPanel(_ vc: FloatingPanelController, layoutFor newCollection: UITraitCollection) -> FloatingPanelLayout {
|
||||
if vc == settingsPanelVC {
|
||||
return IntrinsicPanelLayout()
|
||||
}
|
||||
|
||||
switch useCase {
|
||||
case .showTopPositionedPanel:
|
||||
return TopPositionedPanelLayout()
|
||||
case .showRemovablePanel:
|
||||
return newCollection.verticalSizeClass == .compact ? RemovablePanelLandscapeLayout() : RemovablePanelLayout()
|
||||
case .showIntrinsicView:
|
||||
return IntrinsicPanelLayout()
|
||||
case .showPanelModal:
|
||||
if vc != mainPanelVC && vc != detailPanelVC {
|
||||
return ModalPanelLayout()
|
||||
}
|
||||
fallthrough
|
||||
case .showContentInset:
|
||||
return FloatingPanelBottomLayout()
|
||||
case .showCustomStatePanel:
|
||||
return FloatingPanelLayoutWithCustomState()
|
||||
default:
|
||||
return (newCollection.verticalSizeClass == .compact) ? FloatingPanelBottomLayout() : mainVC
|
||||
}
|
||||
}
|
||||
|
||||
func floatingPanelDidRemove(_ vc: FloatingPanelController) {
|
||||
switch vc {
|
||||
case settingsPanelVC:
|
||||
settingsPanelVC = nil
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension UseCaseController: UIGestureRecognizerDelegate {
|
||||
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
|
||||
switch useCase {
|
||||
case .showNestedScrollView:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRequireFailureOf otherGestureRecognizer: UIGestureRecognizer) -> Bool {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
extension UseCase {
|
||||
func makeContentViewController(with storyboard: UIStoryboard) -> UIViewController {
|
||||
guard let storyboardID = self.storyboardID else { return DebugTableViewController() }
|
||||
return storyboard.instantiateViewController(withIdentifier: storyboardID)
|
||||
}
|
||||
|
||||
func setUpInteraction(for useCaseController: UseCaseController) {
|
||||
let mainVC = useCaseController.mainVC
|
||||
let mainPanelVC = useCaseController.mainPanelVC!
|
||||
|
||||
// Enable tap-to-hide and removal interaction
|
||||
switch self {
|
||||
case .trackingTableView:
|
||||
let tapGesture = UITapGestureRecognizer(target: useCaseController, action: #selector(UseCaseController.handleSurface(tapGesture:)))
|
||||
tapGesture.cancelsTouchesInView = false
|
||||
tapGesture.numberOfTapsRequired = 2
|
||||
// Prevents a delay to response a tap in menus of DebugTableViewController.
|
||||
tapGesture.delaysTouchesEnded = false
|
||||
mainPanelVC.surfaceView.addGestureRecognizer(tapGesture)
|
||||
case .showNestedScrollView:
|
||||
mainPanelVC.panGestureRecognizer.delegateProxy = useCaseController
|
||||
case .showPageContentView:
|
||||
if let page = (mainPanelVC.contentViewController as? UIPageViewController)?.viewControllers?.first {
|
||||
mainPanelVC.track(scrollView: (page as! DebugTableViewController).tableView)
|
||||
}
|
||||
case .showRemovablePanel, .showIntrinsicView:
|
||||
mainPanelVC.isRemovalInteractionEnabled = true
|
||||
mainPanelVC.backdropView.dismissalTapGestureRecognizer.isEnabled = true
|
||||
case .showNavigationController:
|
||||
mainPanelVC.contentInsetAdjustmentBehavior = .never
|
||||
case .showTopPositionedPanel: // For debug
|
||||
let contentVC = UIViewController()
|
||||
contentVC.view.backgroundColor = .red
|
||||
mainPanelVC.set(contentViewController: contentVC)
|
||||
mainPanelVC.addPanel(toParent: mainVC, animated: true)
|
||||
return
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
// Copyright 2018-Present Shin Yamamoto. All rights reserved. MIT license.
|
||||
|
||||
import Foundation
|
||||
import FloatingPanel
|
||||
|
||||
extension FloatingPanelState {
|
||||
static let lastQuart: FloatingPanelState = FloatingPanelState(rawValue: "lastQuart", order: 750)
|
||||
static let firstQuart: FloatingPanelState = FloatingPanelState(rawValue: "firstQuart", order: 250)
|
||||
}
|
||||
|
||||
class FloatingPanelLayoutWithCustomState: FloatingPanelBottomLayout {
|
||||
override var anchors: [FloatingPanelState: FloatingPanelLayoutAnchoring] {
|
||||
return [
|
||||
.full: FloatingPanelLayoutAnchor(absoluteInset: 0.0, edge: .top, referenceGuide: .safeArea),
|
||||
.lastQuart: FloatingPanelLayoutAnchor(fractionalInset: 0.75, edge: .bottom, referenceGuide: .safeArea),
|
||||
.half: FloatingPanelLayoutAnchor(fractionalInset: 0.5, edge: .bottom, referenceGuide: .safeArea),
|
||||
.firstQuart: FloatingPanelLayoutAnchor(fractionalInset: 0.25, edge: .bottom, referenceGuide: .safeArea),
|
||||
.tip: FloatingPanelLayoutAnchor(absoluteInset: 20.0, edge: .bottom, referenceGuide: .safeArea),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
// Copyright 2018 the FloatingPanel authors. All rights reserved. MIT license.
|
||||
|
||||
import UIKit
|
||||
import FloatingPanel
|
||||
|
||||
final class PagePanelController: NSObject {
|
||||
lazy var pages = [UIColor.blue, .red, .green].compactMap({ (color) -> UIViewController in
|
||||
let page = FloatingPanelController(delegate: self)
|
||||
page.view.backgroundColor = color
|
||||
page.panGestureRecognizer.delegateProxy = self
|
||||
page.show()
|
||||
return page
|
||||
})
|
||||
|
||||
func makePageViewControllerForContent() -> UIPageViewController {
|
||||
pages = [DebugTableViewController(), DebugTableViewController(), DebugTableViewController()]
|
||||
let pageVC = UIPageViewController(transitionStyle: .scroll, navigationOrientation: .horizontal, options: [:])
|
||||
pageVC.dataSource = self
|
||||
pageVC.delegate = self
|
||||
pageVC.setViewControllers([pages[0]], direction: .forward, animated: false, completion: nil)
|
||||
return pageVC
|
||||
}
|
||||
|
||||
func makePageViewController(for vc: MainViewController) -> UIPageViewController {
|
||||
let pageVC = UIPageViewController(transitionStyle: .scroll, navigationOrientation: .horizontal, options: [:])
|
||||
let closeButton = UIButton(type: .custom)
|
||||
pageVC.view.addSubview(closeButton)
|
||||
closeButton.setTitle("Close", for: .normal)
|
||||
closeButton.translatesAutoresizingMaskIntoConstraints = false
|
||||
closeButton.addTarget(vc, action: #selector(MainViewController.dismissPresentedVC), for: .touchUpInside)
|
||||
NSLayoutConstraint.activate([
|
||||
closeButton.topAnchor.constraint(equalTo: pageVC.layoutGuide.topAnchor, constant: 16.0),
|
||||
closeButton.leftAnchor.constraint(equalTo: pageVC.view.leftAnchor, constant: 16.0),
|
||||
])
|
||||
pageVC.dataSource = self
|
||||
pageVC.setViewControllers([pages[0]], direction: .forward, animated: false, completion: nil)
|
||||
pageVC.modalPresentationStyle = .fullScreen
|
||||
return pageVC
|
||||
}
|
||||
}
|
||||
|
||||
extension PagePanelController: FloatingPanelControllerDelegate {
|
||||
func floatingPanel(_ vc: FloatingPanelController, layoutFor newCollection: UITraitCollection) -> FloatingPanelLayout {
|
||||
return FloatingPanelBottomLayout()
|
||||
}
|
||||
}
|
||||
|
||||
extension PagePanelController: UIGestureRecognizerDelegate {
|
||||
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
|
||||
return true
|
||||
}
|
||||
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRequireFailureOf otherGestureRecognizer: UIGestureRecognizer) -> Bool {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
extension PagePanelController: UIPageViewControllerDataSource {
|
||||
func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
|
||||
guard
|
||||
let index = pages.firstIndex(of: viewController),
|
||||
index + 1 < pages.count
|
||||
else { return nil }
|
||||
return pages[index + 1]
|
||||
}
|
||||
func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
|
||||
guard
|
||||
let index = pages.firstIndex(of: viewController),
|
||||
index - 1 >= 0
|
||||
else { return nil }
|
||||
return pages[index - 1]
|
||||
}
|
||||
}
|
||||
|
||||
extension PagePanelController: UIPageViewControllerDelegate {
|
||||
// For showPageContent
|
||||
func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) {
|
||||
if completed, let page = pageViewController.viewControllers?.first {
|
||||
(pageViewController.parent as! FloatingPanelController).track(scrollView: (page as! DebugTableViewController).tableView)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
// Copyright 2018-Present Shin Yamamoto. All rights reserved. MIT license.
|
||||
|
||||
import Foundation
|
||||
|
||||
enum UseCase: Int, CaseIterable {
|
||||
case trackingTableView
|
||||
case trackingTextView
|
||||
case showDetail
|
||||
case showModal
|
||||
case showPanelModal
|
||||
case showMultiPanelModal
|
||||
case showPanelInSheetModal
|
||||
case showTabBar
|
||||
case showPageView
|
||||
case showPageContentView
|
||||
case showNestedScrollView
|
||||
case showRemovablePanel
|
||||
case showIntrinsicView
|
||||
case showContentInset
|
||||
case showContainerMargins
|
||||
case showNavigationController
|
||||
case showTopPositionedPanel
|
||||
case showAdaptivePanel
|
||||
case showAdaptivePanelWithCustomGuide
|
||||
case showCustomStatePanel
|
||||
|
||||
var name: String {
|
||||
switch self {
|
||||
case .trackingTableView: return "Scroll tracking(TableView)"
|
||||
case .trackingTextView: return "Scroll tracking(TextView)"
|
||||
case .showDetail: return "Show Detail Panel"
|
||||
case .showModal: return "Show Modal"
|
||||
case .showPanelModal: return "Show Panel Modal"
|
||||
case .showMultiPanelModal: return "Show Multi Panel Modal"
|
||||
case .showPanelInSheetModal: return "Show Panel in Sheet Modal"
|
||||
case .showTabBar: return "Show Tab Bar"
|
||||
case .showPageView: return "Show Page View"
|
||||
case .showPageContentView: return "Show Page Content View"
|
||||
case .showNestedScrollView: return "Show Nested ScrollView"
|
||||
case .showRemovablePanel: return "Show Removable Panel"
|
||||
case .showIntrinsicView: return "Show Intrinsic View"
|
||||
case .showContentInset: return "Show with ContentInset"
|
||||
case .showContainerMargins: return "Show with ContainerMargins"
|
||||
case .showNavigationController: return "Show Navigation Controller"
|
||||
case .showTopPositionedPanel: return "Show Top Positioned Panel"
|
||||
case .showAdaptivePanel: return "Show Adaptive Panel"
|
||||
case .showAdaptivePanelWithCustomGuide: return "Show Adaptive Panel (Custom Layout Guide)"
|
||||
case .showCustomStatePanel: return "Show Panel with Custom state"
|
||||
}
|
||||
}
|
||||
|
||||
var storyboardID: String? {
|
||||
switch self {
|
||||
case .trackingTableView: return nil
|
||||
case .trackingTextView: return "ConsoleViewController" // Storyboard only
|
||||
case .showDetail: return String(describing: DetailViewController.self)
|
||||
case .showModal: return String(describing: ModalViewController.self)
|
||||
case .showMultiPanelModal: return nil
|
||||
case .showPanelInSheetModal: return nil
|
||||
case .showPanelModal: return nil
|
||||
case .showTabBar: return String(describing: TabBarViewController.self)
|
||||
case .showPageView: return nil
|
||||
case .showPageContentView: return nil
|
||||
case .showNestedScrollView: return String(describing: NestedScrollViewController.self)
|
||||
case .showRemovablePanel: return String(describing: DetailViewController.self)
|
||||
case .showIntrinsicView: return "IntrinsicViewController" // Storyboard only
|
||||
case .showContentInset: return nil
|
||||
case .showContainerMargins: return nil
|
||||
case .showNavigationController: return "RootNavigationController" // Storyboard only
|
||||
case .showTopPositionedPanel: return nil
|
||||
case .showAdaptivePanel,
|
||||
.showAdaptivePanelWithCustomGuide:
|
||||
return String(describing: ImageViewController.self)
|
||||
case .showCustomStatePanel:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,272 @@
|
||||
// Copyright 2018 the FloatingPanel authors. All rights reserved. MIT license.
|
||||
|
||||
import UIKit
|
||||
import FloatingPanel
|
||||
|
||||
class DebugTableViewController: InspectableViewController {
|
||||
// MARK: - Views
|
||||
|
||||
lazy var tableView: UITableView = {
|
||||
let tableView = UITableView(frame: .zero, style: .plain)
|
||||
tableView.dataSource = self
|
||||
tableView.delegate = self
|
||||
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "Cell")
|
||||
return tableView
|
||||
}()
|
||||
lazy var buttonStackView: UIStackView = {
|
||||
let stackView = UIStackView()
|
||||
stackView.axis = .vertical
|
||||
stackView.distribution = .fillEqually
|
||||
stackView.alignment = .trailing
|
||||
stackView.spacing = 10.0
|
||||
return stackView
|
||||
}()
|
||||
private lazy var reorderButton: UIButton = {
|
||||
let button = UIButton()
|
||||
button.setTitle(Menu.reorder.rawValue, for: .normal)
|
||||
button.setTitleColor(view.tintColor, for: .normal)
|
||||
button.addTarget(self, action: #selector(reorderItems), for: .touchUpInside)
|
||||
return button
|
||||
}()
|
||||
private lazy var trackingSwitchWrapper: UIStackView = {
|
||||
let stackView = UIStackView()
|
||||
stackView.axis = .horizontal
|
||||
stackView.distribution = .fillProportionally
|
||||
stackView.alignment = .center
|
||||
stackView.spacing = 8.0
|
||||
stackView.addArrangedSubview(trackingLabel)
|
||||
stackView.addArrangedSubview(trackingSwitch)
|
||||
return stackView
|
||||
}()
|
||||
private lazy var trackingLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.text = "Tracking"
|
||||
label.font = UIFont.systemFont(ofSize: 17.0, weight: .regular)
|
||||
return label
|
||||
}()
|
||||
private lazy var trackingSwitch: UISwitch = {
|
||||
let trackingSwitch = UISwitch()
|
||||
trackingSwitch.isOn = true
|
||||
trackingSwitch.addTarget(self, action: #selector(turnTrackingOn), for: .touchUpInside)
|
||||
return trackingSwitch
|
||||
}()
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
private lazy var items: [String] = {
|
||||
let items = (0..<100).map { "Items \($0)" }
|
||||
return Command.replace(items: items)
|
||||
}()
|
||||
private var itemHeight: CGFloat = 66.0
|
||||
|
||||
enum Menu: String, CaseIterable {
|
||||
case turnOffTracking = "Tracking"
|
||||
case reorder = "Reorder"
|
||||
}
|
||||
|
||||
enum Command: Int, CaseIterable {
|
||||
case animateScroll
|
||||
case changeContentSize
|
||||
case moveToFull
|
||||
case moveToHalf
|
||||
var text: String {
|
||||
switch self {
|
||||
case .animateScroll: return "Scroll in the middle"
|
||||
case .changeContentSize: return "Change content size"
|
||||
case .moveToFull: return "Move to Full"
|
||||
case.moveToHalf: return "Move to Half"
|
||||
}
|
||||
}
|
||||
|
||||
static func replace(items: [String]) -> [String] {
|
||||
return items.enumerated().map { (index, text) -> String in
|
||||
if let action = Command(rawValue: index) {
|
||||
return "\(index). \(action.text)"
|
||||
}
|
||||
return text
|
||||
}
|
||||
}
|
||||
|
||||
func execute(for vc: DebugTableViewController) {
|
||||
switch self {
|
||||
case .animateScroll:
|
||||
vc.animateScroll()
|
||||
case .changeContentSize:
|
||||
vc.changeContentSize()
|
||||
case .moveToFull:
|
||||
vc.moveToFull()
|
||||
case .moveToHalf:
|
||||
vc.moveToHalf()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Lifecycle
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
layoutTableView()
|
||||
layoutMenuStackView()
|
||||
setUpMenu()
|
||||
}
|
||||
|
||||
private func layoutTableView() {
|
||||
view.addSubview(tableView)
|
||||
tableView.translatesAutoresizingMaskIntoConstraints = false
|
||||
NSLayoutConstraint.activate([
|
||||
tableView.topAnchor.constraint(equalTo: view.topAnchor),
|
||||
tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
||||
tableView.leftAnchor.constraint(equalTo: view.leftAnchor),
|
||||
tableView.rightAnchor.constraint(equalTo: view.rightAnchor)
|
||||
])
|
||||
}
|
||||
|
||||
private func layoutMenuStackView() {
|
||||
view.addSubview(buttonStackView)
|
||||
buttonStackView.translatesAutoresizingMaskIntoConstraints = false
|
||||
NSLayoutConstraint.activate([
|
||||
buttonStackView.topAnchor.constraint(equalTo: view.topAnchor, constant: 22.0),
|
||||
buttonStackView.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -22.0),
|
||||
])
|
||||
}
|
||||
|
||||
private func setUpMenu() {
|
||||
for menu in Menu.allCases {
|
||||
switch menu {
|
||||
case .reorder:
|
||||
buttonStackView.addArrangedSubview(reorderButton)
|
||||
case .turnOffTracking:
|
||||
buttonStackView.addArrangedSubview(trackingSwitchWrapper)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Menu
|
||||
@objc
|
||||
private func reorderItems() {
|
||||
if reorderButton.titleLabel?.text == Menu.reorder.rawValue {
|
||||
tableView.isEditing = true
|
||||
reorderButton.setTitle("Cancel", for: .normal)
|
||||
} else {
|
||||
tableView.isEditing = false
|
||||
reorderButton.setTitle(Menu.reorder.rawValue, for: .normal)
|
||||
}
|
||||
}
|
||||
|
||||
@objc
|
||||
private func turnTrackingOn(_ sender: UISwitch) {
|
||||
guard let fpc = self.parent as? FloatingPanelController else { return }
|
||||
if sender.isOn {
|
||||
fpc.track(scrollView: tableView)
|
||||
} else {
|
||||
fpc.untrack(scrollView: tableView)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Actions
|
||||
|
||||
private func execute(command: Command) {
|
||||
command.execute(for: self)
|
||||
}
|
||||
|
||||
@objc
|
||||
private func animateScroll() {
|
||||
tableView.scrollToRow(at: IndexPath(row: lround(Double(items.count) / 2.0),
|
||||
section: 0),
|
||||
at: .top, animated: true)
|
||||
}
|
||||
|
||||
@objc
|
||||
private func changeContentSize() {
|
||||
let actionSheet = UIAlertController(title: "Change content size", message: "", preferredStyle: .actionSheet)
|
||||
actionSheet.addAction(UIAlertAction(title: "Large", style: .default, handler: { (_) in
|
||||
self.itemHeight = 66.0
|
||||
self.changeItems(100)
|
||||
}))
|
||||
actionSheet.addAction(UIAlertAction(title: "Match", style: .default, handler: { (_) in
|
||||
switch self.tableView.bounds.height {
|
||||
case 585: // iPhone 6,7,8
|
||||
self.itemHeight = self.tableView.bounds.height / 13.0
|
||||
self.changeItems(13)
|
||||
case 656: // iPhone {6,7,8} Plus
|
||||
self.itemHeight = self.tableView.bounds.height / 16.0
|
||||
self.changeItems(16)
|
||||
default: // iPhone X family
|
||||
self.itemHeight = self.tableView.bounds.height / 12.0
|
||||
self.changeItems(12)
|
||||
}
|
||||
}))
|
||||
actionSheet.addAction(UIAlertAction(title: "Short", style: .default, handler: { (_) in
|
||||
self.itemHeight = 66.0
|
||||
self.changeItems(3)
|
||||
}))
|
||||
|
||||
self.present(actionSheet, animated: true, completion: nil)
|
||||
}
|
||||
|
||||
private func changeItems(_ count: Int) {
|
||||
items = Command.replace(items: (0..<count).map{ "\($0). No action" })
|
||||
tableView.reloadData()
|
||||
}
|
||||
|
||||
@objc
|
||||
private func moveToFull() {
|
||||
(self.parent as! FloatingPanelController).move(to: .full, animated: true)
|
||||
}
|
||||
|
||||
@objc
|
||||
private func moveToHalf() {
|
||||
(self.parent as! FloatingPanelController).move(to: .half, animated: true)
|
||||
}
|
||||
|
||||
@objc
|
||||
private func close(sender: UIButton) {
|
||||
// Remove FloatingPanel from a view
|
||||
(self.parent as! FloatingPanelController).removePanelFromParent(animated: true, completion: nil)
|
||||
}
|
||||
}
|
||||
|
||||
extension DebugTableViewController: UITableViewDataSource {
|
||||
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
return items.count
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
|
||||
return itemHeight
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
|
||||
cell.textLabel?.text = items[indexPath.row]
|
||||
return cell
|
||||
}
|
||||
}
|
||||
|
||||
extension DebugTableViewController: UITableViewDelegate {
|
||||
func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
||||
print("TableView --- ", scrollView.contentOffset, scrollView.contentInset)
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
print("DebugTableViewController -- select row \(indexPath.row)")
|
||||
guard let action = Command(rawValue: indexPath.row) else { return }
|
||||
execute(command: action)
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, editActionsForRowAt indexPath: IndexPath) -> [UITableViewRowAction]? {
|
||||
return [
|
||||
UITableViewRowAction(style: .destructive, title: "Delete", handler: { (action, path) in
|
||||
self.items.remove(at: path.row)
|
||||
tableView.deleteRows(at: [path], with: .automatic)
|
||||
}),
|
||||
]
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, canMoveRowAt indexPath: IndexPath) -> Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {
|
||||
items.insert(items.remove(at: sourceIndexPath.row), at: destinationIndexPath.row)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
// Copyright 2018 the FloatingPanel authors. All rights reserved. MIT license.
|
||||
|
||||
import UIKit
|
||||
|
||||
final class DebugTextViewController: UIViewController, UITextViewDelegate {
|
||||
@IBOutlet weak var textView: UITextView!
|
||||
@IBOutlet weak var textViewTopConstraint: NSLayoutConstraint!
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
textView.delegate = self
|
||||
print("viewDidLoad: TextView --- ", textView.contentOffset, textView.contentInset)
|
||||
|
||||
if #available(iOS 11.0, *) {
|
||||
textView.contentInsetAdjustmentBehavior = .never
|
||||
}
|
||||
}
|
||||
|
||||
override func viewWillLayoutSubviews() {
|
||||
print("viewWillLayoutSubviews: TextView --- ", textView.contentOffset, textView.contentInset, textView.frame)
|
||||
}
|
||||
|
||||
override func viewDidLayoutSubviews() {
|
||||
super.viewDidLayoutSubviews()
|
||||
print("viewDidLayoutSubviews: TextView --- ", textView.contentOffset, textView.contentInset, textView.frame)
|
||||
}
|
||||
|
||||
override func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
print("TextView --- ", textView.contentOffset, textView.contentInset, textView.frame)
|
||||
}
|
||||
|
||||
func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
||||
print("TextView --- ", scrollView.contentOffset, scrollView.contentInset)
|
||||
if #available(iOS 11.0, *) {
|
||||
print("TextView --- ", scrollView.adjustedContentInset)
|
||||
}
|
||||
}
|
||||
|
||||
@IBAction func close(sender: UIButton) {
|
||||
// (self.parent as? FloatingPanelController)?.removePanelFromParent(animated: true, completion: nil)
|
||||
dismiss(animated: true, completion: nil)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
// Copyright 2018-Present Shin Yamamoto. All rights reserved. MIT license.
|
||||
|
||||
import UIKit
|
||||
import FloatingPanel
|
||||
|
||||
final class DetailViewController: InspectableViewController {
|
||||
@IBOutlet weak var modeChangeView: UIStackView!
|
||||
@IBOutlet weak var intrinsicHeightConstraint: NSLayoutConstraint!
|
||||
@IBOutlet weak var closeButton: UIButton!
|
||||
@IBAction func close(sender: UIButton) {
|
||||
// (self.parent as? FloatingPanelController)?.removePanelFromParent(animated: true, completion: nil)
|
||||
dismiss(animated: true, completion: nil)
|
||||
}
|
||||
|
||||
@IBAction func buttonPressed(_ sender: UIButton) {
|
||||
switch sender.titleLabel?.text {
|
||||
case "Show":
|
||||
performSegue(withIdentifier: "ShowSegue", sender: self)
|
||||
case "Present Modally":
|
||||
performSegue(withIdentifier: "PresentModallySegue", sender: self)
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
@IBAction func modeChanged(_ sender: Any) {
|
||||
guard let fpc = parent as? FloatingPanelController else { return }
|
||||
fpc.contentMode = (fpc.contentMode == .static) ? .fitToBounds : .static
|
||||
}
|
||||
|
||||
@IBAction func tapped(_ sender: Any) {
|
||||
print("Detail panel is tapped!")
|
||||
}
|
||||
@IBAction func swipped(_ sender: Any) {
|
||||
print("Detail panel is swipped!")
|
||||
}
|
||||
@IBAction func longPressed(_ sender: Any) {
|
||||
print("Detail panel is longPressed!")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
// Copyright 2018-Present Shin Yamamoto. All rights reserved. MIT license.
|
||||
|
||||
import UIKit
|
||||
import FloatingPanel
|
||||
|
||||
final class ImageViewController: UIViewController {
|
||||
class PanelLayout: FloatingPanelLayout {
|
||||
weak var targetGuide: UILayoutGuide?
|
||||
init(targetGuide: UILayoutGuide?) {
|
||||
self.targetGuide = targetGuide
|
||||
}
|
||||
let position: FloatingPanelPosition = .bottom
|
||||
let initialState: FloatingPanelState = .full
|
||||
var anchors: [FloatingPanelState : FloatingPanelLayoutAnchoring] {
|
||||
if #available(iOS 11.0, *), let targetGuide = targetGuide {
|
||||
return [
|
||||
.full: FloatingPanelAdaptiveLayoutAnchor(absoluteOffset: 0,
|
||||
contentLayout: targetGuide,
|
||||
referenceGuide: .superview),
|
||||
.half: FloatingPanelAdaptiveLayoutAnchor(fractionalOffset: 0.5,
|
||||
contentLayout: targetGuide,
|
||||
referenceGuide: .superview)
|
||||
]
|
||||
} else {
|
||||
return [
|
||||
.full: FloatingPanelLayoutAnchor(absoluteInset: 500,
|
||||
edge: .bottom,
|
||||
referenceGuide: .superview)
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@IBOutlet weak var headerView: UIView!
|
||||
@IBOutlet weak var footerView: UIView!
|
||||
@IBOutlet weak var scrollView: UIScrollView!
|
||||
@IBOutlet weak var stackView: UIStackView!
|
||||
|
||||
enum Mode {
|
||||
case onlyImage
|
||||
case withHeaderFooter
|
||||
}
|
||||
|
||||
@available(iOS 11.0, *)
|
||||
func layoutGuideFor(mode: Mode) -> UILayoutGuide {
|
||||
switch mode {
|
||||
case .onlyImage:
|
||||
self.headerView.isHidden = true
|
||||
self.footerView.isHidden = true
|
||||
return scrollView.contentLayoutGuide
|
||||
case .withHeaderFooter:
|
||||
self.headerView.isHidden = false
|
||||
self.footerView.isHidden = false
|
||||
let guide = UILayoutGuide()
|
||||
view.addLayoutGuide(guide)
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
scrollView.heightAnchor.constraint(equalTo: scrollView.contentLayoutGuide.heightAnchor),
|
||||
|
||||
guide.topAnchor.constraint(equalTo: stackView.topAnchor),
|
||||
guide.leftAnchor.constraint(equalTo: stackView.leftAnchor),
|
||||
guide.bottomAnchor.constraint(equalTo: stackView.bottomAnchor),
|
||||
guide.rightAnchor.constraint(equalTo: stackView.rightAnchor),
|
||||
])
|
||||
return guide
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
// Copyright 2018 the FloatingPanel authors. All rights reserved. MIT license.
|
||||
|
||||
import UIKit
|
||||
|
||||
class InspectableViewController: UIViewController {
|
||||
override func viewWillLayoutSubviews() {
|
||||
super.viewWillLayoutSubviews()
|
||||
print(">>> Content View: viewWillLayoutSubviews", layoutInsets)
|
||||
}
|
||||
|
||||
override func viewDidLayoutSubviews() {
|
||||
super.viewDidLayoutSubviews()
|
||||
print(">>> Content View: viewDidLayoutSubviews", layoutInsets)
|
||||
}
|
||||
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
print(">>> Content View: viewWillAppear", layoutInsets)
|
||||
}
|
||||
|
||||
override func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
print(">>> Content View: viewDidAppear", view.bounds, layoutInsets)
|
||||
}
|
||||
|
||||
override func viewWillDisappear(_ animated: Bool) {
|
||||
super.viewWillDisappear(animated)
|
||||
print(">>> Content View: viewWillDisappear")
|
||||
}
|
||||
|
||||
override func viewDidDisappear(_ animated: Bool) {
|
||||
super.viewDidDisappear(animated)
|
||||
print(">>> Content View: viewDidDisappear")
|
||||
}
|
||||
|
||||
override func willMove(toParent parent: UIViewController?) {
|
||||
super.willMove(toParent: parent)
|
||||
print(">>> Content View: willMove(toParent: \(String(describing: parent))")
|
||||
}
|
||||
|
||||
override func didMove(toParent parent: UIViewController?) {
|
||||
super.didMove(toParent: parent)
|
||||
print(">>> Content View: didMove(toParent: \(String(describing: parent))")
|
||||
}
|
||||
public override func willTransition(to newCollection: UITraitCollection, with coordinator: UIViewControllerTransitionCoordinator) {
|
||||
print(">>> Content View: willTransition(to: \(newCollection), with: \(coordinator))", layoutInsets)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
// Copyright 2018 the FloatingPanel authors. All rights reserved. MIT license.
|
||||
|
||||
import UIKit
|
||||
import FloatingPanel
|
||||
|
||||
final class ModalViewController: UIViewController, FloatingPanelControllerDelegate {
|
||||
var fpc: FloatingPanelController!
|
||||
var consoleVC: DebugTextViewController!
|
||||
|
||||
@IBOutlet weak var safeAreaView: UIView!
|
||||
|
||||
var isNewlayout: Bool = false
|
||||
|
||||
override func viewDidLoad() {
|
||||
// Initialize FloatingPanelController
|
||||
fpc = FloatingPanelController()
|
||||
fpc.delegate = self
|
||||
|
||||
let appearance = SurfaceAppearance()
|
||||
appearance.cornerRadius = 6.0
|
||||
fpc.surfaceView.appearance = appearance
|
||||
|
||||
// Set a content view controller and track the scroll view
|
||||
let consoleVC = storyboard?.instantiateViewController(withIdentifier: "ConsoleViewController") as! DebugTextViewController
|
||||
fpc.set(contentViewController: consoleVC)
|
||||
fpc.track(scrollView: consoleVC.textView)
|
||||
|
||||
self.consoleVC = consoleVC
|
||||
|
||||
// Add FloatingPanel to self.view
|
||||
fpc.addPanel(toParent: self, at: view.subviews.firstIndex(of: safeAreaView) ?? -1)
|
||||
}
|
||||
|
||||
override func viewDidDisappear(_ animated: Bool) {
|
||||
super.viewDidDisappear(animated)
|
||||
// Remove FloatingPanel from a view
|
||||
fpc.removePanelFromParent(animated: false)
|
||||
}
|
||||
|
||||
@IBAction func close(sender: UIButton) {
|
||||
dismiss(animated: true, completion: nil)
|
||||
}
|
||||
|
||||
@IBAction func moveToFull(sender: UIButton) {
|
||||
fpc.move(to: .full, animated: true)
|
||||
}
|
||||
@IBAction func moveToHalf(sender: UIButton) {
|
||||
fpc.move(to: .half, animated: true)
|
||||
}
|
||||
@IBAction func moveToTip(sender: UIButton) {
|
||||
fpc.move(to: .tip, animated: true)
|
||||
}
|
||||
@IBAction func moveToHidden(sender: UIButton) {
|
||||
fpc.move(to: .hidden, animated: true)
|
||||
}
|
||||
@IBAction func updateLayout(_ sender: Any) {
|
||||
isNewlayout = !isNewlayout
|
||||
UIView.animate(withDuration: 0.5) {
|
||||
self.fpc.layout = (self.isNewlayout) ? ModalSecondLayout() : FloatingPanelBottomLayout()
|
||||
self.fpc.invalidateLayout()
|
||||
}
|
||||
}
|
||||
|
||||
func floatingPanel(_ vc: FloatingPanelController, layoutFor newCollection: UITraitCollection) -> FloatingPanelLayout {
|
||||
return (isNewlayout) ? ModalSecondLayout() : FloatingPanelBottomLayout()
|
||||
}
|
||||
|
||||
class ModalSecondLayout: FloatingPanelLayout {
|
||||
var position: FloatingPanelPosition = .bottom
|
||||
var initialState: FloatingPanelState { .half }
|
||||
var anchors: [FloatingPanelState : FloatingPanelLayoutAnchoring] {
|
||||
return [
|
||||
.full: FloatingPanelLayoutAnchor(absoluteInset: 16.0, edge: .top, referenceGuide: .safeArea),
|
||||
.half: FloatingPanelLayoutAnchor(absoluteInset: 262, edge: .top, referenceGuide: .safeArea),
|
||||
.tip: FloatingPanelLayoutAnchor(absoluteInset: 44.0, edge: .bottom, referenceGuide: .safeArea)
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
// Copyright 2018-Present Shin Yamamoto. All rights reserved. MIT license.
|
||||
|
||||
import UIKit
|
||||
import FloatingPanel
|
||||
import WebKit
|
||||
|
||||
final class MultiPanelController: FloatingPanelController, FloatingPanelControllerDelegate {
|
||||
|
||||
private final class FirstPanelContentViewController: UIViewController {
|
||||
|
||||
lazy var webView: WKWebView = WKWebView()
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
view.addSubview(webView)
|
||||
webView.frame = view.bounds
|
||||
webView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
||||
webView.load(URLRequest(url: URL(string: "https://www.apple.com")!))
|
||||
|
||||
let vc = MultiSecondPanelController()
|
||||
vc.setUpContent()
|
||||
vc.addPanel(toParent: self)
|
||||
}
|
||||
}
|
||||
|
||||
private final class MultiSecondPanelController: FloatingPanelController {
|
||||
|
||||
private final class SecondPanelContentViewController: DebugTableViewController {}
|
||||
|
||||
func setUpContent() {
|
||||
contentInsetAdjustmentBehavior = .never
|
||||
let vc = SecondPanelContentViewController()
|
||||
vc.loadViewIfNeeded()
|
||||
vc.title = "Second Panel"
|
||||
vc.buttonStackView.isHidden = true
|
||||
let navigationController = UINavigationController(rootViewController: vc)
|
||||
navigationController.navigationBar.barTintColor = .white
|
||||
navigationController.navigationBar.titleTextAttributes = [
|
||||
.foregroundColor: UIColor.black
|
||||
]
|
||||
set(contentViewController: navigationController)
|
||||
self.track(scrollView: vc.tableView)
|
||||
surfaceView.containerMargins = .init(top: 24.0, left: 0.0, bottom: layoutInsets.bottom, right: 0.0)
|
||||
}
|
||||
}
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
layout = FirstViewLayout()
|
||||
isRemovalInteractionEnabled = true
|
||||
|
||||
let vc = FirstPanelContentViewController()
|
||||
set(contentViewController: vc)
|
||||
track(scrollView: vc.webView.scrollView)
|
||||
}
|
||||
|
||||
private final class FirstViewLayout: FloatingPanelLayout {
|
||||
let position: FloatingPanelPosition = .bottom
|
||||
let initialState: FloatingPanelState = .full
|
||||
var anchors: [FloatingPanelState : FloatingPanelLayoutAnchoring] {
|
||||
return [
|
||||
.full: FloatingPanelLayoutAnchor(absoluteInset: 40.0, edge: .top, referenceGuide: .superview)
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
// Copyright 2018 the FloatingPanel authors. All rights reserved. MIT license.
|
||||
|
||||
import UIKit
|
||||
|
||||
final class NestedScrollViewController: UIViewController {
|
||||
@IBOutlet weak var scrollView: UIScrollView!
|
||||
@IBOutlet weak var nestedScrollView: UIScrollView!
|
||||
|
||||
@IBAction func longPressed(_ sender: Any) {
|
||||
print("LongPressed!")
|
||||
}
|
||||
@IBAction func swipped(_ sender: Any) {
|
||||
print("Swipped!")
|
||||
}
|
||||
@IBAction func tapped(_ sender: Any) {
|
||||
print("Tapped!")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
// Copyright 2018-Present Shin Yamamoto. All rights reserved. MIT license.
|
||||
|
||||
import UIKit
|
||||
import FloatingPanel
|
||||
|
||||
final class SettingsViewController: InspectableViewController {
|
||||
@IBOutlet weak var largeTitlesSwicth: UISwitch!
|
||||
@IBOutlet weak var translucentSwicth: UISwitch!
|
||||
@IBOutlet weak var versionLabel: UILabel!
|
||||
|
||||
override func viewDidLoad() {
|
||||
versionLabel.text = "Version: \(Bundle.main.infoDictionary?["CFBundleVersion"] ?? "--")"
|
||||
}
|
||||
|
||||
override func viewDidLayoutSubviews() {
|
||||
super.viewDidLayoutSubviews()
|
||||
if #available(iOS 11.0, *) {
|
||||
let prefersLargeTitles = navigationController!.navigationBar.prefersLargeTitles
|
||||
largeTitlesSwicth.setOn(prefersLargeTitles, animated: false)
|
||||
} else {
|
||||
largeTitlesSwicth.isEnabled = false
|
||||
}
|
||||
let isTranslucent = navigationController!.navigationBar.isTranslucent
|
||||
translucentSwicth.setOn(isTranslucent, animated: false)
|
||||
}
|
||||
|
||||
@IBAction func toggleLargeTitle(_ sender: UISwitch) {
|
||||
if #available(iOS 11.0, *) {
|
||||
navigationController?.navigationBar.prefersLargeTitles = sender.isOn
|
||||
}
|
||||
}
|
||||
@IBAction func toggleTranslucent(_ sender: UISwitch) {
|
||||
navigationController?.navigationBar.isTranslucent = sender.isOn
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,248 @@
|
||||
// Copyright 2018-Present Shin Yamamoto. All rights reserved. MIT license.
|
||||
|
||||
import UIKit
|
||||
import FloatingPanel
|
||||
|
||||
final class TabBarViewController: UITabBarController {}
|
||||
|
||||
final class TabBarContentViewController: UIViewController {
|
||||
enum Tab3Mode {
|
||||
case changeOffset
|
||||
case changeAutoLayout
|
||||
var label: String {
|
||||
switch self {
|
||||
case .changeAutoLayout: return "Use AutoLayout(OK)"
|
||||
case .changeOffset: return "Use ContentOffset(NG)"
|
||||
}
|
||||
}
|
||||
}
|
||||
lazy var fpc = FloatingPanelController()
|
||||
var consoleVC: DebugTextViewController!
|
||||
|
||||
var threeLayout: ThreeTabBarPanelLayout!
|
||||
var tab3Mode: Tab3Mode = .changeAutoLayout
|
||||
var switcherLabel: UILabel!
|
||||
|
||||
override func viewDidLoad() {
|
||||
fpc.delegate = self
|
||||
|
||||
let appearance = SurfaceAppearance()
|
||||
appearance.cornerRadius = 6.0
|
||||
fpc.surfaceView.appearance = appearance
|
||||
|
||||
// Set a content view controller and track the scroll view
|
||||
let consoleVC = storyboard?.instantiateViewController(withIdentifier: "ConsoleViewController") as! DebugTextViewController
|
||||
fpc.set(contentViewController: consoleVC)
|
||||
consoleVC.textView.delegate = self // MUST call it before fpc.track(scrollView:)
|
||||
fpc.track(scrollView: consoleVC.textView)
|
||||
self.consoleVC = consoleVC
|
||||
|
||||
// Add FloatingPanel to self.view
|
||||
fpc.addPanel(toParent: self)
|
||||
|
||||
|
||||
switch tabBarItem.tag {
|
||||
case 1:
|
||||
fpc.behavior = TwoTabBarPanelBehavior()
|
||||
case 2:
|
||||
let switcher = UISwitch()
|
||||
fpc.view.addSubview(switcher)
|
||||
switcher.translatesAutoresizingMaskIntoConstraints = false
|
||||
NSLayoutConstraint.activate([
|
||||
switcher.bottomAnchor.constraint(equalTo: fpc.surfaceView.topAnchor, constant: -16.0),
|
||||
switcher.rightAnchor.constraint(equalTo: fpc.surfaceView.rightAnchor, constant: -16.0),
|
||||
])
|
||||
switcher.isOn = true
|
||||
switcher.tintColor = .white
|
||||
switcher.backgroundColor = .white
|
||||
switcher.layer.cornerRadius = 16.0
|
||||
switcher.addTarget(self,
|
||||
action: #selector(changeTab3Mode(_:)),
|
||||
for: .valueChanged)
|
||||
let label = UILabel()
|
||||
label.text = tab3Mode.label
|
||||
fpc.view.addSubview(label)
|
||||
switcherLabel = label
|
||||
label.translatesAutoresizingMaskIntoConstraints = false
|
||||
NSLayoutConstraint.activate([
|
||||
label.centerYAnchor.constraint(equalTo: switcher.centerYAnchor, constant: 0.0),
|
||||
label.rightAnchor.constraint(equalTo: switcher.leftAnchor, constant: -16.0),
|
||||
])
|
||||
|
||||
// Turn off the mask instead of content inset change
|
||||
consoleVC.textView.clipsToBounds = false
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
override func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
fpc.invalidateLayout()
|
||||
}
|
||||
|
||||
// MARK: - Action
|
||||
|
||||
@IBAction func close(sender: UIButton) {
|
||||
dismiss(animated: true, completion: nil)
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
@objc
|
||||
private func changeTab3Mode(_ sender: UISwitch) {
|
||||
if sender.isOn {
|
||||
tab3Mode = .changeAutoLayout
|
||||
} else {
|
||||
tab3Mode = .changeOffset
|
||||
}
|
||||
switcherLabel.text = tab3Mode.label
|
||||
}
|
||||
}
|
||||
|
||||
extension TabBarContentViewController: UITextViewDelegate {
|
||||
func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
||||
guard self.tabBarItem.tag == 2 else { return }
|
||||
// Reset an invalid content offset by a user after updating the layout
|
||||
// of `consoleVC.textView`.
|
||||
// NOTE: FloatingPanel doesn't implicitly reset the offset(i.e.
|
||||
// Using KVO of `scrollView.contentOffset`). Because it can lead to an
|
||||
// infinite loop if a user also resets a content offset as below and,
|
||||
// in the situation, a user has to modify the library.
|
||||
if fpc.state != .full, fpc.surfaceLocation.y > fpc.surfaceLocation(for: .full).y {
|
||||
scrollView.contentOffset = .zero
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension TabBarContentViewController: FloatingPanelControllerDelegate {
|
||||
// MARK: - FloatingPanel
|
||||
|
||||
func floatingPanel(_ vc: FloatingPanelController, layoutFor newCollection: UITraitCollection) -> FloatingPanelLayout {
|
||||
switch self.tabBarItem.tag {
|
||||
case 0:
|
||||
return OneTabBarPanelLayout()
|
||||
case 1:
|
||||
return TwoTabBarPanelLayout()
|
||||
case 2:
|
||||
threeLayout = ThreeTabBarPanelLayout(parent: self)
|
||||
return threeLayout
|
||||
default:
|
||||
return FloatingPanelBottomLayout()
|
||||
}
|
||||
}
|
||||
|
||||
func floatingPanelDidMove(_ vc: FloatingPanelController) {
|
||||
guard self.tabBarItem.tag == 2 else { return }
|
||||
switch tab3Mode {
|
||||
case .changeAutoLayout:
|
||||
/* Good solution: Manipulate top constraint */
|
||||
assert(consoleVC.textViewTopConstraint != nil)
|
||||
let safeAreaTop = vc.layoutInsets.top
|
||||
if vc.surfaceLocation.y + threeLayout.topPadding < safeAreaTop {
|
||||
consoleVC.textViewTopConstraint?.constant = min(safeAreaTop - vc.surfaceLocation.y,
|
||||
safeAreaTop)
|
||||
} else {
|
||||
consoleVC.textViewTopConstraint?.constant = threeLayout.topPadding
|
||||
}
|
||||
case .changeOffset:
|
||||
/*
|
||||
Bad solution: Manipulate scroll content inset
|
||||
|
||||
FloatingPanelController keeps a content offset in moving a panel
|
||||
so that changing content inset or offset causes a buggy behavior.
|
||||
*/
|
||||
guard let scrollView = consoleVC.textView else { return }
|
||||
var insets = vc.adjustedContentInsets
|
||||
if vc.surfaceView.frame.minY < vc.layoutInsets.top {
|
||||
insets.top = vc.layoutInsets.top - vc.surfaceView.frame.minY
|
||||
} else {
|
||||
insets.top = 0.0
|
||||
}
|
||||
scrollView.contentInset = insets
|
||||
|
||||
if vc.surfaceView.frame.minY > 0 {
|
||||
scrollView.contentOffset = CGPoint(x: 0.0,
|
||||
y: 0.0 - scrollView.contentInset.top)
|
||||
}
|
||||
}
|
||||
|
||||
if vc.surfaceLocation.y > vc.surfaceLocation(for: .half).y {
|
||||
let progress = (vc.surfaceLocation.y - vc.surfaceLocation(for: .half).y)
|
||||
/ (vc.surfaceLocation(for: .tip).y - vc.surfaceLocation(for: .half).y)
|
||||
threeLayout.leftConstraint.constant = max(min(progress, 1.0), 0.0) * threeLayout.sideMargin
|
||||
threeLayout.rightConstraint.constant = -max(min(progress, 1.0), 0.0) * threeLayout.sideMargin
|
||||
} else {
|
||||
threeLayout.leftConstraint.constant = 0.0
|
||||
threeLayout.rightConstraint.constant = 0.0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class OneTabBarPanelLayout: FloatingPanelLayout {
|
||||
var initialState: FloatingPanelState { .tip }
|
||||
var position: FloatingPanelPosition { .bottom }
|
||||
var anchors: [FloatingPanelState : FloatingPanelLayoutAnchoring] {
|
||||
return [
|
||||
.full: FloatingPanelLayoutAnchor(absoluteInset: 16.0, edge: .top, referenceGuide: .safeArea),
|
||||
.tip: FloatingPanelLayoutAnchor(absoluteInset: 22.0, edge: .bottom, referenceGuide: .safeArea)
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
class TwoTabBarPanelLayout: FloatingPanelLayout {
|
||||
var initialState: FloatingPanelState { .half }
|
||||
var position: FloatingPanelPosition { .bottom }
|
||||
var anchors: [FloatingPanelState : FloatingPanelLayoutAnchoring] {
|
||||
return [
|
||||
.full: FloatingPanelLayoutAnchor(absoluteInset: 100.0, edge: .top, referenceGuide: .safeArea),
|
||||
.half: FloatingPanelLayoutAnchor(absoluteInset: 261.0, edge: .bottom, referenceGuide: .safeArea)
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
class TwoTabBarPanelBehavior: FloatingPanelBehavior {
|
||||
func allowsRubberBanding(for edges: UIRectEdge) -> Bool {
|
||||
return [UIRectEdge.top, UIRectEdge.bottom].contains(edges)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class ThreeTabBarPanelLayout: FloatingPanelLayout {
|
||||
weak var parentVC: UIViewController!
|
||||
|
||||
var leftConstraint: NSLayoutConstraint!
|
||||
var rightConstraint: NSLayoutConstraint!
|
||||
|
||||
let topPadding: CGFloat = 17.0
|
||||
let sideMargin: CGFloat = 16.0
|
||||
|
||||
init(parent: UIViewController) {
|
||||
parentVC = parent
|
||||
}
|
||||
|
||||
var initialState: FloatingPanelState { .half }
|
||||
var position: FloatingPanelPosition { .bottom }
|
||||
var anchors: [FloatingPanelState : FloatingPanelLayoutAnchoring] {
|
||||
return [
|
||||
.full: FloatingPanelLayoutAnchor(absoluteInset: 0.0, edge: .top, referenceGuide: .superview),
|
||||
.half: FloatingPanelLayoutAnchor(absoluteInset: 261.0 + parentVC.layoutInsets.bottom, edge: .bottom, referenceGuide: .superview),
|
||||
.tip: FloatingPanelLayoutAnchor(absoluteInset: 88.0 + parentVC.layoutInsets.bottom, edge: .bottom, referenceGuide: .superview),
|
||||
]
|
||||
}
|
||||
|
||||
func backdropAlpha(for state: FloatingPanelState) -> CGFloat {
|
||||
return 0.3
|
||||
}
|
||||
|
||||
func prepareLayout(surfaceView: UIView, in view: UIView) -> [NSLayoutConstraint] {
|
||||
if #available(iOS 11.0, *) {
|
||||
leftConstraint = surfaceView.leftAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leftAnchor, constant: 0.0)
|
||||
rightConstraint = surfaceView.rightAnchor.constraint(equalTo: view.safeAreaLayoutGuide.rightAnchor, constant: 0.0)
|
||||
} else {
|
||||
leftConstraint = surfaceView.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 0.0)
|
||||
rightConstraint = surfaceView.rightAnchor.constraint(equalTo: view.rightAnchor, constant: 0.0)
|
||||
}
|
||||
return [ leftConstraint, rightConstraint ]
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,22 @@
|
||||
#import "ViewController.h"
|
||||
@import FloatingPanel;
|
||||
|
||||
// Defining a custom FloatingPanelState
|
||||
@interface FloatingPanelState(Extended)
|
||||
+ (FloatingPanelState *)LastQuart;
|
||||
@end
|
||||
|
||||
@implementation FloatingPanelState(Extended)
|
||||
static FloatingPanelState *_lastQuart;
|
||||
+ (FloatingPanelState *)LastQuart {
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
_lastQuart = [[FloatingPanelState alloc] initWithRawValue:@"lastquart" order:750];
|
||||
});
|
||||
return _lastQuart;
|
||||
}
|
||||
@end
|
||||
|
||||
@interface ViewController()<FloatingPanelControllerDelegate>
|
||||
@end
|
||||
|
||||
@@ -20,7 +36,7 @@
|
||||
[fpc setBehavior:[MyFloatingPanelBehavior new]];
|
||||
[fpc setRemovalInteractionEnabled:NO];
|
||||
|
||||
[fpc addPanelToParent:self at:self.view.subviews.count animated:NO];
|
||||
[fpc addPanelToParent:self at:self.view.subviews.count animated:NO completion:nil];
|
||||
[fpc moveToState:FloatingPanelState.Tip animated:true completion:nil];
|
||||
|
||||
[self updateAppearance: fpc];
|
||||
@@ -59,12 +75,15 @@
|
||||
}
|
||||
- (NSDictionary<FloatingPanelState *, id<FloatingPanelLayoutAnchoring>> *)anchors {
|
||||
return @{
|
||||
FloatingPanelState.LastQuart: [[FloatingPanelLayoutAnchor alloc] initWithFractionalInset:0.25
|
||||
edge:FloatingPanelReferenceEdgeTop
|
||||
referenceGuide:FloatingPanelLayoutReferenceGuideSafeArea],
|
||||
FloatingPanelState.Half: [[FloatingPanelLayoutAnchor alloc] initWithFractionalInset:0.5
|
||||
edge:FloatingPanelReferenceEdgeTop
|
||||
referenceGuide:FloatingPanelLayoutReferenceGuideSafeArea],
|
||||
referenceGuide:FloatingPanelLayoutReferenceGuideSafeArea],
|
||||
FloatingPanelState.Tip: [[FloatingPanelLayoutAnchor alloc] initWithAbsoluteInset:44.0
|
||||
edge:FloatingPanelReferenceEdgeBottom
|
||||
referenceGuide:FloatingPanelLayoutReferenceGuideSafeArea],
|
||||
referenceGuide:FloatingPanelLayoutReferenceGuideSafeArea],
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
Pod::Spec.new do |s|
|
||||
|
||||
s.name = "FloatingPanel"
|
||||
s.version = "2.0.0-beta.1"
|
||||
s.version = "2.4.1"
|
||||
s.summary = "FloatingPanel is a clean and easy-to-use UI component of a floating panel interface."
|
||||
s.description = <<-DESC
|
||||
FloatingPanel is a clean and easy-to-use UI component for a new interface introduced in Apple Maps, Shortcuts and Stocks app.
|
||||
@@ -14,7 +14,7 @@ The new interface displays the related contents and utilities in parallel as a u
|
||||
s.platform = :ios, "10.0"
|
||||
s.source = { :git => "https://github.com/SCENEE/FloatingPanel.git", :tag => s.version.to_s }
|
||||
s.source_files = "Sources/*.swift"
|
||||
s.swift_versions = ['5.1', '5.2']
|
||||
s.swift_versions = ['5.1', '5.2', '5.3']
|
||||
|
||||
s.framework = "UIKit"
|
||||
|
||||
|
||||
@@ -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
-3
@@ -10,9 +10,6 @@
|
||||
<Group
|
||||
location = "group:Examples"
|
||||
name = "Examples">
|
||||
<FileRef
|
||||
location = "group:SamplesObjC/SamplesObjC.xcodeproj">
|
||||
</FileRef>
|
||||
<FileRef
|
||||
location = "group:Maps/Maps.xcodeproj">
|
||||
</FileRef>
|
||||
@@ -22,5 +19,8 @@
|
||||
<FileRef
|
||||
location = "group:Samples/Samples.xcodeproj">
|
||||
</FileRef>
|
||||
<FileRef
|
||||
location = "group:SamplesObjC/SamplesObjC.xcodeproj">
|
||||
</FileRef>
|
||||
</Group>
|
||||
</Workspace>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2018 Shin Yamamoto
|
||||
Copyright (c) 2018-Present Shin Yamamoto
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
@@ -18,4 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
SOFTWARE.
|
||||
|
||||
@@ -21,7 +21,7 @@ The new interface displays the related contents and utilities in parallel as a u
|
||||
- [Installation](#installation)
|
||||
- [CocoaPods](#cocoapods)
|
||||
- [Carthage](#carthage)
|
||||
- [Swift Package Manager with Xcode](#swift-package-manager-with-xcode)
|
||||
- [Swift Package Manager](#swift-package-manager)
|
||||
- [Getting Started](#getting-started)
|
||||
- [Add a floating panel as a child view controller](#add-a-floating-panel-as-a-child-view-controller)
|
||||
- [Present a floating panel as a modality](#present-a-floating-panel-as-a-modality)
|
||||
@@ -30,11 +30,13 @@ The new interface displays the related contents and utilities in parallel as a u
|
||||
- [Show/Hide a floating panel in a view with your view hierarchy](#showhide-a-floating-panel-in-a-view-with-your-view-hierarchy)
|
||||
- [Scale the content view when the surface position changes](#scale-the-content-view-when-the-surface-position-changes)
|
||||
- [Customize the layout with `FloatingPanelLayout` protocol](#customize-the-layout-with-floatingpanellayout-protocol)
|
||||
- [Change the initial layout.](#change-the-initial-layout)
|
||||
- [Change the initial layout](#change-the-initial-layout)
|
||||
- [Update your panel layout](#update-your-panel-layout)
|
||||
- [Support your landscape layout](#support-your-landscape-layout)
|
||||
- [Use the intrinsic size of a content in your panel layout](#use-the-intrinsic-size-of-a-content-in-your-panel-layout)
|
||||
- [Specify an anchor for each state by an inset of the `FloatingPanelController.view` frame](#specify-an-anchor-for-each-state-by-an-inset-of-the-floatingpanelcontrollerview-frame)
|
||||
- [Change the backdrop alpha](#change-the-backdrop-alpha)
|
||||
- [Using custome panel states](#using-custome-panel-states)
|
||||
- [Customize the behavior with `FloatingPanelBehavior` protocol](#customize-the-behavior-with-floatingpanelbehavior-protocol)
|
||||
- [Modify your floating panel's interaction](#modify-your-floating-panels-interaction)
|
||||
- [Activate the rubber-band effect on panel edges](#activate-the-rubber-band-effect-on-panel-edges)
|
||||
@@ -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
|
||||
@@ -81,12 +84,16 @@ Examples are here.
|
||||
|
||||
- [Examples/Maps](https://github.com/SCENEE/FloatingPanel/tree/master/Examples/Maps) like Apple Maps.app.
|
||||
- [Examples/Stocks](https://github.com/SCENEE/FloatingPanel/tree/master/Examples/Stocks) like Apple Stocks.app.
|
||||
- [Examples/Samples](https://github.com/SCENEE/FloatingPanel/tree/master/Examples/Samples)
|
||||
- [Examples/SamplesObjC](https://github.com/SCENEE/FloatingPanel/tree/master/Examples/SamplesObjC)
|
||||
|
||||
## Requirements
|
||||
|
||||
FloatingPanel is written in Swift 5.0+. Compatible with iOS 10.0+.
|
||||
FloatingPanel is written in Swift 5.0+. Compatible with iOS 11.0+.
|
||||
|
||||
:pencil2: You would like to use Swift 4.0. Please use FloatingPanel v1.
|
||||
The deployment is still iOS 10, but it is recommended to use this library on iOS 11+.
|
||||
|
||||
:pencil2: You would like to use Swift 4.0. Please use FloatingPanel v1.
|
||||
|
||||
## Installation
|
||||
|
||||
@@ -109,7 +116,7 @@ For [Carthage](https://github.com/Carthage/Carthage), add the following to your
|
||||
github "scenee/FloatingPanel"
|
||||
```
|
||||
|
||||
### Swift Package Manager with Xcode
|
||||
### Swift Package Manager
|
||||
|
||||
Follow [this doc](https://developer.apple.com/documentation/swift_packages/adding_package_dependencies_to_your_app).
|
||||
|
||||
@@ -159,7 +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
|
||||
|
||||
@@ -247,7 +254,7 @@ Otherwise, `FloatingPanelController` fixes the content by the height of the top
|
||||
|
||||
### Customize the layout with `FloatingPanelLayout` protocol
|
||||
|
||||
#### Change the initial layout.
|
||||
#### Change the initial layout
|
||||
|
||||
```swift
|
||||
class ViewController: UIViewController, FloatingPanelControllerDelegate {
|
||||
@@ -258,8 +265,8 @@ class ViewController: UIViewController, FloatingPanelControllerDelegate {
|
||||
}
|
||||
|
||||
class MyFloatingPanelLayout: FloatingPanelLayout {
|
||||
let position = .bottom
|
||||
let initialState = .tip
|
||||
let position: FloatingPanelPosition = .bottom
|
||||
let initialState: FloatingPanelState = .tip
|
||||
var anchors: [FloatingPanelState: FloatingPanelLayoutAnchoring] {
|
||||
return [
|
||||
.full: FloatingPanelLayoutAnchor(absoluteInset: 16.0, edge: .top, referenceGuide: .safeArea),
|
||||
@@ -272,7 +279,7 @@ class MyFloatingPanelLayout: FloatingPanelLayout {
|
||||
|
||||
### Update your panel layout
|
||||
|
||||
There are 2 ways to update the panel layout.
|
||||
There are 2 ways to update the panel layout.
|
||||
|
||||
1. Manually set `FloatingPanelController.layout` to the new layout object directly.
|
||||
|
||||
@@ -281,7 +288,7 @@ fpc.layout = MyPanelLayout()
|
||||
fpc.invalidateLayout() // If needed
|
||||
```
|
||||
|
||||
2. Returns an appropriate layout object in `floatingPanel(_:layoutFor:)` delegate.
|
||||
2. Returns an appropriate layout object in one of 2 `floatingPanel(_:layoutFor:)` delegates.
|
||||
|
||||
```swift
|
||||
class ViewController: UIViewController, FloatingPanelControllerDelegate {
|
||||
@@ -289,6 +296,11 @@ class ViewController: UIViewController, FloatingPanelControllerDelegate {
|
||||
func floatingPanel(_ vc: FloatingPanelController, layoutFor newCollection: UITraitCollection) -> FloatingPanelLayout {
|
||||
return MyFloatingPanelLayout()
|
||||
}
|
||||
|
||||
// OR
|
||||
func floatingPanel(_ vc: FloatingPanelController, layoutFor size: CGSize) -> FloatingPanelLayout {
|
||||
return MyFloatingPanelLayout()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -303,8 +315,8 @@ class ViewController: UIViewController, FloatingPanelControllerDelegate {
|
||||
}
|
||||
|
||||
class LandscapePanelLayout: FloatingPanelLayout {
|
||||
let position = .bottom
|
||||
let initialState = .tip
|
||||
let position: FloatingPanelPosition = .bottom
|
||||
let initialState: FloatingPanelState = .tip
|
||||
var anchors: [FloatingPanelState: FloatingPanelLayoutAnchoring] {
|
||||
return [
|
||||
.full: FloatingPanelLayoutAnchor(absoluteInset: 16.0, edge: .top, referenceGuide: .safeArea),
|
||||
@@ -327,19 +339,19 @@ class LandscapePanelLayout: FloatingPanelLayout {
|
||||
|
||||
```swift
|
||||
class IntrinsicPanelLayout: FloatingPanelLayout {
|
||||
let position = .bottom
|
||||
let initialState = .full
|
||||
let position: FloatingPanelPosition = .bottom
|
||||
let initialState: FloatingPanelState = .full
|
||||
var anchors: [FloatingPanelState: FloatingPanelLayoutAnchoring] {
|
||||
return [
|
||||
.full: FloatingPanelIntrinsicLayoutAnchor(absoluteInset: 0, referenceGuide: .safeArea),
|
||||
.half: FloatingPanelIntrinsicLayoutAnchor(fractionalInset: 0.5, referenceGuide: .safeArea),
|
||||
.full: FloatingPanelIntrinsicLayoutAnchor(absoluteOffset: 0, referenceGuide: .safeArea),
|
||||
.half: FloatingPanelIntrinsicLayoutAnchor(fractionalOffset: 0.5, referenceGuide: .safeArea),
|
||||
]
|
||||
}
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
✏️ `FloatingPanelIntrinsicLayout` is deprecated on v1.
|
||||
:pencil2: `FloatingPanelIntrinsicLayout` is deprecated on v1.
|
||||
|
||||
#### Specify an anchor for each state by an inset of the `FloatingPanelController.view` frame
|
||||
|
||||
@@ -359,7 +371,47 @@ class MyFullScreenLayout: FloatingPanelLayout {
|
||||
}
|
||||
```
|
||||
|
||||
✏️ `FloatingPanelFullScreenLayout` is deprecated on v1.
|
||||
:pencil2: `FloatingPanelFullScreenLayout` is deprecated on v1.
|
||||
|
||||
#### Change the backdrop alpha
|
||||
|
||||
You can change the backdrop alpha by `FloatingPanelLayout.backdropAlpha(for:)` for each state(`.full`, `.half` and `.tip`).
|
||||
|
||||
For instance, if a panel seems like the backdrop view isn't there on `.half` state, it's time to implement the backdropAlpha API and return a value for the state as below.
|
||||
|
||||
```swift
|
||||
class MyPanelLayout: FloatingPanelLayout {
|
||||
func backdropAlpha(for state: FloatingPanelState) -> CGFloat {
|
||||
switch state {
|
||||
case .full, .half: return 0.3
|
||||
default: return 0.0
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Using custome panel states
|
||||
|
||||
You're able to define custom panel states and use them as the following example.
|
||||
|
||||
```swift
|
||||
extension FloatingPanelState {
|
||||
static let lastQuart: FloatingPanelState = FloatingPanelState(rawValue: "lastQuart", order: 750)
|
||||
static let firstQuart: FloatingPanelState = FloatingPanelState(rawValue: "firstQuart", order: 250)
|
||||
}
|
||||
|
||||
class FloatingPanelLayoutWithCustomState: FloatingPanelBottomLayout {
|
||||
override var anchors: [FloatingPanelState: FloatingPanelLayoutAnchoring] {
|
||||
return [
|
||||
.full: FloatingPanelLayoutAnchor(absoluteInset: 0.0, edge: .top, referenceGuide: .safeArea),
|
||||
.lastQuart: FloatingPanelLayoutAnchor(fractionalInset: 0.75, edge: .bottom, referenceGuide: .safeArea),
|
||||
.half: FloatingPanelLayoutAnchor(fractionalInset: 0.5, edge: .bottom, referenceGuide: .safeArea),
|
||||
.firstQuart: FloatingPanelLayoutAnchor(fractionalInset: 0.25, edge: .bottom, referenceGuide: .safeArea),
|
||||
.tip: FloatingPanelLayoutAnchor(absoluteInset: 20.0, edge: .bottom, referenceGuide: .safeArea),
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Customize the behavior with `FloatingPanelBehavior` protocol
|
||||
|
||||
@@ -383,7 +435,7 @@ class CustomPanelBehavior: FloatingPanelBehavior {
|
||||
}
|
||||
```
|
||||
|
||||
✏️ `floatingPanel(_ vc:behaviorFor:)` is deprecated on v1.
|
||||
:pencil2: `floatingPanel(_ vc:behaviorFor:)` is deprecated on v1.
|
||||
|
||||
#### Activate the rubber-band effect on panel edges
|
||||
|
||||
@@ -513,15 +565,15 @@ 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)
|
||||
}
|
||||
```
|
||||
|
||||
#### Interrupt the delegate methods of `FloatingPanelController.panGestureRecognizer`
|
||||
|
||||
`FloatingPanelPanGestureRecognizer.delegateProxy` is able to override the delegate methods by your own delegate object.
|
||||
If you are set `FloatingPanelController.panGestureRecognizer.delegateProxy` to an object adopting `UIGestureRecognizerDelegate`, it overrides delegate methods of the pan gesture recognizer.
|
||||
|
||||
```swift
|
||||
class MyGestureRecognizerDelegate: UIGestureRecognizerDelegate {
|
||||
@@ -579,6 +631,14 @@ func searchBarTextDidBeginEditing(_ searchBar: UISearchBar) {
|
||||
}
|
||||
```
|
||||
|
||||
You can also use a view animation to move a panel.
|
||||
|
||||
```swift
|
||||
UIView.animate(withDuration: 0.25) {
|
||||
self.fpc.move(to: .half, animated: false)
|
||||
}
|
||||
```
|
||||
|
||||
### Work your contents together with a floating panel behavior
|
||||
|
||||
```swift
|
||||
@@ -599,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,9 +1,11 @@
|
||||
// Copyright 2018 the FloatingPanel authors. All rights reserved. MIT license.
|
||||
// Copyright 2018-Present Shin Yamamoto. All rights reserved. MIT license.
|
||||
|
||||
import UIKit
|
||||
|
||||
/// A view that presents a backdrop interface behind a floating panel.
|
||||
/// A view that presents a backdrop interface behind a panel.
|
||||
@objc(FloatingPanelBackdropView)
|
||||
public class BackdropView: UIView {
|
||||
public var dismissalTapGestureRecognizer: UITapGestureRecognizer!
|
||||
|
||||
/// The gesture recognizer for tap gestures to dismiss a panel.
|
||||
@objc public var dismissalTapGestureRecognizer: UITapGestureRecognizer!
|
||||
}
|
||||
|
||||
+28
-23
@@ -1,7 +1,8 @@
|
||||
// Copyright 2018 the FloatingPanel authors. All rights reserved. MIT license.
|
||||
// Copyright 2018-Present Shin Yamamoto. All rights reserved. MIT license.
|
||||
|
||||
import UIKit
|
||||
|
||||
/// An interface for generating behavior information to fine-tune the behavior of a panel.
|
||||
@objc
|
||||
public protocol FloatingPanelBehavior {
|
||||
/// A floating-point value that determines the rate of oscillation magnitude reduction after the user lifts their finger.
|
||||
@@ -24,7 +25,7 @@ public protocol FloatingPanelBehavior {
|
||||
@objc optional
|
||||
var momentumProjectionRate: CGFloat { get }
|
||||
|
||||
/// Asks the behavior if the floating panel should project a momentum of a user interaction to move the proposed position.
|
||||
/// Asks the behavior if a panel should project a momentum of a user interaction to move the proposed position.
|
||||
///
|
||||
/// The default implementation of this method returns true. This method is called for a layout to support all positions(tip, half and full).
|
||||
/// Therefore, `proposedTargetPosition` can only be `FloatingPanelState.tip` or `FloatingPanelState.full`.
|
||||
@@ -33,50 +34,50 @@ public protocol FloatingPanelBehavior {
|
||||
|
||||
/// Returns the progress to redirect to the previous position.
|
||||
///
|
||||
/// The progress is represented by a floating-point value between 0.0 and 1.0, inclusive, where 1.0 indicates the floating panel is impossible to move to the next position. The default value is 0.5. Values less than 0.0 and greater than 1.0 are pinned to those limits.
|
||||
/// The progress is represented by a floating-point value between 0.0 and 1.0, inclusive, where 1.0 indicates a panel is impossible to move to the next position. The default value is 0.5. Values less than 0.0 and greater than 1.0 are pinned to those limits.
|
||||
@objc optional
|
||||
func redirectionalProgress(_ fpc: FloatingPanelController, from: FloatingPanelState, to: FloatingPanelState) -> CGFloat
|
||||
|
||||
/// Asks the behavior whether the rubber band effect is enabled in moving over a given edge of the surface view.
|
||||
///
|
||||
/// This method allows the behavior to activate the rubber band effect to a given edge of the surface view. By default, the effect is disabled.
|
||||
/// This method allows a panel to activate the rubber band effect to a given edge of the surface view. By default, the effect is disabled.
|
||||
@objc optional
|
||||
func allowsRubberBanding(for edge: UIRectEdge) -> Bool
|
||||
|
||||
/// Returns the velocity threshold for the default interactive removal gesture.
|
||||
///
|
||||
/// In case `floatingPanel:shouldRemoveAt:with` is implemented, this value will not be used. The default value of `FloatingPanelDefaultBehavior` is 5.5
|
||||
@objc optional
|
||||
var removalInteractionVelocityThreshold: CGFloat { get }
|
||||
}
|
||||
|
||||
class FloatingPanelDefaultBehavior: FloatingPanelBehavior {
|
||||
var springDecelerationRate: CGFloat {
|
||||
/// The default behavior object for a panel
|
||||
///
|
||||
/// This behavior object is fine-tuned to behave as a search panel(card) in Apple Maps on iPhone portrait orientation.
|
||||
open class FloatingPanelDefaultBehavior: FloatingPanelBehavior {
|
||||
public init() {}
|
||||
|
||||
open var springDecelerationRate: CGFloat {
|
||||
return UIScrollView.DecelerationRate.fast.rawValue + 0.001
|
||||
}
|
||||
|
||||
var springResponseTime: CGFloat {
|
||||
open var springResponseTime: CGFloat {
|
||||
return 0.4
|
||||
}
|
||||
|
||||
var momentumProjectionRate: CGFloat {
|
||||
open var momentumProjectionRate: CGFloat {
|
||||
return UIScrollView.DecelerationRate.normal.rawValue
|
||||
}
|
||||
|
||||
func redirectionalProgress(_ fpc: FloatingPanelController, from: FloatingPanelState, to: FloatingPanelState) -> CGFloat {
|
||||
open func redirectionalProgress(_ fpc: FloatingPanelController, from: FloatingPanelState, to: FloatingPanelState) -> CGFloat {
|
||||
return 0.5
|
||||
}
|
||||
|
||||
func addPanelAnimator(_ fpc: FloatingPanelController, to: FloatingPanelState) -> UIViewPropertyAnimator {
|
||||
return UIViewPropertyAnimator(duration: 0.0,
|
||||
timingParameters: UISpringTimingParameters(decelerationRate: UIScrollView.DecelerationRate.fast.rawValue,
|
||||
frequencyResponse: 0.25))
|
||||
}
|
||||
|
||||
func removePanelAnimator(_ fpc: FloatingPanelController, from: FloatingPanelState, with velocity: CGVector) -> UIViewPropertyAnimator {
|
||||
return UIViewPropertyAnimator(duration: 0.0,
|
||||
timingParameters: UISpringTimingParameters(decelerationRate: UIScrollView.DecelerationRate.fast.rawValue,
|
||||
frequencyResponse: 0.25,
|
||||
initialVelocity: velocity))
|
||||
}
|
||||
|
||||
func allowsRubberBanding(for edge: UIRectEdge) -> Bool {
|
||||
open func allowsRubberBanding(for edge: UIRectEdge) -> Bool {
|
||||
return false
|
||||
}
|
||||
|
||||
open var removalInteractionVelocityThreshold: CGFloat = 5.5
|
||||
}
|
||||
|
||||
class BehaviorAdapter {
|
||||
@@ -99,6 +100,10 @@ class BehaviorAdapter {
|
||||
var momentumProjectionRate: CGFloat {
|
||||
behavior.momentumProjectionRate ?? FloatingPanelDefaultBehavior().momentumProjectionRate
|
||||
}
|
||||
|
||||
var removalInteractionVelocityThreshold: CGFloat {
|
||||
behavior.removalInteractionVelocityThreshold ?? FloatingPanelDefaultBehavior().removalInteractionVelocityThreshold
|
||||
}
|
||||
|
||||
func redirectionalProgress(from: FloatingPanelState, to: FloatingPanelState) -> CGFloat {
|
||||
behavior.redirectionalProgress?(vc, from: from, to: to) ?? FloatingPanelDefaultBehavior().redirectionalProgress(vc,from: from, to: to)
|
||||
|
||||
+97
-57
@@ -1,76 +1,86 @@
|
||||
// Copyright 2018 the FloatingPanel authors. All rights reserved. MIT license.
|
||||
// Copyright 2018-Present Shin Yamamoto. All rights reserved. MIT license.
|
||||
|
||||
import UIKit
|
||||
|
||||
@objc public protocol FloatingPanelControllerDelegate: class {
|
||||
// Returns a FloatingPanelLayout object. If you use the default one, you can use a `FloatingPanelDefaultLayout` object.
|
||||
/// A set of methods implemented by the delegate of a panel controller allows the adopting delegate to respond to
|
||||
/// messages from the FloatingPanelController class and thus respond to, and in some affect, operations such as
|
||||
/// dragging, attracting a panel, layout of a panel and the content, and transition animations.
|
||||
@objc public protocol FloatingPanelControllerDelegate {
|
||||
/// Returns a FloatingPanelLayout object. If you use the default one, you can use a `FloatingPanelBottomLayout` object.
|
||||
@objc(floatingPanel:layoutForTraitCollection:) optional
|
||||
func floatingPanel(_ fpc: FloatingPanelController, layoutFor newCollection: UITraitCollection) -> FloatingPanelLayout
|
||||
|
||||
// Returns a FloatingPanelLayout object. If you use the default one, you can use a `FloatingPanelDefaultLayout` object.
|
||||
/// Returns a FloatingPanelLayout object. If you use the default one, you can use a `FloatingPanelBottomLayout` object.
|
||||
@objc(floatingPanel:layoutForSize:) optional
|
||||
func floatingPanel(_ fpc: FloatingPanelController, layoutFor size: CGSize) -> FloatingPanelLayout
|
||||
|
||||
/// Returns a UIViewPropertyAnimator object to add/present a floating panel to a position.
|
||||
/// Returns a UIViewPropertyAnimator object to add/present the panel to a position.
|
||||
///
|
||||
/// Default is the spring animator with 0.25 sec duration and `UIScrollView.DecelerationRate.fast` deceleration rate.
|
||||
/// Default is the spring animation with 0.25 secs.
|
||||
@objc(floatingPanel:animatorForPresentingToState:) optional
|
||||
func floatingPanel(_ fpc: FloatingPanelController, animatorForPresentingTo state: FloatingPanelState) -> UIViewPropertyAnimator
|
||||
|
||||
/// Returns a UIViewPropertyAnimator object to remove/dismiss a floating panel from a position.
|
||||
/// Returns a UIViewPropertyAnimator object to remove/dismiss a panel from a position.
|
||||
///
|
||||
/// Default is the spring animator with 0.25 sec duration and `UIScrollView.DecelerationRate.fast` deceleration rate.
|
||||
/// Default is the spring animator with 0.25 secs.
|
||||
@objc(floatingPanel:animatorForDismissingWithVelocity:) optional
|
||||
func floatingPanel(_ fpc: FloatingPanelController, animatorForDismissingWith velocity: CGVector) -> UIViewPropertyAnimator
|
||||
|
||||
/// Called when the floating panel has changed to a new position. Can be called inside an animation block, so any
|
||||
/// view properties set inside this function will be automatically animated alongside the panel.
|
||||
/// Called when a panel has changed to a new 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
|
||||
func floatingPanelShouldBeginDragging(_ fpc: FloatingPanelController) -> Bool
|
||||
|
||||
/// Called when the user drags the surface or the surface is attracted to a state anchor.
|
||||
@objc optional
|
||||
func floatingPanelDidMove(_ fpc: FloatingPanelController) // any surface frame changes in dragging
|
||||
|
||||
// called on start of dragging (may require some time and or distance to move)
|
||||
/// Called on start of dragging (may require some time and or distance to move)
|
||||
@objc optional
|
||||
func floatingPanelWillBeginDragging(_ fpc: FloatingPanelController)
|
||||
|
||||
// called on finger up if the user dragged. velocity is in points/second.
|
||||
/// Called on finger up if the user dragged. velocity is in points/second.
|
||||
@objc optional
|
||||
func floatingPanelWillEndDragging(_ fpc: FloatingPanelController, withVelocity velocity: CGPoint, targetState: UnsafeMutablePointer<FloatingPanelState>)
|
||||
|
||||
// called on finger up if the user dragged. decelerate is true if it will continue moving afterwards
|
||||
/// Called on finger up if the user dragged.
|
||||
///
|
||||
/// If `attract` is true, it will continue moving afterwards to a nearby state anchor.
|
||||
@objc optional
|
||||
func floatingPanelDidEndDragging(_ fpc: FloatingPanelController, willDecelerate decelerate: Bool)
|
||||
func floatingPanelDidEndDragging(_ fpc: FloatingPanelController, willAttract attract: Bool)
|
||||
|
||||
/// Called when it is about to be attracted to a state anchor.
|
||||
@objc optional
|
||||
func floatingPanelWillBeginAttracting(_ fpc: FloatingPanelController, to state: FloatingPanelState) // called on finger up as a panel are moving
|
||||
|
||||
/// Called when attracting it is completed.
|
||||
@objc optional
|
||||
func floatingPanelDidEndAttracting(_ fpc: FloatingPanelController) // called when a panel stops
|
||||
|
||||
/// Asks the delegate whether the panel should be removed when dragging ended at the specified location
|
||||
/// Asks the delegate whether a panel should be removed when dragging ended at the specified location
|
||||
///
|
||||
/// This delegate method is called only when `FloatingPanelController.isRemovalInteractionEnabled` is set to true.
|
||||
/// This delegate method is called only where `FloatingPanelController.isRemovalInteractionEnabled` is `true`.
|
||||
/// The velocity vector is calculated from the distance to a point of the hidden state and the pan gesture's velocity.
|
||||
@objc(floatingPanel:shouldRemoveAtLocation:withVelocity:)
|
||||
optional
|
||||
func floatingPanel(_ fpc: FloatingPanelController, shouldRemoveAt location: CGPoint, with velocity: CGVector) -> Bool
|
||||
|
||||
// called on start to remove its view controller from the parent view controller.
|
||||
/// Called on start to remove its view controller from the parent view controller.
|
||||
@objc(floatingPanelWillRemove:)
|
||||
optional
|
||||
func floatingPanelWillRemove(_ fpc: FloatingPanelController)
|
||||
|
||||
// called when its view controller are removed from the parent view controller.
|
||||
/// Called when a panel is removed from the parent view controller.
|
||||
@objc optional
|
||||
func floatingPanelDidRemove(_ fpc: FloatingPanelController)
|
||||
|
||||
/// Asks the delegate for a content offset of the tracked scroll view to be pinned when a floating panel moves
|
||||
/// Asks the delegate for a content offset of the tracking scroll view to be pinned when a panel moves
|
||||
///
|
||||
/// If you do not implement this method, the controller uses a value of the content offset plus the content insets
|
||||
/// of the tracked scroll view. Your implementation of this method can return a value for a navigation bar with a large
|
||||
@@ -83,7 +93,7 @@ import UIKit
|
||||
}
|
||||
|
||||
///
|
||||
/// A container view controller to display a floating panel to present contents in parallel as a user wants.
|
||||
/// A container view controller to display a panel to present contents in parallel as a user wants.
|
||||
///
|
||||
@objc
|
||||
open class FloatingPanelController: UIViewController {
|
||||
@@ -103,7 +113,7 @@ open class FloatingPanelController: UIViewController {
|
||||
case fitToBounds
|
||||
}
|
||||
|
||||
/// The delegate of the floating panel controller object.
|
||||
/// The delegate of a panel controller object.
|
||||
@objc
|
||||
public weak var delegate: FloatingPanelControllerDelegate?{
|
||||
didSet{
|
||||
@@ -135,12 +145,13 @@ open class FloatingPanelController: UIViewController {
|
||||
return floatingPanel.panGestureRecognizer
|
||||
}
|
||||
|
||||
/// The current position of the floating panel controller's contents.
|
||||
/// The current position of a panel controller's contents.
|
||||
@objc
|
||||
public var state: FloatingPanelState {
|
||||
return floatingPanel.state
|
||||
}
|
||||
|
||||
/// A Boolean value indicating whether a panel controller is attracting the surface to a state anchor.
|
||||
@objc
|
||||
public var isAttracting: Bool {
|
||||
return floatingPanel.isAttracting
|
||||
@@ -189,7 +200,7 @@ open class FloatingPanelController: UIViewController {
|
||||
@objc(isRemovalInteractionEnabled) get { return floatingPanel.isRemovalInteractionEnabled }
|
||||
}
|
||||
|
||||
/// The view controller responsible for the content portion of the floating panel.
|
||||
/// The view controller responsible for the content portion of a panel.
|
||||
@objc
|
||||
public var contentViewController: UIViewController? {
|
||||
set { set(contentViewController: newValue) }
|
||||
@@ -202,6 +213,7 @@ open class FloatingPanelController: UIViewController {
|
||||
return floatingPanel.targetPosition(from: currentY, with: .zero)
|
||||
}
|
||||
|
||||
/// Constants that define how a panel content fills in the surface.
|
||||
@objc
|
||||
public var contentMode: ContentMode = .static {
|
||||
didSet {
|
||||
@@ -227,7 +239,7 @@ open class FloatingPanelController: UIViewController {
|
||||
setUp()
|
||||
}
|
||||
|
||||
/// Initialize a newly created floating panel controller.
|
||||
/// Initialize a newly created panel controller.
|
||||
@objc
|
||||
public init(delegate: FloatingPanelControllerDelegate? = nil) {
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
@@ -264,7 +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
|
||||
@@ -279,7 +291,7 @@ open class FloatingPanelController: UIViewController {
|
||||
open override func viewDidLayoutSubviews() {
|
||||
super.viewDidLayoutSubviews()
|
||||
if #available(iOS 11.0, *) {
|
||||
// Ensure the panel's static constraint after rotating a device in static mode
|
||||
// Ensure to update the static constraint of a panel after rotating a device in static mode
|
||||
if contentMode == .static {
|
||||
floatingPanel.layoutAdapter.updateStaticConstraint()
|
||||
}
|
||||
@@ -289,7 +301,6 @@ open class FloatingPanelController: UIViewController {
|
||||
self.update(safeAreaInsets: fp_safeAreaInsets)
|
||||
}
|
||||
}
|
||||
floatingPanel.layoutAdapter.checkLayout()
|
||||
}
|
||||
|
||||
open override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
|
||||
@@ -387,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() {
|
||||
@@ -418,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
|
||||
@@ -431,7 +433,7 @@ open class FloatingPanelController: UIViewController {
|
||||
// 2. The safe area top inset can be variable on the large title navigation bar(iOS11+).
|
||||
// That's why it needs the observation to keep `adjustedContentInsets` correct.
|
||||
safeAreaInsetsObservation = self.view.observe(\.safeAreaInsets, options: [.initial, .new, .old]) { [weak self] (_, change) in
|
||||
// Use `self.view.safeAreaInsets` becauese `change.newValue` can be nil in particular case when
|
||||
// Use `self.view.safeAreaInsets` because `change.newValue` can be nil in particular case when
|
||||
// is reported in https://github.com/SCENEE/FloatingPanel/issues/330
|
||||
guard let self = self, change.oldValue != self.view.safeAreaInsets else { return }
|
||||
self.update(safeAreaInsets: self.view.safeAreaInsets)
|
||||
@@ -459,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
|
||||
@@ -468,8 +471,8 @@ open class FloatingPanelController: UIViewController {
|
||||
assert((parent is UINavigationController) == false, "UINavigationController displays only one child view controller at a time.")
|
||||
assert((parent is UITabBarController) == false, "UITabBarController displays child view controllers with a radio-style selection interface")
|
||||
assert((parent is UISplitViewController) == false, "UISplitViewController manages two child view controllers in a master-detail interface")
|
||||
assert((parent is UITableViewController) == false, "UITableViewController should not be the parent because the view is a table view so that a floating panel doens't work well")
|
||||
assert((parent is UICollectionViewController) == false, "UICollectionViewController should not be the parent because the view is a collection view so that a floating panel doens't work well")
|
||||
assert((parent is UITableViewController) == false, "UITableViewController should not be the parent because the view is a table view so that a panel doesn't work well")
|
||||
assert((parent is UICollectionViewController) == false, "UICollectionViewController should not be the parent because the view is a collection view so that a panel doesn't work well")
|
||||
|
||||
if viewIndex < 0 {
|
||||
parent.view.addSubview(self.view)
|
||||
@@ -489,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?()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -508,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)
|
||||
|
||||
@@ -522,17 +526,17 @@ 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)
|
||||
}
|
||||
|
||||
/// Sets the view controller responsible for the content portion of the floating panel.
|
||||
/// Sets the view controller responsible for the content portion of a panel.
|
||||
public func set(contentViewController: UIViewController?) {
|
||||
if let vc = _contentViewController {
|
||||
vc.willMove(toParent: nil)
|
||||
@@ -580,7 +584,7 @@ open class FloatingPanelController: UIViewController {
|
||||
}
|
||||
}
|
||||
|
||||
/// Cancel tracking the specifiy scroll view.
|
||||
/// Cancel tracking the specify scroll view.
|
||||
///
|
||||
@objc(untrackScrollView:)
|
||||
public func untrack(scrollView: UIScrollView) {
|
||||
@@ -589,6 +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
|
||||
@@ -596,19 +608,28 @@ open class FloatingPanelController: UIViewController {
|
||||
///
|
||||
/// This method updates the `FloatingPanelLayout` object from the delegate and
|
||||
/// then it calls `layoutIfNeeded()` of the root view to force the view
|
||||
/// to update the floating panel's layout immediately. It can be called in an
|
||||
/// to update the layout immediately. It can be called in an
|
||||
/// animation block.
|
||||
@objc
|
||||
public func invalidateLayout() {
|
||||
activateLayout(forceLayout: true)
|
||||
}
|
||||
|
||||
/// Returns the y-coordinate of the point at the origin of the surface view.
|
||||
/// Returns the surface's position in a panel controller's view for the specified state.
|
||||
///
|
||||
/// If a panel is top positioned, this returns a point of the bottom-left corner of the surface. If it is left positioned
|
||||
/// this returns a point of top-right corner of the surface. If it is bottom or right positioned, this returns a point
|
||||
/// of the top-left corner of the surface.
|
||||
@objc
|
||||
public func surfaceLocation(for state: FloatingPanelState) -> CGPoint {
|
||||
return floatingPanel.layoutAdapter.surfaceLocation(for: state)
|
||||
}
|
||||
|
||||
/// The surface's position in a panel controller's view.
|
||||
///
|
||||
/// If a panel is top positioned, this returns a point of the bottom-left corner of the surface. If it is left positioned
|
||||
/// this returns a point of top-right corner of the surface. If it is bottom or right positioned, this returns a point
|
||||
/// of the top-left corner of the surface.
|
||||
@objc
|
||||
public var surfaceLocation: CGPoint {
|
||||
get { floatingPanel.layoutAdapter.surfaceLocation }
|
||||
@@ -623,10 +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:))) {
|
||||
@@ -635,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
|
||||
}()
|
||||
}
|
||||
|
||||
|
||||
+179
-102
@@ -1,13 +1,12 @@
|
||||
// Copyright 2018 the FloatingPanel authors. All rights reserved. MIT license.
|
||||
// Copyright 2018-Present Shin Yamamoto. All rights reserved. MIT license.
|
||||
|
||||
import UIKit
|
||||
|
||||
///
|
||||
/// FloatingPanel presentation model
|
||||
/// The presentation model of FloatingPanel
|
||||
///
|
||||
class Core: NSObject, UIGestureRecognizerDelegate {
|
||||
// MUST be a weak reference to prevent UI freeze on the presentation modally
|
||||
weak var viewcontroller: FloatingPanelController?
|
||||
private weak var ownerVC: FloatingPanelController?
|
||||
|
||||
let surfaceView: SurfaceView
|
||||
let backdropView: BackdropView
|
||||
@@ -24,8 +23,8 @@ class Core: NSObject, UIGestureRecognizerDelegate {
|
||||
private(set) var state: FloatingPanelState = .hidden {
|
||||
didSet {
|
||||
log.debug("state changed: \(oldValue) -> \(state)")
|
||||
if let vc = viewcontroller {
|
||||
vc.delegate?.floatingPanelDidChangePosition?(vc)
|
||||
if let vc = ownerVC {
|
||||
vc.delegate?.floatingPanelDidChangeState?(vc)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -33,7 +32,8 @@ class Core: NSObject, UIGestureRecognizerDelegate {
|
||||
let panGestureRecognizer: FloatingPanelPanGestureRecognizer
|
||||
var isRemovalInteractionEnabled: Bool = false
|
||||
|
||||
fileprivate var animator: UIViewPropertyAnimator?
|
||||
fileprivate var isSuspended: Bool = false // Prevent a memory leak in the modal transition
|
||||
fileprivate var transitionAnimator: UIViewPropertyAnimator?
|
||||
fileprivate var moveAnimator: NumericSpringAnimator?
|
||||
|
||||
private var initialSurfaceLocation: CGPoint = .zero
|
||||
@@ -51,7 +51,7 @@ class Core: NSObject, UIGestureRecognizerDelegate {
|
||||
// Scroll handling
|
||||
private var initialScrollOffset: CGPoint = .zero
|
||||
private var stopScrollDeceleration: Bool = false
|
||||
private var scrollBouncable = false
|
||||
private var scrollBounce = false
|
||||
private var scrollIndictorVisible = false
|
||||
private var grabberAreaFrame: CGRect {
|
||||
return surfaceView.grabberAreaFrame
|
||||
@@ -60,7 +60,7 @@ class Core: NSObject, UIGestureRecognizerDelegate {
|
||||
// MARK: - Interface
|
||||
|
||||
init(_ vc: FloatingPanelController, layout: FloatingPanelLayout, behavior: FloatingPanelBehavior) {
|
||||
viewcontroller = vc
|
||||
ownerVC = vc
|
||||
|
||||
surfaceView = SurfaceView()
|
||||
surfaceView.backgroundColor = .white
|
||||
@@ -69,11 +69,8 @@ class Core: NSObject, UIGestureRecognizerDelegate {
|
||||
backdropView.backgroundColor = .black
|
||||
backdropView.alpha = 0.0
|
||||
|
||||
self.layoutAdapter = LayoutAdapter(vc: vc,
|
||||
surfaceView: surfaceView,
|
||||
backdropView: backdropView,
|
||||
layout: layout)
|
||||
self.behaviorAdapter = BehaviorAdapter(vc: vc, behavior: behavior)
|
||||
layoutAdapter = LayoutAdapter(vc: vc, layout: layout)
|
||||
behaviorAdapter = BehaviorAdapter(vc: vc, behavior: behavior)
|
||||
|
||||
panGestureRecognizer = FloatingPanelPanGestureRecognizer()
|
||||
|
||||
@@ -95,17 +92,22 @@ class Core: NSObject, UIGestureRecognizerDelegate {
|
||||
backdropView.addGestureRecognizer(tapGesture)
|
||||
}
|
||||
|
||||
deinit {
|
||||
// Release `NumericSpringAnimator.displayLink` from the run loop.
|
||||
self.moveAnimator?.stopAnimation(false)
|
||||
}
|
||||
|
||||
func move(to: FloatingPanelState, animated: Bool, completion: (() -> Void)? = nil) {
|
||||
move(from: state, to: to, animated: animated, completion: completion)
|
||||
}
|
||||
|
||||
private func move(from: FloatingPanelState, to: FloatingPanelState, animated: Bool, completion: (() -> Void)? = nil) {
|
||||
assert(layoutAdapter.validStates.contains(to), "Can't move to '\(to)' state because it's not valid in the layout")
|
||||
guard let vc = viewcontroller else {
|
||||
guard let vc = ownerVC else {
|
||||
completion?()
|
||||
return
|
||||
}
|
||||
if state != layoutAdapter.edgeMostState {
|
||||
if state != layoutAdapter.mostExpandedState {
|
||||
lockScrollView()
|
||||
}
|
||||
tearDownActiveInteraction()
|
||||
@@ -113,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()
|
||||
@@ -124,63 +127,106 @@ class Core: NSObject, UIGestureRecognizerDelegate {
|
||||
let animator: UIViewPropertyAnimator
|
||||
switch (from, to) {
|
||||
case (.hidden, let to):
|
||||
animator = vc.delegate?.floatingPanel?(vc, animatorForPresentingTo: to)
|
||||
?? FloatingPanelDefaultBehavior().addPanelAnimator(vc, to: to)
|
||||
case (let from, .hidden):
|
||||
animator = vc.animatorForPresenting(to: to)
|
||||
case (_, .hidden):
|
||||
let animationVector = CGVector(dx: abs(removalVector.dx), dy: abs(removalVector.dy))
|
||||
animator = vc.delegate?.floatingPanel?(vc, animatorForDismissingWith: .zero)
|
||||
?? FloatingPanelDefaultBehavior().removePanelAnimator(vc, from: from, with: animationVector)
|
||||
animator = vc.animatorForDismissing(with: animationVector)
|
||||
default:
|
||||
move(to: to, with: 0) {
|
||||
move(to: to, with: 0) { [weak self] in
|
||||
guard let self = self else { return }
|
||||
|
||||
self.moveAnimator = nil
|
||||
updateScrollView()
|
||||
completion?()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
let shouldDoubleLayout = from == .hidden
|
||||
&& surfaceView.hasStackView()
|
||||
&& layoutAdapter.isIntrinsicAnchor(state: to)
|
||||
|
||||
animator.addAnimations { [weak self] in
|
||||
guard let `self` = self else { return }
|
||||
guard let self = self else { return }
|
||||
|
||||
self.state = to
|
||||
self.updateLayout(to: to)
|
||||
|
||||
if shouldDoubleLayout {
|
||||
log.info("Lay out the surface again to modify an intrinsic size error according to UIStackView")
|
||||
self.updateLayout(to: to)
|
||||
}
|
||||
}
|
||||
animator.addCompletion { [weak self] _ in
|
||||
guard let `self` = self else { return }
|
||||
self.animator = nil
|
||||
guard let self = self else { return }
|
||||
|
||||
self.transitionAnimator = nil
|
||||
updateScrollView()
|
||||
self.viewcontroller?.notifyDidMove()
|
||||
self.ownerVC?.notifyDidMove()
|
||||
completion?()
|
||||
}
|
||||
self.animator = animator
|
||||
self.transitionAnimator = animator
|
||||
if isSuspended {
|
||||
return
|
||||
}
|
||||
animator.startAnimation()
|
||||
} else {
|
||||
self.state = to
|
||||
self.updateLayout(to: to)
|
||||
if self.state == self.layoutAdapter.edgeMostState {
|
||||
if self.state == self.layoutAdapter.mostExpandedState {
|
||||
self.unlockScrollView()
|
||||
} else {
|
||||
self.lockScrollView()
|
||||
|
||||
}
|
||||
viewcontroller?.notifyDidMove()
|
||||
ownerVC?.notifyDidMove()
|
||||
completion?()
|
||||
}
|
||||
}
|
||||
|
||||
// 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 {
|
||||
/* log.debug("currentY: \(currentY) translation: \(translation)") */
|
||||
let forwardY = (translation >= 0)
|
||||
|
||||
let segment = layoutAdapter.segument(at: cur, forward: forwardY)
|
||||
let segment = layoutAdapter.segment(at: cur, forward: forwardY)
|
||||
|
||||
let lowerState = segment.lower ?? layoutAdapter.edgeMostState
|
||||
let upperState = segment.upper ?? layoutAdapter.edgeLeastState
|
||||
let lowerState = segment.lower ?? layoutAdapter.mostExpandedState
|
||||
let upperState = segment.upper ?? layoutAdapter.leastExpandedState
|
||||
|
||||
let preState = forwardY ? lowerState : upperState
|
||||
let nextState = forwardY ? upperState : lowerState
|
||||
@@ -193,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
|
||||
@@ -212,7 +257,7 @@ class Core: NSObject, UIGestureRecognizerDelegate {
|
||||
|
||||
switch otherGestureRecognizer {
|
||||
case is FloatingPanelPanGestureRecognizer:
|
||||
// All visiable panels' pan gesture should be recognized simultaneously.
|
||||
// All visible panels' pan gesture should be recognized simultaneously.
|
||||
return true
|
||||
case is UIPanGestureRecognizer,
|
||||
is UISwipeGestureRecognizer,
|
||||
@@ -228,7 +273,7 @@ class Core: NSObject, UIGestureRecognizerDelegate {
|
||||
default:
|
||||
// Should recognize tap/long press gestures in parallel when the surface view is at an anchor position.
|
||||
let adapterY = layoutAdapter.position(for: state)
|
||||
return abs(value(of: layoutAdapter.surfaceLocation) - adapterY) < (1.0 / surfaceView.traitCollection.displayScale)
|
||||
return abs(value(of: layoutAdapter.surfaceLocation) - adapterY) < (1.0 / surfaceView.fp_displayScale)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -238,7 +283,7 @@ class Core: NSObject, UIGestureRecognizerDelegate {
|
||||
}
|
||||
|
||||
if otherGestureRecognizer is FloatingPanelPanGestureRecognizer {
|
||||
// If this panel is the farthest descendant of visiable panels,
|
||||
// If this panel is the farthest descendant of visible panels,
|
||||
// its ancestors' pan gesture must wait for its pan gesture to fail
|
||||
if let view = otherGestureRecognizer.view, surfaceView.isDescendant(of: view) {
|
||||
return true
|
||||
@@ -290,7 +335,7 @@ class Core: NSObject, UIGestureRecognizerDelegate {
|
||||
|
||||
switch otherGestureRecognizer {
|
||||
case is FloatingPanelPanGestureRecognizer:
|
||||
// If this panel is the farthest descendant of visiable panels,
|
||||
// If this panel is the farthest descendant of visible panels,
|
||||
// its pan gesture does not require its ancestors' pan gesture to fail
|
||||
if let view = otherGestureRecognizer.view, surfaceView.isDescendant(of: view) {
|
||||
return false
|
||||
@@ -321,7 +366,7 @@ class Core: NSObject, UIGestureRecognizerDelegate {
|
||||
|
||||
@objc func handleBackdrop(tapGesture: UITapGestureRecognizer) {
|
||||
removalVector = .zero
|
||||
viewcontroller?.remove()
|
||||
ownerVC?.remove()
|
||||
}
|
||||
|
||||
@objc func handle(panGesture: UIPanGestureRecognizer) {
|
||||
@@ -332,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.traitCollection.displayScale)
|
||||
let belowEdgeMost = 0 > layoutAdapter.offsetFromMostExpandedAnchor + (1.0 / surfaceView.fp_displayScale)
|
||||
|
||||
log.debug("""
|
||||
scroll gesture(\(state):\(panGesture.state)) -- \
|
||||
@@ -346,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)
|
||||
@@ -364,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 {
|
||||
@@ -392,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) {
|
||||
@@ -438,7 +483,7 @@ class Core: NSObject, UIGestureRecognizerDelegate {
|
||||
""")
|
||||
|
||||
if interactionInProgress == false, isAttracting == false,
|
||||
let vc = viewcontroller, vc.delegate?.floatingPanelShouldBeginDragging?(vc) == false {
|
||||
let vc = ownerVC, vc.delegate?.floatingPanelShouldBeginDragging?(vc) == false {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -484,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 {
|
||||
@@ -519,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
|
||||
}
|
||||
@@ -562,6 +612,9 @@ class Core: NSObject, UIGestureRecognizerDelegate {
|
||||
if scrollView.isDecelerating {
|
||||
return true
|
||||
}
|
||||
if let tableView = (scrollView as? UITableView), tableView.isEditing {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
@@ -573,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
|
||||
}
|
||||
@@ -599,7 +652,7 @@ class Core: NSObject, UIGestureRecognizerDelegate {
|
||||
|
||||
guard (pre != cur) else { return }
|
||||
|
||||
if let vc = viewcontroller {
|
||||
if let vc = ownerVC {
|
||||
vc.delegate?.floatingPanelDidMove?(vc)
|
||||
}
|
||||
}
|
||||
@@ -640,10 +693,11 @@ class Core: NSObject, UIGestureRecognizerDelegate {
|
||||
return
|
||||
}
|
||||
|
||||
stopScrollDeceleration = (0 > layoutAdapter.offsetFromEdgeMost + (1.0 / surfaceView.traitCollection.displayScale)) // Projecting the dragging to the scroll dragging or not
|
||||
stopScrollDeceleration = (0 > layoutAdapter.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)
|
||||
}
|
||||
}
|
||||
@@ -662,19 +716,19 @@ class Core: NSObject, UIGestureRecognizerDelegate {
|
||||
case .left, .right:
|
||||
removalVector = (distToHidden != 0) ? CGVector(dx: velocity.x/distToHidden, dy: 0.0) : .zero
|
||||
}
|
||||
if shoulRemove(with: removalVector) {
|
||||
viewcontroller?.remove()
|
||||
if shouldRemove(with: removalVector) {
|
||||
ownerVC?.remove()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if let vc = viewcontroller {
|
||||
if let vc = ownerVC {
|
||||
vc.delegate?.floatingPanelWillEndDragging?(vc, withVelocity: velocity, targetState: &targetPosition)
|
||||
}
|
||||
|
||||
guard shouldDecelerate(to: targetPosition) else {
|
||||
if let vc = viewcontroller {
|
||||
vc.delegate?.floatingPanelDidEndDragging?(vc, willDecelerate: false)
|
||||
guard shouldAttract(to: targetPosition) else {
|
||||
if let vc = ownerVC {
|
||||
vc.delegate?.floatingPanelDidEndDragging?(vc, willAttract: false)
|
||||
}
|
||||
|
||||
self.state = targetPosition
|
||||
@@ -683,20 +737,20 @@ class Core: NSObject, UIGestureRecognizerDelegate {
|
||||
return
|
||||
}
|
||||
|
||||
if let vc = viewcontroller {
|
||||
vc.delegate?.floatingPanelDidEndDragging?(vc, willDecelerate: true)
|
||||
if let vc = ownerVC {
|
||||
vc.delegate?.floatingPanelDidEndDragging?(vc, willAttract: true)
|
||||
}
|
||||
|
||||
// Workaround: Disable a tracking scroll to prevent bouncing a scroll content in a panel animating
|
||||
let isScrollEnabled = scrollView?.isScrollEnabled
|
||||
if let scrollView = scrollView, targetPosition != .full {
|
||||
if let scrollView = scrollView, targetPosition != layoutAdapter.mostExpandedState {
|
||||
scrollView.isScrollEnabled = false
|
||||
}
|
||||
|
||||
startAttration(to: targetPosition, with: velocity)
|
||||
startAttraction(to: targetPosition, with: velocity)
|
||||
|
||||
// Workaround: Reset `self.scrollView.isScrollEnabled`
|
||||
if let scrollView = scrollView, targetPosition != .full,
|
||||
if let scrollView = scrollView, targetPosition != layoutAdapter.mostExpandedState,
|
||||
let isScrollEnabled = isScrollEnabled {
|
||||
scrollView.isScrollEnabled = isScrollEnabled
|
||||
}
|
||||
@@ -704,20 +758,21 @@ class Core: NSObject, UIGestureRecognizerDelegate {
|
||||
|
||||
// MARK: - Behavior
|
||||
|
||||
private func shoulRemove(with velocityVector: CGVector) -> Bool {
|
||||
guard let vc = viewcontroller else { return false }
|
||||
private func shouldRemove(with velocityVector: CGVector) -> Bool {
|
||||
guard let vc = ownerVC else { return false }
|
||||
if let result = vc.delegate?.floatingPanel?(vc, shouldRemoveAt: vc.surfaceLocation, with: velocityVector) {
|
||||
return result
|
||||
}
|
||||
let threshold = behaviorAdapter.removalInteractionVelocityThreshold
|
||||
switch layoutAdapter.position {
|
||||
case .top:
|
||||
return (velocityVector.dy <= -10.0)
|
||||
return (velocityVector.dy <= -threshold)
|
||||
case .left:
|
||||
return (velocityVector.dx <= -10.0)
|
||||
return (velocityVector.dx <= -threshold)
|
||||
case .bottom:
|
||||
return (velocityVector.dy >= 10.0)
|
||||
return (velocityVector.dy >= threshold)
|
||||
case .right:
|
||||
return (velocityVector.dx >= 10.0)
|
||||
return (velocityVector.dx >= threshold)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -729,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 {
|
||||
@@ -753,7 +808,7 @@ class Core: NSObject, UIGestureRecognizerDelegate {
|
||||
|
||||
initialTranslation = translation
|
||||
|
||||
if let vc = viewcontroller {
|
||||
if let vc = ownerVC {
|
||||
vc.delegate?.floatingPanelWillBeginDragging?(vc)
|
||||
}
|
||||
|
||||
@@ -774,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()
|
||||
}
|
||||
|
||||
@@ -782,21 +837,22 @@ 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
|
||||
}
|
||||
|
||||
private func shouldDecelerate(to targetState: FloatingPanelState) -> Bool {
|
||||
private func shouldAttract(to targetState: FloatingPanelState) -> Bool {
|
||||
if layoutAdapter.position(for: targetState) == value(of: layoutAdapter.surfaceLocation) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private func startAttration(to targetPosition: FloatingPanelState, with velocity: CGPoint) {
|
||||
private func startAttraction(to targetPosition: FloatingPanelState, with velocity: CGPoint) {
|
||||
log.debug("startAnimation to \(targetPosition) -- velocity = \(value(of: velocity))")
|
||||
guard let vc = viewcontroller else { return }
|
||||
guard let vc = ownerVC else { return }
|
||||
|
||||
isAttracting = true
|
||||
vc.delegate?.floatingPanelWillBeginAttracting?(vc, to: targetPosition)
|
||||
@@ -811,20 +867,23 @@ class Core: NSObject, UIGestureRecognizerDelegate {
|
||||
moveAnimator = NumericSpringAnimator(
|
||||
initialData: initialData,
|
||||
target: target,
|
||||
displayScale: surfaceView.traitCollection.displayScale,
|
||||
displayScale: surfaceView.fp_displayScale,
|
||||
decelerationRate: behaviorAdapter.springDecelerationRate,
|
||||
responseTime: behaviorAdapter.springResponseTime,
|
||||
update: { [weak self] data in
|
||||
guard let self = self else { return }
|
||||
guard let self = self,
|
||||
let ownerVC = self.ownerVC // Ensure the owner vc is existing for `layoutAdapter.surfaceLocation`
|
||||
else { return }
|
||||
animationConstraint.constant = data.value
|
||||
let current = self.value(of: self.layoutAdapter.surfaceLocation)
|
||||
let translation = data.value - initialData.value
|
||||
self.backdropView.alpha = self.getBackdropAlpha(at: current, with: translation)
|
||||
self.viewcontroller?.notifyDidMove()
|
||||
ownerVC.notifyDidMove()
|
||||
},
|
||||
completion: { [weak self] in
|
||||
guard let self = self else { return }
|
||||
self.layoutAdapter.activateLayout(for: targetPosition, forceLayout: true)
|
||||
guard let self = self,
|
||||
self.ownerVC != nil else { return }
|
||||
self.updateLayout(to: targetPosition)
|
||||
completion()
|
||||
})
|
||||
moveAnimator?.startAnimation()
|
||||
@@ -835,7 +894,7 @@ class Core: NSObject, UIGestureRecognizerDelegate {
|
||||
self.isAttracting = false
|
||||
self.moveAnimator = nil
|
||||
|
||||
if let vc = viewcontroller {
|
||||
if let vc = ownerVC {
|
||||
vc.delegate?.floatingPanelDidEndAttracting?(vc)
|
||||
}
|
||||
|
||||
@@ -848,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()
|
||||
}
|
||||
}
|
||||
@@ -877,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
|
||||
@@ -885,14 +944,14 @@ 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
|
||||
|
||||
let distance = (currentY - layoutAdapter.position(for: state))
|
||||
let forwardY = velocity == 0 ? distance > 0 : velocity > 0
|
||||
|
||||
let segment = layoutAdapter.segument(at: pY, forward: forwardY)
|
||||
let segment = layoutAdapter.segment(at: pY, forward: forwardY)
|
||||
|
||||
var fromPos: FloatingPanelState
|
||||
var toPos: FloatingPanelState
|
||||
@@ -902,7 +961,7 @@ class Core: NSObject, UIGestureRecognizerDelegate {
|
||||
|
||||
if behaviorAdapter.shouldProjectMomentum(to: toPos) == false {
|
||||
log.debug("targetPosition -- negate projection: distance = \(distance)")
|
||||
let segment = layoutAdapter.segument(at: currentY, forward: forwardY)
|
||||
let segment = layoutAdapter.segment(at: currentY, forward: forwardY)
|
||||
var (lowerPos, upperPos) = (segment.lower ?? sortedPositions.first!, segment.upper ?? sortedPositions.last!)
|
||||
// Equate the segment out of {top,bottom} most state to the {top,bottom} most segment
|
||||
if lowerPos == upperPos {
|
||||
@@ -939,7 +998,7 @@ class Core: NSObject, UIGestureRecognizerDelegate {
|
||||
}
|
||||
log.debug("lock scroll view")
|
||||
|
||||
scrollBouncable = scrollView.bounces
|
||||
scrollBounce = scrollView.bounces
|
||||
scrollIndictorVisible = scrollView.showsVerticalScrollIndicator
|
||||
|
||||
scrollView.isDirectionalLockEnabled = true
|
||||
@@ -952,7 +1011,7 @@ class Core: NSObject, UIGestureRecognizerDelegate {
|
||||
log.debug("unlock scroll view")
|
||||
|
||||
scrollView.isDirectionalLockEnabled = false
|
||||
scrollView.bounces = scrollBouncable
|
||||
scrollView.bounces = scrollBounce
|
||||
scrollView.showsVerticalScrollIndicator = scrollIndictorVisible
|
||||
}
|
||||
|
||||
@@ -965,7 +1024,7 @@ class Core: NSObject, UIGestureRecognizerDelegate {
|
||||
}
|
||||
|
||||
private func contentOffsetForPinning(of scrollView: UIScrollView) -> CGPoint {
|
||||
if let vc = viewcontroller, let origin = vc.delegate?.floatingPanel?(vc, contentOffsetForPinning: scrollView) {
|
||||
if let vc = ownerVC, let origin = vc.delegate?.floatingPanel?(vc, contentOffsetForPinning: scrollView) {
|
||||
return origin
|
||||
}
|
||||
switch layoutAdapter.position {
|
||||
@@ -981,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:
|
||||
@@ -1005,6 +1064,7 @@ class Core: NSObject, UIGestureRecognizerDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
/// A gesture recognizer that looks for panning (dragging) gestures in a panel.
|
||||
public final class FloatingPanelPanGestureRecognizer: UIPanGestureRecognizer {
|
||||
fileprivate weak var floatingPanel: Core?
|
||||
fileprivate var initialLocation: CGPoint = .zero
|
||||
@@ -1012,10 +1072,14 @@ public final class FloatingPanelPanGestureRecognizer: UIPanGestureRecognizer {
|
||||
public override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
|
||||
super.touchesBegan(touches, with: event)
|
||||
initialLocation = touches.first?.location(in: view) ?? .zero
|
||||
if floatingPanel?.animator != nil || floatingPanel?.moveAnimator != nil {
|
||||
if floatingPanel?.transitionAnimator != nil || floatingPanel?.moveAnimator != nil {
|
||||
self.state = .began
|
||||
}
|
||||
}
|
||||
/// The delegate of the gesture recognizer.
|
||||
///
|
||||
/// - Note: The delegate is used by FloatingPanel itself. If you set your own delegate object, an
|
||||
/// exception is raised. If you want to handle the methods of UIGestureRecognizerDelegate, you can use `delegateProxy`.
|
||||
public override weak var delegate: UIGestureRecognizerDelegate? {
|
||||
get {
|
||||
return super.delegate
|
||||
@@ -1031,6 +1095,10 @@ public final class FloatingPanelPanGestureRecognizer: UIPanGestureRecognizer {
|
||||
super.delegate = newValue
|
||||
}
|
||||
}
|
||||
|
||||
/// An object to intercept the delegate of the gesture recognizer.
|
||||
///
|
||||
/// If an object adopting `UIGestureRecognizerDelegate` is set, the delegate methods are proxied to it.
|
||||
public weak var delegateProxy: UIGestureRecognizerDelegate? {
|
||||
didSet {
|
||||
self.delegate = floatingPanel // Update the cached IMP
|
||||
@@ -1040,7 +1108,7 @@ public final class FloatingPanelPanGestureRecognizer: UIPanGestureRecognizer {
|
||||
|
||||
// MARK: - Animator
|
||||
|
||||
class NumericSpringAnimator: NSObject {
|
||||
private class NumericSpringAnimator: NSObject {
|
||||
struct Data {
|
||||
let value: CGFloat
|
||||
let velocity: CGFloat
|
||||
@@ -1165,3 +1233,12 @@ class NumericSpringAnimator: NSObject {
|
||||
v = (v + h * o2 * (xt - x)) / det
|
||||
}
|
||||
}
|
||||
|
||||
extension FloatingPanelController {
|
||||
func suspendTransitionAnimator(_ suspended: Bool) {
|
||||
self.floatingPanel.isSuspended = suspended
|
||||
}
|
||||
var transitionAnimator: UIViewPropertyAnimator? {
|
||||
return self.floatingPanel.transitionAnimator
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,39 @@
|
||||
// Copyright 2018 the FloatingPanel authors. All rights reserved. MIT license.
|
||||
// Copyright 2018-Present Shin Yamamoto. All rights reserved. MIT license.
|
||||
|
||||
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 }
|
||||
@@ -72,7 +94,7 @@ extension UIViewController {
|
||||
}
|
||||
|
||||
// The reason why UIView has no extensions of safe area insets and top/bottom guides
|
||||
// is for iOS10 compat.
|
||||
// is for iOS10 compatibility.
|
||||
extension UIView {
|
||||
var fp_safeAreaLayoutGuide: LayoutGuideProvider {
|
||||
if #available(iOS 11.0, *) {
|
||||
@@ -85,6 +107,19 @@ extension UIView {
|
||||
var presentationFrame: CGRect {
|
||||
return layer.presentation()?.frame ?? frame
|
||||
}
|
||||
|
||||
/// Returns non-zero displayScale
|
||||
///
|
||||
/// On iOS 11 or earlier the `traitCollection.displayScale` of a view can be
|
||||
/// 0.0(indicating unspecified) when its view hasn't been added yet into a view tree in a window.
|
||||
/// So this method returns `UIScreen.main` scale if the scale value is zero, for testing mainly.
|
||||
var fp_displayScale: CGFloat {
|
||||
let ret = traitCollection.displayScale
|
||||
if ret == 0.0 {
|
||||
return UIScreen.main.scale
|
||||
}
|
||||
return ret
|
||||
}
|
||||
}
|
||||
|
||||
extension UIView {
|
||||
@@ -112,7 +147,7 @@ extension UIGestureRecognizer.State: CustomDebugStringConvertible {
|
||||
case .changed: return "changed"
|
||||
case .failed: return "failed"
|
||||
case .cancelled: return "cancelled"
|
||||
case .ended: return "endeded"
|
||||
case .ended: return "ended"
|
||||
case .possible: return "possible"
|
||||
@unknown default: return ""
|
||||
}
|
||||
@@ -151,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 }
|
||||
@@ -188,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,7 +1,8 @@
|
||||
// Copyright 2018 the FloatingPanel authors. All rights reserved. MIT license.
|
||||
// Copyright 2018-Present Shin Yamamoto. All rights reserved. MIT license.
|
||||
|
||||
import UIKit
|
||||
|
||||
/// A view that presents a grabber handle in the surface of a panel.
|
||||
@objc(FloatingPanelGrabberView)
|
||||
public class GrabberView: UIView {
|
||||
|
||||
|
||||
+1
-1
@@ -15,7 +15,7 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>FMWK</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>2.0.0-beta.1</string>
|
||||
<string>2.4.1</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||
</dict>
|
||||
|
||||
+147
-129
@@ -1,29 +1,26 @@
|
||||
// Copyright 2018 the FloatingPanel authors. All rights reserved. MIT license.
|
||||
// Copyright 2018-Present Shin Yamamoto. All rights reserved. MIT license.
|
||||
|
||||
import UIKit
|
||||
|
||||
/// An interface for generating layout information for a panel.
|
||||
@objc public protocol FloatingPanelLayout {
|
||||
/// The position of the panel in the view of `FloatingPanelController`.
|
||||
/// Returns the position of a panel in a `FloatingPanelController` view .
|
||||
@objc var position: FloatingPanelPosition { get }
|
||||
|
||||
/// The initial state when the layout is applied.
|
||||
/// Returns the initial state when a panel is presented.
|
||||
@objc var initialState: FloatingPanelState { get }
|
||||
|
||||
/// The layout anchors to specify the snapping locations for each state.
|
||||
/// Returns the layout anchors to specify the snapping locations for each state.
|
||||
@objc var anchors: [FloatingPanelState: FloatingPanelLayoutAnchoring] { get }
|
||||
|
||||
/// Returns X-axis and width layout constraints of the surface view of a floating panel.
|
||||
/// You must not include any Y-axis and height layout constraints of the surface view
|
||||
/// because their constraints will be configured by the floating panel controller.
|
||||
/// By default, the width of a surface view fits a safe area.
|
||||
/// Returns layout constraints to determine the cross dimension of a panel.
|
||||
@objc optional func prepareLayout(surfaceView: UIView, in view: UIView) -> [NSLayoutConstraint]
|
||||
|
||||
/// Returns a CGFloat value to determine the backdrop view's alpha for a state.
|
||||
///
|
||||
/// Default is 0.3 at full state, otherwise 0.0.
|
||||
/// Returns the alpha value of the backdrop of a panel for each state.
|
||||
@objc optional func backdropAlpha(for state: FloatingPanelState) -> CGFloat
|
||||
}
|
||||
|
||||
/// A layout object that lays out a panel in bottom sheet style.
|
||||
@objcMembers
|
||||
open class FloatingPanelBottomLayout: NSObject, FloatingPanelLayout {
|
||||
public override init() {
|
||||
@@ -63,9 +60,7 @@ struct LayoutSegment {
|
||||
}
|
||||
|
||||
class LayoutAdapter {
|
||||
weak var vc: FloatingPanelController!
|
||||
private weak var surfaceView: SurfaceView!
|
||||
private weak var backdropView: BackdropView!
|
||||
private unowned var vc: FloatingPanelController
|
||||
private let defaultLayout = FloatingPanelBottomLayout()
|
||||
|
||||
fileprivate var layout: FloatingPanelLayout {
|
||||
@@ -74,16 +69,21 @@ class LayoutAdapter {
|
||||
}
|
||||
}
|
||||
|
||||
private var surfaceView: SurfaceView {
|
||||
return vc.surfaceView
|
||||
}
|
||||
private var backdropView: BackdropView {
|
||||
return vc.backdropView
|
||||
}
|
||||
private var safeAreaInsets: UIEdgeInsets {
|
||||
return vc?.fp_safeAreaInsets ?? .zero
|
||||
return vc.fp_safeAreaInsets
|
||||
}
|
||||
|
||||
private var initialConst: CGFloat = 0.0
|
||||
|
||||
private var fixedConstraints: [NSLayoutConstraint] = []
|
||||
private var fullConstraints: [NSLayoutConstraint] = []
|
||||
private var halfConstraints: [NSLayoutConstraint] = []
|
||||
private var tipConstraints: [NSLayoutConstraint] = []
|
||||
|
||||
private var stateConstraints: [FloatingPanelState: [NSLayoutConstraint]] = [:]
|
||||
private var offConstraints: [NSLayoutConstraint] = []
|
||||
private var fitToBoundsConstraint: NSLayoutConstraint?
|
||||
|
||||
@@ -92,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
|
||||
}
|
||||
@@ -104,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
|
||||
@@ -125,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 {
|
||||
@@ -223,8 +219,7 @@ class LayoutAdapter {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let displayScale = surfaceView.traitCollection.displayScale
|
||||
pos = displayTrunc(edgePosition(surfaceView.frame), by: displayScale)
|
||||
pos = edgePosition(surfaceView.frame).rounded(by: surfaceView.fp_displayScale)
|
||||
}
|
||||
switch position {
|
||||
case .top, .bottom:
|
||||
@@ -266,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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -288,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.traitCollection.displayScale)
|
||||
let pos = position(for: state).rounded(by: surfaceView.fp_displayScale)
|
||||
switch layout.position {
|
||||
case .top, .bottom:
|
||||
return CGPoint(x: 0.0, y: pos)
|
||||
@@ -313,19 +303,39 @@ class LayoutAdapter {
|
||||
let anchor = layout.anchors[state] ?? self.hiddenAnchor
|
||||
|
||||
switch anchor {
|
||||
case let ianchor as FloatingPanelIntrinsicLayoutAnchor:
|
||||
let surfaceIntrinsicLength = position.mainDimension(surfaceView.intrinsicContentSize)
|
||||
let diff = ianchor.isAbsolute ? ianchor.offset : surfaceIntrinsicLength * ianchor.offset
|
||||
case let anchor as FloatingPanelIntrinsicLayoutAnchor:
|
||||
let intrinsicLength = position.mainDimension(surfaceView.intrinsicContentSize)
|
||||
let diff = anchor.isAbsolute ? anchor.offset : intrinsicLength * anchor.offset
|
||||
|
||||
var referenceBoundsLength = position.mainDimension(bounds.size)
|
||||
switch position {
|
||||
case .top, .left:
|
||||
return referenceBoundsLength - surfaceIntrinsicLength - diff
|
||||
var base: CGFloat = 0.0
|
||||
if anchor.referenceGuide == .safeArea {
|
||||
base += position.inset(safeAreaInsets)
|
||||
}
|
||||
return base + intrinsicLength - diff
|
||||
case .bottom, .right:
|
||||
var base = position.mainDimension(bounds.size)
|
||||
if anchor.referenceGuide == .safeArea {
|
||||
base -= position.inset(safeAreaInsets)
|
||||
}
|
||||
return base - intrinsicLength + diff
|
||||
}
|
||||
case let anchor as FloatingPanelAdaptiveLayoutAnchor:
|
||||
let dimension = layout.position.mainDimension(anchor.contentLayoutGuide.layoutFrame.size)
|
||||
let diff = anchor.distance(from: dimension)
|
||||
var referenceBoundsLength = layout.position.mainDimension(bounds.size)
|
||||
switch layout.position {
|
||||
case .top, .left:
|
||||
if anchor.referenceGuide == .safeArea {
|
||||
referenceBoundsLength += position.inset(safeAreaInsets)
|
||||
}
|
||||
return dimension - diff
|
||||
case .bottom, .right:
|
||||
if anchor.referenceGuide == .safeArea {
|
||||
referenceBoundsLength -= position.inset(safeAreaInsets)
|
||||
}
|
||||
return referenceBoundsLength - surfaceIntrinsicLength + diff
|
||||
return referenceBoundsLength - dimension + diff
|
||||
}
|
||||
case let anchor as FloatingPanelLayoutAnchor:
|
||||
let referenceBounds = anchor.referenceGuide == .safeArea ? bounds.inset(by: safeAreaInsets) : bounds
|
||||
@@ -343,7 +353,11 @@ class LayoutAdapter {
|
||||
default:
|
||||
fatalError("Unsupported a FloatingPanelLayoutAnchoring object")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func isIntrinsicAnchor(state: FloatingPanelState) -> Bool {
|
||||
return layout.anchors[state] is FloatingPanelIntrinsicLayoutAnchor
|
||||
}
|
||||
|
||||
private func edgePosition(_ frame: CGRect) -> CGFloat {
|
||||
switch position {
|
||||
@@ -360,7 +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
|
||||
@@ -428,25 +443,17 @@ class LayoutAdapter {
|
||||
fitToBoundsConstraint?.priority = .defaultHigh
|
||||
}
|
||||
|
||||
NSLayoutConstraint.deactivate(fullConstraints + halfConstraints + tipConstraints + offConstraints)
|
||||
updateStateConstraints()
|
||||
}
|
||||
|
||||
if let fullAnchor = layout.anchors[.full] {
|
||||
fullConstraints = fullAnchor.layoutConstraints(vc, for: position)
|
||||
fullConstraints.forEach {
|
||||
$0.identifier = "FloatingPanel-full-constraint"
|
||||
}
|
||||
}
|
||||
if let halfAnchor = layout.anchors[.half] {
|
||||
halfConstraints = halfAnchor.layoutConstraints(vc, for: position)
|
||||
halfConstraints.forEach {
|
||||
$0.identifier = "FloatingPanel-half-constraint"
|
||||
}
|
||||
}
|
||||
if let tipAnchors = layout.anchors[.tip] {
|
||||
tipConstraints = tipAnchors.layoutConstraints(vc, for: position)
|
||||
tipConstraints.forEach {
|
||||
$0.identifier = "FloatingPanel-tip-constraint"
|
||||
}
|
||||
private func updateStateConstraints() {
|
||||
let allStateConstraints = stateConstraints.flatMap { $1 }
|
||||
NSLayoutConstraint.deactivate(allStateConstraints + offConstraints)
|
||||
stateConstraints.removeAll()
|
||||
for state in layout.anchors.keys {
|
||||
stateConstraints[state] = layout.anchors[state]?
|
||||
.layoutConstraints(vc, for: position)
|
||||
.map{ $0.identifier = "FloatingPanel-\(state)-constraint"; return $0 }
|
||||
}
|
||||
let hiddenAnchor = layout.anchors[.hidden] ?? self.hiddenAnchor
|
||||
offConstraints = hiddenAnchor.layoutConstraints(vc, for: position)
|
||||
@@ -463,7 +470,7 @@ class LayoutAdapter {
|
||||
|
||||
tearDownAttraction()
|
||||
|
||||
NSLayoutConstraint.deactivate(fullConstraints + halfConstraints + tipConstraints + offConstraints)
|
||||
NSLayoutConstraint.deactivate(stateConstraints.flatMap { $1 } + offConstraints)
|
||||
|
||||
initialConst = edgePosition(surfaceView.frame) + offset.y
|
||||
|
||||
@@ -479,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])
|
||||
@@ -491,8 +498,8 @@ class LayoutAdapter {
|
||||
// unsatisfiable constraints
|
||||
|
||||
if self.interactionConstraint == nil {
|
||||
// Actiavate `interactiveTopConstraint` for `fitToBounds` mode.
|
||||
// It goes throught this path when the pan gesture state jumps
|
||||
// Activate `interactiveTopConstraint` for `fitToBounds` mode.
|
||||
// It goes through this path when the pan gesture state jumps
|
||||
// from .begin to .end.
|
||||
startInteraction(at: state)
|
||||
}
|
||||
@@ -503,7 +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
|
||||
|
||||
@@ -622,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
|
||||
|
||||
@@ -631,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))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -665,22 +683,22 @@ 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
|
||||
|
||||
let base = position.mainDimension(vc.view.bounds.size)
|
||||
// Rubberbanding top buffer
|
||||
// Rubber-banding top buffer
|
||||
if allowsRubberBanding(.top), const < minConst {
|
||||
let buffer = minConst - const
|
||||
const = minConst - rubberbandEffect(for: buffer, base: base)
|
||||
const = minConst - rubberBandEffect(for: buffer, base: base)
|
||||
}
|
||||
|
||||
// Rubberbanding bottom buffer
|
||||
// Rubber-banding bottom buffer
|
||||
if allowsRubberBanding(.bottom), const > maxConst {
|
||||
let buffer = const - maxConst
|
||||
const = maxConst + rubberbandEffect(for: buffer, base: base)
|
||||
const = maxConst + rubberBandEffect(for: buffer, base: base)
|
||||
}
|
||||
|
||||
if overflow == false {
|
||||
@@ -694,7 +712,7 @@ class LayoutAdapter {
|
||||
// x = distance from the edge
|
||||
// c = constant value, UIScrollView uses 0.55
|
||||
// d = dimension, either width or height
|
||||
private func rubberbandEffect(for buffer: CGFloat, base: CGFloat) -> CGFloat {
|
||||
private func rubberBandEffect(for buffer: CGFloat, base: CGFloat) -> CGFloat {
|
||||
return (1.0 - (1.0 / ((buffer * 0.55 / base) + 1.0))) * base
|
||||
}
|
||||
|
||||
@@ -722,24 +740,25 @@ class LayoutAdapter {
|
||||
|
||||
var state = state
|
||||
|
||||
setBackdropAlpha(of: state)
|
||||
|
||||
if validStates.contains(state) == false {
|
||||
state = layout.initialState
|
||||
}
|
||||
|
||||
NSLayoutConstraint.deactivate(fullConstraints + halfConstraints + tipConstraints + offConstraints)
|
||||
// Recalculate the intrinsic size of a content view. This is because
|
||||
// UIView.systemLayoutSizeFitting() returns a different size between an
|
||||
// on-screen and off-screen view which includes
|
||||
// UIStackView(i.e. Settings view in Samples.app)
|
||||
updateStateConstraints()
|
||||
|
||||
switch state {
|
||||
case .full:
|
||||
NSLayoutConstraint.activate(fullConstraints)
|
||||
case .half:
|
||||
NSLayoutConstraint.activate(halfConstraints)
|
||||
case .tip:
|
||||
NSLayoutConstraint.activate(tipConstraints)
|
||||
case .hidden:
|
||||
NSLayoutConstraint.activate(offConstraints)
|
||||
default:
|
||||
break
|
||||
if let constraints = stateConstraints[state] {
|
||||
NSLayoutConstraint.activate(constraints)
|
||||
} else {
|
||||
log.error("Couldn't find any constraints for \(state)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -750,39 +769,33 @@ 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)
|
||||
}
|
||||
|
||||
func checkLayout() {
|
||||
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))")
|
||||
let statePosOrder = activeStates.sorted(by: { position(for: $0) < position(for: $1) })
|
||||
assert(sortedDirectionalStates == statePosOrder,
|
||||
/* This assertion does not work in a device rotating
|
||||
let statePosOrder = activeStates.sorted(by: { position(for: $0) < position(for: $1) })
|
||||
assert(sortedDirectionalStates == statePosOrder,
|
||||
"Check your layout anchors because the state order(\(statePosOrder)) must be (\(sortedDirectionalStates))).")
|
||||
*/
|
||||
}
|
||||
}
|
||||
|
||||
extension LayoutAdapter {
|
||||
func segument(at pos: CGFloat, forward: Bool) -> LayoutSegment {
|
||||
func segment(at pos: CGFloat, forward: Bool) -> LayoutSegment {
|
||||
/// ----------------------->Y
|
||||
/// --> forward <-- backward
|
||||
/// |-------|===o===|-------| |-------|-------|===o===|
|
||||
/// |-------|-------x=======| |-------|=======x-------|
|
||||
/// |-------|-------|===o===| |-------|===o===|-------|
|
||||
/// pos: o/x, seguement: =
|
||||
/// pos: o/x, segment: =
|
||||
|
||||
let sortedStates = sortedDirectionalStates
|
||||
let sortedStates = sortedAnchorStatesByCoordinate
|
||||
|
||||
let upperIndex: Int?
|
||||
if forward {
|
||||
@@ -804,7 +817,12 @@ extension LayoutAdapter {
|
||||
|
||||
extension FloatingPanelController {
|
||||
var _layout: FloatingPanelLayout {
|
||||
get { floatingPanel.layoutAdapter.layout }
|
||||
set { floatingPanel.layoutAdapter.layout = newValue}
|
||||
get {
|
||||
floatingPanel.layoutAdapter.layout
|
||||
}
|
||||
set {
|
||||
floatingPanel.layoutAdapter.layout = newValue
|
||||
floatingPanel.layoutAdapter.checkLayout()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +1,32 @@
|
||||
// Copyright 2018 the FloatingPanel authors. All rights reserved. MIT license.
|
||||
// Copyright 2018-Present Shin Yamamoto. All rights reserved. MIT license.
|
||||
|
||||
import UIKit
|
||||
|
||||
/// An interface for implementing custom layout anchor objects.
|
||||
@objc public protocol FloatingPanelLayoutAnchoring {
|
||||
var referenceGuide: FloatingPanelLayoutReferenceGuide { get }
|
||||
func layoutConstraints(_ fpc: FloatingPanelController, for position: FloatingPanelPosition) -> [NSLayoutConstraint]
|
||||
}
|
||||
|
||||
/// An object that defines how to settles a panel with insets from an edge of a reference rectangle.
|
||||
@objc final public class FloatingPanelLayoutAnchor: NSObject, FloatingPanelLayoutAnchoring /*, NSCopying */ {
|
||||
|
||||
/// Returns a layout anchor with the specified inset by an absolute value, edge and reference guide for a panel.
|
||||
///
|
||||
/// The inset is an amount to inset a panel from an edge of the reference guide. The edge refers to a panel
|
||||
/// positioning.
|
||||
@objc public init(absoluteInset: CGFloat, edge: FloatingPanelReferenceEdge, referenceGuide: FloatingPanelLayoutReferenceGuide) {
|
||||
self.inset = absoluteInset
|
||||
self.referenceGuide = referenceGuide
|
||||
self.referenceEdge = edge
|
||||
self.isAbsolute = true
|
||||
}
|
||||
|
||||
/// Returns a layout anchor with the specified inset by a fractional value, edge and reference guide for a panel.
|
||||
///
|
||||
/// The inset is an amount to inset a panel from the edge of the specified reference guide. The value is
|
||||
/// a floating-point number in the range 0.0 to 1.0, where 0.0 represents zero distance from the edge and
|
||||
/// 1.0 represents a distance to the opposite edge.
|
||||
@objc public init(fractionalInset: CGFloat, edge: FloatingPanelReferenceEdge, referenceGuide: FloatingPanelLayoutReferenceGuide) {
|
||||
self.inset = fractionalInset
|
||||
self.referenceGuide = referenceGuide
|
||||
@@ -22,8 +35,9 @@ import UIKit
|
||||
}
|
||||
let inset: CGFloat
|
||||
let isAbsolute: Bool
|
||||
/// The reference rectangle area for the inset.
|
||||
@objc public let referenceGuide: FloatingPanelLayoutReferenceGuide
|
||||
@objc public let referenceEdge: FloatingPanelReferenceEdge
|
||||
@objc let referenceEdge: FloatingPanelReferenceEdge
|
||||
}
|
||||
|
||||
public extension FloatingPanelLayoutAnchor {
|
||||
@@ -80,14 +94,23 @@ public extension FloatingPanelLayoutAnchor {
|
||||
}
|
||||
}
|
||||
|
||||
/// An object that defines how to settles a panel with the intrinsic size for a content.
|
||||
@objc final public class FloatingPanelIntrinsicLayoutAnchor: NSObject, FloatingPanelLayoutAnchoring /*, NSCopying */ {
|
||||
|
||||
/// Returns a layout anchor with the specified offset by an absolute value and reference guide for a panel.
|
||||
///
|
||||
/// The offset is an amount to offset a position of panel that displays the entire content from an edge of
|
||||
/// the reference guide. The edge refers to a panel positioning.
|
||||
@objc public init(absoluteOffset offset: CGFloat, referenceGuide: FloatingPanelLayoutReferenceGuide = .safeArea) {
|
||||
self.offset = offset
|
||||
self.referenceGuide = referenceGuide
|
||||
self.isAbsolute = true
|
||||
}
|
||||
// offset = 0.0 -> All content visible
|
||||
// offset = 1.0 -> All content invisible
|
||||
|
||||
/// Returns a layout anchor with the specified offset by a fractional value and reference guide for a panel.
|
||||
///
|
||||
/// The offset value is a floating-point number in the range 0.0 to 1.0, where 0.0 represents the full content
|
||||
/// is displayed and 0.5 represents the half of content is displayed.
|
||||
@objc public init(fractionalOffset offset: CGFloat, referenceGuide: FloatingPanelLayoutReferenceGuide = .safeArea) {
|
||||
self.offset = offset
|
||||
self.referenceGuide = referenceGuide
|
||||
@@ -95,6 +118,8 @@ public extension FloatingPanelLayoutAnchor {
|
||||
}
|
||||
let offset: CGFloat
|
||||
let isAbsolute: Bool
|
||||
|
||||
/// The reference rectangle area for the offset
|
||||
@objc public let referenceGuide: FloatingPanelLayoutReferenceGuide
|
||||
}
|
||||
|
||||
@@ -115,3 +140,62 @@ public extension FloatingPanelIntrinsicLayoutAnchor {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// An object that defines how to settles a panel with a layout guide of a content view.
|
||||
@objc final public class FloatingPanelAdaptiveLayoutAnchor: NSObject, FloatingPanelLayoutAnchoring /*, NSCopying */ {
|
||||
|
||||
/// Returns a layout anchor with the specified offset by an absolute value, layout guide to display content and reference guide for a panel.
|
||||
///
|
||||
/// The offset is an amount to offset a position of panel that displays the entire content of the specified guide from an edge of
|
||||
/// the reference guide. The edge refers to a panel positioning.
|
||||
@objc public init(absoluteOffset offset: CGFloat, contentLayout: UILayoutGuide, referenceGuide: FloatingPanelLayoutReferenceGuide = .safeArea) {
|
||||
self.offset = offset
|
||||
self.contentLayoutGuide = contentLayout
|
||||
self.referenceGuide = referenceGuide
|
||||
self.isAbsolute = true
|
||||
|
||||
}
|
||||
|
||||
/// Returns a layout anchor with the specified offset by a fractional value, layout guide to display content and reference guide for a panel.
|
||||
///
|
||||
/// The offset value is a floating-point number in the range 0.0 to 1.0, where 0.0 represents the full content
|
||||
/// is displayed and 0.5 represents the half of content is displayed.
|
||||
@objc public init(fractionalOffset offset: CGFloat, contentLayout: UILayoutGuide, referenceGuide: FloatingPanelLayoutReferenceGuide = .safeArea) {
|
||||
self.offset = offset
|
||||
self.contentLayoutGuide = contentLayout
|
||||
self.referenceGuide = referenceGuide
|
||||
self.isAbsolute = false
|
||||
}
|
||||
fileprivate let offset: CGFloat
|
||||
fileprivate let isAbsolute: Bool
|
||||
let contentLayoutGuide: UILayoutGuide
|
||||
@objc public let referenceGuide: FloatingPanelLayoutReferenceGuide
|
||||
}
|
||||
|
||||
public extension FloatingPanelAdaptiveLayoutAnchor {
|
||||
func layoutConstraints(_ vc: FloatingPanelController, for position: FloatingPanelPosition) -> [NSLayoutConstraint] {
|
||||
let layoutGuide = referenceGuide.layoutGuide(vc: vc)
|
||||
let offsetAnchor: NSLayoutDimension
|
||||
switch position {
|
||||
case .top:
|
||||
offsetAnchor = layoutGuide.topAnchor.anchorWithOffset(to: vc.surfaceView.bottomAnchor)
|
||||
case .left:
|
||||
offsetAnchor = layoutGuide.leftAnchor.anchorWithOffset(to: vc.surfaceView.rightAnchor)
|
||||
case .bottom:
|
||||
offsetAnchor = vc.surfaceView.topAnchor.anchorWithOffset(to: layoutGuide.bottomAnchor)
|
||||
case .right:
|
||||
offsetAnchor = vc.surfaceView.leftAnchor.anchorWithOffset(to: layoutGuide.rightAnchor)
|
||||
}
|
||||
if isAbsolute {
|
||||
return [offsetAnchor.constraint(equalTo: position.mainDimensionAnchor(contentLayoutGuide), constant: -offset)]
|
||||
} else {
|
||||
return [offsetAnchor.constraint(equalTo: position.mainDimensionAnchor(contentLayoutGuide), multiplier: (1 - offset))]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension FloatingPanelAdaptiveLayoutAnchor {
|
||||
func distance(from dimension: CGFloat) -> CGFloat {
|
||||
return isAbsolute ? offset : dimension * offset
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
// Copyright 2018 the FloatingPanel authors. All rights reserved. MIT license.
|
||||
// Copyright 2018-Present Shin Yamamoto. All rights reserved. MIT license.
|
||||
|
||||
import UIKit
|
||||
|
||||
/// Constants that specify the edge of the container of a panel.
|
||||
@objc public enum FloatingPanelReferenceEdge: Int {
|
||||
case top
|
||||
case left
|
||||
@@ -26,6 +27,7 @@ extension FloatingPanelReferenceEdge {
|
||||
}
|
||||
}
|
||||
|
||||
/// Constants that specify a layout guide to lay out a panel.
|
||||
@objc public enum FloatingPanelLayoutReferenceGuide: Int {
|
||||
case superview = 0
|
||||
case safeArea = 1
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
// Copyright 2018 the FloatingPanel authors. All rights reserved. MIT license.
|
||||
// Copyright 2018-Present Shin Yamamoto. All rights reserved. MIT license.
|
||||
|
||||
import Foundation
|
||||
import os.log
|
||||
|
||||
// Must be a variable to use `hook` property in testing
|
||||
var log = {
|
||||
return Logger()
|
||||
}()
|
||||
@@ -68,7 +69,7 @@ struct Logger {
|
||||
|
||||
hook?(log, level)
|
||||
|
||||
os_log("%@", log: osLog, type: level.osLogType, log)
|
||||
os_log("%{public}@", log: osLog, type: level.osLogType, log)
|
||||
}
|
||||
|
||||
private func getPrettyFunction(_ function: String, _ file: String) -> String {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
// Copyright 2018 the FloatingPanel authors. All rights reserved. MIT license.
|
||||
// Copyright 2018-Present Shin Yamamoto. All rights reserved. MIT license.
|
||||
|
||||
import UIKit
|
||||
|
||||
@objc(FloatingPanelPassThroughView)
|
||||
class PassThroughView: UIView {
|
||||
@objc(FloatingPanelPassthroughView)
|
||||
class PassthroughView: UIView {
|
||||
public weak var eventForwardingView: UIView?
|
||||
public override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||
let hitView = super.hitTest(point, with: event)
|
||||
@@ -1,7 +1,8 @@
|
||||
// Copyright 2018 the FloatingPanel authors. All rights reserved. MIT license.
|
||||
// Copyright 2018-Present Shin Yamamoto. All rights reserved. MIT license.
|
||||
|
||||
import UIKit
|
||||
|
||||
/// Constants describing the position of a panel in a screen
|
||||
@objc public enum FloatingPanelPosition: Int {
|
||||
case top
|
||||
case left
|
||||
|
||||
+11
-3
@@ -1,9 +1,10 @@
|
||||
// Copyright 2018 the FloatingPanel authors. All rights reserved. MIT license.
|
||||
// Copyright 2018-Present Shin Yamamoto. All rights reserved. MIT license.
|
||||
|
||||
import Foundation
|
||||
|
||||
/// An object that represents the display state of a panel in a screen.
|
||||
@objc
|
||||
public class FloatingPanelState: NSObject, NSCopying, RawRepresentable {
|
||||
open class FloatingPanelState: NSObject, NSCopying, RawRepresentable {
|
||||
public typealias RawValue = String
|
||||
|
||||
required public init?(rawValue: RawValue) {
|
||||
@@ -12,13 +13,16 @@ public class FloatingPanelState: NSObject, NSCopying, RawRepresentable {
|
||||
super.init()
|
||||
}
|
||||
|
||||
@objc
|
||||
public init(rawValue: RawValue, order: Int) {
|
||||
self.rawValue = rawValue
|
||||
self.order = order
|
||||
super.init()
|
||||
}
|
||||
|
||||
/// The corresponding value of the raw type.
|
||||
public let rawValue: RawValue
|
||||
/// The sorting order for states
|
||||
public let order: Int
|
||||
|
||||
public func copy(with zone: NSZone? = nil) -> Any {
|
||||
@@ -30,12 +34,16 @@ public class FloatingPanelState: NSObject, NSCopying, RawRepresentable {
|
||||
}
|
||||
|
||||
public override var debugDescription: String {
|
||||
return description
|
||||
return "<FloatingPanel.FloatingPanelState: \(Unmanaged.passUnretained(self).toOpaque())>"
|
||||
}
|
||||
|
||||
/// A panel state indicates the entire panel is shown.
|
||||
@objc(Full) public static let full: FloatingPanelState = FloatingPanelState(rawValue: "full", order: 1000)
|
||||
/// A panel state indicates the half of a panel is shown.
|
||||
@objc(Half) public static let half: FloatingPanelState = FloatingPanelState(rawValue: "half", order: 500)
|
||||
/// A panel state indicates the tip of a panel is shown.
|
||||
@objc(Tip) public static let tip: FloatingPanelState = FloatingPanelState(rawValue: "tip", order: 100)
|
||||
/// A panel state indicates it is hidden.
|
||||
@objc(Hidden) public static let hidden: FloatingPanelState = FloatingPanelState(rawValue: "hidden", order: 0)
|
||||
}
|
||||
|
||||
|
||||
+44
-31
@@ -1,38 +1,41 @@
|
||||
// Copyright 2018 the FloatingPanel authors. All rights reserved. MIT license.
|
||||
// Copyright 2018-Present Shin Yamamoto. All rights reserved. MIT license.
|
||||
|
||||
import UIKit
|
||||
|
||||
/// An object for customizing the appearance of a surface view
|
||||
@objc(FloatingPanelSurfaceAppearance)
|
||||
@objcMembers
|
||||
public class SurfaceAppearance: NSObject {
|
||||
|
||||
/// An object that represents information to render a shadow
|
||||
@objc(FloatingPanelSurfaceAppearanceShadow)
|
||||
public class Shadow: NSObject {
|
||||
/// A Boolean indicating whether the surface shadow is displayed.
|
||||
/// A Boolean indicating whether a shadow is displayed.
|
||||
@objc
|
||||
public var hidden: Bool = false
|
||||
|
||||
/// The color of the surface shadow.
|
||||
/// The color of a shadow.
|
||||
@objc
|
||||
public var color: UIColor = .black
|
||||
|
||||
/// The offset (in points) of the surface shadow.
|
||||
/// The offset (in points) of a shadow.
|
||||
@objc
|
||||
public var offset: CGSize = CGSize(width: 0.0, height: 1.0)
|
||||
|
||||
/// The opacity of the surface shadow.
|
||||
/// The opacity of a shadow.
|
||||
@objc
|
||||
public var opacity: Float = 0.2
|
||||
|
||||
/// The blur radius (in points) used to render the surface shadow.
|
||||
/// The blur radius (in points) used to render a shadow.
|
||||
@objc
|
||||
public var radius: CGFloat = 3
|
||||
|
||||
// The inflated amount of the surface shadow prior to applying the blur.
|
||||
/// The inflated amount of a shadow prior to applying the blur.
|
||||
@objc
|
||||
public var spread: CGFloat = 0
|
||||
|
||||
}
|
||||
/// The background color.
|
||||
/// The background color of a surface view
|
||||
public var backgroundColor: UIColor? = {
|
||||
if #available(iOS 13, *) {
|
||||
return UIColor.systemBackground
|
||||
@@ -41,29 +44,35 @@ public class SurfaceAppearance: NSObject {
|
||||
}
|
||||
}()
|
||||
|
||||
/// The radius to use when drawing top rounded corners.
|
||||
/// The radius to use when drawing the top rounded corners.
|
||||
///
|
||||
/// `self.contentView` is masked with the top rounded corners automatically on iOS 11 and later.
|
||||
/// On iOS 10, they are not automatically masked because of a UIVisualEffectView issue. See https://forums.developer.apple.com/thread/50854
|
||||
public var cornerRadius: CGFloat = 0.0
|
||||
|
||||
/// Defines the curve used for rendering the rounded corners of the layer.
|
||||
///
|
||||
/// Defaults to `.circular`.
|
||||
@available(iOS 13.0, *)
|
||||
public lazy var cornerCurve: CALayerCornerCurve = .circular
|
||||
|
||||
/// An array of shadows used to create drop shadows underneath a surface view.
|
||||
public var shadows: [Shadow] = [Shadow()]
|
||||
|
||||
/// The width of the surface border.
|
||||
/// The border width of a surface view.
|
||||
public var borderColor: UIColor?
|
||||
|
||||
/// The color of the surface border.
|
||||
/// The border color of a surface view.
|
||||
public var borderWidth: CGFloat = 0.0
|
||||
}
|
||||
|
||||
/// A view that presents a surface interface in a floating panel.
|
||||
/// A view that presents a surface interface in a panel.
|
||||
@objc(FloatingPanelSurfaceView)
|
||||
@objcMembers
|
||||
public class SurfaceView: UIView {
|
||||
/// A FloatingPanelGrabberView object displayed at the top of the surface view.
|
||||
/// A `FloatingPanelGrabberView` object displayed at the top of the surface view.
|
||||
///
|
||||
/// To use a custom grabber handle, hide this and then add the custom one
|
||||
/// to the surface view at appropriate coordinates.
|
||||
/// To use a custom grabber, hide this and then add it to the surface view at appropriate point.
|
||||
public let grabberHandle = GrabberView()
|
||||
|
||||
/// Offset of the grabber handle from the interactive edge.
|
||||
@@ -71,18 +80,18 @@ public class SurfaceView: UIView {
|
||||
setNeedsUpdateConstraints()
|
||||
} }
|
||||
|
||||
/// The offset from the interactive edge which prevents the conetent scroll
|
||||
/// The offset from the move edge to prevent the content scroll
|
||||
public var grabberAreaOffset: CGFloat = 36.0
|
||||
|
||||
/// The grabber handle size
|
||||
///
|
||||
/// On left/right position, the width dimension is used as the height of `grabberHandle`, and vice versa.
|
||||
/// On left/right positioned panel the width dimension is used as the height of `grabberHandle`, and vice versa.
|
||||
public var grabberHandleSize: CGSize = CGSize(width: 36.0, height: 5.0) { didSet {
|
||||
setNeedsUpdateConstraints()
|
||||
} }
|
||||
|
||||
/// A root view of a content view controller
|
||||
public weak var contentView: UIView!
|
||||
/// The content view to be assigned a view of the content view controller of `FloatingPanelController`
|
||||
public weak var contentView: UIView?
|
||||
|
||||
/// The content insets specifying the insets around the content view.
|
||||
public var contentPadding: UIEdgeInsets = .zero {
|
||||
@@ -97,6 +106,7 @@ public class SurfaceView: UIView {
|
||||
set { appearance.backgroundColor = newValue; setNeedsLayout() }
|
||||
}
|
||||
|
||||
/// The appearance settings for a surface view.
|
||||
public var appearance = SurfaceAppearance() { didSet {
|
||||
shadowLayers = appearance.shadows.map { _ in CAShapeLayer() }
|
||||
setNeedsLayout()
|
||||
@@ -107,7 +117,7 @@ public class SurfaceView: UIView {
|
||||
setNeedsUpdateConstraints()
|
||||
} }
|
||||
|
||||
/// The view presents an actual surface shape.
|
||||
/// The view that displays an actual surface shape.
|
||||
///
|
||||
/// It renders the background color, border line and top rounded corners,
|
||||
/// specified by other properties. The reason why they're not be applied to
|
||||
@@ -118,7 +128,7 @@ public class SurfaceView: UIView {
|
||||
var containerOverflow: CGFloat = 0.0 { // Must not call setNeedsLayout()
|
||||
didSet {
|
||||
// Calling setNeedsUpdateConstraints() is necessary to fix a layout break
|
||||
// when the contentMode is changed after laying out a floating panel, for instance,
|
||||
// when the contentMode is changed after laying out a panel, for instance,
|
||||
// after calling viewDidAppear(_:) of the parent view controller.
|
||||
setNeedsUpdateConstraints()
|
||||
}
|
||||
@@ -307,8 +317,8 @@ public class SurfaceView: UIView {
|
||||
|
||||
containerView.backgroundColor = appearance.backgroundColor
|
||||
|
||||
updateShadow()
|
||||
updateCornerRadius()
|
||||
updateShadow()
|
||||
updateBorder()
|
||||
|
||||
grabberHandle.layer.cornerRadius = grabberHandleSize.height / 2
|
||||
@@ -332,10 +342,9 @@ public class SurfaceView: UIView {
|
||||
shadowLayer.frame = layer.bounds
|
||||
|
||||
let spread = shadow.spread
|
||||
let shadowPath = UIBezierPath(roundedRect: containerView.frame.insetBy(dx: -spread,
|
||||
dy: -spread),
|
||||
byRoundingCorners: [.allCorners],
|
||||
cornerRadii: CGSize(width: appearance.cornerRadius, height: 0))
|
||||
let shadowRect = containerView.frame.insetBy(dx: -spread, dy: -spread)
|
||||
let shadowPath = UIBezierPath.path(roundedRect: shadowRect,
|
||||
appearance: appearance)
|
||||
shadowLayer.shadowPath = shadowPath.cgPath
|
||||
shadowLayer.shadowColor = shadow.color.cgColor
|
||||
shadowLayer.shadowOffset = shadow.offset
|
||||
@@ -344,16 +353,16 @@ public class SurfaceView: UIView {
|
||||
shadowLayer.shadowOpacity = shadow.opacity
|
||||
|
||||
let mask = CAShapeLayer()
|
||||
let path = UIBezierPath(roundedRect: containerView.frame,
|
||||
byRoundingCorners: [.allCorners],
|
||||
cornerRadii: CGSize(width: appearance.cornerRadius, height: 0))
|
||||
let path = UIBezierPath.path(roundedRect: containerView.frame,
|
||||
appearance: appearance)
|
||||
let size = window?.bounds.size ?? CGSize(width: 1000.0, height: 1000.0)
|
||||
path.append(UIBezierPath(rect: layer.bounds.insetBy(dx: -size.width,
|
||||
dy: -size.height)))
|
||||
mask.fillRule = .evenOdd
|
||||
mask.path = path.cgPath
|
||||
if #available(iOS 13.0, *) {
|
||||
mask.cornerCurve = containerView.layer.cornerCurve
|
||||
containerView.layer.cornerCurve = appearance.cornerCurve
|
||||
mask.cornerCurve = appearance.cornerCurve
|
||||
}
|
||||
shadowLayer.mask = mask
|
||||
}
|
||||
@@ -415,7 +424,7 @@ public class SurfaceView: UIView {
|
||||
rightConstraint,
|
||||
bottomConstraint,
|
||||
].map {
|
||||
$0.priority = .defaultHigh;
|
||||
$0.priority = .required - 1;
|
||||
$0.identifier = "FloatingPanel-surface-content"
|
||||
return $0;
|
||||
})
|
||||
@@ -424,4 +433,8 @@ public class SurfaceView: UIView {
|
||||
self.contentViewRightConstraint = rightConstraint
|
||||
self.contentViewBottomConstraint = bottomConstraint
|
||||
}
|
||||
|
||||
func hasStackView() -> Bool {
|
||||
return contentView?.subviews.reduce(false) { $0 || ($1 is UIStackView) } ?? false
|
||||
}
|
||||
}
|
||||
|
||||
+40
-13
@@ -1,4 +1,4 @@
|
||||
// Copyright 2018 the FloatingPanel authors. All rights reserved. MIT license.
|
||||
// Copyright 2018-Present Shin Yamamoto. All rights reserved. MIT license.
|
||||
|
||||
import UIKit
|
||||
|
||||
@@ -21,7 +21,7 @@ class ModalTransition: NSObject, UIViewControllerTransitioningDelegate {
|
||||
class PresentationController: UIPresentationController {
|
||||
override func presentationTransitionWillBegin() {
|
||||
// Must call here even if duplicating on in containerViewWillLayoutSubviews()
|
||||
// Because it let the floating panel present correctly with the presentation animation
|
||||
// Because it let the panel present correctly with the presentation animation
|
||||
addFloatingPanel()
|
||||
}
|
||||
|
||||
@@ -44,8 +44,15 @@ class PresentationController: UIPresentationController {
|
||||
|
||||
override func containerViewWillLayoutSubviews() {
|
||||
guard
|
||||
let fpc = presentedViewController as? FloatingPanelController
|
||||
else { fatalError() }
|
||||
let fpc = presentedViewController as? FloatingPanelController,
|
||||
/**
|
||||
This condition fixes https://github.com/SCENEE/FloatingPanel/issues/369.
|
||||
The issue is that this method is called in presenting a
|
||||
UIImagePickerViewController and then a FloatingPanelController
|
||||
view is added unnecessarily.
|
||||
*/
|
||||
fpc.presentedViewController == nil
|
||||
else { return }
|
||||
|
||||
/*
|
||||
* Layout the views managed by `FloatingPanelController` here for the
|
||||
@@ -54,7 +61,7 @@ class PresentationController: UIPresentationController {
|
||||
addFloatingPanel()
|
||||
|
||||
// Forward touch events to the presenting view controller
|
||||
(fpc.view as? PassThroughView)?.eventForwardingView = presentingViewController.view
|
||||
(fpc.view as? PassthroughView)?.eventForwardingView = presentingViewController.view
|
||||
}
|
||||
|
||||
@objc func handleBackdrop(tapGesture: UITapGestureRecognizer) {
|
||||
@@ -79,19 +86,29 @@ class ModalPresentTransition: NSObject, UIViewControllerAnimatedTransitioning {
|
||||
let fpc = transitionContext?.viewController(forKey: .to) as? FloatingPanelController
|
||||
else { fatalError()}
|
||||
|
||||
let animator = fpc.delegate?.floatingPanel?(fpc, animatorForPresentingTo: fpc.layout.initialState)
|
||||
?? FloatingPanelDefaultBehavior().addPanelAnimator(fpc, to: fpc.layout.initialState)
|
||||
let animator = fpc.animatorForPresenting(to: fpc.layout.initialState)
|
||||
return TimeInterval(animator.duration)
|
||||
}
|
||||
|
||||
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
|
||||
func interruptibleAnimator(using transitionContext: UIViewControllerContextTransitioning) -> UIViewImplicitlyAnimating {
|
||||
guard
|
||||
let fpc = transitionContext.viewController(forKey: .to) as? FloatingPanelController
|
||||
else { fatalError() }
|
||||
|
||||
fpc.show(animated: true) {
|
||||
if let animator = fpc.transitionAnimator {
|
||||
return animator
|
||||
}
|
||||
|
||||
fpc.suspendTransitionAnimator(true)
|
||||
fpc.show(animated: true) { [weak fpc] in
|
||||
fpc?.suspendTransitionAnimator(false)
|
||||
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
|
||||
}
|
||||
return fpc.transitionAnimator!
|
||||
}
|
||||
|
||||
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
|
||||
self.interruptibleAnimator(using: transitionContext).startAnimation()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,19 +118,29 @@ class ModalDismissTransition: NSObject, UIViewControllerAnimatedTransitioning {
|
||||
let fpc = transitionContext?.viewController(forKey: .from) as? FloatingPanelController
|
||||
else { fatalError()}
|
||||
|
||||
let animator = fpc.delegate?.floatingPanel?(fpc, animatorForDismissingWith: .zero)
|
||||
?? FloatingPanelDefaultBehavior().removePanelAnimator(fpc, from: fpc.state, with: .zero)
|
||||
let animator = fpc.animatorForDismissing(with: .zero)
|
||||
return TimeInterval(animator.duration)
|
||||
}
|
||||
|
||||
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
|
||||
func interruptibleAnimator(using transitionContext: UIViewControllerContextTransitioning) -> UIViewImplicitlyAnimating {
|
||||
guard
|
||||
let fpc = transitionContext.viewController(forKey: .from) as? FloatingPanelController
|
||||
else { fatalError() }
|
||||
|
||||
fpc.hide(animated: true) {
|
||||
if let animator = fpc.transitionAnimator {
|
||||
return animator
|
||||
}
|
||||
|
||||
fpc.suspendTransitionAnimator(true)
|
||||
fpc.hide(animated: true) { [weak fpc] in
|
||||
fpc?.suspendTransitionAnimator(false)
|
||||
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
|
||||
}
|
||||
return fpc.transitionAnimator!
|
||||
}
|
||||
|
||||
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
|
||||
self.interruptibleAnimator(using: transitionContext).startAnimation()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -86,7 +86,7 @@ class ControllerTests: XCTestCase {
|
||||
}
|
||||
XCTAssertEqual(fpc.state, .full)
|
||||
XCTAssertEqual(delegate.position, .full)
|
||||
wait(for: [exp], timeout: 0.5)
|
||||
wait(for: [exp], timeout: 1.0)
|
||||
}
|
||||
|
||||
XCTContext.runActivity(named: "move to half(animated)") { act in
|
||||
@@ -97,7 +97,7 @@ class ControllerTests: XCTestCase {
|
||||
}
|
||||
XCTAssertEqual(fpc.state, .half)
|
||||
XCTAssertEqual(delegate.position, .half)
|
||||
wait(for: [exp], timeout: 0.8)
|
||||
wait(for: [exp], timeout: 1.0)
|
||||
}
|
||||
|
||||
XCTContext.runActivity(named: "move to tip(animated)") { act in
|
||||
@@ -108,7 +108,7 @@ class ControllerTests: XCTestCase {
|
||||
}
|
||||
XCTAssertEqual(fpc.state, .tip)
|
||||
XCTAssertEqual(delegate.position, .tip)
|
||||
wait(for: [exp], timeout: 0.8)
|
||||
wait(for: [exp], timeout: 1.0)
|
||||
}
|
||||
|
||||
fpc.move(to: .hidden, animated: true)
|
||||
@@ -159,7 +159,7 @@ class ControllerTests: XCTestCase {
|
||||
}
|
||||
XCTAssertEqual(fpc.state, .full)
|
||||
XCTAssertEqual(delegate.position, .full)
|
||||
wait(for: [exp], timeout: 0.5)
|
||||
wait(for: [exp], timeout: 1.0)
|
||||
}
|
||||
|
||||
XCTContext.runActivity(named: "move to half(animated)") { act in
|
||||
@@ -170,7 +170,7 @@ class ControllerTests: XCTestCase {
|
||||
}
|
||||
XCTAssertEqual(fpc.state, .half)
|
||||
XCTAssertEqual(delegate.position, .half)
|
||||
wait(for: [exp], timeout: 0.8)
|
||||
wait(for: [exp], timeout: 1.0)
|
||||
}
|
||||
|
||||
XCTContext.runActivity(named: "move to tip(animated)") { act in
|
||||
@@ -181,7 +181,7 @@ class ControllerTests: XCTestCase {
|
||||
}
|
||||
XCTAssertEqual(fpc.state, .tip)
|
||||
XCTAssertEqual(delegate.position, .tip)
|
||||
wait(for: [exp], timeout: 0.8)
|
||||
wait(for: [exp], timeout: 1.0)
|
||||
}
|
||||
|
||||
fpc.move(to: .hidden, animated: true)
|
||||
@@ -314,7 +314,7 @@ class ControllerTests: XCTestCase {
|
||||
fpc.move(to: .full, animated: false)
|
||||
XCTAssertEqual(fpc.surfaceView.frame.height, fpc.view.bounds.height - fpc.surfaceLocation(for: .full).y)
|
||||
fpc.move(to: .half, animated: false)
|
||||
print(1 / fpc.surfaceView.traitCollection.displayScale)
|
||||
print(1 / fpc.surfaceView.fp_displayScale)
|
||||
XCTAssertEqual(fpc.surfaceView.frame.height, fpc.view.bounds.height - fpc.surfaceLocation(for: .half).y)
|
||||
fpc.move(to: .tip, animated: false)
|
||||
XCTAssertEqual(fpc.surfaceView.frame.height, fpc.view.bounds.height - fpc.surfaceLocation(for: .tip).y)
|
||||
|
||||
@@ -185,6 +185,57 @@ class CoreTests: XCTestCase {
|
||||
XCTAssertEqual(fpc.floatingPanel.getBackdropAlpha(at: halfPos, with: -1 * distance2), 0.0)
|
||||
}
|
||||
|
||||
|
||||
func test_updateBackdropAlpha() {
|
||||
class BackdropTestLayout: FloatingPanelTestLayout {
|
||||
override var initialState: FloatingPanelState { .hidden }
|
||||
func backdropAlpha(for state: FloatingPanelState) -> CGFloat {
|
||||
switch state {
|
||||
case .full: return 0.3
|
||||
case .half: return 0.0
|
||||
case .tip: return 0.3
|
||||
default: return 0.0
|
||||
}
|
||||
}
|
||||
}
|
||||
let fpc = FloatingPanelController()
|
||||
fpc.layout = BackdropTestLayout()
|
||||
|
||||
fpc.showForTest()
|
||||
|
||||
fpc.move(to: .full, animated: false)
|
||||
XCTAssertEqual(floor(fpc.backdropView.alpha * 1000_000) / 1000_000, 0.3)
|
||||
|
||||
fpc.move(to: .half, animated: false)
|
||||
XCTAssertEqual(fpc.backdropView.alpha, 0.0)
|
||||
|
||||
fpc.move(to: .tip, animated: false)
|
||||
XCTAssertEqual(floor(fpc.backdropView.alpha * 1000_000) / 1000_000, 0.3)
|
||||
|
||||
let exp1 = expectation(description: "move to full with animation")
|
||||
fpc.move(to: .full, animated: true) {
|
||||
exp1.fulfill()
|
||||
}
|
||||
wait(for: [exp1], timeout: 1.0)
|
||||
XCTAssertEqual(floor(fpc.backdropView.alpha * 1000_000) / 1000_000, 0.3)
|
||||
|
||||
let exp2 = expectation(description: "move to half with animation")
|
||||
fpc.move(to: .half, animated: true) {
|
||||
exp2.fulfill()
|
||||
}
|
||||
wait(for: [exp2], timeout: 1.0)
|
||||
XCTAssertEqual(fpc.backdropView.alpha, 0.0)
|
||||
|
||||
let exp3 = expectation(description: "move to tip with animation")
|
||||
fpc.move(to: .tip, animated: true) {
|
||||
exp3.fulfill()
|
||||
}
|
||||
fpc.contentMode = .fitToBounds
|
||||
XCTAssertEqual(fpc.backdropView.alpha, 0.0) // Must not affect the backdrop alpha by changing the content mode
|
||||
wait(for: [exp3], timeout: 1.0)
|
||||
XCTAssertEqual(floor(fpc.backdropView.alpha * 1000_000) / 1000_000, 0.3)
|
||||
}
|
||||
|
||||
func test_targetPosition_1positions() {
|
||||
class FloatingPanelLayout1Positions: FloatingPanelLayout {
|
||||
let initialState: FloatingPanelState = .full
|
||||
@@ -715,6 +766,13 @@ class CoreTests: XCTestCase {
|
||||
(#line, tipPos, CGPoint(x: 0.0, y: 1000.0), .hidden),
|
||||
])
|
||||
}
|
||||
|
||||
func test_keep_pan_gesture_disabled() {
|
||||
let fpc = FloatingPanelController()
|
||||
fpc.panGestureRecognizer.isEnabled = false
|
||||
fpc.showForTest()
|
||||
XCTAssertFalse(fpc.panGestureRecognizer.isEnabled)
|
||||
}
|
||||
}
|
||||
|
||||
private class FloatingPanelLayout3Positions: FloatingPanelTestLayout {
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
// Copyright 2018 the FloatingPanel authors. All rights reserved. MIT license.
|
||||
|
||||
import XCTest
|
||||
@testable import FloatingPanel
|
||||
|
||||
class ExtensionTests: XCTestCase {
|
||||
func test_roundedByDisplayScale() {
|
||||
XCTAssertEqual(CGFloat(333.222).rounded(by: 3), 333.3333333333333)
|
||||
XCTAssertNotEqual(CGFloat(333.5).rounded(by: 3), 333.66666666666674)
|
||||
XCTAssertTrue(CGFloat(333.5).isEqual(to: 333.66666666666674, on: 3.0))
|
||||
}
|
||||
}
|
||||
+19
-11
@@ -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() {
|
||||
@@ -469,7 +469,11 @@ class LayoutTests: XCTestCase {
|
||||
let c = prop.anchor.layoutConstraints(fpc, for: position)[0]
|
||||
XCTAssertEqual(c.multiplier, CGFloat(prop.result.multiplier), line: UInt(prop.result.0))
|
||||
XCTAssertTrue(c.firstAnchor is NSLayoutAnchor<NSLayoutDimension>, line: UInt(prop.result.0))
|
||||
XCTAssertEqual(c.secondAnchor, prop.result.secondAnchor, line: UInt(prop.result.0))
|
||||
// On iOS 10, `c.secondAnchor` can't be equal object to `prop.result.secondAnchor`
|
||||
// because there is no safe area on iOS 10 and `fp_safeAreaLayoutGuide` emulates it.
|
||||
if #available(iOS 11, *) {
|
||||
XCTAssertEqual(c.secondAnchor, prop.result.secondAnchor, line: UInt(prop.result.0))
|
||||
}
|
||||
print(c)
|
||||
}
|
||||
}
|
||||
@@ -532,7 +536,11 @@ class LayoutTests: XCTestCase {
|
||||
let c = prop.anchor.layoutConstraints(fpc, for: position)[0]
|
||||
XCTAssertEqual(c.multiplier, CGFloat(prop.result.multiplier), line: UInt(prop.result.0))
|
||||
XCTAssertTrue(c.firstAnchor is NSLayoutAnchor<NSLayoutDimension>, line: UInt(prop.result.0))
|
||||
XCTAssertEqual(c.secondAnchor, prop.result.secondAnchor, line: UInt(prop.result.0))
|
||||
// On iOS 10, `c.secondAnchor` can't be equal object to `prop.result.secondAnchor`
|
||||
// because there is no safe area on iOS 10 and `fp_safeAreaLayoutGuide` emulates it.
|
||||
if #available(iOS 11, *) {
|
||||
XCTAssertEqual(c.secondAnchor, prop.result.secondAnchor, line: UInt(prop.result.0))
|
||||
}
|
||||
print(c)
|
||||
}
|
||||
}
|
||||
@@ -641,9 +649,9 @@ class LayoutTests: XCTestCase {
|
||||
private typealias LayoutSegmentTestParameter = (UInt, pos: CGFloat, forwardY: Bool, lower: FloatingPanelState?, upper: FloatingPanelState?)
|
||||
private func assertLayoutSegment(_ floatingPanel: Core, with params: [LayoutSegmentTestParameter]) {
|
||||
params.forEach { (line, pos, forwardY, lowr, upper) in
|
||||
let segument = floatingPanel.layoutAdapter.segument(at: pos, forward: forwardY)
|
||||
XCTAssertEqual(segument.lower, lowr, line: line)
|
||||
XCTAssertEqual(segument.upper, upper, line: line)
|
||||
let segment = floatingPanel.layoutAdapter.segment(at: pos, forward: forwardY)
|
||||
XCTAssertEqual(segment.lower, lowr, line: line)
|
||||
XCTAssertEqual(segment.upper, upper, line: line)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -59,7 +59,7 @@ class SurfaceViewTests: XCTestCase {
|
||||
surface.containerOverflow = height
|
||||
surface.setNeedsLayout()
|
||||
surface.layoutIfNeeded()
|
||||
XCTAssertEqual(surface.contentView.frame, surface.bounds)
|
||||
XCTAssertEqual(surface.contentView?.frame ?? .zero, surface.bounds)
|
||||
}
|
||||
|
||||
XCTContext.runActivity(named: "Top sheet") { _ in
|
||||
@@ -75,7 +75,7 @@ class SurfaceViewTests: XCTestCase {
|
||||
surface.setNeedsLayout()
|
||||
surface.layoutIfNeeded()
|
||||
XCTAssertEqual(surface.containerView.frame, CGRect(x: 0.0, y: -height, width: 320.0, height: 480.0 * 3))
|
||||
XCTAssertEqual(surface.convert(surface.contentView.frame, from: surface.containerView),
|
||||
XCTAssertEqual(surface.convert(surface.contentView?.frame ?? .zero, from: surface.containerView),
|
||||
surface.bounds)
|
||||
}
|
||||
}
|
||||
@@ -146,22 +146,22 @@ class SurfaceViewTests: XCTestCase {
|
||||
let contentView = UIView()
|
||||
surface.set(contentView: contentView)
|
||||
surface.layoutIfNeeded()
|
||||
XCTAssertEqual(surface.contentView.frame, surface.bounds)
|
||||
XCTAssertEqual(surface.contentView?.frame ?? .zero, surface.bounds)
|
||||
surface.contentPadding = UIEdgeInsets(top: 16.0, left: 16.0, bottom: 16.0, right: 16.0)
|
||||
surface.setNeedsLayout()
|
||||
surface.layoutIfNeeded()
|
||||
XCTAssertEqual(surface.contentView.frame, surface.bounds.inset(by: surface.contentPadding))
|
||||
XCTAssertEqual(surface.contentView?.frame ?? .zero, surface.bounds.inset(by: surface.contentPadding))
|
||||
}
|
||||
XCTContext.runActivity(named: "Bottom sheet") { _ in
|
||||
let surface = SurfaceView(frame: CGRect(x: 0.0, y: 0.0, width: 320.0, height: 480.0))
|
||||
let contentView = UIView()
|
||||
surface.set(contentView: contentView)
|
||||
surface.layoutIfNeeded()
|
||||
XCTAssertEqual(surface.contentView.frame, surface.bounds)
|
||||
XCTAssertEqual(surface.contentView?.frame ?? .zero, surface.bounds)
|
||||
surface.contentPadding = UIEdgeInsets(top: 16.0, left: 16.0, bottom: 16.0, right: 16.0)
|
||||
surface.setNeedsLayout()
|
||||
surface.layoutIfNeeded()
|
||||
XCTAssertEqual(surface.contentView.frame, surface.bounds.inset(by: surface.contentPadding))
|
||||
XCTAssertEqual(surface.contentView?.frame ?? .zero, surface.bounds.inset(by: surface.contentPadding))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -170,13 +170,13 @@ class SurfaceViewTests: XCTestCase {
|
||||
let contentView = UIView()
|
||||
surface.set(contentView: contentView)
|
||||
surface.layoutIfNeeded()
|
||||
XCTAssertEqual(surface.contentView.frame, surface.bounds)
|
||||
XCTAssertEqual(surface.contentView?.frame ?? .zero, surface.bounds)
|
||||
surface.containerMargins = UIEdgeInsets(top: 16.0, left: 16.0, bottom: 16.0, right: 16.0)
|
||||
surface.contentPadding = UIEdgeInsets(top: 16.0, left: 16.0, bottom: 16.0, right: 16.0)
|
||||
surface.setNeedsLayout()
|
||||
surface.layoutIfNeeded()
|
||||
XCTAssertEqual(surface.containerView.frame, surface.bounds.inset(by: surface.containerMargins))
|
||||
XCTAssertEqual(surface.contentView.frame, surface.containerView.bounds.inset(by: surface.contentPadding))
|
||||
XCTAssertEqual(surface.contentView?.frame ?? .zero, surface.containerView.bounds.inset(by: surface.contentPadding))
|
||||
}
|
||||
|
||||
func test_surfaceView_cornderRaduis() {
|
||||
|
||||
@@ -18,7 +18,7 @@ extension FloatingPanelController {
|
||||
class FloatingPanelTestDelegate: FloatingPanelControllerDelegate {
|
||||
var position: FloatingPanelState = .hidden
|
||||
var didMoveCallback: ((FloatingPanelController) -> Void)?
|
||||
func floatingPanelDidChangePosition(_ vc: FloatingPanelController) {
|
||||
func floatingPanelDidChangeState(_ vc: FloatingPanelController) {
|
||||
position = vc.state
|
||||
}
|
||||
func floatingPanelDidMove(_ vc: FloatingPanelController) {
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
// Copyright 2018 the FloatingPanel authors. All rights reserved. MIT license.
|
||||
|
||||
import XCTest
|
||||
@testable import FloatingPanel
|
||||
|
||||
class UtilTests: XCTestCase {
|
||||
func test_displayTrunc() {
|
||||
XCTAssertEqual(displayTrunc(333.222, by: 3), 333.3333333333333)
|
||||
XCTAssertNotEqual(displayTrunc(333.5, by: 3), 333.66666666666674)
|
||||
XCTAssertTrue(displayEqual(333.5, 333.66666666666674, by: 3))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user