Compare commits

..

4 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
5 changed files with 88 additions and 13 deletions
@@ -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()
+13 -8
View File
@@ -563,7 +563,7 @@ class Core: NSObject, UIGestureRecognizerDelegate {
}
private func interruptAnimationIfNeeded() {
if let animator = self.moveAnimator, animator.isRunning, 0 <= layoutAdapter.offsetFromMostExpandedAnchor {
if let animator = self.moveAnimator, animator.isRunning {
os_log(msg, log: devLog, type: .debug, "the attraction animator interrupted!!!")
animator.stopAnimation(true)
endAttraction(false)
@@ -610,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
@@ -1197,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 {
+42
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,40 @@ 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()