From 0e27410460690ef11fa349fd1fcc722c14b44997 Mon Sep 17 00:00:00 2001 From: Ivan Levin <100189059+LevinIvan@users.noreply.github.com> Date: Mon, 5 Aug 2024 16:06:43 +0300 Subject: [PATCH 01/23] Replace fatal errors in transitionDuration delegate methods (#642) This PR modifies the transitionDuration(using:) method in FloatingPanelController to return 0.0 instead of calling fatalError when the FloatingPanelController instance is not found. This change is crucial for several reasons: 1. Avoiding Crashes in Production: Using fatalError in production can lead to unexpected crashes, which are detrimental to user experience. It's safer to return a default value like 0.0 and handle the scenario gracefully. 2. Improved Stability: By returning 0.0, the application can continue running, allowing for better stability and user satisfaction. This approach also aligns with the principle of fail-safety, where the system continues operating under error conditions. 3. Real-World Case: In our large-scale project, we encountered a crash at this specific point due to the fatalError. Given the size and complexity of our application, it has been challenging to pinpoint the exact cause. Switching to a return value of 0.0 would significantly help us mitigate the issue and maintain app stability while we investigate further. 4. Maintainability: Returning a default value makes the codebase more maintainable and easier to debug, as it avoids abrupt termination and allows for logging or other error handling mechanisms. --- Sources/Transitioning.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/Transitioning.swift b/Sources/Transitioning.swift index 88ab23482..ff771f8b5 100644 --- a/Sources/Transitioning.swift +++ b/Sources/Transitioning.swift @@ -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) From 71f419a3cd212afc7615e2179c2fec1df1aa74da Mon Sep 17 00:00:00 2001 From: Shin Yamamoto Date: Mon, 5 Aug 2024 22:07:48 +0900 Subject: [PATCH 02/23] Version 2.8.5 --- FloatingPanel.podspec | 2 +- README.md | 2 +- Sources/Info.plist | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/FloatingPanel.podspec b/FloatingPanel.podspec index 85742e92b..ebba6818a 100644 --- a/FloatingPanel.podspec +++ b/FloatingPanel.podspec @@ -1,7 +1,7 @@ Pod::Spec.new do |s| s.name = "FloatingPanel" - s.version = "2.8.4" + s.version = "2.8.5" 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. diff --git a/README.md b/README.md index f86f655d7..a004be48c 100644 --- a/README.md +++ b/README.md @@ -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.4/documentation/floatingpanel) for more details. +Please see also [the API reference@SPI](https://swiftpackageindex.com/scenee/FloatingPanel/2.8.5/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) diff --git a/Sources/Info.plist b/Sources/Info.plist index 32767e9e3..4d226d9f3 100644 --- a/Sources/Info.plist +++ b/Sources/Info.plist @@ -15,7 +15,7 @@ CFBundlePackageType FMWK CFBundleShortVersionString - 2.8.4 + 2.8.5 CFBundleVersion $(CURRENT_PROJECT_VERSION) From 3a3d53424c0728706ac8e984d21f1daddbbf98ec Mon Sep 17 00:00:00 2001 From: Shin Yamamoto Date: Fri, 6 Sep 2024 13:49:56 +0900 Subject: [PATCH 03/23] Fix doc comments' errors (#644) * Fix doc comment errors in ObjC APIs * Improve doc comments in LayoutAnchoring.swift --- Sources/LayoutAnchoring.swift | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/Sources/LayoutAnchoring.swift b/Sources/LayoutAnchoring.swift index 17e1aa5dc..83388738e 100644 --- a/Sources/LayoutAnchoring.swift +++ b/Sources/LayoutAnchoring.swift @@ -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 From b6e8928b1a3ad909e6db6a0278d286c33cfd0dc3 Mon Sep 17 00:00:00 2001 From: Shin Yamamoto Date: Sat, 7 Sep 2024 10:12:44 +0900 Subject: [PATCH 04/23] Version 2.8.6 --- FloatingPanel.podspec | 2 +- README.md | 2 +- Sources/Info.plist | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/FloatingPanel.podspec b/FloatingPanel.podspec index ebba6818a..2766c6e9a 100644 --- a/FloatingPanel.podspec +++ b/FloatingPanel.podspec @@ -1,7 +1,7 @@ Pod::Spec.new do |s| s.name = "FloatingPanel" - s.version = "2.8.5" + s.version = "2.8.6" 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. diff --git a/README.md b/README.md index a004be48c..30f7cb7be 100644 --- a/README.md +++ b/README.md @@ -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.5/documentation/floatingpanel) for more details. +Please see also [the API reference@SPI](https://swiftpackageindex.com/scenee/FloatingPanel/2.8.6/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) diff --git a/Sources/Info.plist b/Sources/Info.plist index 4d226d9f3..73958c7c8 100644 --- a/Sources/Info.plist +++ b/Sources/Info.plist @@ -15,7 +15,7 @@ CFBundlePackageType FMWK CFBundleShortVersionString - 2.8.5 + 2.8.6 CFBundleVersion $(CURRENT_PROJECT_VERSION) From b0fd0d4427cc82afa5f912da28083ed2586902d7 Mon Sep 17 00:00:00 2001 From: Shin Yamamoto Date: Sat, 9 Nov 2024 13:16:47 +0900 Subject: [PATCH 05/23] 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. --- Sources/Core.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Core.swift b/Sources/Core.swift index 9188a7d23..579763a98 100644 --- a/Sources/Core.swift +++ b/Sources/Core.swift @@ -561,7 +561,7 @@ class Core: NSObject, UIGestureRecognizerDelegate { } private func interruptAnimationIfNeeded() { - if let animator = self.moveAnimator, animator.isRunning { + if let animator = self.moveAnimator, animator.isRunning, 0 <= layoutAdapter.offsetFromMostExpandedAnchor { os_log(msg, log: devLog, type: .debug, "the attraction animator interrupted!!!") animator.stopAnimation(true) endAttraction(false) From 479cce4546f22c1780982052fae6537c51b0265a Mon Sep 17 00:00:00 2001 From: Shin Yamamoto Date: Fri, 7 Mar 2025 18:57:21 +0900 Subject: [PATCH 06/23] 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 --- .circleci/config.yml | 31 ++++++++++++++++++++++++- .github/workflows/ci.yml | 21 ----------------- Sources/Core.swift | 49 ++++++++++++++++++++++++---------------- Sources/Logging.swift | 11 +++++++++ Tests/CoreTests.swift | 28 +++++++++++++++++++++++ 5 files changed, 99 insertions(+), 41 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 5d2aae3c0..05c2d44fa 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -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) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4f2e3508f..dbb14f2fb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -27,15 +27,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 }} @@ -59,11 +50,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 }} @@ -137,13 +123,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" diff --git a/Sources/Core.swift b/Sources/Core.swift index 579763a98..1f764e1ca 100644 --- a/Sources/Core.swift +++ b/Sources/Core.swift @@ -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) } } @@ -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) } diff --git a/Sources/Logging.swift b/Sources/Logging.swift index 5c4f3c0f2..e813254b9 100644 --- a/Sources/Logging.swift +++ b/Sources/Logging.swift @@ -15,3 +15,14 @@ struct Logging { static let category = "FloatingPanel" private init() {} } + +extension String.StringInterpolation { + mutating func appendInterpolation(optional: T?, defaultValue: String = "nil") { + switch optional { + case let value?: + appendLiteral(String(describing: value)) + case nil: + appendLiteral(defaultValue) + } + } +} diff --git a/Tests/CoreTests.swift b/Tests/CoreTests.swift index 11201949d..fd2f502b5 100644 --- a/Tests/CoreTests.swift +++ b/Tests/CoreTests.swift @@ -916,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? From 370e30690463010a023618a11a2f5d12c22ce423 Mon Sep 17 00:00:00 2001 From: Shin Yamamoto Date: Fri, 7 Mar 2025 20:42:16 +0900 Subject: [PATCH 07/23] 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 [, , , , ] available to use with iphonesimulator SDK version * Used macos-15 to fix random build fails --- .github/workflows/ci.yml | 33 ++++++++++++++++++++++++++------- FloatingPanel.podspec | 2 +- 2 files changed, 27 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dbb14f2fb..da1fb4ee6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,6 +18,9 @@ jobs: fail-fast: false matrix: include: + - swift: "5" + xcode: "16.2" + runs-on: macos-14 - swift: "5.10" xcode: "15.4" runs-on: macos-14 @@ -40,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" @@ -62,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: @@ -76,6 +84,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Building ${{ matrix.example }} + # Need to use iphonesimulator18.1 because randomly 18.2 isn't available. run: | xcodebuild clean build \ -workspace FloatingPanel.xcworkspace \ @@ -85,20 +94,30 @@ jobs: swiftpm: runs-on: macos-14 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 @@ -132,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 diff --git a/FloatingPanel.podspec b/FloatingPanel.podspec index 2766c6e9a..836036fb8 100644 --- a/FloatingPanel.podspec +++ b/FloatingPanel.podspec @@ -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 } From 9592baa16c7cc1924ba2f9985cc9b38c1aa912d3 Mon Sep 17 00:00:00 2001 From: Shin Yamamoto Date: Thu, 13 Mar 2025 09:33:17 +0900 Subject: [PATCH 08/23] ci: use macos-15 for all testing to use Xcode 16.2 --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index da1fb4ee6..1bcefcde6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,7 +20,7 @@ jobs: include: - swift: "5" xcode: "16.2" - runs-on: macos-14 + runs-on: macos-15 - swift: "5.10" xcode: "15.4" runs-on: macos-14 @@ -92,7 +92,7 @@ jobs: -sdk iphonesimulator swiftpm: - runs-on: macos-14 + runs-on: macos-15 env: DEVELOPER_DIR: /Applications/Xcode_${{ matrix.xcode }}.app/Contents/Developer strategy: From a1f20cedb14bd1ddc63e30a8dd10c85e8f1fa011 Mon Sep 17 00:00:00 2001 From: Shin Yamamoto Date: Sat, 5 Apr 2025 14:11:55 +0900 Subject: [PATCH 09/23] Version 2.8.7 --- FloatingPanel.podspec | 2 +- README.md | 2 +- Sources/Info.plist | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/FloatingPanel.podspec b/FloatingPanel.podspec index 836036fb8..dfef7ca9d 100644 --- a/FloatingPanel.podspec +++ b/FloatingPanel.podspec @@ -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. diff --git a/README.md b/README.md index 30f7cb7be..2d1bfc157 100644 --- a/README.md +++ b/README.md @@ -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) diff --git a/Sources/Info.plist b/Sources/Info.plist index 73958c7c8..da2b3eb4b 100644 --- a/Sources/Info.plist +++ b/Sources/Info.plist @@ -15,7 +15,7 @@ CFBundlePackageType FMWK CFBundleShortVersionString - 2.8.6 + 2.8.7 CFBundleVersion $(CURRENT_PROJECT_VERSION) From dd49fdea5e2741f94aeb5a9589188c694df2e5f8 Mon Sep 17 00:00:00 2001 From: Shin Yamamoto Date: Mon, 21 Apr 2025 18:09:24 +0900 Subject: [PATCH 10/23] Revert "Disallow interrupting the panel interaction while bouncing over the most expanded state (#652)" This reverts commit b0fd0d4427cc82afa5f912da28083ed2586902d7. This change had a problem normal cases. For example, in Maps example a panel interaction jumps occurs because of this. --- Sources/Core.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Core.swift b/Sources/Core.swift index 1f764e1ca..8071343fd 100644 --- a/Sources/Core.swift +++ b/Sources/Core.swift @@ -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) From afff000d8ca901dee90ad0da9d5cf83ad3a3a252 Mon Sep 17 00:00:00 2001 From: Shin Yamamoto Date: Mon, 21 Apr 2025 20:18:02 +0900 Subject: [PATCH 11/23] 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. --- Sources/Core.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Core.swift b/Sources/Core.swift index 8071343fd..94da75e12 100644 --- a/Sources/Core.swift +++ b/Sources/Core.swift @@ -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 From dfa9a7781618c491d906fc769eb96b1212568460 Mon Sep 17 00:00:00 2001 From: Shin Yamamoto Date: Mon, 21 Apr 2025 20:18:12 +0900 Subject: [PATCH 12/23] Fix a miss spell --- Examples/Samples/Sources/UseCases/UseCaseController.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Examples/Samples/Sources/UseCases/UseCaseController.swift b/Examples/Samples/Sources/UseCases/UseCaseController.swift index 4e1ac6864..8497ec919 100644 --- a/Examples/Samples/Sources/UseCases/UseCaseController.swift +++ b/Examples/Samples/Sources/UseCases/UseCaseController.swift @@ -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() From 09f90365666bcb48539750cb0c003a8e2cca7fe1 Mon Sep 17 00:00:00 2001 From: Shin Yamamoto Date: Tue, 22 Apr 2025 21:45:02 +0900 Subject: [PATCH 13/23] Address #661 issue since v2.8.0 (#662) See this comment for more details. https://github.com/scenee/FloatingPanel/issues/661#issuecomment-2818064324 --- Examples/Samples/Sources/PanelLayouts.swift | 15 +++++++ .../Samples/Sources/UseCases/UseCase.swift | 5 ++- .../Sources/UseCases/UseCaseController.swift | 10 +++++ Sources/Core.swift | 17 +++++--- Tests/CoreTests.swift | 42 +++++++++++++++++++ 5 files changed, 82 insertions(+), 7 deletions(-) diff --git a/Examples/Samples/Sources/PanelLayouts.swift b/Examples/Samples/Sources/PanelLayouts.swift index 8fae78bb0..90a758af2 100644 --- a/Examples/Samples/Sources/PanelLayouts.swift +++ b/Examples/Samples/Sources/PanelLayouts.swift @@ -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 + } +} + diff --git a/Examples/Samples/Sources/UseCases/UseCase.swift b/Examples/Samples/Sources/UseCases/UseCase.swift index 14fbe8e5c..a555d1fa5 100644 --- a/Examples/Samples/Sources/UseCases/UseCase.swift +++ b/Examples/Samples/Sources/UseCases/UseCase.swift @@ -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()) diff --git a/Examples/Samples/Sources/UseCases/UseCaseController.swift b/Examples/Samples/Sources/UseCases/UseCaseController.swift index 8497ec919..f1a52d61d 100644 --- a/Examples/Samples/Sources/UseCases/UseCaseController.swift +++ b/Examples/Samples/Sources/UseCases/UseCaseController.swift @@ -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) @@ -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() diff --git a/Sources/Core.swift b/Sources/Core.swift index 94da75e12..2abc55ccf 100644 --- a/Sources/Core.swift +++ b/Sources/Core.swift @@ -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 { diff --git a/Tests/CoreTests.swift b/Tests/CoreTests.swift index fd2f502b5..8a18dd690 100644 --- a/Tests/CoreTests.swift +++ b/Tests/CoreTests.swift @@ -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() From 1d2e3a0304d5d56bae7d0c0bb861921374390bb0 Mon Sep 17 00:00:00 2001 From: Shin Yamamoto Date: Tue, 22 Apr 2025 21:46:23 +0900 Subject: [PATCH 14/23] Version 2.8.8 --- FloatingPanel.podspec | 2 +- README.md | 2 +- Sources/Info.plist | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/FloatingPanel.podspec b/FloatingPanel.podspec index dfef7ca9d..e0dfc4d72 100644 --- a/FloatingPanel.podspec +++ b/FloatingPanel.podspec @@ -1,7 +1,7 @@ Pod::Spec.new do |s| s.name = "FloatingPanel" - s.version = "2.8.7" + s.version = "2.8.8" 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. diff --git a/README.md b/README.md index 2d1bfc157..941a403f9 100644 --- a/README.md +++ b/README.md @@ -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.7/documentation/floatingpanel) for more details. +Please see also [the API reference@SPI](https://swiftpackageindex.com/scenee/FloatingPanel/2.8.8/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) diff --git a/Sources/Info.plist b/Sources/Info.plist index da2b3eb4b..173b005c4 100644 --- a/Sources/Info.plist +++ b/Sources/Info.plist @@ -15,7 +15,7 @@ CFBundlePackageType FMWK CFBundleShortVersionString - 2.8.7 + 2.8.8 CFBundleVersion $(CURRENT_PROJECT_VERSION) From 2e8a0f3682c09b953b3fa8fe9bfc6861ab711471 Mon Sep 17 00:00:00 2001 From: sina Date: Mon, 17 Jun 2024 12:48:32 +0330 Subject: [PATCH 15/23] Fixed WKWebView scroll issues --- Sources/Controller.swift | 25 ++++++++ Sources/Core.swift | 124 ++++++++++++++++++++++++++++----------- 2 files changed, 116 insertions(+), 33 deletions(-) diff --git a/Sources/Controller.swift b/Sources/Controller.swift index ca29df14b..20edc98b9 100644 --- a/Sources/Controller.swift +++ b/Sources/Controller.swift @@ -328,6 +328,16 @@ open class FloatingPanelController: UIViewController { self.view = view as UIView } + + open override func viewDidLoad() { + super.viewDidLoad() + + // Detect swipe back gesture to let Core handle vertical scroll-locks. + let edgeSwipeGestureRecognizer = UIScreenEdgePanGestureRecognizer(target: self, action: #selector(handleEdgeSwipeGesture)) + edgeSwipeGestureRecognizer.edges = .left + edgeSwipeGestureRecognizer.delegate = self + view.addGestureRecognizer(edgeSwipeGestureRecognizer) + } open override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() @@ -727,6 +737,21 @@ extension FloatingPanelController { } } +extension FloatingPanelController: UIGestureRecognizerDelegate { + + @objc func handleEdgeSwipeGesture(_ gesture: UIScreenEdgePanGestureRecognizer) { + switch gesture.state { + case .began, .changed: + floatingPanel.isSwipingBack = true + case .ended, .cancelled: + floatingPanel.isSwipingBack = false + default: + break + } + } + +} + // MARK: - Swizzling private var originalDismissImp: IMP? diff --git a/Sources/Core.swift b/Sources/Core.swift index 2abc55ccf..ab116a075 100644 --- a/Sources/Core.swift +++ b/Sources/Core.swift @@ -1,6 +1,7 @@ // Copyright 2018-Present Shin Yamamoto. All rights reserved. MIT license. import UIKit +import WebKit import os.log /// @@ -20,24 +21,44 @@ class Core: NSObject, UIGestureRecognizerDelegate { let layoutAdapter: LayoutAdapter let behaviorAdapter: BehaviorAdapter - weak var scrollView: UIScrollView? { - didSet { - oldValue?.panGestureRecognizer.removeTarget(self, action: nil) - scrollView?.panGestureRecognizer.addTarget(self, action: #selector(handle(panGesture:))) + // _scrollView is the main UIScrollView + weak var _scrollView: UIScrollView? = nil + // innerScrollView is used to hold WKWebView's inner scroll view. It contains correct content-offset. + private weak var _innerScrollView: UIScrollView? = nil + var scrollView: UIScrollView? { + get { + return _innerScrollView ?? _scrollView + } + set { + _scrollView?.panGestureRecognizer.removeTarget(self, action: nil) + newValue?.panGestureRecognizer.addTarget(self, action: #selector(handle(panGesture:))) if let cur = scrollView { - if oldValue == nil { + if _scrollView == nil { initialScrollOffset = cur.contentOffset scrollBounce = cur.bounces scrollIndictorVisible = cur.showsVerticalScrollIndicator } scrollLocked = false } else { - if let pre = oldValue { + if let pre = _scrollView { pre.isDirectionalLockEnabled = false pre.bounces = scrollBounce pre.showsVerticalScrollIndicator = scrollIndictorVisible } } + _scrollView = newValue + } + } + + // Called to find inner scroll view to handle WKWebView scroll views. + private func updateInnerScrollView() { + guard let _scrollView else { + return + } + let newScrollView = findScrollView(in: _scrollView, isParent: true) + if (newScrollView != _innerScrollView) { + _innerScrollView = newScrollView + _innerScrollView?.panGestureRecognizer.addTarget(self, action: #selector(handle(panGesture:))) } } @@ -74,8 +95,9 @@ class Core: NSObject, UIGestureRecognizerDelegate { private var initialScrollOffset: CGPoint? private var scrollBounce = false private var scrollIndictorVisible = false - private var scrollBounceThreshold: CGFloat = -30.0 + private var scrollBounceThreshold: CGFloat = -30 private var scrollLocked = false + var isSwipingBack = false // MARK: - Interface @@ -292,7 +314,8 @@ class Core: NSObject, UIGestureRecognizerDelegate { } // all gestures of the tracking scroll view should be recognized in parallel // and handle them in self.handle(panGesture:) - return scrollView?.gestureRecognizers?.contains(otherGestureRecognizer) ?? false + return (_scrollView?.gestureRecognizers?.contains(otherGestureRecognizer) ?? false) || + (_innerScrollView?.gestureRecognizers?.contains(otherGestureRecognizer) ?? false) default: // Should recognize tap/long press gestures in parallel when the surface view is at an anchor position. let adapterY = layoutAdapter.position(for: state) @@ -323,31 +346,44 @@ class Core: NSObject, UIGestureRecognizerDelegate { public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRequireFailureOf otherGestureRecognizer: UIGestureRecognizer) -> Bool { guard gestureRecognizer == panGestureRecognizer else { return false } - // Should begin the pan gesture without waiting for the tracking scroll view's gestures. - // `scrollView.gestureRecognizers` can contains the following gestures - // * UIScrollViewDelayedTouchesBeganGestureRecognizer - // * UIScrollViewPanGestureRecognizer (scrollView.panGestureRecognizer) - // * _UIDragAutoScrollGestureRecognizer - // * _UISwipeActionPanGestureRecognizer - // * UISwipeDismissalGestureRecognizer - if let scrollView = scrollView { - // On short contents scroll, `_UISwipeActionPanGestureRecognizer` blocks - // the panel's pan gesture if not returns false - if let scrollGestureRecognizers = scrollView.gestureRecognizers, - scrollGestureRecognizers.contains(otherGestureRecognizer) { - switch otherGestureRecognizer { - case scrollView.panGestureRecognizer: - if surfaceView.grabberAreaContains(gestureRecognizer.location(in: surfaceView)) { + // Workaround to fix unexpected scrolling issues on iOS 17.4 due to WKWebView gesture changes! + if _scrollView?.superview is WKWebView { + if otherGestureRecognizer is UIPanGestureRecognizer { + updateInnerScrollView() + } + } + + for sv in [_innerScrollView, _scrollView] { + // Should begin the pan gesture without waiting for the tracking scroll view's gestures. + // `scrollView.gestureRecognizers` can contains the following gestures + // * UIScrollViewDelayedTouchesBeganGestureRecognizer + // * UIScrollViewPanGestureRecognizer (scrollView.panGestureRecognizer) + // * _UIDragAutoScrollGestureRecognizer + // * _UISwipeActionPanGestureRecognizer + // * UISwipeDismissalGestureRecognizer + if let scrollView = sv { + // On short contents scroll, `_UISwipeActionPanGestureRecognizer` blocks + // the panel's pan gesture if not returns false + + if let scrollGestureRecognizers = scrollView.gestureRecognizers, + scrollGestureRecognizers.contains(otherGestureRecognizer) { + switch otherGestureRecognizer { + case scrollView.panGestureRecognizer: + if surfaceView.grabberAreaContains(gestureRecognizer.location(in: surfaceView)) { + return false + } + + guard isScrollable(state: state) else { return false } + + // The condition where offset > 0 must not be included here. Because it will stop recognizing + // the panel pan gesture if a user starts scrolling content from an offset greater than 0. + if let _innerScrollView, (allowScrollPanGesture(of: _innerScrollView) { offset in offset <= scrollBounceThreshold }) { + return true + } + return allowScrollPanGesture(of: _scrollView!) { offset in offset <= scrollBounceThreshold } + default: return false } - - guard isScrollable(state: state) else { return false } - - // The condition where offset > 0 must not be included here. Because it will stop recognizing - // the panel pan gesture if a user starts scrolling content from an offset greater than 0. - return allowScrollPanGesture(of: scrollView) { offset in offset <= scrollBounceThreshold } - default: - return false } } } @@ -389,8 +425,8 @@ class Core: NSObject, UIGestureRecognizerDelegate { @objc func handle(panGesture: UIPanGestureRecognizer) { switch panGesture { - case scrollView?.panGestureRecognizer: - guard let scrollView = scrollView else { return } + case scrollView?.panGestureRecognizer, _innerScrollView?.panGestureRecognizer: + let scrollView = panGesture == scrollView?.panGestureRecognizer ? _scrollView! : _innerScrollView! let velocity = value(of: panGesture.velocity(in: panGesture.view)) let location = panGesture.location(in: surfaceView) @@ -590,6 +626,12 @@ class Core: NSObject, UIGestureRecognizerDelegate { // When no scrollView, nothing to handle. guard let scrollView = scrollView, scrollView.frame.contains(initialLocation) else { return false } + if isSwipingBack { + // If WKWebView content be small and not scrollable, _innerScrollView won't be found. + // In this situation, panGestureRecognizer is hard to track, so we should prevent swipe back separately. + return true + } + // Prevents moving a panel on swipe actions using _UISwipeActionPanGestureRecognizer. // [Warning] Do not apply this to WKWebView. Since iOS 17.4, WKWebView has an additional pan // gesture recognizer besides UIScrollViewPanGestureRecognizer. Applying this to WKWebView @@ -1088,6 +1130,9 @@ class Core: NSObject, UIGestureRecognizerDelegate { } private func lockScrollView(strict: Bool = false) { + if _innerScrollView != nil { + return // no not lock webViews :) + } guard let scrollView = scrollView else { return } if scrollLocked { @@ -1459,3 +1504,16 @@ extension FloatingPanelController { return self.floatingPanel.transitionAnimator } } + +// Function to find UIScrollView in a UIView's subviews +fileprivate func findScrollView(in view: UIView, isParent: Bool = false) -> UIScrollView? { + if let scrollView = view as? UIScrollView, !isParent { + return scrollView + } + for subview in view.subviews { + if let found = findScrollView(in: subview) { + return found + } + } + return nil +} From 107229d47deb94c51db3c562ee12dd0a9c1dcb2b Mon Sep 17 00:00:00 2001 From: sina Date: Wed, 19 Jun 2024 22:19:27 +0330 Subject: [PATCH 16/23] Fixed scrolling issue when two scrollable content where pushed on each other and transition was not completed yet. --- Sources/Core.swift | 31 ++++++++++++++++--------------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/Sources/Core.swift b/Sources/Core.swift index ab116a075..c0245d87c 100644 --- a/Sources/Core.swift +++ b/Sources/Core.swift @@ -55,7 +55,21 @@ class Core: NSObject, UIGestureRecognizerDelegate { guard let _scrollView else { return } - let newScrollView = findScrollView(in: _scrollView, isParent: true) + // Function to find UIScrollViews in a web-view's subviews + var foundScrollViews = [UIScrollView]() + func findScrollView(in view: UIView, isParent: Bool = false) { + if isParent { + foundScrollViews = [] + } + if let scrollView = view as? UIScrollView, !isParent { + foundScrollViews.append(scrollView) + } + for subview in view.subviews { + findScrollView(in: subview) + } + } + findScrollView(in: _scrollView, isParent: true) + let newScrollView = foundScrollViews.last if (newScrollView != _innerScrollView) { _innerScrollView = newScrollView _innerScrollView?.panGestureRecognizer.addTarget(self, action: #selector(handle(panGesture:))) @@ -1234,7 +1248,7 @@ class Core: NSObject, UIGestureRecognizerDelegate { func isScrollable(state: FloatingPanelState) -> Bool { guard let scrollView = scrollView else { return false } - if let fpc = ownerVC, + if let fpc = ownerVC, let scrollable = fpc.delegate?.floatingPanel?(fpc, shouldAllowToScroll: scrollView, in: state) { return scrollable @@ -1504,16 +1518,3 @@ extension FloatingPanelController { return self.floatingPanel.transitionAnimator } } - -// Function to find UIScrollView in a UIView's subviews -fileprivate func findScrollView(in view: UIView, isParent: Bool = false) -> UIScrollView? { - if let scrollView = view as? UIScrollView, !isParent { - return scrollView - } - for subview in view.subviews { - if let found = findScrollView(in: subview) { - return found - } - } - return nil -} From 0a21396f98a78bab7840399ba94a192acf70837b Mon Sep 17 00:00:00 2001 From: sina Date: Thu, 20 Jun 2024 02:21:52 +0330 Subject: [PATCH 17/23] Prevent simultaniously scrolling the FloatingPanel in two directions. --- Sources/Controller.swift | 25 ------------------------- Sources/Core.swift | 22 ++++++++++++++-------- 2 files changed, 14 insertions(+), 33 deletions(-) diff --git a/Sources/Controller.swift b/Sources/Controller.swift index 20edc98b9..ca29df14b 100644 --- a/Sources/Controller.swift +++ b/Sources/Controller.swift @@ -328,16 +328,6 @@ open class FloatingPanelController: UIViewController { self.view = view as UIView } - - open override func viewDidLoad() { - super.viewDidLoad() - - // Detect swipe back gesture to let Core handle vertical scroll-locks. - let edgeSwipeGestureRecognizer = UIScreenEdgePanGestureRecognizer(target: self, action: #selector(handleEdgeSwipeGesture)) - edgeSwipeGestureRecognizer.edges = .left - edgeSwipeGestureRecognizer.delegate = self - view.addGestureRecognizer(edgeSwipeGestureRecognizer) - } open override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() @@ -737,21 +727,6 @@ extension FloatingPanelController { } } -extension FloatingPanelController: UIGestureRecognizerDelegate { - - @objc func handleEdgeSwipeGesture(_ gesture: UIScreenEdgePanGestureRecognizer) { - switch gesture.state { - case .began, .changed: - floatingPanel.isSwipingBack = true - case .ended, .cancelled: - floatingPanel.isSwipingBack = false - default: - break - } - } - -} - // MARK: - Swizzling private var originalDismissImp: IMP? diff --git a/Sources/Core.swift b/Sources/Core.swift index c0245d87c..d5db17b45 100644 --- a/Sources/Core.swift +++ b/Sources/Core.swift @@ -111,7 +111,6 @@ class Core: NSObject, UIGestureRecognizerDelegate { private var scrollIndictorVisible = false private var scrollBounceThreshold: CGFloat = -30 private var scrollLocked = false - var isSwipingBack = false // MARK: - Interface @@ -437,6 +436,7 @@ class Core: NSObject, UIGestureRecognizerDelegate { ownerVC?.remove() } + private var isScrollingHorizontally = false @objc func handle(panGesture: UIPanGestureRecognizer) { switch panGesture { case scrollView?.panGestureRecognizer, _innerScrollView?.panGestureRecognizer: @@ -579,6 +579,7 @@ class Core: NSObject, UIGestureRecognizerDelegate { interruptAnimationIfNeeded() if panGesture.state == .began { + isScrollingHorizontally = abs(velocity.x) > abs(velocity.y) panningBegan(at: location) return } @@ -604,6 +605,8 @@ class Core: NSObject, UIGestureRecognizerDelegate { allowsRubberBanding: behaviorAdapter.allowsRubberBanding(for:)) } panningEnd(with: translation, velocity: velocity) + // Reset the scroll direction lock + isScrollingHorizontally = false default: break } @@ -640,12 +643,6 @@ class Core: NSObject, UIGestureRecognizerDelegate { // When no scrollView, nothing to handle. guard let scrollView = scrollView, scrollView.frame.contains(initialLocation) else { return false } - if isSwipingBack { - // If WKWebView content be small and not scrollable, _innerScrollView won't be found. - // In this situation, panGestureRecognizer is hard to track, so we should prevent swipe back separately. - return true - } - // Prevents moving a panel on swipe actions using _UISwipeActionPanGestureRecognizer. // [Warning] Do not apply this to WKWebView. Since iOS 17.4, WKWebView has an additional pan // gesture recognizer besides UIScrollViewPanGestureRecognizer. Applying this to WKWebView @@ -662,6 +659,11 @@ class Core: NSObject, UIGestureRecognizerDelegate { } } } + + // If is scrolling horizontally, consider it as a swipe back! + if isScrollingHorizontally { + return true + } guard isScrollable(state: state), // When not top most(i.e. .full), don't scroll. @@ -794,7 +796,11 @@ class Core: NSObject, UIGestureRecognizerDelegate { let distToHidden = CGFloat(abs(currentPos - layoutAdapter.position(for: .hidden))) switch layoutAdapter.position { case .top, .bottom: - removalVector = (distToHidden != 0) ? CGVector(dx: 0.0, dy: velocity.y/distToHidden) : .zero + if isScrollingHorizontally { + removalVector = .zero + } else { + removalVector = (distToHidden != 0) ? CGVector(dx: 0.0, dy: velocity.y/distToHidden) : .zero + } case .left, .right: removalVector = (distToHidden != 0) ? CGVector(dx: velocity.x/distToHidden, dy: 0.0) : .zero } From 75d162bd9dfa65851d8e6df884280906297d2de3 Mon Sep 17 00:00:00 2001 From: sina Date: Thu, 20 Jun 2024 10:56:22 +0330 Subject: [PATCH 18/23] Prevent unwanted scroll scenarios on NBS, if _innerScrollView should catch the panGesture. --- Sources/Core.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Core.swift b/Sources/Core.swift index d5db17b45..7043fbbcb 100644 --- a/Sources/Core.swift +++ b/Sources/Core.swift @@ -686,7 +686,7 @@ class Core: NSObject, UIGestureRecognizerDelegate { return true } case .bottom, .right: - if offset > 0.0 { + if offset > 0.0 || (scrollView == _innerScrollView && offset == 0) { return true } if velocity <= 0, offset < 0.0 { From edd74fc1aeab2277afd7c44693e83aa00d118a4b Mon Sep 17 00:00:00 2001 From: sina Date: Thu, 20 Jun 2024 17:03:34 +0330 Subject: [PATCH 19/23] Lock all the scroll-views on animations --- Sources/Controller.swift | 10 ++++++++++ Sources/Core.swift | 40 +++++++++++++++++++++++++++++++++++----- 2 files changed, 45 insertions(+), 5 deletions(-) diff --git a/Sources/Controller.swift b/Sources/Controller.swift index ca29df14b..af5af86f5 100644 --- a/Sources/Controller.swift +++ b/Sources/Controller.swift @@ -265,6 +265,16 @@ open class FloatingPanelController: UIViewController { private var _contentViewController: UIViewController? + public var isAnimating: Bool = false { + didSet { + if isAnimating { + floatingPanel.lockAllScrollViews() + } else { + floatingPanel.unlockAllScrollViews() + } + } + } + private(set) var floatingPanel: Core! private var preSafeAreaInsets: UIEdgeInsets = .zero // Capture the latest one private var safeAreaInsetsObservation: NSKeyValueObservation? diff --git a/Sources/Core.swift b/Sources/Core.swift index 7043fbbcb..73b58ba1b 100644 --- a/Sources/Core.swift +++ b/Sources/Core.swift @@ -71,7 +71,13 @@ class Core: NSObject, UIGestureRecognizerDelegate { findScrollView(in: _scrollView, isParent: true) let newScrollView = foundScrollViews.last if (newScrollView != _innerScrollView) { + // Check if existing innerScrollView was locked + let wasInnerLocked = _innerScrollView?.bounces == false _innerScrollView = newScrollView + if wasInnerLocked { + // Lock the new one also. + lockAllScrollViews(isInnerScrollViewUpdated: true) + } _innerScrollView?.panGestureRecognizer.addTarget(self, action: #selector(handle(panGesture:))) } } @@ -171,6 +177,7 @@ class Core: NSObject, UIGestureRecognizerDelegate { interruptAnimationIfNeeded() if animated { + lockAllScrollViews() let updateScrollView: () -> Void = { [weak self] in guard let self = self else { return } if self.isScrollable(state: self.state), 0 == self.layoutAdapter.offset(from: self.state) { @@ -178,6 +185,7 @@ class Core: NSObject, UIGestureRecognizerDelegate { } else { self.lockScrollView() } + unlockAllScrollViews() } let animator: UIViewPropertyAnimator @@ -1150,10 +1158,7 @@ class Core: NSObject, UIGestureRecognizerDelegate { } private func lockScrollView(strict: Bool = false) { - if _innerScrollView != nil { - return // no not lock webViews :) - } - guard let scrollView = scrollView else { return } + guard let scrollView = _scrollView else { return } if scrollLocked { os_log(msg, log: devLog, type: .debug, "Already scroll locked") @@ -1185,7 +1190,7 @@ class Core: NSObject, UIGestureRecognizerDelegate { } private func unlockScrollView() { - guard let scrollView = scrollView else { return } + guard let scrollView = _scrollView else { return } if !scrollLocked { os_log(msg, log: devLog, type: .debug, "Already scroll unlocked.") return @@ -1202,6 +1207,31 @@ class Core: NSObject, UIGestureRecognizerDelegate { scrollView.showsHorizontalScrollIndicator = scrollIndictorVisible } } + + // Locks all the scrollviews (Used to lock when animating) + private var lockedInnerScrollViews = [UIScrollView]() + func lockAllScrollViews(isInnerScrollViewUpdated: Bool = false) { + // First update inner scroll view + if !isInnerScrollViewUpdated { + updateInnerScrollView() + } + // Now lock the new inner scroll view + guard let _innerScrollView else {return} + if !lockedInnerScrollViews.contains(_innerScrollView) { + _innerScrollView.isUserInteractionEnabled = false + lockedInnerScrollViews.append(_innerScrollView) + _scrollView?.isUserInteractionEnabled = false + } + } + func unlockAllScrollViews() { + if lockedInnerScrollViews.isEmpty { + return + } + _scrollView?.isUserInteractionEnabled = true + for _innerScrollView in lockedInnerScrollViews { + _innerScrollView.isUserInteractionEnabled = true + } + } private var shouldLooselyLockScrollView: Bool { if surfaceView.frame == .zero { From 4595ece50d7a89c3766114f913d34a742abb4ee8 Mon Sep 17 00:00:00 2001 From: sina Date: Mon, 8 Jul 2024 20:32:34 +0330 Subject: [PATCH 20/23] Ignore small scroll-view touches and do NOT pass them to outer scroll-view. --- Sources/Core.swift | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/Sources/Core.swift b/Sources/Core.swift index 73b58ba1b..ae15f11e1 100644 --- a/Sources/Core.swift +++ b/Sources/Core.swift @@ -49,7 +49,7 @@ class Core: NSObject, UIGestureRecognizerDelegate { _scrollView = newValue } } - + // Called to find inner scroll view to handle WKWebView scroll views. private func updateInnerScrollView() { guard let _scrollView else { @@ -336,7 +336,8 @@ class Core: NSObject, UIGestureRecognizerDelegate { // all gestures of the tracking scroll view should be recognized in parallel // and handle them in self.handle(panGesture:) return (_scrollView?.gestureRecognizers?.contains(otherGestureRecognizer) ?? false) || - (_innerScrollView?.gestureRecognizers?.contains(otherGestureRecognizer) ?? false) + (_innerScrollView?.contentSize.width ?? 0 >= _scrollView?.contentSize.width ?? 0 && + _innerScrollView?.gestureRecognizers?.contains(otherGestureRecognizer) ?? false) default: // Should recognize tap/long press gestures in parallel when the surface view is at an anchor position. let adapterY = layoutAdapter.position(for: state) @@ -385,7 +386,7 @@ class Core: NSObject, UIGestureRecognizerDelegate { if let scrollView = sv { // On short contents scroll, `_UISwipeActionPanGestureRecognizer` blocks // the panel's pan gesture if not returns false - + if let scrollGestureRecognizers = scrollView.gestureRecognizers, scrollGestureRecognizers.contains(otherGestureRecognizer) { switch otherGestureRecognizer { @@ -393,9 +394,9 @@ class Core: NSObject, UIGestureRecognizerDelegate { if surfaceView.grabberAreaContains(gestureRecognizer.location(in: surfaceView)) { return false } - + guard isScrollable(state: state) else { return false } - + // The condition where offset > 0 must not be included here. Because it will stop recognizing // the panel pan gesture if a user starts scrolling content from an offset greater than 0. if let _innerScrollView, (allowScrollPanGesture(of: _innerScrollView) { offset in offset <= scrollBounceThreshold }) { @@ -667,7 +668,7 @@ class Core: NSObject, UIGestureRecognizerDelegate { } } } - + // If is scrolling horizontally, consider it as a swipe back! if isScrollingHorizontally { return true @@ -1207,7 +1208,7 @@ class Core: NSObject, UIGestureRecognizerDelegate { scrollView.showsHorizontalScrollIndicator = scrollIndictorVisible } } - + // Locks all the scrollviews (Used to lock when animating) private var lockedInnerScrollViews = [UIScrollView]() func lockAllScrollViews(isInnerScrollViewUpdated: Bool = false) { From fa94043c21d06e9fe0386f3fd65ddd13bf19d6a5 Mon Sep 17 00:00:00 2001 From: sina Date: Fri, 20 Sep 2024 14:01:12 +0330 Subject: [PATCH 21/23] Consider inner scroll-view as modal gesture handler scroll-view if it's with is larger than `parentWidth - 32` instead of `parentWidth` --- Sources/Core.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Core.swift b/Sources/Core.swift index ae15f11e1..e50472b37 100644 --- a/Sources/Core.swift +++ b/Sources/Core.swift @@ -336,7 +336,7 @@ class Core: NSObject, UIGestureRecognizerDelegate { // all gestures of the tracking scroll view should be recognized in parallel // and handle them in self.handle(panGesture:) return (_scrollView?.gestureRecognizers?.contains(otherGestureRecognizer) ?? false) || - (_innerScrollView?.contentSize.width ?? 0 >= _scrollView?.contentSize.width ?? 0 && + ((_innerScrollView?.contentSize.width ?? 0) + 32 >= _scrollView?.contentSize.width ?? 0 && _innerScrollView?.gestureRecognizers?.contains(otherGestureRecognizer) ?? false) default: // Should recognize tap/long press gestures in parallel when the surface view is at an anchor position. From 6f6cd7fceacee9ce3f44283e3087481fa96c97b8 Mon Sep 17 00:00:00 2001 From: sina Date: Thu, 10 Oct 2024 15:04:42 +0330 Subject: [PATCH 22/23] Fixed unwanted scroll-view locks --- Sources/Core.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Sources/Core.swift b/Sources/Core.swift index e50472b37..28ebe98c4 100644 --- a/Sources/Core.swift +++ b/Sources/Core.swift @@ -72,8 +72,8 @@ class Core: NSObject, UIGestureRecognizerDelegate { let newScrollView = foundScrollViews.last if (newScrollView != _innerScrollView) { // Check if existing innerScrollView was locked - let wasInnerLocked = _innerScrollView?.bounces == false _innerScrollView = newScrollView + let wasInnerLocked = !lockedInnerScrollViews.isEmpty if wasInnerLocked { // Lock the new one also. lockAllScrollViews(isInnerScrollViewUpdated: true) @@ -1232,6 +1232,7 @@ class Core: NSObject, UIGestureRecognizerDelegate { for _innerScrollView in lockedInnerScrollViews { _innerScrollView.isUserInteractionEnabled = true } + lockedInnerScrollViews.removeAll() } private var shouldLooselyLockScrollView: Bool { From 242a050bb8e56b7778367ce8e5aa7f1a9218db02 Mon Sep 17 00:00:00 2001 From: sina Date: Sat, 26 Oct 2024 19:01:10 +0330 Subject: [PATCH 23/23] Added passthroughView to let presented view controller pass touches to the previous view controller when view is totally hidden --- Sources/Transitioning.swift | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/Sources/Transitioning.swift b/Sources/Transitioning.swift index ff771f8b5..c4bc7da8b 100644 --- a/Sources/Transitioning.swift +++ b/Sources/Transitioning.swift @@ -19,10 +19,13 @@ class ModalTransition: NSObject, UIViewControllerTransitioningDelegate { } class PresentationController: UIPresentationController { + private var passthroughView: PassthroughView? + override func presentationTransitionWillBegin() { // Must call here even if duplicating on in containerViewWillLayoutSubviews() // Because it let the panel present correctly with the presentation animation addFloatingPanel() + addPassthroughView() } override func presentationTransitionDidEnd(_ completed: Bool) { @@ -40,6 +43,7 @@ class PresentationController: UIPresentationController { } fpc.view.removeFromSuperview() } + passthroughView?.removeFromSuperview() } override func containerViewWillLayoutSubviews() { @@ -62,12 +66,22 @@ class PresentationController: UIPresentationController { // Forward touch events to the presenting view controller (fpc.view as? PassthroughView)?.eventForwardingView = presentingViewController.view + passthroughView?.eventForwardingView = presentingViewController.view } @objc func handleBackdrop(tapGesture: UITapGestureRecognizer) { presentedViewController.dismiss(animated: true, completion: nil) } + private func addPassthroughView() { + guard let containerView = self.containerView else { return } + + passthroughView = PassthroughView() + passthroughView?.frame = containerView.bounds + passthroughView?.autoresizingMask = [.flexibleWidth, .flexibleHeight] + containerView.addSubview(passthroughView!) + } + private func addFloatingPanel() { guard let containerView = self.containerView,