Compare commits

..

9 Commits

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

This change had a problem normal cases. For example, in Maps example a
panel interaction jumps occurs because of this.
2025-04-21 18:09:24 +09:00
Shin Yamamoto a1f20cedb1 Version 2.8.7 2025-04-05 14:11:55 +09:00
Shin Yamamoto 9592baa16c ci: use macos-15 for all testing to use Xcode 16.2 2025-03-13 09:33:17 +09:00
Shin Yamamoto 370e306904 ci: use Xcode 16.2 (#653)
* Added '--verbose' in cocoapods job
* Fixed 'error: No simulator runtime version'. Some of the example apps cannot be built on github actions.
> /Users/runner/work/FloatingPanel/FloatingPanel/Examples/Samples/Sources/Assets.xcassets: error: No simulator runtime version from [<DVTBuildVersion 21A342>, <DVTBuildVersion 21C62>, <DVTBuildVersion 21E213>, <DVTBuildVersion 21F79>, <DVTBuildVersion 22B81>] available to use with iphonesimulator SDK version <DVTBuildVersion 22C146>
* Used macos-15 to fix random build fails
2025-03-07 20:42:16 +09:00
Shin Yamamoto 479cce4546 Reset initialScrollOffset after the attracting animation ends (#659)
* Stop pinning the scroll offset in moving programmatically
* Add 'optional' string interpolation
* Add comments
* Add CoreTests.test_initial_scroll_offset_reset()
* ci: remove macos-12 jobs for the deprecation
* ci: name circleci jobs
2025-03-07 18:57:21 +09:00
Shin Yamamoto b0fd0d4427 Disallow interrupting the panel interaction while bouncing over the most expanded state (#652)
I decided to disallow interrupting panel interactions while bouncing over the most expanded state in order to fix the 2nd issue in #633, https://github.com/scenee/FloatingPanel/issues/633#issuecomment-2324666767.
2024-11-09 13:16:47 +09:00
11 changed files with 217 additions and 65 deletions
+30 -1
View File
@@ -1,6 +1,27 @@
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
@@ -8,7 +29,15 @@ jobs:
- checkout
- run: xcodebuild clean test -scheme FloatingPanel -workspace FloatingPanel.xcworkspace -destination 'platform=iOS Simulator,OS=14.5,name=iPhone 12 Pro'
workflows:
test:
jobs:
- test-ios14_5-iPhone_12_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)
+27 -29
View File
@@ -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,15 +30,6 @@ 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
steps:
- uses: actions/checkout@v4
- name: Building in Swift ${{ matrix.swift }}
@@ -49,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"
@@ -59,11 +58,6 @@ 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@v4
- name: Testing in iOS ${{ matrix.os }}
@@ -76,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:
@@ -90,6 +84,7 @@ jobs:
steps:
- 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 \
@@ -97,22 +92,32 @@ 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@v4
@@ -137,13 +142,6 @@ 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@v4
- name: "Swift Package Manager build"
@@ -153,12 +151,12 @@ jobs:
-Xswiftc "-target" -Xswiftc "${{ matrix.target }}"
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@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
@@ -76,3 +76,18 @@ class ModalPanelLayout: FloatingPanelLayout {
return 0.3
}
}
class ModalPanelLayout2: FloatingPanelLayout {
let position: FloatingPanelPosition = .bottom
let initialState: FloatingPanelState = .half
var anchors: [FloatingPanelState: FloatingPanelLayoutAnchoring] {
[
.full: FloatingPanelLayoutAnchor(fractionalInset: 0.0, edge: .top, referenceGuide: .superview),
.half: FloatingPanelLayoutAnchor(fractionalInset: 0.5, edge: .bottom, referenceGuide: .superview)
]
}
func backdropAlpha(for _: FloatingPanelState) -> CGFloat {
0.6
}
}
@@ -9,6 +9,7 @@ enum UseCase: Int, CaseIterable {
case showDetail
case showModal
case showPanelModal
case showPanelModal2
case showMultiPanelModal
case showPanelInSheetModal
case showOnWindow
@@ -39,6 +40,7 @@ extension UseCase {
case .showDetail: return "Show Detail Panel"
case .showModal: return "Show Modal"
case .showPanelModal: return "Show Panel Modal"
case .showPanelModal2: return "Show Panel Modal 2"
case .showMultiPanelModal: return "Show Multi Panel Modal"
case .showOnWindow: return "Show Panel over Window"
case .showPanelInSheetModal: return "Show Panel in Sheet Modal"
@@ -81,10 +83,11 @@ extension UseCase {
case .trackingTextView: return .storyboard("ConsoleViewController") // Storyboard only
case .showDetail: return .storyboard(String(describing: DetailViewController.self))
case .showModal: return .storyboard(String(describing: ModalViewController.self))
case .showPanelModal: return .viewController(DebugTableViewController())
case .showPanelModal2: return .storyboard("ConsoleViewController")
case .showMultiPanelModal: return .viewController(DebugTableViewController())
case .showOnWindow: return .viewController(DebugTableViewController())
case .showPanelInSheetModal: return .viewController(DebugTableViewController())
case .showPanelModal: return .viewController(DebugTableViewController())
case .showTabBar: return .storyboard(String(describing: TabBarViewController.self))
case .showPageView: return .viewController(DebugTableViewController())
case .showPageContentView: return .viewController(DebugTableViewController())
@@ -178,6 +178,14 @@ extension UseCaseController {
mainVC.present(fpc, animated: true, completion: nil)
case .showPanelModal2:
let fpc = FloatingPanelController()
fpc.set(contentViewController: contentVC)
fpc.delegate = self
fpc.track(scrollView: (contentVC as? DebugTextViewController)!.textView)
mainVC.present(fpc, animated: true, completion: nil)
case .showMultiPanelModal:
let fpc = MultiPanelController()
mainVC.present(fpc, animated: true, completion: nil)
@@ -202,10 +210,10 @@ extension UseCaseController {
fpc.set(contentViewController: contentVC)
fpc.delegate = self
let apprearance = SurfaceAppearance()
apprearance.cornerRadius = 38.5
apprearance.shadows = []
fpc.surfaceView.appearance = apprearance
let appearance = SurfaceAppearance()
appearance.cornerRadius = 38.5
appearance.shadows = []
fpc.surfaceView.appearance = appearance
fpc.isRemovalInteractionEnabled = true
let mvc = UIViewController()
@@ -435,6 +443,8 @@ extension UseCaseController: FloatingPanelControllerDelegate {
return newCollection.verticalSizeClass == .compact ? RemovablePanelLandscapeLayout() : RemovablePanelLayout()
case .showIntrinsicView:
return IntrinsicPanelLayout()
case .showPanelModal2:
return ModalPanelLayout2()
case .showPanelModal:
if vc != mainPanelVC && vc != detailPanelVC {
return ModalPanelLayout()
+2 -2
View File
@@ -1,7 +1,7 @@
Pod::Spec.new do |s|
s.name = "FloatingPanel"
s.version = "2.8.6"
s.version = "2.8.7"
s.summary = "FloatingPanel is a clean and easy-to-use UI component of a floating panel interface."
s.description = <<-DESC
FloatingPanel is a clean and easy-to-use UI component for a new interface introduced in Apple Maps, Shortcuts and Stocks app.
@@ -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 }
+1 -1
View File
@@ -8,7 +8,7 @@
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.6/documentation/floatingpanel) for more details.
Please see also [the API reference@SPI](https://swiftpackageindex.com/scenee/FloatingPanel/2.8.7/documentation/floatingpanel) for more details.
![Maps](https://github.com/SCENEE/FloatingPanel/blob/master/assets/maps.gif)
![Stocks](https://github.com/SCENEE/FloatingPanel/blob/master/assets/stocks.gif)
+42 -26
View File
@@ -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
@@ -847,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
@@ -894,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 }
@@ -934,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)")
}
@@ -959,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)
}
@@ -981,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)
}
@@ -1186,16 +1197,21 @@ class Core: NSObject, UIGestureRecognizerDelegate {
return state == layoutAdapter.mostExpandedState
}
/// Adjust content inset of the tracking scroll view if the controller's
/// `contentInsetAdjustmentBehavior` is `.always` and its `contentMode` is `.static`.
/// if its content is scrollable, the content might not be fully visible on `.half`
/// state, for example. Therefore the content inset needs to adjust to display the
/// full content.
// Adjusts content inset of the tracking scroll view when the following conditions are met:
// - The controller's `contentInsetAdjustmentBehavior` is `.always`
// - Its `contentMode` is `.static`
// - Its content is scrollable
// This ensures that the content remains fully visible in intermediate states like `.half`,
// by using `UIScrollView.safeAreaInsets` and the panel's current position.
// This method must not be invoked in the fully expanded state, as it may lead to unexpected
// behavior under the top safe area (i.e., the status bar).
func adjustScrollContentInsetIfNeeded() {
guard
let fpc = ownerVC,
let scrollView = scrollView,
fpc.contentInsetAdjustmentBehavior == .always
fpc.contentInsetAdjustmentBehavior == .always,
fpc.state != layoutAdapter.mostExpandedState,
isScrollable(state: fpc.state)
else { return }
switch fpc.contentMode {
+1 -1
View File
@@ -15,7 +15,7 @@
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>2.8.6</string>
<string>2.8.7</string>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
</dict>
+11
View File
@@ -15,3 +15,14 @@ struct Logging {
static let category = "FloatingPanel"
private init() {}
}
extension String.StringInterpolation {
mutating func appendInterpolation<T>(optional: T?, defaultValue: String = "nil") {
switch optional {
case let value?:
appendLiteral(String(describing: value))
case nil:
appendLiteral(defaultValue)
}
}
}
+70
View File
@@ -863,11 +863,18 @@ class CoreTests: XCTestCase {
customSafeAreaInsets
}
}
class PanelDelegate: FloatingPanelControllerDelegate {
func floatingPanel(_ fpc: FloatingPanelController, shouldAllowToScroll scrollView: UIScrollView, in state: FloatingPanelState) -> Bool {
return state == .full || state == .half
}
}
let delegate = PanelDelegate()
do {
let scrollView = CustomScrollView()
scrollView.customSafeAreaInsets = UIEdgeInsets(top: 0, left: 0, bottom: 34, right: 0)
let fpc = FloatingPanelController()
fpc.delegate = delegate
fpc.track(scrollView: scrollView)
fpc.layout = FloatingPanelBottomLayout()
fpc.contentInsetAdjustmentBehavior = .always
@@ -894,6 +901,7 @@ class CoreTests: XCTestCase {
let scrollView = CustomScrollView()
scrollView.customSafeAreaInsets = UIEdgeInsets(top: 91, left: 0, bottom: 0, right: 0)
let fpc = FloatingPanelController()
fpc.delegate = delegate
fpc.track(scrollView: scrollView)
fpc.layout = FloatingPanelTopPositionedLayout()
fpc.contentInsetAdjustmentBehavior = .always
@@ -916,6 +924,68 @@ class CoreTests: XCTestCase {
}
}
func test_adjustScrollContentInsetIfNeeded_normal() {
class CustomScrollView: UIScrollView {
var customSafeAreaInsets: UIEdgeInsets = .zero
override var safeAreaInsets: UIEdgeInsets {
customSafeAreaInsets
}
}
do {
let scrollView = CustomScrollView()
scrollView.customSafeAreaInsets = UIEdgeInsets(top: 42, left: 0, bottom: 34, right: 0)
let fpc = FloatingPanelController()
fpc.track(scrollView: scrollView)
fpc.layout = FloatingPanelBottomLayout()
fpc.contentInsetAdjustmentBehavior = .always
fpc.contentMode = .static
fpc.showForTest()
fpc.move(to: .half, animated: false)
fpc.floatingPanel.adjustScrollContentInsetIfNeeded()
XCTAssertEqual(
scrollView.contentInset,
UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
)
fpc.move(to: .full, animated: false)
fpc.floatingPanel.adjustScrollContentInsetIfNeeded()
XCTAssertEqual(
scrollView.contentInset,
UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
)
}
}
func test_initial_scroll_offset_reset() {
let fpc = FloatingPanelController()
let scrollView = UIScrollView()
fpc.layout = FloatingPanelBottomLayout()
fpc.track(scrollView: scrollView)
fpc.showForTest()
fpc.move(to: .full, animated: false)
fpc.panGestureRecognizer.state = .began
fpc.floatingPanel.handle(panGesture: fpc.panGestureRecognizer)
fpc.panGestureRecognizer.state = .cancelled
fpc.floatingPanel.handle(panGesture: fpc.panGestureRecognizer)
waitRunLoop(secs: 1.0)
let expect = CGPoint(x: 0, y: 100)
scrollView.setContentOffset(expect, animated: false)
fpc.move(to: .half, animated: true)
waitRunLoop(secs: 1.0)
XCTAssertEqual(expect, scrollView.contentOffset)
}
func test_handleGesture_endWithoutAttraction() throws {
class Delegate: FloatingPanelControllerDelegate {
var willAttract: Bool?