Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| dfa9a77816 | |||
| afff000d8c | |||
| dd49fdea5e | |||
| a1f20cedb1 | |||
| 9592baa16c | |||
| 370e306904 | |||
| 479cce4546 | |||
| b0fd0d4427 | |||
| b6e8928b1a | |||
| 3a3d53424c | |||
| 71f419a3cd | |||
| 0e27410460 | |||
| 29185a47bd | |||
| 6821b26706 | |||
| 5bdbe0f0ea | |||
| afe2a9bced | |||
| c0d88af234 | |||
| 3b4c1bd51c | |||
| 3c9f556533 | |||
| 7f1a74825d |
+30
-8
@@ -1,21 +1,43 @@
|
||||
version: 2.1
|
||||
|
||||
jobs:
|
||||
build-swift_5_7:
|
||||
macos:
|
||||
xcode: 13.4.1
|
||||
steps:
|
||||
- checkout
|
||||
- run: xcodebuild -scheme FloatingPanel -workspace FloatingPanel.xcworkspace SWIFT_VERSION=5.7 clean build
|
||||
|
||||
build-swiftpm_ios15_7:
|
||||
macos:
|
||||
xcode: 13.4.1
|
||||
steps:
|
||||
- checkout
|
||||
- run: swift build -Xswiftc "-sdk" -Xswiftc "`xcrun --sdk iphonesimulator --show-sdk-path`" -Xswiftc "-target" -Xswiftc "x86_64-apple-ios15.7-simulator"
|
||||
- run: swift build -Xswiftc "-sdk" -Xswiftc "`xcrun --sdk iphonesimulator --show-sdk-path`" -Xswiftc "-target" -Xswiftc "arm64-apple-ios15.7-simulator"
|
||||
|
||||
test-ios15_5-iPhone_13_Pro:
|
||||
macos:
|
||||
xcode: 13.4.1
|
||||
steps:
|
||||
- checkout
|
||||
- run: xcodebuild clean test -scheme FloatingPanel -workspace FloatingPanel.xcworkspace -destination 'platform=iOS Simulator,OS=15.5,name=iPhone 13 Pro'
|
||||
test-ios14_5-iPhone_12_Pro:
|
||||
macos:
|
||||
xcode: 13.4.1
|
||||
steps:
|
||||
- checkout
|
||||
- run: xcodebuild clean test -scheme FloatingPanel -workspace FloatingPanel.xcworkspace -destination 'platform=iOS Simulator,OS=14.5,name=iPhone 12 Pro'
|
||||
test-ios13_7-iPhone_11_Pro:
|
||||
macos:
|
||||
xcode: 12.5.1
|
||||
steps:
|
||||
- checkout
|
||||
- run: xcodebuild clean test -scheme FloatingPanel -workspace FloatingPanel.xcworkspace -destination 'platform=iOS Simulator,OS=13.7,name=iPhone 11 Pro'
|
||||
|
||||
|
||||
workflows:
|
||||
test:
|
||||
jobs:
|
||||
- test-ios14_5-iPhone_12_Pro
|
||||
- test-ios13_7-iPhone_11_Pro
|
||||
- build-swift_5_7:
|
||||
name: build (5.7, 13.4.1)
|
||||
- build-swiftpm_ios15_7:
|
||||
name: swiftpm ({x86_64,arm64}-apple-ios15.5-simulator, 13.4.1)
|
||||
- test-ios14_5-iPhone_12_Pro:
|
||||
name: test (15.5, 13.4.1, iPhone 12 Pro)
|
||||
- test-ios15_5-iPhone_13_Pro:
|
||||
name: test (14.5, 13.4.1, iPhone 13 Pro)
|
||||
|
||||
+33
-51
@@ -18,6 +18,9 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- swift: "5"
|
||||
xcode: "16.2"
|
||||
runs-on: macos-15
|
||||
- swift: "5.10"
|
||||
xcode: "15.4"
|
||||
runs-on: macos-14
|
||||
@@ -27,26 +30,8 @@ jobs:
|
||||
- swift: "5.8"
|
||||
xcode: "14.3.1"
|
||||
runs-on: macos-13
|
||||
- swift: "5.7"
|
||||
xcode: "14.1"
|
||||
runs-on: macos-12
|
||||
- swift: "5.6"
|
||||
xcode: "13.4.1"
|
||||
runs-on: macos-12
|
||||
- swift: "5.5"
|
||||
xcode: "13.2.1"
|
||||
runs-on: macos-12
|
||||
- swift: "5.4"
|
||||
xcode: "12.5.1"
|
||||
runs-on: macos-11
|
||||
- swift: "5.3"
|
||||
xcode: "12.4"
|
||||
runs-on: macos-11
|
||||
- swift: "5.2"
|
||||
xcode: "11.7"
|
||||
runs-on: macos-11
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- name: Building in Swift ${{ matrix.swift }}
|
||||
run: xcodebuild -scheme FloatingPanel SWIFT_VERSION=${{ matrix.swift }} clean build
|
||||
|
||||
@@ -58,6 +43,11 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- os: "18.2"
|
||||
xcode: "16.2"
|
||||
sim: "iPhone 16 Pro"
|
||||
parallel: NO # Stop random test job failures
|
||||
runs-on: macos-15
|
||||
- os: "17.5"
|
||||
xcode: "15.4"
|
||||
sim: "iPhone 15 Pro"
|
||||
@@ -68,13 +58,8 @@ jobs:
|
||||
sim: "iPhone 14 Pro"
|
||||
parallel: NO # Stop random test job failures
|
||||
runs-on: macos-13
|
||||
- os: "15.5"
|
||||
xcode: "13.4.1"
|
||||
sim: "iPhone 13 Pro"
|
||||
parallel: NO # Stop random test job failures
|
||||
runs-on: macos-12
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- name: Testing in iOS ${{ matrix.os }}
|
||||
run: |
|
||||
xcodebuild clean test \
|
||||
@@ -85,9 +70,9 @@ jobs:
|
||||
timeout-minutes: 20
|
||||
|
||||
example:
|
||||
runs-on: macos-14
|
||||
runs-on: macos-15
|
||||
env:
|
||||
DEVELOPER_DIR: /Applications/Xcode_15.4.app/Contents/Developer
|
||||
DEVELOPER_DIR: /Applications/Xcode_16.2.app/Contents/Developer
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -97,8 +82,9 @@ jobs:
|
||||
- example: "Stocks"
|
||||
- example: "Samples"
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- name: Building ${{ matrix.example }}
|
||||
# Need to use iphonesimulator18.1 because randomly 18.2<DVTBuildVersion 22C146> isn't available.
|
||||
run: |
|
||||
xcodebuild clean build \
|
||||
-workspace FloatingPanel.xcworkspace \
|
||||
@@ -106,25 +92,35 @@ jobs:
|
||||
-sdk iphonesimulator
|
||||
|
||||
swiftpm:
|
||||
runs-on: macos-14
|
||||
runs-on: macos-15
|
||||
env:
|
||||
DEVELOPER_DIR: /Applications/Xcode_15.4.app/Contents/Developer
|
||||
DEVELOPER_DIR: /Applications/Xcode_${{ matrix.xcode }}.app/Contents/Developer
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
xcode: ["16.2", "15.4"]
|
||||
platform: [iphoneos, iphonesimulator]
|
||||
arch: [x86_64, arm64]
|
||||
exclude:
|
||||
- platform: iphoneos
|
||||
arch: x86_64
|
||||
include:
|
||||
# 18.2
|
||||
- platform: iphoneos
|
||||
xcode: "16.2"
|
||||
sys: "ios18.2"
|
||||
- platform: iphonesimulator
|
||||
xcode: "16.2"
|
||||
sys: "ios18.2-simulator"
|
||||
# 17.2
|
||||
- platform: iphoneos
|
||||
xcode: "15.4"
|
||||
sys: "ios17.2"
|
||||
- platform: iphonesimulator
|
||||
xcode: "15.4"
|
||||
sys: "ios17.2-simulator"
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- name: "Swift Package Manager build"
|
||||
run: |
|
||||
xcrun swift build \
|
||||
@@ -146,35 +142,21 @@ jobs:
|
||||
- target: "arm64-apple-ios16.4-simulator"
|
||||
xcode: "14.3.1"
|
||||
runs-on: macos-13
|
||||
# 15.7
|
||||
- target: "x86_64-apple-ios15.7-simulator"
|
||||
xcode: "14.1"
|
||||
runs-on: macos-12
|
||||
- target: "arm64-apple-ios15.7-simulator"
|
||||
xcode: "14.1"
|
||||
runs-on: macos-12
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- name: "Swift Package Manager build"
|
||||
run: |
|
||||
swift build \
|
||||
-Xswiftc "-sdk" -Xswiftc "`xcrun --sdk iphonesimulator --show-sdk-path`" \
|
||||
-Xswiftc "-target" -Xswiftc "${{ matrix.target }}"
|
||||
|
||||
carthage:
|
||||
runs-on: macos-11
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: "Carthage build"
|
||||
run: carthage build --use-xcframeworks --no-skip-current
|
||||
|
||||
cocoapods:
|
||||
runs-on: macos-14
|
||||
runs-on: macos-15
|
||||
env:
|
||||
DEVELOPER_DIR: /Applications/Xcode_15.4.app/Contents/Developer
|
||||
DEVELOPER_DIR: /Applications/Xcode_16.2.app/Contents/Developer
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- name: "CocoaPods: pod lib lint"
|
||||
run: pod lib lint --allow-warnings
|
||||
run: pod lib lint --allow-warnings --verbose
|
||||
- name: "CocoaPods: pod spec lint"
|
||||
run: pod spec lint --allow-warnings
|
||||
run: pod spec lint --allow-warnings --verbose
|
||||
|
||||
@@ -202,10 +202,10 @@ extension UseCaseController {
|
||||
fpc.set(contentViewController: contentVC)
|
||||
fpc.delegate = self
|
||||
|
||||
let apprearance = SurfaceAppearance()
|
||||
apprearance.cornerRadius = 38.5
|
||||
apprearance.shadows = []
|
||||
fpc.surfaceView.appearance = apprearance
|
||||
let appearance = SurfaceAppearance()
|
||||
appearance.cornerRadius = 38.5
|
||||
appearance.shadows = []
|
||||
fpc.surfaceView.appearance = appearance
|
||||
fpc.isRemovalInteractionEnabled = true
|
||||
|
||||
let mvc = UIViewController()
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
Pod::Spec.new do |s|
|
||||
|
||||
s.name = "FloatingPanel"
|
||||
s.version = "2.8.3"
|
||||
s.version = "2.8.7"
|
||||
s.summary = "FloatingPanel is a clean and easy-to-use UI component of a floating panel interface."
|
||||
s.description = <<-DESC
|
||||
FloatingPanel is a clean and easy-to-use UI component for a new interface introduced in Apple Maps, Shortcuts and Stocks app.
|
||||
@@ -9,7 +9,7 @@ The new interface displays the related contents and utilities in parallel as a u
|
||||
DESC
|
||||
s.homepage = "https://github.com/scenee/FloatingPanel"
|
||||
s.author = "Shin Yamamoto"
|
||||
s.social_media_url = "https://twitter.com/scenee"
|
||||
s.social_media_url = "https://x.com/scenee"
|
||||
|
||||
s.platform = :ios, "11.0"
|
||||
s.source = { :git => "https://github.com/scenee/FloatingPanel.git", :tag => s.version.to_s }
|
||||
|
||||
@@ -2,14 +2,13 @@
|
||||
[](https://cocoapods.org/pods/FloatingPanel)
|
||||
[](https://cocoapods.org/pods/FloatingPanel)
|
||||

|
||||
[](https://github.com/Carthage/Carthage)
|
||||
|
||||
# FloatingPanel
|
||||
|
||||
FloatingPanel is a simple and easy-to-use UI component designed for a user interface featured in Apple Maps, Shortcuts and Stocks app.
|
||||
The user interface displays related content and utilities alongside the main content.
|
||||
|
||||
Please see also [the API reference@SPI](https://swiftpackageindex.com/scenee/FloatingPanel/2.8.3/documentation/floatingpanel) for more details.
|
||||
Please see also [the API reference@SPI](https://swiftpackageindex.com/scenee/FloatingPanel/2.8.7/documentation/floatingpanel) for more details.
|
||||
|
||||

|
||||

|
||||
@@ -22,7 +21,6 @@ Please see also [the API reference@SPI](https://swiftpackageindex.com/scenee/Flo
|
||||
- [Requirements](#requirements)
|
||||
- [Installation](#installation)
|
||||
- [CocoaPods](#cocoapods)
|
||||
- [Carthage](#carthage)
|
||||
- [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)
|
||||
@@ -104,14 +102,6 @@ it, simply add the following line to your Podfile:
|
||||
pod 'FloatingPanel'
|
||||
```
|
||||
|
||||
### Carthage
|
||||
|
||||
For [Carthage](https://github.com/Carthage/Carthage), add the following to your `Cartfile`:
|
||||
|
||||
```ogdl
|
||||
github "scenee/FloatingPanel"
|
||||
```
|
||||
|
||||
### Swift Package Manager
|
||||
|
||||
Follow [this doc](https://developer.apple.com/documentation/swift_packages/adding_package_dependencies_to_your_app).
|
||||
|
||||
+55
-34
@@ -71,7 +71,7 @@ class Core: NSObject, UIGestureRecognizerDelegate {
|
||||
var removalVector: CGVector = .zero
|
||||
|
||||
// Scroll handling
|
||||
private var initialScrollOffset: CGPoint = .zero
|
||||
private var initialScrollOffset: CGPoint?
|
||||
private var scrollBounce = false
|
||||
private var scrollIndictorVisible = false
|
||||
private var scrollBounceThreshold: CGFloat = -30.0
|
||||
@@ -411,7 +411,7 @@ class Core: NSObject, UIGestureRecognizerDelegate {
|
||||
|
||||
if insideMostExpandedAnchor {
|
||||
// Prevent scrolling if needed
|
||||
if isScrollable(state: state) {
|
||||
if isScrollable(state: state), let initialScrollOffset = initialScrollOffset {
|
||||
if interactionInProgress {
|
||||
os_log(msg, log: devLog, type: .debug, "settle offset -- \(value(of: initialScrollOffset))")
|
||||
// Return content offset to initial offset to prevent scrolling
|
||||
@@ -429,7 +429,7 @@ class Core: NSObject, UIGestureRecognizerDelegate {
|
||||
stopScrolling(at: initialScrollOffset)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
} else if let initialScrollOffset = initialScrollOffset {
|
||||
// Return content offset to initial offset to prevent scrolling
|
||||
stopScrolling(at: initialScrollOffset)
|
||||
}
|
||||
@@ -471,7 +471,8 @@ class Core: NSObject, UIGestureRecognizerDelegate {
|
||||
}
|
||||
if isScrollable(state: state) {
|
||||
// Adjust a small gap of the scroll offset just after swiping down starts in the grabber area.
|
||||
if surfaceView.grabberAreaContains(location), surfaceView.grabberAreaContains(initialLocation) {
|
||||
if surfaceView.grabberAreaContains(location), surfaceView.grabberAreaContains(initialLocation),
|
||||
let initialScrollOffset = initialScrollOffset {
|
||||
stopScrolling(at: initialScrollOffset)
|
||||
}
|
||||
}
|
||||
@@ -499,7 +500,8 @@ class Core: NSObject, UIGestureRecognizerDelegate {
|
||||
}
|
||||
}
|
||||
// Adjust a small gap of the scroll offset just before swiping down starts in the grabber area,
|
||||
if surfaceView.grabberAreaContains(location), surfaceView.grabberAreaContains(initialLocation) {
|
||||
if surfaceView.grabberAreaContains(location), surfaceView.grabberAreaContains(initialLocation),
|
||||
let initialScrollOffset = initialScrollOffset {
|
||||
stopScrolling(at: initialScrollOffset)
|
||||
}
|
||||
}
|
||||
@@ -608,7 +610,7 @@ class Core: NSObject, UIGestureRecognizerDelegate {
|
||||
guard
|
||||
isScrollable(state: state), // When not top most(i.e. .full), don't scroll.
|
||||
interactionInProgress == false, // When interaction already in progress, don't scroll.
|
||||
0 == layoutAdapter.offset(from: state),
|
||||
abs(layoutAdapter.offset(from: state)) < 1, // Indistinguishably close to an anchor point.
|
||||
!surfaceView.grabberAreaContains(initialLocation) // When the initial point is within grabber area, don't scroll
|
||||
else {
|
||||
return false
|
||||
@@ -656,14 +658,19 @@ class Core: NSObject, UIGestureRecognizerDelegate {
|
||||
}
|
||||
|
||||
private func panningChange(with translation: CGPoint) {
|
||||
os_log(msg, log: devLog, type: .debug, "panningChange -- translation = \(value(of: translation))")
|
||||
let pre = value(of: layoutAdapter.surfaceLocation)
|
||||
let diff = value(of: translation - initialTranslation)
|
||||
let next = pre + diff
|
||||
|
||||
layoutAdapter.updateInteractiveEdgeConstraint(diff: diff,
|
||||
scrollingContent: shouldScrollingContentInMoving(from: pre, to: next),
|
||||
allowsRubberBanding: behaviorAdapter.allowsRubberBanding(for:))
|
||||
os_log(msg, log: devLog, type: .debug, """
|
||||
panningChange -- translation = \(value(of: translation)), diff = \(diff), pre = \(pre), next = \(next)
|
||||
""")
|
||||
|
||||
layoutAdapter.updateInteractiveEdgeConstraint(
|
||||
diff: diff,
|
||||
scrollingContent: shouldScrollingContentInMoving(from: pre, to: next),
|
||||
allowsRubberBanding: behaviorAdapter.allowsRubberBanding(for:)
|
||||
)
|
||||
|
||||
let cur = value(of: layoutAdapter.surfaceLocation)
|
||||
|
||||
@@ -676,31 +683,36 @@ class Core: NSObject, UIGestureRecognizerDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
private func shouldScrollingContentInMoving(from pre: CGFloat, to next: CGFloat) -> Bool {
|
||||
/// Determines if the content should scroll while the surface is moving from `cur` to `target`.
|
||||
///
|
||||
/// - Note: `cur` argument starts from an anchor location of surface view in a state. For example,
|
||||
/// it starts from zero if the state is full whose FloatingPanelLayoutAnchor.absoluteInset is zero
|
||||
/// and there is no additional safe area insets like a navigation bar. Therefore, `cur` argument
|
||||
/// can be minus if the absoluteInset is minus with such a condition.
|
||||
private func shouldScrollingContentInMoving(from cur: CGFloat, to target: CGFloat) -> Bool {
|
||||
// Don't allow scrolling if the initial panning location is in the grabber area.
|
||||
if surfaceView.grabberAreaContains(initialLocation) {
|
||||
return false
|
||||
}
|
||||
if let scrollView = scrollView, scrollView.panGestureRecognizer.state == .changed {
|
||||
if let sv = scrollView, sv.panGestureRecognizer.state == .changed {
|
||||
let (contentSize, bounds, alwaysBounceHorizontal, alwaysBounceVertical)
|
||||
= (sv.contentSize, sv.bounds, sv.alwaysBounceHorizontal, sv.alwaysBounceVertical)
|
||||
|
||||
switch layoutAdapter.position {
|
||||
case .top:
|
||||
if pre > .zero, pre < next,
|
||||
scrollView.contentSize.height > scrollView.bounds.height || scrollView.alwaysBounceVertical {
|
||||
if cur < target, contentSize.height > bounds.height || alwaysBounceVertical {
|
||||
return true
|
||||
}
|
||||
case .left:
|
||||
if pre > .zero, pre < next,
|
||||
scrollView.contentSize.width > scrollView.bounds.width || scrollView.alwaysBounceHorizontal {
|
||||
if cur < target, contentSize.width > bounds.width || alwaysBounceHorizontal {
|
||||
return true
|
||||
}
|
||||
case .bottom:
|
||||
if pre > .zero, pre > next,
|
||||
scrollView.contentSize.height > scrollView.bounds.height || scrollView.alwaysBounceVertical {
|
||||
if cur > target, contentSize.height > bounds.height || alwaysBounceVertical {
|
||||
return true
|
||||
}
|
||||
case .right:
|
||||
if pre > .zero, pre > next,
|
||||
scrollView.contentSize.width > scrollView.bounds.width || scrollView.alwaysBounceHorizontal {
|
||||
if cur > target, contentSize.width > bounds.width || alwaysBounceHorizontal {
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -837,7 +849,7 @@ class Core: NSObject, UIGestureRecognizerDelegate {
|
||||
} else {
|
||||
initialScrollOffset = scrollView.contentOffset
|
||||
}
|
||||
os_log(msg, log: devLog, type: .debug, "initial scroll offset -- \(initialScrollOffset)")
|
||||
os_log(msg, log: devLog, type: .debug, "initial scroll offset -- \(optional: initialScrollOffset)")
|
||||
}
|
||||
|
||||
initialTranslation = translation
|
||||
@@ -884,17 +896,6 @@ class Core: NSObject, UIGestureRecognizerDelegate {
|
||||
return true
|
||||
}
|
||||
|
||||
func endWithoutAttraction(_ target: FloatingPanelState) {
|
||||
self.state = target
|
||||
self.updateLayout(to: target)
|
||||
self.unlockScrollView()
|
||||
// The `floatingPanelDidEndDragging(_:willAttract:)` must be called after the state property changes.
|
||||
// This allows library users to get the correct state in the delegate method.
|
||||
if let vc = ownerVC {
|
||||
vc.delegate?.floatingPanelDidEndDragging?(vc, willAttract: false)
|
||||
}
|
||||
}
|
||||
|
||||
private func startAttraction(to state: FloatingPanelState, with velocity: CGPoint, completion: @escaping (() -> Void)) {
|
||||
os_log(msg, log: devLog, type: .debug, "startAnimation to \(state) -- velocity = \(value(of: velocity))")
|
||||
guard let vc = ownerVC else { return }
|
||||
@@ -924,8 +925,8 @@ class Core: NSObject, UIGestureRecognizerDelegate {
|
||||
self.backdropView.alpha = self.getBackdropAlpha(at: current, with: translation)
|
||||
|
||||
// Pin the offset of the tracking scroll view while moving by this animator
|
||||
if let scrollView = self.scrollView {
|
||||
self.stopScrolling(at: self.initialScrollOffset)
|
||||
if let scrollView = self.scrollView, let initialScrollOffset = self.initialScrollOffset {
|
||||
self.stopScrolling(at: initialScrollOffset)
|
||||
os_log(msg, log: devLog, type: .debug, "move -- pinning scroll offset = \(scrollView.contentOffset)")
|
||||
}
|
||||
|
||||
@@ -949,6 +950,12 @@ class Core: NSObject, UIGestureRecognizerDelegate {
|
||||
self.isAttracting = false
|
||||
self.moveAnimator = nil
|
||||
|
||||
// We need to reset `initialScrollOffset` because the scroll offset can become unexpected
|
||||
// under the following circumstances:
|
||||
// 1. The scroll offset changes while the panel does not move.
|
||||
// 2. The panel is then moved using `move(to:animate:completion:)`.
|
||||
self.initialScrollOffset = nil
|
||||
|
||||
if let vc = ownerVC {
|
||||
vc.delegate?.floatingPanelDidEndAttracting?(vc)
|
||||
}
|
||||
@@ -971,6 +978,20 @@ class Core: NSObject, UIGestureRecognizerDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
func endWithoutAttraction(_ target: FloatingPanelState) {
|
||||
// See comments in `endAttraction`
|
||||
self.initialScrollOffset = nil
|
||||
|
||||
self.state = target
|
||||
self.updateLayout(to: target)
|
||||
self.unlockScrollView()
|
||||
// The `floatingPanelDidEndDragging(_:willAttract:)` must be called after the state property changes.
|
||||
// This allows library users to get the correct state in the delegate method.
|
||||
if let vc = ownerVC {
|
||||
vc.delegate?.floatingPanelDidEndDragging?(vc, willAttract: false)
|
||||
}
|
||||
}
|
||||
|
||||
func value(of point: CGPoint) -> CGFloat {
|
||||
return layoutAdapter.position.mainLocation(point)
|
||||
}
|
||||
|
||||
+1
-1
@@ -15,7 +15,7 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>FMWK</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>2.8.3</string>
|
||||
<string>2.8.7</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||
</dict>
|
||||
|
||||
@@ -17,7 +17,7 @@ import UIKit
|
||||
/// positioning.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - absoluteOffset: An absolute offset to attach the panel from the edge.
|
||||
/// - absoluteInset: An absolute distance to attach the panel from the specified edge.
|
||||
/// - edge: Specify the edge of ``FloatingPanelController``'s view. This is the staring point of the offset.
|
||||
/// - referenceGuide: The rectangular area to lay out the content. If it's set to `.safeArea`, the panel content lays out inside the safe area of its ``FloatingPanelController``'s view.
|
||||
@objc public init(absoluteInset: CGFloat, edge: FloatingPanelReferenceEdge, referenceGuide: FloatingPanelLayoutReferenceGuide) {
|
||||
@@ -34,7 +34,7 @@ import UIKit
|
||||
/// 1.0 represents a distance to the opposite edge.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - fractionalOffset: A fractional value of the size of ``FloatingPanelController``'s view to attach the panel from the edge.
|
||||
/// - fractionalInset: A fractional value of the size of ``FloatingPanelController``'s view to attach the panel from the specified edge.
|
||||
/// - edge: Specify the edge of ``FloatingPanelController``'s view. This is the staring point of the offset.
|
||||
/// - referenceGuide: The rectangular area to lay out the content. If it's set to `.safeArea`, the panel content lays out inside the safe area of its ``FloatingPanelController``'s view.
|
||||
@objc public init(fractionalInset: CGFloat, edge: FloatingPanelReferenceEdge, referenceGuide: FloatingPanelLayoutReferenceGuide) {
|
||||
@@ -115,8 +115,8 @@ public extension FloatingPanelLayoutAnchor {
|
||||
/// - Parameters:
|
||||
/// - absoluteOffset: An absolute offset from the content size in the main dimension(i.e. y axis for a bottom panel) to attach the panel.
|
||||
/// - referenceGuide: The rectangular area to lay out the content. If it's set to `.safeArea`, the panel content lays out inside the safe area of its ``FloatingPanelController``'s view.
|
||||
@objc public init(absoluteOffset offset: CGFloat, referenceGuide: FloatingPanelLayoutReferenceGuide = .safeArea) {
|
||||
self.offset = offset
|
||||
@objc public init(absoluteOffset: CGFloat, referenceGuide: FloatingPanelLayoutReferenceGuide = .safeArea) {
|
||||
self.offset = absoluteOffset
|
||||
self.referenceGuide = referenceGuide
|
||||
self.isAbsolute = true
|
||||
}
|
||||
@@ -129,8 +129,8 @@ public extension FloatingPanelLayoutAnchor {
|
||||
/// - Parameters:
|
||||
/// - fractionalOffset: A fractional offset of the content size in the main dimension(i.e. y axis for a bottom panel) to attach the panel.
|
||||
/// - referenceGuide: The rectangular area to lay out the content. If it's set to `.safeArea`, the panel content lays out inside the safe area of its ``FloatingPanelController``'s view.
|
||||
@objc public init(fractionalOffset offset: CGFloat, referenceGuide: FloatingPanelLayoutReferenceGuide = .safeArea) {
|
||||
self.offset = offset
|
||||
@objc public init(fractionalOffset: CGFloat, referenceGuide: FloatingPanelLayoutReferenceGuide = .safeArea) {
|
||||
self.offset = fractionalOffset
|
||||
self.referenceGuide = referenceGuide
|
||||
self.isAbsolute = false
|
||||
}
|
||||
@@ -177,12 +177,12 @@ public extension FloatingPanelIntrinsicLayoutAnchor {
|
||||
///
|
||||
/// - Warning: If ``contentBoundingGuide`` is set to none, the panel may expand out of the screen size, depending on the intrinsic size of its content.
|
||||
@objc public init(
|
||||
absoluteOffset offset: CGFloat,
|
||||
absoluteOffset: CGFloat,
|
||||
contentLayout: UILayoutGuide,
|
||||
referenceGuide: FloatingPanelLayoutReferenceGuide,
|
||||
contentBoundingGuide: FloatingPanelLayoutContentBoundingGuide = .none
|
||||
) {
|
||||
self.offset = offset
|
||||
self.offset = absoluteOffset
|
||||
self.contentLayoutGuide = contentLayout
|
||||
self.referenceGuide = referenceGuide
|
||||
self.contentBoundingGuide = contentBoundingGuide
|
||||
@@ -204,12 +204,12 @@ public extension FloatingPanelIntrinsicLayoutAnchor {
|
||||
///
|
||||
/// - Warning: If ``contentBoundingGuide`` is set to none, the panel may expand out of the screen size, depending on the intrinsic size of its content.
|
||||
@objc public init(
|
||||
fractionalOffset offset: CGFloat,
|
||||
fractionalOffset: CGFloat,
|
||||
contentLayout: UILayoutGuide,
|
||||
referenceGuide: FloatingPanelLayoutReferenceGuide,
|
||||
contentBoundingGuide: FloatingPanelLayoutContentBoundingGuide = .none
|
||||
) {
|
||||
self.offset = offset
|
||||
self.offset = fractionalOffset
|
||||
self.contentLayoutGuide = contentLayout
|
||||
self.referenceGuide = referenceGuide
|
||||
self.contentBoundingGuide = contentBoundingGuide
|
||||
|
||||
@@ -15,3 +15,14 @@ struct Logging {
|
||||
static let category = "FloatingPanel"
|
||||
private init() {}
|
||||
}
|
||||
|
||||
extension String.StringInterpolation {
|
||||
mutating func appendInterpolation<T>(optional: T?, defaultValue: String = "nil") {
|
||||
switch optional {
|
||||
case let value?:
|
||||
appendLiteral(String(describing: value))
|
||||
case nil:
|
||||
appendLiteral(defaultValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,7 +84,7 @@ class ModalPresentTransition: NSObject, UIViewControllerAnimatedTransitioning {
|
||||
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
|
||||
guard
|
||||
let fpc = transitionContext?.viewController(forKey: .to) as? FloatingPanelController
|
||||
else { fatalError()}
|
||||
else { return 0.0 }
|
||||
|
||||
let animator = fpc.animatorForPresenting(to: fpc.layout.initialState)
|
||||
return TimeInterval(animator.duration)
|
||||
@@ -119,7 +119,7 @@ class ModalDismissTransition: NSObject, UIViewControllerAnimatedTransitioning {
|
||||
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
|
||||
guard
|
||||
let fpc = transitionContext?.viewController(forKey: .from) as? FloatingPanelController
|
||||
else { fatalError()}
|
||||
else { return 0.0 }
|
||||
|
||||
let animator = fpc.animatorForDismissing(with: .zero)
|
||||
return TimeInterval(animator.duration)
|
||||
|
||||
+13
-10
@@ -64,6 +64,7 @@ class ControllerTests: XCTestCase {
|
||||
}
|
||||
|
||||
func test_moveTo() {
|
||||
let timeout = 3.0
|
||||
let delegate = FloatingPanelTestDelegate()
|
||||
let fpc = FloatingPanelController(delegate: delegate)
|
||||
XCTAssertEqual(delegate.position, .hidden)
|
||||
@@ -102,7 +103,7 @@ class ControllerTests: XCTestCase {
|
||||
}
|
||||
XCTAssertEqual(fpc.state, .full)
|
||||
XCTAssertEqual(delegate.position, .full)
|
||||
wait(for: [exp], timeout: 1.0)
|
||||
wait(for: [exp], timeout: timeout)
|
||||
}
|
||||
|
||||
XCTContext.runActivity(named: "move to half(animated)") { act in
|
||||
@@ -113,7 +114,7 @@ class ControllerTests: XCTestCase {
|
||||
}
|
||||
XCTAssertEqual(fpc.state, .half)
|
||||
XCTAssertEqual(delegate.position, .half)
|
||||
wait(for: [exp], timeout: 1.0)
|
||||
wait(for: [exp], timeout: timeout)
|
||||
}
|
||||
|
||||
XCTContext.runActivity(named: "move to tip(animated)") { act in
|
||||
@@ -124,7 +125,7 @@ class ControllerTests: XCTestCase {
|
||||
}
|
||||
XCTAssertEqual(fpc.state, .tip)
|
||||
XCTAssertEqual(delegate.position, .tip)
|
||||
wait(for: [exp], timeout: 1.0)
|
||||
wait(for: [exp], timeout: timeout)
|
||||
}
|
||||
|
||||
fpc.move(to: .hidden, animated: true)
|
||||
@@ -137,6 +138,7 @@ class ControllerTests: XCTestCase {
|
||||
class MyFloatingPanelTop2BottomLayout: FloatingPanelTop2BottomTestLayout {
|
||||
override var initialState: FloatingPanelState { return .half }
|
||||
}
|
||||
let timeout = 3.0
|
||||
let delegate = FloatingPanelTestDelegate()
|
||||
let fpc = FloatingPanelController(delegate: delegate)
|
||||
fpc.layout = MyFloatingPanelTop2BottomLayout()
|
||||
@@ -175,7 +177,7 @@ class ControllerTests: XCTestCase {
|
||||
}
|
||||
XCTAssertEqual(fpc.state, .full)
|
||||
XCTAssertEqual(delegate.position, .full)
|
||||
wait(for: [exp], timeout: 1.0)
|
||||
wait(for: [exp], timeout: timeout)
|
||||
}
|
||||
|
||||
XCTContext.runActivity(named: "move to half(animated)") { act in
|
||||
@@ -186,7 +188,7 @@ class ControllerTests: XCTestCase {
|
||||
}
|
||||
XCTAssertEqual(fpc.state, .half)
|
||||
XCTAssertEqual(delegate.position, .half)
|
||||
wait(for: [exp], timeout: 1.0)
|
||||
wait(for: [exp], timeout: timeout)
|
||||
}
|
||||
|
||||
XCTContext.runActivity(named: "move to tip(animated)") { act in
|
||||
@@ -197,7 +199,7 @@ class ControllerTests: XCTestCase {
|
||||
}
|
||||
XCTAssertEqual(fpc.state, .tip)
|
||||
XCTAssertEqual(delegate.position, .tip)
|
||||
wait(for: [exp], timeout: 1.0)
|
||||
wait(for: [exp], timeout: timeout)
|
||||
}
|
||||
|
||||
fpc.move(to: .hidden, animated: true)
|
||||
@@ -223,6 +225,7 @@ class ControllerTests: XCTestCase {
|
||||
}
|
||||
|
||||
func test_moveTo_didMoveDelegate() {
|
||||
let timeout = 3.0
|
||||
let delegate = FloatingPanelTestDelegate()
|
||||
let fpc = FloatingPanelController(delegate: delegate)
|
||||
XCTAssertEqual(delegate.position, .hidden)
|
||||
@@ -237,7 +240,7 @@ class ControllerTests: XCTestCase {
|
||||
exp.fulfill()
|
||||
}
|
||||
fpc.move(to: .full, animated: false)
|
||||
wait(for: [exp], timeout: 1.0)
|
||||
wait(for: [exp], timeout: timeout)
|
||||
|
||||
XCTAssertEqual(count, 1)
|
||||
}
|
||||
@@ -253,7 +256,7 @@ class ControllerTests: XCTestCase {
|
||||
fpc.move(to: .half, animated: true) {
|
||||
exp.fulfill()
|
||||
}
|
||||
wait(for: [exp], timeout: 1.0)
|
||||
wait(for: [exp], timeout: timeout)
|
||||
|
||||
XCTAssertGreaterThan(count, 1)
|
||||
}
|
||||
@@ -270,7 +273,7 @@ class ControllerTests: XCTestCase {
|
||||
exp.fulfill()
|
||||
}
|
||||
}
|
||||
wait(for: [exp], timeout: 1.0)
|
||||
wait(for: [exp], timeout: timeout)
|
||||
|
||||
XCTAssertEqual(count, 1)
|
||||
}
|
||||
@@ -288,7 +291,7 @@ class ControllerTests: XCTestCase {
|
||||
exp.fulfill()
|
||||
}
|
||||
}
|
||||
wait(for: [exp], timeout: 1.0)
|
||||
wait(for: [exp], timeout: timeout)
|
||||
|
||||
XCTAssertGreaterThan(count, 1)
|
||||
}
|
||||
|
||||
+33
-4
@@ -209,6 +209,8 @@ class CoreTests: XCTestCase {
|
||||
return floor(fpc.backdropView.alpha * 1e+06) / 1e+06
|
||||
}
|
||||
|
||||
let timeout = 3.0
|
||||
|
||||
let delegate = TestDelegate()
|
||||
let fpc = FloatingPanelController(delegate: delegate)
|
||||
fpc.layout = BackdropTestLayout()
|
||||
@@ -228,14 +230,14 @@ class CoreTests: XCTestCase {
|
||||
fpc.move(to: .full, animated: true) {
|
||||
exp1.fulfill()
|
||||
}
|
||||
wait(for: [exp1], timeout: 1.0)
|
||||
wait(for: [exp1], timeout: timeout)
|
||||
XCTAssertEqual(_floor(fpc.backdropView.alpha), 0.3)
|
||||
|
||||
let exp2 = expectation(description: "move to half with animation")
|
||||
fpc.move(to: .half, animated: true) {
|
||||
exp2.fulfill()
|
||||
}
|
||||
wait(for: [exp2], timeout: 1.0)
|
||||
wait(for: [exp2], timeout: timeout)
|
||||
XCTAssertEqual(fpc.backdropView.alpha, 0.0)
|
||||
|
||||
// Test a content mode change of FloatingPanelController
|
||||
@@ -246,7 +248,7 @@ class CoreTests: XCTestCase {
|
||||
}
|
||||
fpc.contentMode = .fitToBounds
|
||||
XCTAssertEqual(fpc.backdropView.alpha, 0.0) // Must not affect the backdrop alpha by changing the content mode
|
||||
wait(for: [exp3], timeout: 1.0)
|
||||
wait(for: [exp3], timeout: timeout)
|
||||
XCTAssertEqual(_floor(fpc.backdropView.alpha), 0.3)
|
||||
|
||||
// Test a size class change of FloatingPanelController.view
|
||||
@@ -914,6 +916,34 @@ class CoreTests: XCTestCase {
|
||||
}
|
||||
}
|
||||
|
||||
func test_initial_scroll_offset_reset() {
|
||||
let fpc = FloatingPanelController()
|
||||
let scrollView = UIScrollView()
|
||||
fpc.layout = FloatingPanelBottomLayout()
|
||||
fpc.track(scrollView: scrollView)
|
||||
fpc.showForTest()
|
||||
|
||||
fpc.move(to: .full, animated: false)
|
||||
|
||||
fpc.panGestureRecognizer.state = .began
|
||||
fpc.floatingPanel.handle(panGesture: fpc.panGestureRecognizer)
|
||||
|
||||
fpc.panGestureRecognizer.state = .cancelled
|
||||
fpc.floatingPanel.handle(panGesture: fpc.panGestureRecognizer)
|
||||
|
||||
waitRunLoop(secs: 1.0)
|
||||
|
||||
let expect = CGPoint(x: 0, y: 100)
|
||||
|
||||
scrollView.setContentOffset(expect, animated: false)
|
||||
|
||||
fpc.move(to: .half, animated: true)
|
||||
|
||||
waitRunLoop(secs: 1.0)
|
||||
|
||||
XCTAssertEqual(expect, scrollView.contentOffset)
|
||||
}
|
||||
|
||||
func test_handleGesture_endWithoutAttraction() throws {
|
||||
class Delegate: FloatingPanelControllerDelegate {
|
||||
var willAttract: Bool?
|
||||
@@ -922,7 +952,6 @@ class CoreTests: XCTestCase {
|
||||
}
|
||||
}
|
||||
let fpc = FloatingPanelController()
|
||||
let scrollView = UIScrollView()
|
||||
let delegate = Delegate()
|
||||
fpc.showForTest()
|
||||
fpc.delegate = delegate
|
||||
|
||||
Reference in New Issue
Block a user