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/62] 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/62] 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/62] 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/62] 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/62] 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/62] 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/62] 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/62] 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/62] 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/62] 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/62] 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/62] 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/62] 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/62] 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 63a01ced9a9172eeb6148a0b66bf9044bf8762f9 Mon Sep 17 00:00:00 2001 From: Shin Yamamoto Date: Fri, 9 May 2025 21:48:39 +0900 Subject: [PATCH 15/62] ci: add workflow_dispatch event trigger --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1bcefcde6..3d3360d38 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,10 +4,10 @@ on: push: branches: - master - - next pull_request: branches: - '*' + workflow_dispatch: jobs: build: From dd93e959992013784650615f2eb5320960e88e67 Mon Sep 17 00:00:00 2001 From: Shin Yamamoto Date: Mon, 5 May 2025 17:47:26 +0900 Subject: [PATCH 16/62] Update FloatingPanelController --- Sources/Controller.swift | 29 ++++++++++++++++++++--------- 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/Sources/Controller.swift b/Sources/Controller.swift index ca29df14b..a3f8ce7cc 100644 --- a/Sources/Controller.swift +++ b/Sources/Controller.swift @@ -709,24 +709,35 @@ extension FloatingPanelController { if let animator = delegate?.floatingPanel?(self, animatorForPresentingTo: to) { return animator } - let timingParameters = UISpringTimingParameters(decelerationRate: UIScrollView.DecelerationRate.fast.rawValue, - frequencyResponse: 0.25) - return UIViewPropertyAnimator(duration: 0.0, - timingParameters: timingParameters) + return makeDefaultAnimator() } func animatorForDismissing(with velocity: CGVector) -> UIViewPropertyAnimator { if let animator = delegate?.floatingPanel?(self, animatorForDismissingWith: velocity) { return animator } - let timingParameters = UISpringTimingParameters(decelerationRate: UIScrollView.DecelerationRate.fast.rawValue, - frequencyResponse: 0.25, - initialVelocity: velocity) - return UIViewPropertyAnimator(duration: 0.0, - timingParameters: timingParameters) + return makeDefaultAnimator(initialVelocity: velocity) } } + +// MARK: - Animation + +extension FloatingPanelController { + func makeDefaultAnimator(initialVelocity: CGVector = .zero) -> UIViewPropertyAnimator { + let timingParameters = UISpringTimingParameters( + decelerationRate: UIScrollView.DecelerationRate.fast.rawValue, + frequencyResponse: 0.25, + initialVelocity: initialVelocity + ) + return UIViewPropertyAnimator( + duration: 0.0, + timingParameters: timingParameters + ) + } +} + + // MARK: - Swizzling private var originalDismissImp: IMP? From 507ce9c3132e1786777d16d2d58c4c667fd2a412 Mon Sep 17 00:00:00 2001 From: Shin Yamamoto Date: Thu, 24 Apr 2025 15:09:50 +0900 Subject: [PATCH 17/62] Dump the default config of swift-format on Xcode 16.3 --- .swift-format | 79 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 .swift-format diff --git a/.swift-format b/.swift-format new file mode 100644 index 000000000..545beaa17 --- /dev/null +++ b/.swift-format @@ -0,0 +1,79 @@ +{ + "fileScopedDeclarationPrivacy" : { + "accessLevel" : "private" + }, + "indentConditionalCompilationBlocks" : true, + "indentSwitchCaseLabels" : false, + "indentation" : { + "spaces" : 2 + }, + "lineBreakAroundMultilineExpressionChainComponents" : false, + "lineBreakBeforeControlFlowKeywords" : false, + "lineBreakBeforeEachArgument" : false, + "lineBreakBeforeEachGenericRequirement" : false, + "lineBreakBetweenDeclarationAttributes" : false, + "lineLength" : 100, + "maximumBlankLines" : 1, + "multiElementCollectionTrailingCommas" : true, + "noAssignmentInExpressions" : { + "allowedFunctions" : [ + "XCTAssertNoThrow" + ] + }, + "prioritizeKeepingFunctionOutputTogether" : false, + "reflowMultilineStringLiterals" : { + "never" : { + + } + }, + "respectsExistingLineBreaks" : true, + "rules" : { + "AllPublicDeclarationsHaveDocumentation" : false, + "AlwaysUseLiteralForEmptyCollectionInit" : false, + "AlwaysUseLowerCamelCase" : true, + "AmbiguousTrailingClosureOverload" : true, + "AvoidRetroactiveConformances" : true, + "BeginDocumentationCommentWithOneLineSummary" : false, + "DoNotUseSemicolons" : true, + "DontRepeatTypeInStaticProperties" : true, + "FileScopedDeclarationPrivacy" : true, + "FullyIndirectEnum" : true, + "GroupNumericLiterals" : true, + "IdentifiersMustBeASCII" : true, + "NeverForceUnwrap" : false, + "NeverUseForceTry" : false, + "NeverUseImplicitlyUnwrappedOptionals" : false, + "NoAccessLevelOnExtensionDeclaration" : true, + "NoAssignmentInExpressions" : true, + "NoBlockComments" : true, + "NoCasesWithOnlyFallthrough" : true, + "NoEmptyLinesOpeningClosingBraces" : false, + "NoEmptyTrailingClosureParentheses" : true, + "NoLabelsInCasePatterns" : true, + "NoLeadingUnderscores" : false, + "NoParensAroundConditions" : true, + "NoPlaygroundLiterals" : true, + "NoVoidReturnOnFunctionSignature" : true, + "OmitExplicitReturns" : false, + "OneCasePerLine" : true, + "OneVariableDeclarationPerLine" : true, + "OnlyOneTrailingClosureArgument" : true, + "OrderedImports" : true, + "ReplaceForEachWithForLoop" : true, + "ReturnVoidInsteadOfEmptyTuple" : true, + "TypeNamesShouldBeCapitalized" : true, + "UseEarlyExits" : false, + "UseExplicitNilCheckInConditions" : true, + "UseLetInEveryBoundCaseVariable" : true, + "UseShorthandTypeNames" : true, + "UseSingleLinePropertyGetter" : true, + "UseSynthesizedInitializer" : true, + "UseTripleSlashForDocumentationComments" : true, + "UseWhereClausesInForLoops" : false, + "ValidateDocumentationComments" : false + }, + "spacesAroundRangeFormationOperators" : false, + "spacesBeforeEndOfLineComments" : 2, + "tabWidth" : 8, + "version" : 1 +} From ec72f7ce8e75350a6773dd0933713eed64aa9a07 Mon Sep 17 00:00:00 2001 From: Shin Yamamoto Date: Fri, 25 Oct 2024 16:22:20 +0900 Subject: [PATCH 18/62] Lint the library code by built-in swift-format --- BuildTools/.gitignore | 9 ++++ BuildTools/Package.swift | 19 +++++++++ BuildTools/Plugins/swift-format-plugin.swift | 45 ++++++++++++++++++++ FloatingPanel.xcodeproj/project.pbxproj | 28 +++++++++++- 4 files changed, 100 insertions(+), 1 deletion(-) create mode 100644 BuildTools/.gitignore create mode 100644 BuildTools/Package.swift create mode 100644 BuildTools/Plugins/swift-format-plugin.swift diff --git a/BuildTools/.gitignore b/BuildTools/.gitignore new file mode 100644 index 000000000..578674aa5 --- /dev/null +++ b/BuildTools/.gitignore @@ -0,0 +1,9 @@ +.DS_Store +/.build +/Packages +xcuserdata/ +DerivedData/ +.swiftpm/configuration/registries.json +.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +.netrc + diff --git a/BuildTools/Package.swift b/BuildTools/Package.swift new file mode 100644 index 000000000..07a67c3e8 --- /dev/null +++ b/BuildTools/Package.swift @@ -0,0 +1,19 @@ +// swift-tools-version: 6.0 + +import PackageDescription + +let package = Package( + name: "build-tools", + products: [ + .plugin( + name: "swift-format-plugin", + targets: ["swift-format-plugin"] + ) + ], + targets: [ + .plugin( + name: "swift-format-plugin", + capability: .buildTool() + ) + ] +) diff --git a/BuildTools/Plugins/swift-format-plugin.swift b/BuildTools/Plugins/swift-format-plugin.swift new file mode 100644 index 000000000..4de9b7d5f --- /dev/null +++ b/BuildTools/Plugins/swift-format-plugin.swift @@ -0,0 +1,45 @@ +import PackagePlugin + +@main +struct SwiftFormatBuildToolPlugin: BuildToolPlugin { + func createBuildCommands(context: PluginContext, target: Target) async throws -> [Command] { + return [] + } +} + +#if canImport(XcodeProjectPlugin) +import XcodeProjectPlugin + +extension SwiftFormatBuildToolPlugin: XcodeBuildToolPlugin { + // Entry point for creating build commands for targets in Xcode projects. + func createBuildCommands(context: XcodePluginContext, target: XcodeTarget) throws -> [Command] { + // Find the code generator tool to run (replace this with the actual one). + print("SwiftFormatBuildToolPlugin -> \(context.xcodeProject.directoryURL.path())") + let configFile = context.xcodeProject.directoryURL.appending(path: ".swift-format") + // Currently check only 'SwiftUI' source code. + let sourceFiles = context.xcodeProject.directoryURL.appending(path: "Sources/SwiftUI") + // let sourceFiles = context.xcodeProject.directoryURL.appending(path: "Sources") + // let testFiles = context.xcodeProject.directoryURL.appending(path: "Tests") + let buildToolsFiles = context.xcodeProject.directoryURL.appending(path: "BuildTools") + return [ + .buildCommand( + displayName: "Run swift format(xcode)", + executable: try context.tool(named: "swift").url, + arguments: [ + "format", + "lint", + "--configuration", + configFile.path(), + "-r", + sourceFiles.path(), + //testFiles.path(), + buildToolsFiles.path(), + ], + inputFiles: [], + outputFiles: [] + ) + ] + } +} + +#endif diff --git a/FloatingPanel.xcodeproj/project.pbxproj b/FloatingPanel.xcodeproj/project.pbxproj index b603782f0..fbad0049b 100644 --- a/FloatingPanel.xcodeproj/project.pbxproj +++ b/FloatingPanel.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 50; + objectVersion = 60; objects = { /* Begin PBXBuildFile section */ @@ -193,6 +193,7 @@ buildRules = ( ); dependencies = ( + 54A0CBA92CCB77390058BD47 /* PBXTargetDependency */, ); name = FloatingPanel; productName = FloatingModalController; @@ -245,6 +246,9 @@ Base, ); mainGroup = 545DB9B72151169500CA77B8; + packageReferences = ( + 54A0CBA72CCB772E0058BD47 /* XCLocalSwiftPackageReference "BuildTools" */, + ); productRefGroup = 545DB9C22151169500CA77B8 /* Products */; projectDirPath = ""; projectRoot = ""; @@ -319,6 +323,10 @@ target = 545DB9C02151169500CA77B8 /* FloatingPanel */; targetProxy = 545DB9CC2151169500CA77B8 /* PBXContainerItemProxy */; }; + 54A0CBA92CCB77390058BD47 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + productRef = 54A0CBA82CCB77390058BD47 /* swift-format-plugin */; + }; /* End PBXTargetDependency section */ /* Begin XCBuildConfiguration section */ @@ -460,6 +468,7 @@ DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_USER_SCRIPT_SANDBOXING = YES; INFOPLIST_FILE = Sources/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; IPHONEOS_DEPLOYMENT_TARGET = 11.0; @@ -492,6 +501,7 @@ DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_USER_SCRIPT_SANDBOXING = YES; INFOPLIST_FILE = Sources/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; IPHONEOS_DEPLOYMENT_TARGET = 11.0; @@ -629,6 +639,7 @@ DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_USER_SCRIPT_SANDBOXING = YES; INFOPLIST_FILE = Sources/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; IPHONEOS_DEPLOYMENT_TARGET = 11.0; @@ -704,6 +715,21 @@ defaultConfigurationName = Release; }; /* End XCConfigurationList section */ + +/* Begin XCLocalSwiftPackageReference section */ + 54A0CBA72CCB772E0058BD47 /* XCLocalSwiftPackageReference "BuildTools" */ = { + isa = XCLocalSwiftPackageReference; + relativePath = BuildTools; + }; +/* End XCLocalSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + 54A0CBA82CCB77390058BD47 /* swift-format-plugin */ = { + isa = XCSwiftPackageProductDependency; + package = 54A0CBA72CCB772E0058BD47 /* XCLocalSwiftPackageReference "BuildTools" */; + productName = "plugin:swift-format-plugin"; + }; +/* End XCSwiftPackageProductDependency section */ }; rootObject = 545DB9B82151169500CA77B8 /* Project object */; } From 3a00ac98cd645c3b9d0be518a8a1863f5280a27b Mon Sep 17 00:00:00 2001 From: Shin Yamamoto Date: Fri, 25 Oct 2024 16:24:34 +0900 Subject: [PATCH 19/62] Ignore project.xcworkspace dir --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index e69908207..04f469af0 100644 --- a/.gitignore +++ b/.gitignore @@ -18,6 +18,7 @@ DerivedData/ !default.perspectivev3 xcuserdata/ IDEWorkspaceChecks.plis +*.xcodeproj/project.xcworkspace ## Other *.moved-aside From df9ddb02cb7981d3f85efa6c9d0f70d75b77a52a Mon Sep 17 00:00:00 2001 From: Shin Yamamoto Date: Thu, 24 Apr 2025 15:20:18 +0900 Subject: [PATCH 20/62] Update .swift-format --- .swift-format | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.swift-format b/.swift-format index 545beaa17..adf28df56 100644 --- a/.swift-format +++ b/.swift-format @@ -2,17 +2,17 @@ "fileScopedDeclarationPrivacy" : { "accessLevel" : "private" }, - "indentConditionalCompilationBlocks" : true, + "indentConditionalCompilationBlocks" : false, "indentSwitchCaseLabels" : false, "indentation" : { - "spaces" : 2 + "spaces" : 4 }, "lineBreakAroundMultilineExpressionChainComponents" : false, "lineBreakBeforeControlFlowKeywords" : false, "lineBreakBeforeEachArgument" : false, "lineBreakBeforeEachGenericRequirement" : false, "lineBreakBetweenDeclarationAttributes" : false, - "lineLength" : 100, + "lineLength" : 120, "maximumBlankLines" : 1, "multiElementCollectionTrailingCommas" : true, "noAssignmentInExpressions" : { From c9ae0d7e9273fe036c622183f6e69e8e8f56f511 Mon Sep 17 00:00:00 2001 From: Shin Yamamoto Date: Sun, 27 Apr 2025 21:00:06 +0900 Subject: [PATCH 21/62] Update the minimum deployment target to iOS 13 --- FloatingPanel.podspec | 2 +- FloatingPanel.xcodeproj/project.pbxproj | 12 ++++++------ Package.swift | 4 ++-- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/FloatingPanel.podspec b/FloatingPanel.podspec index e0dfc4d72..eda4d0d84 100644 --- a/FloatingPanel.podspec +++ b/FloatingPanel.podspec @@ -11,7 +11,7 @@ The new interface displays the related contents and utilities in parallel as a u s.author = "Shin Yamamoto" s.social_media_url = "https://x.com/scenee" - s.platform = :ios, "11.0" + s.platform = :ios, "13.0" s.source = { :git => "https://github.com/scenee/FloatingPanel.git", :tag => s.version.to_s } s.source_files = "Sources/*.swift" s.swift_version = '5.0' diff --git a/FloatingPanel.xcodeproj/project.pbxproj b/FloatingPanel.xcodeproj/project.pbxproj index fbad0049b..6cb58b53a 100644 --- a/FloatingPanel.xcodeproj/project.pbxproj +++ b/FloatingPanel.xcodeproj/project.pbxproj @@ -471,7 +471,7 @@ ENABLE_USER_SCRIPT_SANDBOXING = YES; INFOPLIST_FILE = Sources/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -504,7 +504,7 @@ ENABLE_USER_SCRIPT_SANDBOXING = YES; INFOPLIST_FILE = Sources/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -527,7 +527,7 @@ CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = ""; INFOPLIST_FILE = Tests/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -548,7 +548,7 @@ CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = ""; INFOPLIST_FILE = Tests/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -642,7 +642,7 @@ ENABLE_USER_SCRIPT_SANDBOXING = YES; INFOPLIST_FILE = Sources/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -667,7 +667,7 @@ CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = ""; INFOPLIST_FILE = Tests/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", diff --git a/Package.swift b/Package.swift index 41f701e96..8f86014a4 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:5.0 +// swift-tools-version:5.1 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription @@ -6,7 +6,7 @@ import PackageDescription let package = Package( name: "FloatingPanel", platforms: [ - .iOS(.v11) + .iOS(.v13) ], products: [ // Products define the executables and libraries produced by a package, and make them visible to other packages. From 0a8e1bc5f84f3300dbc60a6f936e06948592efb0 Mon Sep 17 00:00:00 2001 From: Shin Yamamoto Date: Sun, 27 Apr 2025 21:02:56 +0900 Subject: [PATCH 22/62] Drop Xcode 13.4.1 support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The following errors occur only on Xcode 13.4.1. Addressing them would significantly impair the usability of this library’s API. Therefore, I have decided not to support Xcode 13.4.1. > /.../FloatingPanel/Sources/SwiftUI/FloatingPanelView.swift:9:33: error: protocol 'FloatingPanelCoordinator' can only be used as a generic constraint because it has Self or associated type requirements > let coordinator: () -> (any FloatingPanelCoordinator) > ^ > /.../FloatingPanel/Sources/SwiftUI/FloatingPanelView.swift:78:42: error: protocol 'FloatingPanelCoordinator' can only be used as a generic constraint because it has Self or associated type requirements > public func makeCoordinator() -> any FloatingPanelCoordinator { > ^ > /.../FloatingPanel/Sources/SwiftUI/View+floatingPanel.swift:26:67: error: 'some' types are only implemented for the declared type of properties and subscripts and the return type of functions > @ViewBuilder _ content: @escaping (FloatingPanelProxy) -> some View > ^ > /.../FloatingPanel/Sources/SwiftUI/View+floatingPanel.swift:26:9: error: result builder attribute 'ViewBuilder' can only be applied to a parameter of function type > @ViewBuilder _ content: @escaping (FloatingPanelProxy) -> some View > ^ > /.../FloatingPanel/Sources/SwiftUI/View+floatingPanel.swift:24:31: error: default argument value of type 'FloatingPanelDefaultCoordinator.Type' cannot be converted to type 'T.Type' > coordinator: T.Type = FloatingPanelDefaultCoordinator.self, > ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ > as! T.Type > /.../FloatingPanel/Sources/SwiftUI/View+floatingPanel.swift:28:9: error: generic parameter 'ContentView' could not be inferred > FloatingPanelView( > ^ > /.../FloatingPanel/Sources/SwiftUI/FloatingPanelView.swift:8:49: note: 'ContentView' declared as parameter to type 'FloatingPanelView' > public struct FloatingPanelView: UIViewControllerRepresentable { > ^ > /.../FloatingPanel/Sources/SwiftUI/View+floatingPanel.swift:28:9: note: explicitly specify the generic arguments to fix this issue > FloatingPanelView( > ^ > > --- .circleci/config.yml | 43 ------------------------------------------- 1 file changed, 43 deletions(-) delete mode 100644 .circleci/config.yml diff --git a/.circleci/config.yml b/.circleci/config.yml deleted file mode 100644 index 05c2d44fa..000000000 --- a/.circleci/config.yml +++ /dev/null @@ -1,43 +0,0 @@ -version: 2.1 - -jobs: - build-swift_5_7: - macos: - xcode: 13.4.1 - steps: - - checkout - - run: xcodebuild -scheme FloatingPanel -workspace FloatingPanel.xcworkspace SWIFT_VERSION=5.7 clean build - - build-swiftpm_ios15_7: - macos: - xcode: 13.4.1 - steps: - - checkout - - run: swift build -Xswiftc "-sdk" -Xswiftc "`xcrun --sdk iphonesimulator --show-sdk-path`" -Xswiftc "-target" -Xswiftc "x86_64-apple-ios15.7-simulator" - - run: swift build -Xswiftc "-sdk" -Xswiftc "`xcrun --sdk iphonesimulator --show-sdk-path`" -Xswiftc "-target" -Xswiftc "arm64-apple-ios15.7-simulator" - - test-ios15_5-iPhone_13_Pro: - macos: - xcode: 13.4.1 - steps: - - checkout - - run: xcodebuild clean test -scheme FloatingPanel -workspace FloatingPanel.xcworkspace -destination 'platform=iOS Simulator,OS=15.5,name=iPhone 13 Pro' - test-ios14_5-iPhone_12_Pro: - macos: - xcode: 13.4.1 - steps: - - checkout - - run: xcodebuild clean test -scheme FloatingPanel -workspace FloatingPanel.xcworkspace -destination 'platform=iOS Simulator,OS=14.5,name=iPhone 12 Pro' - - -workflows: - test: - jobs: - - 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) From cf84ca05344b4b4eb611a11ffa32dcdcaea506db Mon Sep 17 00:00:00 2001 From: Shin Yamamoto Date: Sun, 11 May 2025 23:49:40 +0900 Subject: [PATCH 23/62] Expand timeout for some test cases in {Controller,Core}Tests --- Tests/ControllerTests.swift | 6 +++--- Tests/CoreTests.swift | 7 ++++--- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/Tests/ControllerTests.swift b/Tests/ControllerTests.swift index 178dc2168..e0f41b162 100644 --- a/Tests/ControllerTests.swift +++ b/Tests/ControllerTests.swift @@ -64,7 +64,7 @@ class ControllerTests: XCTestCase { } func test_moveTo() { - let timeout = 3.0 + let timeout = 5.0 let delegate = FloatingPanelTestDelegate() let fpc = FloatingPanelController(delegate: delegate) XCTAssertEqual(delegate.position, .hidden) @@ -138,7 +138,7 @@ class ControllerTests: XCTestCase { class MyFloatingPanelTop2BottomLayout: FloatingPanelTop2BottomTestLayout { override var initialState: FloatingPanelState { return .half } } - let timeout = 3.0 + let timeout = 5.0 let delegate = FloatingPanelTestDelegate() let fpc = FloatingPanelController(delegate: delegate) fpc.layout = MyFloatingPanelTop2BottomLayout() @@ -225,7 +225,7 @@ class ControllerTests: XCTestCase { } func test_moveTo_didMoveDelegate() { - let timeout = 3.0 + let timeout = 5.0 let delegate = FloatingPanelTestDelegate() let fpc = FloatingPanelController(delegate: delegate) XCTAssertEqual(delegate.position, .hidden) diff --git a/Tests/CoreTests.swift b/Tests/CoreTests.swift index 8a18dd690..b6f6e6d28 100644 --- a/Tests/CoreTests.swift +++ b/Tests/CoreTests.swift @@ -8,6 +8,7 @@ class CoreTests: XCTestCase { override func tearDown() {} func test_scrollLock() { + let timeout = 5.0 let fpc = FloatingPanelController() let contentVC1 = UITableViewController(nibName: nil, bundle: nil) @@ -35,7 +36,7 @@ class CoreTests: XCTestCase { XCTAssertEqual(contentVC1.tableView.bounces, true) exp1.fulfill() } - wait(for: [exp1], timeout: 1.0) + wait(for: [exp1], timeout: timeout) let exp2 = expectation(description: "move to tip with animation") fpc.move(to: .tip, animated: false) { @@ -43,7 +44,7 @@ class CoreTests: XCTestCase { XCTAssertEqual(contentVC1.tableView.bounces, false) exp2.fulfill() } - wait(for: [exp2], timeout: 1.0) + wait(for: [exp2], timeout: timeout) // Reset the content vc let contentVC2 = UITableViewController(nibName: nil, bundle: nil) @@ -209,7 +210,7 @@ class CoreTests: XCTestCase { return floor(fpc.backdropView.alpha * 1e+06) / 1e+06 } - let timeout = 3.0 + let timeout = 5.0 let delegate = TestDelegate() let fpc = FloatingPanelController(delegate: delegate) From 57480c3df2440ce2ea47c630ea8fd926ac168cac Mon Sep 17 00:00:00 2001 From: Shin Yamamoto Date: Mon, 7 Oct 2024 23:15:02 +0900 Subject: [PATCH 24/62] Implement SwiftUI APIs based on the Maps-SwiftUI app * Added SamplesSwiftUI * Moved Maps-SwiftUI project in workspace * Updated build tools * Fixed xcode 15.1 build * Added FloatingPanel.SurfaceAppearance.box() --- BuildTools/Package.swift | 2 +- BuildTools/Plugins/swift-format-plugin.swift | 28 +- .../Maps-SwiftUI.xcodeproj/project.pbxproj | 40 +- .../FloatingPanel/FloatingPanelView.swift | 133 ------- .../UIHostingController+ignoreKeyboard.swift | 44 --- .../FloatingPanel/View+floatingPanel.swift | 31 -- ...gPanelContentInsetAdjustmentBehavior.swift | 38 -- .../View+floatingPanelContentMode.swift | 37 -- ...ew+floatingPanelGrabberHandlePadding.swift | 36 -- .../View+floatingPanelSurfaceAppearance.swift | 44 --- .../Maps/FloatingPanelContentView.swift | 9 +- Examples/Maps-SwiftUI/Maps/MapsApp.swift | 90 ++++- Examples/Maps-SwiftUI/Maps/ResultsList.swift | 9 +- .../UIHostingController+ignoreKeyboard.swift | 46 +++ .../SamplesObjC.xcodeproj/project.pbxproj | 2 - Examples/SamplesSwiftUI/README.md | 6 + .../SamplesSwiftUI.xcodeproj/project.pbxproj | 364 ++++++++++++++++++ .../AccentColor.colorset/Contents.json | 11 + .../AppIcon.appiconset/Contents.json | 35 ++ .../Assets.xcassets/Contents.json | 6 + .../Preview Assets.xcassets/Contents.json | 6 + .../SamplesSwiftUI/SampleApp.swift | 12 + .../SamplesSwiftUI/UseCases/InsideTab.swift | 34 ++ .../SamplesSwiftUI/UseCases/MainView.swift | 102 +++++ .../SamplesSwiftUI/UseCases/MultiPanel.swift | 31 ++ .../SamplesSwiftUI/Views/ContentView.swift | 54 +++ FloatingPanel.xcodeproj/project.pbxproj | 52 +++ .../contents.xcworkspacedata | 7 +- Sources/Controller.swift | 38 +- Sources/Core.swift | 39 +- .../SwiftUI/FloatingPanelCoordinator.swift | 125 ++++++ Sources/SwiftUI/FloatingPanelProxy.swift | 98 +++++ Sources/SwiftUI/FloatingPanelView.swift | 284 ++++++++++++++ Sources/SwiftUI/SurfaceAppearance+.swift | 80 ++++ Sources/SwiftUI/View+floatingPanel.swift | 64 +++ .../SwiftUI/View+floatingPanelBehavior.swift | 62 +++ .../View+floatingPanelConfiguration.swift | 102 +++++ .../SwiftUI/View+floatingPanelLayout.swift | 71 ++++ .../View+floatingPanelScrollTracking.swift | 119 ++++++ Sources/SwiftUI/View+floatingPanelState.swift | 68 ++++ .../SwiftUI/View+floatingPanelSurface.swift | 88 +++++ 41 files changed, 2109 insertions(+), 438 deletions(-) delete mode 100644 Examples/Maps-SwiftUI/Maps/FloatingPanel/FloatingPanelView.swift delete mode 100644 Examples/Maps-SwiftUI/Maps/FloatingPanel/UIHostingController+ignoreKeyboard.swift delete mode 100644 Examples/Maps-SwiftUI/Maps/FloatingPanel/View+floatingPanel.swift delete mode 100644 Examples/Maps-SwiftUI/Maps/FloatingPanel/View+floatingPanelContentInsetAdjustmentBehavior.swift delete mode 100644 Examples/Maps-SwiftUI/Maps/FloatingPanel/View+floatingPanelContentMode.swift delete mode 100644 Examples/Maps-SwiftUI/Maps/FloatingPanel/View+floatingPanelGrabberHandlePadding.swift delete mode 100644 Examples/Maps-SwiftUI/Maps/FloatingPanel/View+floatingPanelSurfaceAppearance.swift create mode 100644 Examples/Maps-SwiftUI/Maps/UIHostingController+ignoreKeyboard.swift create mode 100644 Examples/SamplesSwiftUI/README.md create mode 100644 Examples/SamplesSwiftUI/SamplesSwiftUI.xcodeproj/project.pbxproj create mode 100644 Examples/SamplesSwiftUI/SamplesSwiftUI/Assets.xcassets/AccentColor.colorset/Contents.json create mode 100644 Examples/SamplesSwiftUI/SamplesSwiftUI/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 Examples/SamplesSwiftUI/SamplesSwiftUI/Assets.xcassets/Contents.json create mode 100644 Examples/SamplesSwiftUI/SamplesSwiftUI/Preview Content/Preview Assets.xcassets/Contents.json create mode 100644 Examples/SamplesSwiftUI/SamplesSwiftUI/SampleApp.swift create mode 100644 Examples/SamplesSwiftUI/SamplesSwiftUI/UseCases/InsideTab.swift create mode 100644 Examples/SamplesSwiftUI/SamplesSwiftUI/UseCases/MainView.swift create mode 100644 Examples/SamplesSwiftUI/SamplesSwiftUI/UseCases/MultiPanel.swift create mode 100644 Examples/SamplesSwiftUI/SamplesSwiftUI/Views/ContentView.swift create mode 100644 Sources/SwiftUI/FloatingPanelCoordinator.swift create mode 100644 Sources/SwiftUI/FloatingPanelProxy.swift create mode 100644 Sources/SwiftUI/FloatingPanelView.swift create mode 100644 Sources/SwiftUI/SurfaceAppearance+.swift create mode 100644 Sources/SwiftUI/View+floatingPanel.swift create mode 100644 Sources/SwiftUI/View+floatingPanelBehavior.swift create mode 100644 Sources/SwiftUI/View+floatingPanelConfiguration.swift create mode 100644 Sources/SwiftUI/View+floatingPanelLayout.swift create mode 100644 Sources/SwiftUI/View+floatingPanelScrollTracking.swift create mode 100644 Sources/SwiftUI/View+floatingPanelState.swift create mode 100644 Sources/SwiftUI/View+floatingPanelSurface.swift diff --git a/BuildTools/Package.swift b/BuildTools/Package.swift index 07a67c3e8..3545ea127 100644 --- a/BuildTools/Package.swift +++ b/BuildTools/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version: 6.0 +// swift-tools-version: 5.9 import PackageDescription diff --git a/BuildTools/Plugins/swift-format-plugin.swift b/BuildTools/Plugins/swift-format-plugin.swift index 4de9b7d5f..543790168 100644 --- a/BuildTools/Plugins/swift-format-plugin.swift +++ b/BuildTools/Plugins/swift-format-plugin.swift @@ -1,3 +1,4 @@ +import Foundation import PackagePlugin @main @@ -13,18 +14,28 @@ import XcodeProjectPlugin extension SwiftFormatBuildToolPlugin: XcodeBuildToolPlugin { // Entry point for creating build commands for targets in Xcode projects. func createBuildCommands(context: XcodePluginContext, target: XcodeTarget) throws -> [Command] { + #if swift(>=6.0) + let swift = try context.tool(named: "swift").url + let xcodeProjectDirectoryURL = context.xcodeProject.directoryURL + #else + let swift = try context.tool(named: "swift").path + let xcodeProjectDirectoryURL = URL(fileURLWithPath: context.xcodeProject.directory.string) + #endif // Find the code generator tool to run (replace this with the actual one). - print("SwiftFormatBuildToolPlugin -> \(context.xcodeProject.directoryURL.path())") - let configFile = context.xcodeProject.directoryURL.appending(path: ".swift-format") + print("SwiftFormatBuildToolPlugin -> \(xcodeProjectDirectoryURL.path())") + let configFile = xcodeProjectDirectoryURL.appending(path: ".swift-format") // Currently check only 'SwiftUI' source code. - let sourceFiles = context.xcodeProject.directoryURL.appending(path: "Sources/SwiftUI") - // let sourceFiles = context.xcodeProject.directoryURL.appending(path: "Sources") - // let testFiles = context.xcodeProject.directoryURL.appending(path: "Tests") - let buildToolsFiles = context.xcodeProject.directoryURL.appending(path: "BuildTools") + let sourceFiles = xcodeProjectDirectoryURL.appending(path: "Sources/SwiftUI") + // let sourceFiles = xcodeProjectDirectoryURL.appending(path: "Sources") + // let testFiles = xcodeProjectDirectoryURL.appending(path: "Tests") + let buildToolsFiles = xcodeProjectDirectoryURL.appending(path: "BuildTools") + let examplesFiles = [ + xcodeProjectDirectoryURL.appending(path: "Examples/SamplesSwiftUI").path() + ] return [ .buildCommand( displayName: "Run swift format(xcode)", - executable: try context.tool(named: "swift").url, + executable: swift, arguments: [ "format", "lint", @@ -34,12 +45,11 @@ extension SwiftFormatBuildToolPlugin: XcodeBuildToolPlugin { sourceFiles.path(), //testFiles.path(), buildToolsFiles.path(), - ], + ] + examplesFiles, inputFiles: [], outputFiles: [] ) ] } } - #endif diff --git a/Examples/Maps-SwiftUI/Maps-SwiftUI.xcodeproj/project.pbxproj b/Examples/Maps-SwiftUI/Maps-SwiftUI.xcodeproj/project.pbxproj index 71e73f6af..1163657ec 100644 --- a/Examples/Maps-SwiftUI/Maps-SwiftUI.xcodeproj/project.pbxproj +++ b/Examples/Maps-SwiftUI/Maps-SwiftUI.xcodeproj/project.pbxproj @@ -7,10 +7,9 @@ objects = { /* Begin PBXBuildFile section */ + 54476E712DBA5F3C00603086 /* UIHostingController+ignoreKeyboard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54476E702DBA5F3C00603086 /* UIHostingController+ignoreKeyboard.swift */; }; 6467E8642699AC5F00565F4F /* SurfaceAppearance+phone.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6467E8632699AC5F00565F4F /* SurfaceAppearance+phone.swift */; }; 6467E86A2699B19D00565F4F /* SearchPanelPhoneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6467E8692699B19D00565F4F /* SearchPanelPhoneDelegate.swift */; }; - 649A122926C14D0900DAB961 /* UIHostingController+ignoreKeyboard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 649A122826C14D0900DAB961 /* UIHostingController+ignoreKeyboard.swift */; }; - 649A122D26C168CF00DAB961 /* View+floatingPanelGrabberHandlePadding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 649A122C26C168CF00DAB961 /* View+floatingPanelGrabberHandlePadding.swift */; }; 64A5B7232691323900BCAA05 /* MapsApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64A5B7222691323900BCAA05 /* MapsApp.swift */; }; 64A5B7252691323900BCAA05 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64A5B7242691323900BCAA05 /* ContentView.swift */; }; 64A5B734269133DC00BCAA05 /* FloatingPanel.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 64A5B733269133DC00BCAA05 /* FloatingPanel.framework */; }; @@ -20,11 +19,6 @@ 64A5B73E269147DC00BCAA05 /* SearchBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64A5B73D269147DC00BCAA05 /* SearchBar.swift */; }; 64A5B7402691532400BCAA05 /* ResultsList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64A5B73F2691532400BCAA05 /* ResultsList.swift */; }; 64A5B7422691541A00BCAA05 /* HostingCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64A5B7412691541A00BCAA05 /* HostingCell.swift */; }; - 64F7E83126AD70EB00A0E0F7 /* View+floatingPanelContentInsetAdjustmentBehavior.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64F7E82C26AD70EB00A0E0F7 /* View+floatingPanelContentInsetAdjustmentBehavior.swift */; }; - 64F7E83226AD70EB00A0E0F7 /* View+floatingPanelContentMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64F7E82D26AD70EB00A0E0F7 /* View+floatingPanelContentMode.swift */; }; - 64F7E83326AD70EB00A0E0F7 /* FloatingPanelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64F7E82E26AD70EB00A0E0F7 /* FloatingPanelView.swift */; }; - 64F7E83426AD70EB00A0E0F7 /* View+floatingPanel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64F7E82F26AD70EB00A0E0F7 /* View+floatingPanel.swift */; }; - 64F7E83526AD70EB00A0E0F7 /* View+floatingPanelSurfaceAppearance.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64F7E83026AD70EB00A0E0F7 /* View+floatingPanelSurfaceAppearance.swift */; }; /* End PBXBuildFile section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -42,10 +36,9 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 54476E702DBA5F3C00603086 /* UIHostingController+ignoreKeyboard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIHostingController+ignoreKeyboard.swift"; sourceTree = ""; }; 6467E8632699AC5F00565F4F /* SurfaceAppearance+phone.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SurfaceAppearance+phone.swift"; sourceTree = ""; }; 6467E8692699B19D00565F4F /* SearchPanelPhoneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchPanelPhoneDelegate.swift; sourceTree = ""; }; - 649A122826C14D0900DAB961 /* UIHostingController+ignoreKeyboard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIHostingController+ignoreKeyboard.swift"; sourceTree = ""; }; - 649A122C26C168CF00DAB961 /* View+floatingPanelGrabberHandlePadding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+floatingPanelGrabberHandlePadding.swift"; sourceTree = ""; }; 64A5B71F2691323900BCAA05 /* Maps-SwiftUI.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Maps-SwiftUI.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 64A5B7222691323900BCAA05 /* MapsApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapsApp.swift; sourceTree = ""; }; 64A5B7242691323900BCAA05 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; @@ -55,11 +48,6 @@ 64A5B73D269147DC00BCAA05 /* SearchBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchBar.swift; sourceTree = ""; }; 64A5B73F2691532400BCAA05 /* ResultsList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResultsList.swift; sourceTree = ""; }; 64A5B7412691541A00BCAA05 /* HostingCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HostingCell.swift; sourceTree = ""; }; - 64F7E82C26AD70EB00A0E0F7 /* View+floatingPanelContentInsetAdjustmentBehavior.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "View+floatingPanelContentInsetAdjustmentBehavior.swift"; sourceTree = ""; }; - 64F7E82D26AD70EB00A0E0F7 /* View+floatingPanelContentMode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "View+floatingPanelContentMode.swift"; sourceTree = ""; }; - 64F7E82E26AD70EB00A0E0F7 /* FloatingPanelView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FloatingPanelView.swift; sourceTree = ""; }; - 64F7E82F26AD70EB00A0E0F7 /* View+floatingPanel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "View+floatingPanel.swift"; sourceTree = ""; }; - 64F7E83026AD70EB00A0E0F7 /* View+floatingPanelSurfaceAppearance.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "View+floatingPanelSurfaceAppearance.swift"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -94,10 +82,10 @@ 64A5B7212691323900BCAA05 /* Maps */ = { isa = PBXGroup; children = ( - 64F7E82B26AD70EB00A0E0F7 /* FloatingPanel */, 64BE55702691BBB0006D98BD /* Representable */, 64A5B7222691323900BCAA05 /* MapsApp.swift */, 64A5B7242691323900BCAA05 /* ContentView.swift */, + 54476E702DBA5F3C00603086 /* UIHostingController+ignoreKeyboard.swift */, 64A5B73B2691469900BCAA05 /* FloatingPanelContentView.swift */, 6467E8692699B19D00565F4F /* SearchPanelPhoneDelegate.swift */, 6467E8632699AC5F00565F4F /* SurfaceAppearance+phone.swift */, @@ -124,20 +112,6 @@ path = Representable; sourceTree = ""; }; - 64F7E82B26AD70EB00A0E0F7 /* FloatingPanel */ = { - isa = PBXGroup; - children = ( - 64F7E82E26AD70EB00A0E0F7 /* FloatingPanelView.swift */, - 64F7E82F26AD70EB00A0E0F7 /* View+floatingPanel.swift */, - 64F7E82C26AD70EB00A0E0F7 /* View+floatingPanelContentInsetAdjustmentBehavior.swift */, - 64F7E82D26AD70EB00A0E0F7 /* View+floatingPanelContentMode.swift */, - 64F7E83026AD70EB00A0E0F7 /* View+floatingPanelSurfaceAppearance.swift */, - 649A122C26C168CF00DAB961 /* View+floatingPanelGrabberHandlePadding.swift */, - 649A122826C14D0900DAB961 /* UIHostingController+ignoreKeyboard.swift */, - ); - path = FloatingPanel; - sourceTree = ""; - }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -212,17 +186,11 @@ 64A5B738269134CA00BCAA05 /* VisualEffectBlur.swift in Sources */, 64A5B7402691532400BCAA05 /* ResultsList.swift in Sources */, 6467E8642699AC5F00565F4F /* SurfaceAppearance+phone.swift in Sources */, - 64F7E83226AD70EB00A0E0F7 /* View+floatingPanelContentMode.swift in Sources */, + 54476E712DBA5F3C00603086 /* UIHostingController+ignoreKeyboard.swift in Sources */, 64A5B7252691323900BCAA05 /* ContentView.swift in Sources */, 64A5B7232691323900BCAA05 /* MapsApp.swift in Sources */, 64A5B73C2691469900BCAA05 /* FloatingPanelContentView.swift in Sources */, - 649A122D26C168CF00DAB961 /* View+floatingPanelGrabberHandlePadding.swift in Sources */, - 64F7E83326AD70EB00A0E0F7 /* FloatingPanelView.swift in Sources */, - 64F7E83426AD70EB00A0E0F7 /* View+floatingPanel.swift in Sources */, - 64F7E83526AD70EB00A0E0F7 /* View+floatingPanelSurfaceAppearance.swift in Sources */, - 64F7E83126AD70EB00A0E0F7 /* View+floatingPanelContentInsetAdjustmentBehavior.swift in Sources */, 64A5B73E269147DC00BCAA05 /* SearchBar.swift in Sources */, - 649A122926C14D0900DAB961 /* UIHostingController+ignoreKeyboard.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Examples/Maps-SwiftUI/Maps/FloatingPanel/FloatingPanelView.swift b/Examples/Maps-SwiftUI/Maps/FloatingPanel/FloatingPanelView.swift deleted file mode 100644 index e03763231..000000000 --- a/Examples/Maps-SwiftUI/Maps/FloatingPanel/FloatingPanelView.swift +++ /dev/null @@ -1,133 +0,0 @@ -// Copyright 2021 the FloatingPanel authors. All rights reserved. MIT license. - -import FloatingPanel -import SwiftUI - -/// A proxy for exposing the methods of the floating panel controller. -public struct FloatingPanelProxy { - /// The associated floating panel controller. - public weak var fpc: FloatingPanelController? - - /// Tracks the specified scroll view to correspond with the scroll. - /// - /// - Parameter scrollView: Specify a scroll view to continuously and - /// seamlessly work in concert with interactions of the surface view. - public func track(scrollView: UIScrollView) { - fpc?.track(scrollView: scrollView) - } - - /// Moves the floating panel to the specified position. - /// - /// - Parameters: - /// - floatingPanelState: The state to move to. - /// - animated: `true` to animate the transition to the new state; `false` - /// otherwise. - public func move( - to floatingPanelState: FloatingPanelState, - animated: Bool, - completion: (() -> Void)? = nil - ) { - fpc?.move(to: floatingPanelState, animated: animated, completion: completion) - } -} - -/// A view with an associated floating panel. -struct FloatingPanelView: UIViewControllerRepresentable { - /// A type that conforms to the `FloatingPanelControllerDelegate` protocol. - var delegate: FloatingPanelControllerDelegate? - - /// The behavior for determining the adjusted content offsets. - @Environment(\.contentInsetAdjustmentBehavior) var contentInsetAdjustmentBehavior - - /// Constants that define how a panel content fills in the surface. - @Environment(\.contentMode) var contentMode - - /// The floating panel grabber handle offset. - @Environment(\.grabberHandlePadding) var grabberHandlePadding - - /// The floating panel `surfaceView` appearance. - @Environment(\.surfaceAppearance) var surfaceAppearance - - /// The view builder that creates the floating panel parent view content. - @ViewBuilder var content: Content - - /// The view builder that creates the floating panel content. - @ViewBuilder var floatingPanelContent: (FloatingPanelProxy) -> FloatingPanelContent - - public func makeUIViewController(context: Context) -> UIHostingController { - let hostingController = UIHostingController(rootView: content) - hostingController.view.backgroundColor = nil - // We need to wait for the current runloop cycle to complete before our - // view is actually added (into the view hierarchy), otherwise the - // environment is not ready yet. - DispatchQueue.main.async { - context.coordinator.setupFloatingPanel(hostingController) - } - return hostingController - } - - public func updateUIViewController( - _ uiViewController: UIHostingController, - context: Context - ) { - context.coordinator.updateIfNeeded() - } - - public func makeCoordinator() -> Coordinator { - Coordinator(parent: self) - } - - /// `FloatingPanelView` coordinator. - /// - /// Responsible to setup the view hierarchy and floating panel. - final class Coordinator { - private let parent: FloatingPanelView - private lazy var fpc = FloatingPanelController() - - init(parent: FloatingPanelView) { - self.parent = parent - } - - func setupFloatingPanel(_ parentViewController: UIViewController) { - updateIfNeeded() - let panelContent = parent.floatingPanelContent(FloatingPanelProxy(fpc: fpc)) - let hostingViewController = UIHostingController( - rootView: panelContent, - ignoresKeyboard: true - ) - hostingViewController.view.backgroundColor = nil - let contentViewController = UIViewController() - contentViewController.view.addSubview(hostingViewController.view) - fpc.set(contentViewController: contentViewController) - fpc.addPanel(toParent: parentViewController, animated: false) - - hostingViewController.view.translatesAutoresizingMaskIntoConstraints = false - let bottomConstraint = hostingViewController.view.bottomAnchor.constraint(equalTo: contentViewController.view.bottomAnchor) - bottomConstraint.priority = .defaultHigh - NSLayoutConstraint.activate([ - hostingViewController.view.topAnchor.constraint(equalTo: contentViewController.view.topAnchor), - hostingViewController.view.leadingAnchor.constraint(equalTo: contentViewController.view.leadingAnchor), - hostingViewController.view.trailingAnchor.constraint(equalTo: contentViewController.view.trailingAnchor), - bottomConstraint - ]) - } - - func updateIfNeeded() { - if fpc.contentInsetAdjustmentBehavior != parent.contentInsetAdjustmentBehavior { - fpc.contentInsetAdjustmentBehavior = parent.contentInsetAdjustmentBehavior - } - if fpc.contentMode != parent.contentMode { - fpc.contentMode = parent.contentMode - } - if fpc.delegate !== parent.delegate { - fpc.delegate = parent.delegate - } - if fpc.surfaceView.grabberHandlePadding != parent.grabberHandlePadding { - fpc.surfaceView.grabberHandlePadding = parent.grabberHandlePadding - } - if fpc.surfaceView.appearance != parent.surfaceAppearance { - fpc.surfaceView.appearance = parent.surfaceAppearance - } - } - } -} diff --git a/Examples/Maps-SwiftUI/Maps/FloatingPanel/UIHostingController+ignoreKeyboard.swift b/Examples/Maps-SwiftUI/Maps/FloatingPanel/UIHostingController+ignoreKeyboard.swift deleted file mode 100644 index 7d9e53083..000000000 --- a/Examples/Maps-SwiftUI/Maps/FloatingPanel/UIHostingController+ignoreKeyboard.swift +++ /dev/null @@ -1,44 +0,0 @@ -// Copyright 2021 the FloatingPanel authors. All rights reserved. MIT license. - -import SwiftUI - -/// This extension makes sure SwiftUI views are not affected by iOS keyboard. -/// -/// Credits to https://steipete.me/posts/disabling-keyboard-avoidance-in-swiftui-uihostingcontroller/ -extension UIHostingController { - public convenience init(rootView: Content, ignoresKeyboard: Bool) { - self.init(rootView: rootView) - - if ignoresKeyboard { - guard let viewClass = object_getClass(view) else { return } - - let viewSubclassName = String( - cString: class_getName(viewClass) - ).appending("_IgnoresKeyboard") - - if let viewSubclass = NSClassFromString(viewSubclassName) { - object_setClass(view, viewSubclass) - } else { - guard - let viewClassNameUtf8 = (viewSubclassName as NSString).utf8String, - let viewSubclass = objc_allocateClassPair(viewClass, viewClassNameUtf8, 0) - else { return } - - if let method = class_getInstanceMethod( - viewClass, - NSSelectorFromString("keyboardWillShowWithNotification:") - ) { - let keyboardWillShow: @convention(block) (AnyObject, AnyObject) -> Void = { _, _ in } - class_addMethod( - viewSubclass, - NSSelectorFromString("keyboardWillShowWithNotification:"), - imp_implementationWithBlock(keyboardWillShow), - method_getTypeEncoding(method) - ) - } - objc_registerClassPair(viewSubclass) - object_setClass(view, viewSubclass) - } - } - } -} diff --git a/Examples/Maps-SwiftUI/Maps/FloatingPanel/View+floatingPanel.swift b/Examples/Maps-SwiftUI/Maps/FloatingPanel/View+floatingPanel.swift deleted file mode 100644 index 43ed1c3d1..000000000 --- a/Examples/Maps-SwiftUI/Maps/FloatingPanel/View+floatingPanel.swift +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright 2021 the FloatingPanel authors. All rights reserved. MIT license. - -import FloatingPanel -import SwiftUI - -extension View { - /// Presents a floating panel using the given closure as its content. - /// - /// The modifier's content view builder receives a `FloatingPanelProxy` - /// instance; you use the proxy's methods to interact with the associated - /// `FloatingPanelController`. - /// - /// - Parameters: - /// - delegate: A type that conforms to the - /// `FloatingPanelControllerDelegate` protocol. You have comprehensive - /// control over the floating panel behavior when you use a delegate. - /// - floatingPanelContent: The floating panel content. This view builder - /// receives a `FloatingPanelProxy` instance that you use to interact - /// with the `FloatingPanelController`. - public func floatingPanel( - delegate: FloatingPanelControllerDelegate? = nil, - @ViewBuilder _ floatingPanelContent: @escaping (_: FloatingPanelProxy) -> FloatingPanelContent - ) -> some View { - FloatingPanelView( - delegate: delegate, - content: { self }, - floatingPanelContent: floatingPanelContent - ) - .ignoresSafeArea() - } -} diff --git a/Examples/Maps-SwiftUI/Maps/FloatingPanel/View+floatingPanelContentInsetAdjustmentBehavior.swift b/Examples/Maps-SwiftUI/Maps/FloatingPanel/View+floatingPanelContentInsetAdjustmentBehavior.swift deleted file mode 100644 index 99094dc35..000000000 --- a/Examples/Maps-SwiftUI/Maps/FloatingPanel/View+floatingPanelContentInsetAdjustmentBehavior.swift +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright 2021 the FloatingPanel authors. All rights reserved. MIT license. - -import FloatingPanel -import SwiftUI - -struct ContentInsetKey: EnvironmentKey { - static var defaultValue: FloatingPanelController.ContentInsetAdjustmentBehavior = .always -} - -extension EnvironmentValues { - /// The behavior for determining the adjusted content offsets. - var contentInsetAdjustmentBehavior: FloatingPanelController.ContentInsetAdjustmentBehavior { - get { self[ContentInsetKey.self] } - set { self[ContentInsetKey.self] = newValue } - } -} - -extension View { - /// Sets the content inset adjustment behavior for floating panels within - /// this view. - /// - /// Use this modifier to set a specific content inset adjustment behavior - /// for floating panel instances within a view: - /// - /// MainView() - /// .floatingPanel { _ in - /// FloatingPanelContent() - /// } - /// .floatingPanelContentInsetAdjustmentBehavior(.never) - /// - /// - Parameter contentInsetAdjustmentBehavior: The content inset adjustment - /// behavior to set. - public func floatingPanelContentInsetAdjustmentBehavior( - _ contentInsetAdjustmentBehavior: FloatingPanelController.ContentInsetAdjustmentBehavior - ) -> some View { - environment(\.contentInsetAdjustmentBehavior, contentInsetAdjustmentBehavior) - } -} diff --git a/Examples/Maps-SwiftUI/Maps/FloatingPanel/View+floatingPanelContentMode.swift b/Examples/Maps-SwiftUI/Maps/FloatingPanel/View+floatingPanelContentMode.swift deleted file mode 100644 index 05594c2f9..000000000 --- a/Examples/Maps-SwiftUI/Maps/FloatingPanel/View+floatingPanelContentMode.swift +++ /dev/null @@ -1,37 +0,0 @@ -// Copyright 2021 the FloatingPanel authors. All rights reserved. MIT license. - -import FloatingPanel -import SwiftUI - -struct ContentModeKey: EnvironmentKey { - static var defaultValue: FloatingPanelController.ContentMode = .static -} - -extension EnvironmentValues { - /// Used to determine how the floating panel controller lays out the content - /// view when the surface position changes. - var contentMode: FloatingPanelController.ContentMode { - get { self[ContentModeKey.self] } - set { self[ContentModeKey.self] = newValue } - } -} - -extension View { - /// Sets the content mode for floating panels within this view. - /// - /// Use this modifier to set a specific content mode for floating panel - /// instances within a view: - /// - /// MainView() - /// .floatingPanel { _ in - /// FloatingPanelContent() - /// } - /// .floatingPanelContentMode(.static) - /// - /// - Parameter contentMode: The content mode to set. - public func floatingPanelContentMode( - _ contentMode: FloatingPanelController.ContentMode - ) -> some View { - environment(\.contentMode, contentMode) - } -} diff --git a/Examples/Maps-SwiftUI/Maps/FloatingPanel/View+floatingPanelGrabberHandlePadding.swift b/Examples/Maps-SwiftUI/Maps/FloatingPanel/View+floatingPanelGrabberHandlePadding.swift deleted file mode 100644 index 64ff6e106..000000000 --- a/Examples/Maps-SwiftUI/Maps/FloatingPanel/View+floatingPanelGrabberHandlePadding.swift +++ /dev/null @@ -1,36 +0,0 @@ -// Copyright 2021 the FloatingPanel authors. All rights reserved. MIT license. - -import FloatingPanel -import SwiftUI - -struct GrabberHandlePaddingKey: EnvironmentKey { - static var defaultValue: CGFloat = 6.0 -} - -extension EnvironmentValues { - /// The offset of the grabber handle from the interactive edge. - var grabberHandlePadding: CGFloat { - get { self[GrabberHandlePaddingKey.self] } - set { self[GrabberHandlePaddingKey.self] = newValue } - } -} - -extension View { - /// Sets the grabber handle padding for floating panels within this view. - /// - /// Use this modifier to set a specific padding to floating panel instances - /// within a view: - /// - /// MainView() - /// .floatingPanel { _ in - /// FloatingPanelContent() - /// } - /// .floatingPanelGrabberHandlePadding(16) - /// - /// - Parameter padding: The grabber handle padding to set. - public func floatingPanelGrabberHandlePadding( - _ padding: CGFloat - ) -> some View { - environment(\.grabberHandlePadding, padding) - } -} diff --git a/Examples/Maps-SwiftUI/Maps/FloatingPanel/View+floatingPanelSurfaceAppearance.swift b/Examples/Maps-SwiftUI/Maps/FloatingPanel/View+floatingPanelSurfaceAppearance.swift deleted file mode 100644 index 0f0045418..000000000 --- a/Examples/Maps-SwiftUI/Maps/FloatingPanel/View+floatingPanelSurfaceAppearance.swift +++ /dev/null @@ -1,44 +0,0 @@ -// Copyright 2021 the FloatingPanel authors. All rights reserved. MIT license. - -import FloatingPanel -import SwiftUI - -struct SurfaceAppearanceKey: EnvironmentKey { - static var defaultValue = SurfaceAppearance() -} - -extension EnvironmentValues { - /// The appearance of a surface view. - var surfaceAppearance: SurfaceAppearance { - get { self[SurfaceAppearanceKey.self] } - set { self[SurfaceAppearanceKey.self] = newValue } - } -} - -extension View { - /// Sets the surface appearance for floating panels within this view. - /// - /// Use this modifier to set a specific surface appearance for floating - /// panel instances within a view: - /// - /// MainView() - /// .floatingPanel { _ in - /// FloatingPanelContent() - /// } - /// .floatingPanelSurfaceAppearance(.transparent) - /// - /// extension SurfaceAppearance { - /// static var transparent: SurfaceAppearance { - /// let appearance = SurfaceAppearance() - /// appearance.backgroundColor = .clear - /// return appearance - /// } - /// } - /// - /// - Parameter surfaceAppearance: The surface appearance to set. - public func floatingPanelSurfaceAppearance( - _ surfaceAppearance: SurfaceAppearance - ) -> some View { - environment(\.surfaceAppearance, surfaceAppearance) - } -} diff --git a/Examples/Maps-SwiftUI/Maps/FloatingPanelContentView.swift b/Examples/Maps-SwiftUI/Maps/FloatingPanelContentView.swift index 1be68d935..e82c31727 100644 --- a/Examples/Maps-SwiftUI/Maps/FloatingPanelContentView.swift +++ b/Examples/Maps-SwiftUI/Maps/FloatingPanelContentView.swift @@ -1,6 +1,7 @@ // Copyright 2021 the FloatingPanel authors. All rights reserved. MIT license. import SwiftUI +import FloatingPanel struct FloatingPanelContentView: View { @State private var searchText = "" @@ -10,7 +11,9 @@ struct FloatingPanelContentView: View { var body: some View { VStack(spacing: 0) { searchBar - resultsList + ResultsList() + .ignoresSafeArea() // Needs here with `floatingPanelScrollTracking(proxy:)` on iOS 15 + .floatingPanelScrollTracking(proxy: proxy) } // 👇🏻 for the floating panel grabber handle. .padding(.top, 6) @@ -36,8 +39,4 @@ struct FloatingPanelContentView: View { UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil) } } - - var resultsList: some View { - ResultsList(onScrollViewCreated: proxy.track(scrollView:)) - } } diff --git a/Examples/Maps-SwiftUI/Maps/MapsApp.swift b/Examples/Maps-SwiftUI/Maps/MapsApp.swift index 78a40f38b..aa1b459d1 100644 --- a/Examples/Maps-SwiftUI/Maps/MapsApp.swift +++ b/Examples/Maps-SwiftUI/Maps/MapsApp.swift @@ -1,13 +1,16 @@ // Copyright 2021 the FloatingPanel authors. All rights reserved. MIT license. import SwiftUI +import FloatingPanel @main struct MapsApp: App { var body: some Scene { WindowGroup { ContentView() - .floatingPanel(delegate: SearchPanelPhoneDelegate()) { proxy in + .floatingPanel( + coordinator: MapPanelCoordinator.self + ) { proxy in FloatingPanelContentView(proxy: proxy) } .floatingPanelSurfaceAppearance(.phone) @@ -15,4 +18,89 @@ struct MapsApp: App { .floatingPanelContentInsetAdjustmentBehavior(.never) } } + func onFloatingPanelEvent(_ event: MapPanelCoordinator.Event) {} +} + +final class MapPanelCoordinator: FloatingPanelCoordinator { + enum Event {} + + let action: (Event) -> () + let proxy: FloatingPanelProxy + + private lazy var delegate: FloatingPanelControllerDelegate? = self + + init(action: @escaping (Event) -> ()) { + self.action = action + self.proxy = .init(controller: FloatingPanelController()) + } + + public func setupFloatingPanel( + mainHostingController: UIHostingController
, + contentHostingController: UIHostingController + ) { + mainHostingController.ignoresKeyboardSafeArea() + contentHostingController.ignoresKeyboardSafeArea() + + if #available(iOS 16, *) { + // Set the delegate object + controller.delegate = delegate + + // Set up the content + contentHostingController.view.backgroundColor = nil + controller.set(contentViewController: contentHostingController) + + // Show the panel + controller.addPanel(toParent: mainHostingController, animated: false) + } else { + // NOTE: Fix floating panel content view constraints (#549) + // This issue happens on iOS 15 or earlier. + + // Set the delegate object + controller.delegate = delegate + + // Set up the content + contentHostingController.view.backgroundColor = nil + let contentWrapperViewController = UIViewController() + contentWrapperViewController.view.addSubview(contentHostingController.view) + contentWrapperViewController.addChild(contentHostingController) + contentHostingController.didMove(toParent: contentWrapperViewController) + controller.set(contentViewController: contentWrapperViewController) + + // Show the panel + controller.addPanel(toParent: mainHostingController, animated: false) + + contentHostingController.view.translatesAutoresizingMaskIntoConstraints = false + let bottomConstraint = contentHostingController.view.bottomAnchor.constraint( + equalTo: contentWrapperViewController.view.bottomAnchor + ) + bottomConstraint.priority = .defaultHigh + NSLayoutConstraint.activate([ + contentHostingController.view.topAnchor.constraint( + equalTo: contentWrapperViewController.view.topAnchor + ), + contentHostingController.view.leadingAnchor.constraint( + equalTo: contentWrapperViewController.view.leadingAnchor + ), + contentHostingController.view.trailingAnchor.constraint( + equalTo: contentWrapperViewController.view.trailingAnchor + ), + bottomConstraint + ]) + } + } + + func onUpdate( + context: UIViewControllerRepresentableContext + ) where Representable: UIViewControllerRepresentable {} +} + +extension MapPanelCoordinator: FloatingPanelControllerDelegate { + func floatingPanelWillBeginAttracting( + _ fpc: FloatingPanelController, + to state: FloatingPanelState + ) { + if fpc.state == .full { + UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil) + } + } } diff --git a/Examples/Maps-SwiftUI/Maps/ResultsList.swift b/Examples/Maps-SwiftUI/Maps/ResultsList.swift index df7c5dee5..372d2dbda 100644 --- a/Examples/Maps-SwiftUI/Maps/ResultsList.swift +++ b/Examples/Maps-SwiftUI/Maps/ResultsList.swift @@ -3,21 +3,16 @@ import SwiftUI struct ResultsList: UIViewControllerRepresentable { - var onScrollViewCreated: (_ scrollView: UIScrollView) -> Void - func makeUIViewController( context: Context ) -> ResultsTableViewController { - let rtvc = ResultsTableViewController() - onScrollViewCreated(rtvc.tableView) - return rtvc + ResultsTableViewController() } func updateUIViewController( _ uiViewController: ResultsTableViewController, context: Context - ) { - } + ) {} } final class ResultsTableViewController: UITableViewController { diff --git a/Examples/Maps-SwiftUI/Maps/UIHostingController+ignoreKeyboard.swift b/Examples/Maps-SwiftUI/Maps/UIHostingController+ignoreKeyboard.swift new file mode 100644 index 000000000..3acd78118 --- /dev/null +++ b/Examples/Maps-SwiftUI/Maps/UIHostingController+ignoreKeyboard.swift @@ -0,0 +1,46 @@ +// Copyright 2021 the FloatingPanel authors. All rights reserved. MIT license. + +import SwiftUI + +/// This extension makes sure SwiftUI views are not affected by iOS keyboard. +/// +/// Credits to https://steipete.me/posts/disabling-keyboard-avoidance-in-swiftui-uihostingcontroller/ +extension UIHostingController { + convenience init(rootView: Content, ignoresKeyboard: Bool) { + self.init(rootView: rootView) + if ignoresKeyboard { + ignoresKeyboardSafeArea() + } + } + func ignoresKeyboardSafeArea() { + guard let viewClass = object_getClass(view) else { return } + + let viewSubclassName = String( + cString: class_getName(viewClass) + ).appending("_IgnoresKeyboard") + + if let viewSubclass = NSClassFromString(viewSubclassName) { + object_setClass(view, viewSubclass) + } else { + guard + let viewClassNameUtf8 = (viewSubclassName as NSString).utf8String, + let viewSubclass = objc_allocateClassPair(viewClass, viewClassNameUtf8, 0) + else { return } + + if let method = class_getInstanceMethod( + viewClass, + NSSelectorFromString("keyboardWillShowWithNotification:") + ) { + let keyboardWillShow: @convention(block) (AnyObject, AnyObject) -> Void = { _, _ in } + class_addMethod( + viewSubclass, + NSSelectorFromString("keyboardWillShowWithNotification:"), + imp_implementationWithBlock(keyboardWillShow), + method_getTypeEncoding(method) + ) + } + objc_registerClassPair(viewSubclass) + object_setClass(view, viewSubclass) + } + } +} diff --git a/Examples/SamplesObjC/SamplesObjC.xcodeproj/project.pbxproj b/Examples/SamplesObjC/SamplesObjC.xcodeproj/project.pbxproj index c689452b3..346599ab6 100644 --- a/Examples/SamplesObjC/SamplesObjC.xcodeproj/project.pbxproj +++ b/Examples/SamplesObjC/SamplesObjC.xcodeproj/project.pbxproj @@ -324,7 +324,6 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_STYLE = Automatic; DEFINES_MODULE = NO; - DEVELOPMENT_TEAM = J3D7L9FHSS; INFOPLIST_FILE = SamplesObjC/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -350,7 +349,6 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_STYLE = Automatic; DEFINES_MODULE = NO; - DEVELOPMENT_TEAM = J3D7L9FHSS; INFOPLIST_FILE = SamplesObjC/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", diff --git a/Examples/SamplesSwiftUI/README.md b/Examples/SamplesSwiftUI/README.md new file mode 100644 index 000000000..0f46e79ec --- /dev/null +++ b/Examples/SamplesSwiftUI/README.md @@ -0,0 +1,6 @@ +# SamplesSwiftUI + +## Requirements + +* iOS 15 or later +* Xcode 16 or later diff --git a/Examples/SamplesSwiftUI/SamplesSwiftUI.xcodeproj/project.pbxproj b/Examples/SamplesSwiftUI/SamplesSwiftUI.xcodeproj/project.pbxproj new file mode 100644 index 000000000..cdbe75e0e --- /dev/null +++ b/Examples/SamplesSwiftUI/SamplesSwiftUI.xcodeproj/project.pbxproj @@ -0,0 +1,364 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 77; + objects = { + +/* Begin PBXBuildFile section */ + 549197B42CB41D48006C2328 /* FloatingPanel.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 549197B32CB41D48006C2328 /* FloatingPanel.framework */; }; + 549197B52CB41D48006C2328 /* FloatingPanel.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 549197B32CB41D48006C2328 /* FloatingPanel.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 54EF58B52DC8AAEE00B4880E /* README.md in Resources */ = {isa = PBXBuildFile; fileRef = 54EF58B42DC8AAEA00B4880E /* README.md */; }; +/* End PBXBuildFile section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 549197B62CB41D48006C2328 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + 549197B52CB41D48006C2328 /* FloatingPanel.framework in Embed Frameworks */, + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 5491979E2CB41D01006C2328 /* SamplesSwiftUI.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SamplesSwiftUI.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 549197B32CB41D48006C2328 /* FloatingPanel.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = FloatingPanel.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 54EF58B42DC8AAEA00B4880E /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFileSystemSynchronizedRootGroup section */ + 549197A02CB41D01006C2328 /* SamplesSwiftUI */ = { + isa = PBXFileSystemSynchronizedRootGroup; + path = SamplesSwiftUI; + sourceTree = ""; + }; +/* End PBXFileSystemSynchronizedRootGroup section */ + +/* Begin PBXFrameworksBuildPhase section */ + 5491979B2CB41D01006C2328 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 549197B42CB41D48006C2328 /* FloatingPanel.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 549197952CB41D01006C2328 = { + isa = PBXGroup; + children = ( + 54EF58B42DC8AAEA00B4880E /* README.md */, + 549197A02CB41D01006C2328 /* SamplesSwiftUI */, + 549197B22CB41D48006C2328 /* Frameworks */, + 5491979F2CB41D01006C2328 /* Products */, + ); + sourceTree = ""; + }; + 5491979F2CB41D01006C2328 /* Products */ = { + isa = PBXGroup; + children = ( + 5491979E2CB41D01006C2328 /* SamplesSwiftUI.app */, + ); + name = Products; + sourceTree = ""; + }; + 549197B22CB41D48006C2328 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 549197B32CB41D48006C2328 /* FloatingPanel.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 5491979D2CB41D01006C2328 /* SamplesSwiftUI */ = { + isa = PBXNativeTarget; + buildConfigurationList = 549197AC2CB41D02006C2328 /* Build configuration list for PBXNativeTarget "SamplesSwiftUI" */; + buildPhases = ( + 5491979A2CB41D01006C2328 /* Sources */, + 5491979B2CB41D01006C2328 /* Frameworks */, + 5491979C2CB41D01006C2328 /* Resources */, + 549197B62CB41D48006C2328 /* Embed Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + fileSystemSynchronizedGroups = ( + 549197A02CB41D01006C2328 /* SamplesSwiftUI */, + ); + name = SamplesSwiftUI; + packageProductDependencies = ( + ); + productName = SamplesSwiftUI; + productReference = 5491979E2CB41D01006C2328 /* SamplesSwiftUI.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 549197962CB41D01006C2328 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1600; + LastUpgradeCheck = 1600; + TargetAttributes = { + 5491979D2CB41D01006C2328 = { + CreatedOnToolsVersion = 16.0; + }; + }; + }; + buildConfigurationList = 549197992CB41D01006C2328 /* Build configuration list for PBXProject "SamplesSwiftUI" */; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 549197952CB41D01006C2328; + minimizedProjectReferenceProxies = 1; + preferredProjectObjectVersion = 77; + productRefGroup = 5491979F2CB41D01006C2328 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 5491979D2CB41D01006C2328 /* SamplesSwiftUI */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 5491979C2CB41D01006C2328 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 54EF58B52DC8AAEE00B4880E /* README.md in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 5491979A2CB41D01006C2328 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 549197AA2CB41D02006C2328 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 18.0; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 549197AB2CB41D02006C2328 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 18.0; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 549197AD2CB41D02006C2328 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = "\"SamplesSwiftUI/Preview Content\""; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = example.SamplesSwiftUI; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 549197AE2CB41D02006C2328 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = "\"SamplesSwiftUI/Preview Content\""; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = example.SamplesSwiftUI; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 549197992CB41D01006C2328 /* Build configuration list for PBXProject "SamplesSwiftUI" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 549197AA2CB41D02006C2328 /* Debug */, + 549197AB2CB41D02006C2328 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 549197AC2CB41D02006C2328 /* Build configuration list for PBXNativeTarget "SamplesSwiftUI" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 549197AD2CB41D02006C2328 /* Debug */, + 549197AE2CB41D02006C2328 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 549197962CB41D01006C2328 /* Project object */; +} diff --git a/Examples/SamplesSwiftUI/SamplesSwiftUI/Assets.xcassets/AccentColor.colorset/Contents.json b/Examples/SamplesSwiftUI/SamplesSwiftUI/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 000000000..eb8789700 --- /dev/null +++ b/Examples/SamplesSwiftUI/SamplesSwiftUI/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Examples/SamplesSwiftUI/SamplesSwiftUI/Assets.xcassets/AppIcon.appiconset/Contents.json b/Examples/SamplesSwiftUI/SamplesSwiftUI/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 000000000..230588010 --- /dev/null +++ b/Examples/SamplesSwiftUI/SamplesSwiftUI/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,35 @@ +{ + "images" : [ + { + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "tinted" + } + ], + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Examples/SamplesSwiftUI/SamplesSwiftUI/Assets.xcassets/Contents.json b/Examples/SamplesSwiftUI/SamplesSwiftUI/Assets.xcassets/Contents.json new file mode 100644 index 000000000..73c00596a --- /dev/null +++ b/Examples/SamplesSwiftUI/SamplesSwiftUI/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Examples/SamplesSwiftUI/SamplesSwiftUI/Preview Content/Preview Assets.xcassets/Contents.json b/Examples/SamplesSwiftUI/SamplesSwiftUI/Preview Content/Preview Assets.xcassets/Contents.json new file mode 100644 index 000000000..73c00596a --- /dev/null +++ b/Examples/SamplesSwiftUI/SamplesSwiftUI/Preview Content/Preview Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Examples/SamplesSwiftUI/SamplesSwiftUI/SampleApp.swift b/Examples/SamplesSwiftUI/SamplesSwiftUI/SampleApp.swift new file mode 100644 index 000000000..2e08eb8ab --- /dev/null +++ b/Examples/SamplesSwiftUI/SamplesSwiftUI/SampleApp.swift @@ -0,0 +1,12 @@ +// Copyright 2025 the FloatingPanel authors. All rights reserved. MIT license. + +import SwiftUI + +@main +struct SampleApp: App { + var body: some Scene { + WindowGroup { + MainView() + } + } +} diff --git a/Examples/SamplesSwiftUI/SamplesSwiftUI/UseCases/InsideTab.swift b/Examples/SamplesSwiftUI/SamplesSwiftUI/UseCases/InsideTab.swift new file mode 100644 index 000000000..fdfa1a7f9 --- /dev/null +++ b/Examples/SamplesSwiftUI/SamplesSwiftUI/UseCases/InsideTab.swift @@ -0,0 +1,34 @@ +// Copyright 2025 the FloatingPanel authors. All rights reserved. MIT license. + +import FloatingPanel +import SwiftUI + +struct InsideTab: View { + var body: some View { + if #available(iOS 18.0, *) { + TabView { + Tab("Main", systemImage: "lanyardcard") { + MainView() + } + Tab("Multi Panel", systemImage: "lanyardcard") { + MultiPanelView() + } + } + } else { + TabView { + MainView() + .tabItem { + Label("Main", systemImage: "lanyardcard") + } + MultiPanelView() + .tabItem { + Label("Multi Panel 22", systemImage: "lanyardcard") + } + } + } + } +} + +#Preview { + InsideTab() +} diff --git a/Examples/SamplesSwiftUI/SamplesSwiftUI/UseCases/MainView.swift b/Examples/SamplesSwiftUI/SamplesSwiftUI/UseCases/MainView.swift new file mode 100644 index 000000000..d0cd5b062 --- /dev/null +++ b/Examples/SamplesSwiftUI/SamplesSwiftUI/UseCases/MainView.swift @@ -0,0 +1,102 @@ +// Copyright 2025 the FloatingPanel authors. All rights reserved. MIT license. + +import FloatingPanel +import SwiftUI +import UIKit + +struct MainView: View { + @State private var panelLayout: FloatingPanelLayout? = MyFloatingPanelLayout() + @State private var panelState: FloatingPanelState? + + var body: some View { + ZStack { + Color.orange + .ignoresSafeArea() + .floatingPanel( + coordinator: MyPanelCoordinator.self + ) { proxy in + ContentView(proxy: proxy) + } + .floatingPanelSurfaceAppearance(.transparent()) + .floatingPanelLayout(panelLayout) + .floatingPanelState($panelState) + + VStack(spacing: 32) { + Button("Move to full") { + withAnimation(.interactiveSpring) { + panelState = .full + } + } + Button { + withAnimation(.interactiveSpring) { + if panelLayout is MyFloatingPanelLayout { + panelLayout = nil + } else { + panelLayout = MyFloatingPanelLayout() + } + } + } label: { + if panelLayout is MyFloatingPanelLayout { + Text("Switch to Default layout") + } else { + Text("Switch to My layout") + } + } + } + } + } +} + +// A custom coordinator object which handles panel context updates and setting up `FloatingPanelControllerDelegate` methods +class MyPanelCoordinator: FloatingPanelCoordinator { + enum Event {} + + let action: (Event) -> Void + let proxy: FloatingPanelProxy + + required init(action: @escaping (MyPanelCoordinator.Event) -> Void) { + self.action = action + self.proxy = .init(controller: FloatingPanelController()) + } + + func setupFloatingPanel( + mainHostingController: UIHostingController
, + contentHostingController: UIHostingController + ) where Main: View, Content: View { + // Set this as the delegate object + controller.delegate = self + + // Set up the content + contentHostingController.view.backgroundColor = .clear + controller.set(contentViewController: contentHostingController) + + // Show the panel + controller.addPanel(toParent: mainHostingController, animated: false) + } + + func onUpdate( + context: UIViewControllerRepresentableContext + ) where Representable: UIViewControllerRepresentable {} +} + +extension MyPanelCoordinator: FloatingPanelControllerDelegate { + func floatingPanelDidChangeState(_ fpc: FloatingPanelController) { + // NOTE: This timing is difference from one of the change of the binding value + // to `floatingPanelState(_:)` modifier + } +} + +// A custom layout object +class MyFloatingPanelLayout: FloatingPanelLayout { + let position: FloatingPanelPosition = .bottom + let initialState: FloatingPanelState = .tip + let anchors: [FloatingPanelState: FloatingPanelLayoutAnchoring] = [ + .full: FloatingPanelLayoutAnchor(absoluteInset: 16.0, edge: .top, referenceGuide: .safeArea), + .half: FloatingPanelLayoutAnchor(fractionalInset: 0.4, edge: .bottom, referenceGuide: .safeArea), + .tip: FloatingPanelLayoutAnchor(absoluteInset: 44.0, edge: .bottom, referenceGuide: .safeArea), + ] +} + +#Preview("MainView") { + MainView() +} diff --git a/Examples/SamplesSwiftUI/SamplesSwiftUI/UseCases/MultiPanel.swift b/Examples/SamplesSwiftUI/SamplesSwiftUI/UseCases/MultiPanel.swift new file mode 100644 index 000000000..cb32bbfdf --- /dev/null +++ b/Examples/SamplesSwiftUI/SamplesSwiftUI/UseCases/MultiPanel.swift @@ -0,0 +1,31 @@ +// Copyright 2025 the FloatingPanel authors. All rights reserved. MIT license. + +import FloatingPanel +import SwiftUI + +struct MultiPanelView: View { + var body: some View { + ZStack { + Color.orange + .ignoresSafeArea() + .floatingPanel( + coordinator: MyPanelCoordinator.self + ) { proxy in + ContentView(proxy: proxy) + } + .floatingPanelSurfaceAppearance(.transparent()) + .floatingPanelContentMode(.fitToBounds) + .floatingPanel( + coordinator: MyPanelCoordinator.self + ) { proxy in + ContentView(proxy: proxy) + } + .floatingPanelContentMode(.static) + .floatingPanelSurfaceAppearance(.transparent(cornerRadius: 24)) + } + } +} + +#Preview { + MultiPanelView() +} diff --git a/Examples/SamplesSwiftUI/SamplesSwiftUI/Views/ContentView.swift b/Examples/SamplesSwiftUI/SamplesSwiftUI/Views/ContentView.swift new file mode 100644 index 000000000..d7d01e96c --- /dev/null +++ b/Examples/SamplesSwiftUI/SamplesSwiftUI/Views/ContentView.swift @@ -0,0 +1,54 @@ +// Copyright 2025 the FloatingPanel authors. All rights reserved. MIT license. + +import FloatingPanel +import SwiftUI + +struct ContentView: View { + let proxy: FloatingPanelProxy + var body: some View { + Group { + if #available(iOS 17.0, *) { + ScrollView { + LazyVStack(spacing: 0) { + ForEach(0...100, id: \.self) { i in + Text("Index \(i)") + .frame(maxWidth: .infinity, alignment: .leading) + .frame(height: 60) + .background(.clear) + } + } + } + .scrollClipDisabled() + .floatingPanelScrollTracking(proxy: proxy) + } else { + ScrollView { + LazyVStack(spacing: 0) { + ForEach(0...100, id: \.self) { i in + Text("Index \(i)") + .frame(maxWidth: .infinity, alignment: .leading) + .frame(height: 60) + .background(.clear) + } + } + } + .floatingPanelScrollTracking(proxy: proxy) { scrollView, _ in + scrollView.clipsToBounds = false + } + } + } + // Prevent revealing underlying content at the bottom of the panel when the panel is moving beyond its fully‑expanded position. + .background { + GeometryReader { geometry in + Rectangle() + .fill(.clear) + .frame(height: geometry.size.height * 2) + .background(.regularMaterial) + } + } + } +} + +#Preview("ContentView") { + // `FloatingPanelProxy` can be instantiated like this. + ContentView(proxy: FloatingPanelProxy(controller: FloatingPanelController())) +} diff --git a/FloatingPanel.xcodeproj/project.pbxproj b/FloatingPanel.xcodeproj/project.pbxproj index 6cb58b53a..7d9486d09 100644 --- a/FloatingPanel.xcodeproj/project.pbxproj +++ b/FloatingPanel.xcodeproj/project.pbxproj @@ -8,6 +8,11 @@ /* Begin PBXBuildFile section */ 542753C622C49A6E00D17955 /* LayoutTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 542753C522C49A6E00D17955 /* LayoutTests.swift */; }; + 5431025B2DB8AAB800A927EF /* View+floatingPanelSurface.swift in Sources */ = {isa = PBXBuildFile; fileRef = 543102592DB8AAB800A927EF /* View+floatingPanelSurface.swift */; }; + 5431025E2DB8AAB800A927EF /* View+floatingPanelConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 543102572DB8AAB800A927EF /* View+floatingPanelConfiguration.swift */; }; + 543102B22DB8B4AC00A927EF /* View+floatingPanelLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 543102B12DB8B4A300A927EF /* View+floatingPanelLayout.swift */; }; + 543102EA2DB8F02E00A927EF /* View+floatingPanelBehavior.swift in Sources */ = {isa = PBXBuildFile; fileRef = 543102E92DB8F02D00A927EF /* View+floatingPanelBehavior.swift */; }; + 543103472DB9B4D400A927EF /* View+floatingPanelScrollTracking.swift in Sources */ = {isa = PBXBuildFile; fileRef = 543103462DB9B4C900A927EF /* View+floatingPanelScrollTracking.swift */; }; 54352E9621A51A2500CBCA08 /* Transitioning.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54352E9521A51A2500CBCA08 /* Transitioning.swift */; }; 54352E9821A521CA00CBCA08 /* PassthroughView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54352E9721A521CA00CBCA08 /* PassthroughView.swift */; }; 5450EEE421646DF500135936 /* Behavior.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5450EEE321646DF500135936 /* Behavior.swift */; }; @@ -21,7 +26,12 @@ 5469F4B024B30E1500537F8A /* LayoutAnchoring.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5469F4AF24B30E1500537F8A /* LayoutAnchoring.swift */; }; 5469F4B224B30F1100537F8A /* Position.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5469F4B124B30F1100537F8A /* Position.swift */; }; 5469F4B424B30F3500537F8A /* LayoutProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5469F4B324B30F3500537F8A /* LayoutProperties.swift */; }; + 5471AF8E2CB53FC8001B7A64 /* View+floatingPanel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5471AF8D2CB53FC8001B7A64 /* View+floatingPanel.swift */; }; + 5471AF902CB53FFC001B7A64 /* FloatingPanelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5471AF8F2CB53FFC001B7A64 /* FloatingPanelView.swift */; }; + 5471AF922CB5462C001B7A64 /* FloatingPanelProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5471AF912CB54628001B7A64 /* FloatingPanelProxy.swift */; }; + 547B44912CC0E8B300F7E68C /* FloatingPanelCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 547B44902CC0E8AD00F7E68C /* FloatingPanelCoordinator.swift */; }; 547F7A9C2A6E946000303905 /* GestureTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 547F7A9B2A6E946000303905 /* GestureTests.swift */; }; + 5484E7432DCF90AC0058E962 /* View+floatingPanelState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5484E7422DCF90A60058E962 /* View+floatingPanelState.swift */; }; 549C371F2361E15E007D8058 /* ExtensionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 549C371E2361E15D007D8058 /* ExtensionTests.swift */; }; 549E944522CF295D0050AECF /* StateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 549E944422CF295D0050AECF /* StateTests.swift */; }; 54A6B6B122968B530077F348 /* CoreTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54A6B6B022968B530077F348 /* CoreTests.swift */; }; @@ -32,6 +42,7 @@ 54CFBFC3215CD045006B5735 /* Layout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CFBFC2215CD045006B5735 /* Layout.swift */; }; 54CFBFC5215CD09C006B5735 /* Core.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CFBFC4215CD09C006B5735 /* Core.swift */; }; 54DBA3DC262E938500D75969 /* Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54DBA3DB262E938500D75969 /* Extensions.swift */; }; + 54DFB9762DC9DF8800006C84 /* SurfaceAppearance+.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54DFB9752DC9DF8200006C84 /* SurfaceAppearance+.swift */; }; 54E3992727141F5100A8F9ED /* FloatingPanel.docc in Sources */ = {isa = PBXBuildFile; fileRef = 54E3992627141F5100A8F9ED /* FloatingPanel.docc */; }; 5D82A6B528D18464006A44BA /* libswiftCoreGraphics.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 5D82A6B428D18461006A44BA /* libswiftCoreGraphics.tbd */; }; /* End PBXBuildFile section */ @@ -49,6 +60,11 @@ /* Begin PBXFileReference section */ 542753C522C49A6E00D17955 /* LayoutTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LayoutTests.swift; sourceTree = ""; }; 542753C722C49A8F00D17955 /* TestSupports.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestSupports.swift; sourceTree = ""; }; + 543102572DB8AAB800A927EF /* View+floatingPanelConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+floatingPanelConfiguration.swift"; sourceTree = ""; }; + 543102592DB8AAB800A927EF /* View+floatingPanelSurface.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+floatingPanelSurface.swift"; sourceTree = ""; }; + 543102B12DB8B4A300A927EF /* View+floatingPanelLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+floatingPanelLayout.swift"; sourceTree = ""; }; + 543102E92DB8F02D00A927EF /* View+floatingPanelBehavior.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+floatingPanelBehavior.swift"; sourceTree = ""; }; + 543103462DB9B4C900A927EF /* View+floatingPanelScrollTracking.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+floatingPanelScrollTracking.swift"; sourceTree = ""; }; 54352E9521A51A2500CBCA08 /* Transitioning.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Transitioning.swift; sourceTree = ""; }; 54352E9721A521CA00CBCA08 /* PassthroughView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PassthroughView.swift; sourceTree = ""; }; 5450EEE321646DF500135936 /* Behavior.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Behavior.swift; sourceTree = ""; }; @@ -64,7 +80,12 @@ 5469F4AF24B30E1500537F8A /* LayoutAnchoring.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LayoutAnchoring.swift; sourceTree = ""; }; 5469F4B124B30F1100537F8A /* Position.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Position.swift; sourceTree = ""; }; 5469F4B324B30F3500537F8A /* LayoutProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LayoutProperties.swift; sourceTree = ""; }; + 5471AF8D2CB53FC8001B7A64 /* View+floatingPanel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+floatingPanel.swift"; sourceTree = ""; }; + 5471AF8F2CB53FFC001B7A64 /* FloatingPanelView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FloatingPanelView.swift; sourceTree = ""; }; + 5471AF912CB54628001B7A64 /* FloatingPanelProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FloatingPanelProxy.swift; sourceTree = ""; }; + 547B44902CC0E8AD00F7E68C /* FloatingPanelCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FloatingPanelCoordinator.swift; sourceTree = ""; }; 547F7A9B2A6E946000303905 /* GestureTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GestureTests.swift; sourceTree = ""; }; + 5484E7422DCF90A60058E962 /* View+floatingPanelState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+floatingPanelState.swift"; sourceTree = ""; }; 549C371E2361E15D007D8058 /* ExtensionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExtensionTests.swift; sourceTree = ""; }; 549E944422CF295D0050AECF /* StateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StateTests.swift; sourceTree = ""; }; 54A6B6B022968B530077F348 /* CoreTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreTests.swift; sourceTree = ""; }; @@ -75,6 +96,7 @@ 54CFBFC2215CD045006B5735 /* Layout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Layout.swift; sourceTree = ""; }; 54CFBFC4215CD09C006B5735 /* Core.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Core.swift; sourceTree = ""; }; 54DBA3DB262E938500D75969 /* Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Extensions.swift; sourceTree = ""; }; + 54DFB9752DC9DF8200006C84 /* SurfaceAppearance+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SurfaceAppearance+.swift"; sourceTree = ""; }; 54E3992627141F5100A8F9ED /* FloatingPanel.docc */ = {isa = PBXFileReference; lastKnownFileType = folder.documentationcatalog; path = FloatingPanel.docc; sourceTree = ""; }; 5D82A6B428D18461006A44BA /* libswiftCoreGraphics.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libswiftCoreGraphics.tbd; path = Platforms/MacOSX.platform/Developer/SDKs/MacOSX12.3.sdk/usr/lib/swift/libswiftCoreGraphics.tbd; sourceTree = DEVELOPER_DIR; }; /* End PBXFileReference section */ @@ -121,6 +143,7 @@ 545DB9C32151169500CA77B8 /* Sources */ = { isa = PBXGroup; children = ( + 5471AF8C2CB53FB4001B7A64 /* SwiftUI */, 545DB9DF21511AC100CA77B8 /* Controller.swift */, 5469F4AD24B30D7E00537F8A /* State.swift */, 5469F4B124B30F1100537F8A /* Position.swift */, @@ -159,6 +182,24 @@ path = Tests; sourceTree = ""; }; + 5471AF8C2CB53FB4001B7A64 /* SwiftUI */ = { + isa = PBXGroup; + children = ( + 5471AF8F2CB53FFC001B7A64 /* FloatingPanelView.swift */, + 5471AF912CB54628001B7A64 /* FloatingPanelProxy.swift */, + 547B44902CC0E8AD00F7E68C /* FloatingPanelCoordinator.swift */, + 5471AF8D2CB53FC8001B7A64 /* View+floatingPanel.swift */, + 5484E7422DCF90A60058E962 /* View+floatingPanelState.swift */, + 543102B12DB8B4A300A927EF /* View+floatingPanelLayout.swift */, + 543102E92DB8F02D00A927EF /* View+floatingPanelBehavior.swift */, + 543102572DB8AAB800A927EF /* View+floatingPanelConfiguration.swift */, + 543102592DB8AAB800A927EF /* View+floatingPanelSurface.swift */, + 543103462DB9B4C900A927EF /* View+floatingPanelScrollTracking.swift */, + 54DFB9752DC9DF8200006C84 /* SurfaceAppearance+.swift */, + ); + path = SwiftUI; + sourceTree = ""; + }; 5D82A6B328D18460006A44BA /* Frameworks */ = { isa = PBXGroup; children = ( @@ -282,21 +323,32 @@ buildActionMask = 2147483647; files = ( 5469F4B224B30F1100537F8A /* Position.swift in Sources */, + 5484E7432DCF90AC0058E962 /* View+floatingPanelState.swift in Sources */, 5469F4B024B30E1500537F8A /* LayoutAnchoring.swift in Sources */, + 547B44912CC0E8B300F7E68C /* FloatingPanelCoordinator.swift in Sources */, + 543102EA2DB8F02E00A927EF /* View+floatingPanelBehavior.swift in Sources */, 54CDC5D3215B6D5A007D205C /* SurfaceView.swift in Sources */, 54CFBFC3215CD045006B5735 /* Layout.swift in Sources */, 5469F4B424B30F3500537F8A /* LayoutProperties.swift in Sources */, 54CDC5D5215B6D8D007D205C /* BackdropView.swift in Sources */, 54352E9821A521CA00CBCA08 /* PassthroughView.swift in Sources */, + 5471AF922CB5462C001B7A64 /* FloatingPanelProxy.swift in Sources */, 54CFBFC5215CD09C006B5735 /* Core.swift in Sources */, 54ABD7AF216CCFF7002E6C13 /* Logging.swift in Sources */, + 5471AF902CB53FFC001B7A64 /* FloatingPanelView.swift in Sources */, 545DB9E021511AC100CA77B8 /* Controller.swift in Sources */, + 5431025B2DB8AAB800A927EF /* View+floatingPanelSurface.swift in Sources */, + 543102B22DB8B4AC00A927EF /* View+floatingPanelLayout.swift in Sources */, + 5431025E2DB8AAB800A927EF /* View+floatingPanelConfiguration.swift in Sources */, 54DBA3DC262E938500D75969 /* Extensions.swift in Sources */, 5450EEE421646DF500135936 /* Behavior.swift in Sources */, 545DBA2B2152383100CA77B8 /* GrabberView.swift in Sources */, + 5471AF8E2CB53FC8001B7A64 /* View+floatingPanel.swift in Sources */, + 54DFB9762DC9DF8800006C84 /* SurfaceAppearance+.swift in Sources */, 54E3992727141F5100A8F9ED /* FloatingPanel.docc in Sources */, 54352E9621A51A2500CBCA08 /* Transitioning.swift in Sources */, 5469F4AE24B30D7E00537F8A /* State.swift in Sources */, + 543103472DB9B4D400A927EF /* View+floatingPanelScrollTracking.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/FloatingPanel.xcworkspace/contents.xcworkspacedata b/FloatingPanel.xcworkspace/contents.xcworkspacedata index 1f19e899b..229e32121 100644 --- a/FloatingPanel.xcworkspace/contents.xcworkspacedata +++ b/FloatingPanel.xcworkspace/contents.xcworkspacedata @@ -11,10 +11,10 @@ location = "group:Examples" name = "Examples"> + location = "group:Maps/Maps.xcodeproj"> + location = "group:Maps-SwiftUI/Maps-SwiftUI.xcodeproj"> @@ -25,5 +25,8 @@ + + diff --git a/Sources/Controller.swift b/Sources/Controller.swift index a3f8ce7cc..099c50a41 100644 --- a/Sources/Controller.swift +++ b/Sources/Controller.swift @@ -15,18 +15,30 @@ import os.log @objc(floatingPanel:layoutForSize:) optional func floatingPanel(_ fpc: FloatingPanelController, layoutFor size: CGSize) -> FloatingPanelLayout - /// Returns a UIViewPropertyAnimator object to add/present the panel to a position. + /// Returns a UIViewPropertyAnimator object to add/present the panel to a state anchor. /// /// Default is the spring animation with 0.25 secs. @objc(floatingPanel:animatorForPresentingToState:) optional func floatingPanel(_ fpc: FloatingPanelController, animatorForPresentingTo state: FloatingPanelState) -> UIViewPropertyAnimator - /// Returns a UIViewPropertyAnimator object to remove/dismiss a panel from a position. + /// Returns a UIViewPropertyAnimator object to remove/dismiss a panel. /// /// Default is the spring animator with 0.25 secs. @objc(floatingPanel:animatorForDismissingWithVelocity:) optional func floatingPanel(_ fpc: FloatingPanelController, animatorForDismissingWith velocity: CGVector) -> UIViewPropertyAnimator + /// Returns a UIViewPropertyAnimator object to move a panel to a state anchor. + /// + /// When this method is not implemented, FloatingPanelController uses its own custom spring animation when + /// ``FloatingPanelController/move(to:animated:completion:)`` is called. The custom animation is the same as + /// attracting animation after a user moves a panel by finger. + /// If you implement this method, the returned UIViewPropertyAnimator will be used when + /// ``FloatingPanelController/move(to:animated:completion:)`` is called if the current state or the target state + /// (`to` argument) are not `.hidden`. + @objc(floatingPanel:animatorForMovingTo:) optional + func floatingPanel(_ fpc: FloatingPanelController, animatorForMovingTo state: FloatingPanelState) -> UIViewPropertyAnimator + + /// Called when a panel has changed to a new state. /// /// This can be called inside an animation block for presenting, dismissing a panel or moving a panel with your @@ -585,7 +597,21 @@ open class FloatingPanelController: UIViewController { /// - completion: The block to execute after the view controller has finished moving. This block has no return value and takes no parameters. You may specify nil for this parameter. @objc(moveToState:animated:completion:) public func move(to: FloatingPanelState, animated: Bool, completion: (() -> Void)? = nil) { - floatingPanel.move(to: to, animated: animated, completion: completion) + floatingPanel.move( + to: to, + animated: animated, + moveAnimator: animatorForMoving(to: to), + completion: completion + ) + } + + func moveForSwiftUI(to: FloatingPanelState, animated: Bool, completion: (() -> Void)? = nil) { + floatingPanel.move( + to: to, + animated: animated, + moveAnimator: animatorForMoving(to: to) ?? makeDefaultAnimator(), + completion: completion + ) } /// Sets the view controller responsible for the content portion of a panel. @@ -718,13 +744,17 @@ extension FloatingPanelController { } return makeDefaultAnimator(initialVelocity: velocity) } + + func animatorForMoving(to: FloatingPanelState) -> UIViewPropertyAnimator? { + return delegate?.floatingPanel?(self, animatorForMovingTo: to) + } } // MARK: - Animation extension FloatingPanelController { - func makeDefaultAnimator(initialVelocity: CGVector = .zero) -> UIViewPropertyAnimator { + public func makeDefaultAnimator(initialVelocity: CGVector = .zero) -> UIViewPropertyAnimator { let timingParameters = UISpringTimingParameters( decelerationRate: UIScrollView.DecelerationRate.fast.rawValue, frequencyResponse: 0.25, diff --git a/Sources/Core.swift b/Sources/Core.swift index 2abc55ccf..ee8de6737 100644 --- a/Sources/Core.swift +++ b/Sources/Core.swift @@ -1,5 +1,6 @@ // Copyright 2018-Present Shin Yamamoto. All rights reserved. MIT license. +import Combine import UIKit import os.log @@ -49,6 +50,7 @@ class Core: NSObject, UIGestureRecognizerDelegate { } } } + private(set) var statePublisher: CurrentValueSubject = .init(.hidden) let panGestureRecognizer: FloatingPanelPanGestureRecognizer let panGestureDelegateRouter: FloatingPanelPanGestureRecognizer.DelegateRouter @@ -118,11 +120,28 @@ class Core: NSObject, UIGestureRecognizerDelegate { self.moveAnimator?.stopAnimation(false) } - func move(to: FloatingPanelState, animated: Bool, completion: (() -> Void)? = nil) { - move(from: state, to: to, animated: animated, completion: completion) + func move( + to: FloatingPanelState, + animated: Bool, + moveAnimator: UIViewPropertyAnimator? = nil, + completion: (() -> Void)? = nil + ) { + move( + from: state, + to: to, + animated: animated, + moveAnimator: moveAnimator, + completion: completion + ) } - private func move(from: FloatingPanelState, to: FloatingPanelState, animated: Bool, completion: (() -> Void)? = nil) { + private func move( + from: FloatingPanelState, + to: FloatingPanelState, + animated: Bool, + moveAnimator: UIViewPropertyAnimator?, + completion: (() -> Void)? = nil + ) { assert(layoutAdapter.validStates.contains(to), "Can't move to '\(to)' state because it's not valid in the layout") guard let vc = ownerVC else { completion?() @@ -153,12 +172,15 @@ class Core: NSObject, UIGestureRecognizerDelegate { let animationVector = CGVector(dx: abs(removalVector.dx), dy: abs(removalVector.dy)) animator = vc.animatorForDismissing(with: animationVector) default: - startAttraction(to: to, with: .zero) { [weak self] in - self?.endAttraction(false) - updateScrollView() - completion?() + guard let moveAnimator = moveAnimator else { + startAttraction(to: to, with: .zero) { [weak self] in + self?.endAttraction(false) + updateScrollView() + completion?() + } + return } - return + animator = moveAnimator } let shouldDoubleLayout = from == .hidden @@ -240,6 +262,7 @@ class Core: NSObject, UIGestureRecognizerDelegate { layoutAdapter.activateLayout(for: target, forceLayout: true) backdropView.alpha = getBackdropAlpha(for: target) adjustScrollContentInsetIfNeeded() + statePublisher.send(target) } private func getBackdropAlpha(for target: FloatingPanelState) -> CGFloat { diff --git a/Sources/SwiftUI/FloatingPanelCoordinator.swift b/Sources/SwiftUI/FloatingPanelCoordinator.swift new file mode 100644 index 000000000..408b4def5 --- /dev/null +++ b/Sources/SwiftUI/FloatingPanelCoordinator.swift @@ -0,0 +1,125 @@ +// Copyright 2025 the FloatingPanel authors. All rights reserved. MIT license. + +#if canImport(SwiftUI) +import SwiftUI + +/// A protocol that defines the coordination between SwiftUI and UIKit for FloatingPanel integration. +/// +/// The FloatingPanelCoordinator is responsible for managing the connection between the SwiftUI view hierarchy +/// and the underlying ``FloatingPanelController``. It handles the setup, configuration, and event dispatching +/// for floating panels within SwiftUI. +/// +/// Implementations of this protocol should handle the following responsibilities: +/// - Creating and configuring the FloatingPanelController +/// - Setting up the content and main views +/// - Managing panel state and position +/// - Handling events and passing them back to SwiftUI +/// +/// By default, you can use the built-in ``FloatingPanelDefaultCoordinator`` for basic floating panel integration, +/// or implement a custom coordinator for more advanced functionality and control over events. +/// +/// To implement a custom coordinator, you must: +/// 1. Define an associated Event type based on the events you want to monitor +/// 2. Implement the required initializer and properties +/// 3. Handle the setup of the floating panel with the provided hosting controllers +/// 4. Optionally provide custom implementation for `onUpdate` method +@available(iOS 14, *) +public protocol FloatingPanelCoordinator { + /// The type of events this coordinator can dispatch to its host view. + /// + /// This can be an empty enum like in `FloatingPanelDefaultCoordinator.Event` if you don't need + /// to handle any events, or a more complex type that represents the various events that can occur + /// in your floating panel implementation. + associatedtype Event + + /// Creates a new coordinator with an action handler for events. + /// + /// - Parameter action: A closure that will be called when events occur in the floating panel. + /// The closure takes an event of the associated `Event` type and performs any necessary actions. + init(action: @escaping (Event) -> Void) + + /// A proxy object that provides access to the underlying FloatingPanelController. + /// + /// Use this property to interact with the floating panel, such as moving it to different states + /// or tracking scroll views. + var proxy: FloatingPanelProxy { get } + + /// Sets up the floating panel with main and content views. + /// + /// This method is called during the creation of the floating panel to configure the relationship + /// between the main view (the view containing the floating panel) and the content view (the view + /// displayed within the floating panel). + /// + /// - Parameters: + /// - mainHostingController: The UIHostingController that hosts the main SwiftUI view. + /// - contentHostingController: The UIHostingController that hosts the content SwiftUI view + /// to be displayed in the floating panel. + func setupFloatingPanel( + mainHostingController: UIHostingController
, + contentHostingController: UIHostingController + ) + + /// Called when the SwiftUI context updates. + /// + /// Use this method to respond to changes in the SwiftUI environment or to update + /// the floating panel's configuration based on new context. + /// + /// - Parameter context: The UIViewControllerRepresentableContext providing context for the update. + func onUpdate( + context: UIViewControllerRepresentableContext + ) where Representable: UIViewControllerRepresentable +} + +@available(iOS 14, *) +extension FloatingPanelCoordinator { + /// A convenience property that returns the underlying FloatingPanelController. + /// + /// This property provides direct access to the controller for advanced configurations + /// and operations. + public var controller: FloatingPanelController { + proxy.controller + } + +} + +/// A default implementation of the `FloatingPanelCoordinator` protocol. +/// +/// This coordinator provides a simple implementation for setting up the floating panel with +/// minimal configuration. It creates a standard ``FloatingPanelController`` with default settings +/// and an empty event enumeration. Use this coordinator for basic floating panel integration +/// when you don't need custom event handling or special configuration. +@available(iOS 14, *) +public final class FloatingPanelDefaultCoordinator: FloatingPanelCoordinator { + public enum Event {} + + public let proxy: FloatingPanelProxy + public let action: (FloatingPanelDefaultCoordinator.Event) -> Void + + public init(action: @escaping (FloatingPanelDefaultCoordinator.Event) -> Void) { + self.action = action + self.proxy = .init(controller: FloatingPanelController()) + } + + /// Default implementation for setting up the floating panel with main and content views. + /// + /// - Parameters: + /// - mainHostingController: The UIHostingController that hosts the main SwiftUI view. + /// - contentHostingController: The UIHostingController that hosts the content SwiftUI view + /// to be displayed in the floating panel. + public func setupFloatingPanel( + mainHostingController: UIHostingController
, + contentHostingController: UIHostingController + ) { + // Set up the content + contentHostingController.view.backgroundColor = .clear + controller.set(contentViewController: contentHostingController) + + // Show the panel + controller.addPanel(toParent: mainHostingController, animated: false) + } + + public func onUpdate( + context: UIViewControllerRepresentableContext + ) where Representable: UIViewControllerRepresentable {} +} +#endif diff --git a/Sources/SwiftUI/FloatingPanelProxy.swift b/Sources/SwiftUI/FloatingPanelProxy.swift new file mode 100644 index 000000000..f786c9909 --- /dev/null +++ b/Sources/SwiftUI/FloatingPanelProxy.swift @@ -0,0 +1,98 @@ +// Copyright 2021 the FloatingPanel authors. All rights reserved. MIT license. + +#if canImport(SwiftUI) +import SwiftUI + +/// A proxy for exposing and controlling the floating panel within SwiftUI views. +/// +/// `FloatingPanelProxy` provides a bridge between SwiftUI views and the underlying +/// `FloatingPanelController`, enabling you to programmatically interact with the +/// floating panel from your SwiftUI content. This proxy is automatically provided to +/// the content view through the `floatingPanel()` modifier's content closure. +/// +/// Use this proxy to: +/// - Programmatically move the panel to different positions +/// - Access the underlying UIKit controller for advanced customization +/// +/// ```swift +/// MyView() +/// .floatingPanel { proxy in +/// ScrollView { +/// VStack { +/// // Your content +/// +/// Button("Move To Full") { +/// proxy.move(to: .full, animated: true) +/// } +/// } +/// } +/// } +/// ``` +@available(iOS 14, *) +public struct FloatingPanelProxy { + /// The associated floating panel controller. + /// + /// This gives direct access to the underlying `FloatingPanelController` instance, + /// allowing you to use any features not directly exposed by the proxy methods. + /// Use this property when you need advanced control over the panel's behavior. + public let controller: FloatingPanelController + + public init(controller: FloatingPanelController) { + self.controller = controller + } + + /// Moves the floating panel to the specified position. + /// + /// Use this method to programmatically change the panel's position in response to + /// user actions or application state changes. The available positions are defined + /// by the current `FloatingPanelLayout` and typically include `.full`, `.half`, + /// and `.tip`. + /// + /// ```swift + /// Button("Show Full Panel") { + /// proxy.move(to: .full, animated: true) + /// } + /// ``` + /// + /// You can also use this method with a completion handler to perform actions + /// after the panel has finished moving: + /// + /// ```swift + /// proxy.move(to: .full, animated: true) { + /// // Code to execute after the panel reaches the full position + /// self.loadDetailedData() + /// } + /// ``` + /// + /// - Parameters: + /// - floatingPanelState: The state to move to (e.g., `.full`, `.half`, `.tip`). + /// The available states depend on the current `FloatingPanelLayout`. + /// - animated: `true` to animate the transition to the new state; `false` + /// for an immediate transition without animation. + /// - completion: An optional closure that will be executed after the panel + /// has completed moving to the new position. + public func move( + to floatingPanelState: FloatingPanelState, + animated: Bool, + completion: (() -> Void)? = nil + ) { + // Need to use this method which doesn't use the custom NumericSpringingAnimator because it doesn't work with + // SwiftUI animation. + controller.moveForSwiftUI(to: floatingPanelState, animated: animated, completion: completion) + } +} + +@available(iOS 14, *) +extension FloatingPanelProxy { + /// Tracks the specified scroll view to coordinate panel and scroll movements. + /// + /// - Important: It is strongly recommended to use ``SwiftUICore/View/floatingPanelScrollTracking(proxy:onScrollViewDetected:)`` + /// instead of this method, as it provides a more SwiftUI-friendly approach to scroll tracking. + /// + /// - Parameter scrollView: The scroll view to track. The panel will coordinate + /// its movements with this scroll view. + public func track(scrollView: UIScrollView) { + controller.track(scrollView: scrollView) + } +} +#endif diff --git a/Sources/SwiftUI/FloatingPanelView.swift b/Sources/SwiftUI/FloatingPanelView.swift new file mode 100644 index 000000000..972a150c8 --- /dev/null +++ b/Sources/SwiftUI/FloatingPanelView.swift @@ -0,0 +1,284 @@ +// Copyright 2021 the FloatingPanel authors. All rights reserved. MIT license. + +#if canImport(SwiftUI) +import SwiftUI +import Combine + +/// A SwiftUI view that integrates a floating panel with customizable content. +/// +/// ``FloatingPanelView`` provides a SwiftUI wrapper around the UIKit-based ``FloatingPanelController``, +/// allowing you to easily add floating panels to your SwiftUI interface. The view consists of +/// two main components: +/// +/// - A main view that serves as the background or parent view +/// - A floating panel that contains custom content and can be positioned and animated +/// +/// While you can use this view directly, it's recommended to use the ``SwiftUICore/View/floatingPanel(coordinator:onEvent:content:)`` +/// view modifier instead, which provides a more SwiftUI-friendly API: +/// +/// ```swift +/// MyView() +/// .floatingPanel { proxy in +/// // Your floating panel content +/// Text("Panel Content") +/// } +/// .floatingPanelLayout(MyCustomLayout()) +/// .floatingPanelBehavior(MyCustomBehavior()) +/// ``` +/// +/// You can also provide a custom coordinator and handle events: +/// +/// ```swift +/// MyView() +/// .floatingPanel( +/// coordinator: MyCustomCoordinator.self, +/// onEvent: { event in +/// // Handle panel events +/// } +/// ) { proxy in +/// // Your floating panel content +/// } +/// ``` +/// +/// By default, ``FloatingPanelView`` uses ``FloatingPanelDefaultCoordinator`` to manage the +/// relationship between SwiftUI and UIKit components, but you can provide a custom +/// coordinator for more advanced control and event handling. +@available(iOS 14, *) +struct FloatingPanelView: UIViewControllerRepresentable { + /// A closure that creates the coordinator responsible for managing the floating panel. + let coordinator: () -> (any FloatingPanelCoordinator) + + /// The view builder that creates the main content underneath the floating panel. + @ViewBuilder + var main: MainView + + /// The view builder that creates the content displayed inside the floating panel. + @ViewBuilder + var content: (FloatingPanelProxy) -> ContentView + + /// A binding to the floating panel's current anchor state. + @Environment(\.state) + private var state: Binding + + /// The layout object that defines the position and size of the floating panel. + @Environment(\.layout) + private var layout: FloatingPanelLayout + + /// The behavior object that defines the interaction dynamics of the floating panel. + @Environment(\.behavior) + private var behavior: FloatingPanelBehavior + + /// The behavior for determining the adjusted content insets in the panel. + @Environment(\.contentInsetAdjustmentBehavior) + private var contentInsetAdjustmentBehavior + + /// Constants that define how a panel's content fills the surface. + @Environment(\.contentMode) + private var contentMode + + /// The vertical padding between the grabber handle and the content. + @Environment(\.grabberHandlePadding) + private var grabberHandlePadding + + /// The appearance configuration for the floating panel's surface view. + @Environment(\.surfaceAppearance) + private var surfaceAppearance + + func makeCoordinator() -> FloatingPanelCoordinatorProxy { + return FloatingPanelCoordinatorProxy( + coordinator: coordinator(), + state: state + ) + } + + func makeUIViewController(context: Context) -> UIHostingController { + let mainHostingController = UIHostingController(rootView: main) + mainHostingController.view.backgroundColor = nil + let contentHostingController = UIHostingController(rootView: content(context.coordinator.proxy)) + context.coordinator.setupFloatingPanel( + mainHostingController: mainHostingController, + contentHostingController: contentHostingController + ) + + context.coordinator.observeStateChanges() + context.coordinator.update(layout: layout, behavior: behavior) + + return mainHostingController + } + + func updateUIViewController( + _ uiViewController: UIHostingController, + context: Context + ) { + context.coordinator.onUpdate(context: context) + applyEnvironment(context: context) + applyAnimatableEnvironment(context: context) + } +} + +@available(iOS 14, *) +extension FloatingPanelView { + // MARK: - Environment updates + /// Applies environment values to the floating panel controller. + func applyEnvironment(context: Context) { + let fpc = context.coordinator.controller + if fpc.contentInsetAdjustmentBehavior != contentInsetAdjustmentBehavior { + fpc.contentInsetAdjustmentBehavior = contentInsetAdjustmentBehavior + } + if fpc.contentMode != contentMode { + fpc.contentMode = contentMode + } + if fpc.surfaceView.grabberHandlePadding != grabberHandlePadding { + fpc.surfaceView.grabberHandlePadding = grabberHandlePadding + } + if fpc.surfaceView.appearance != surfaceAppearance { + fpc.surfaceView.appearance = surfaceAppearance + } + } + + /// Applies environment values to the floating panel controller with animations if needed. + func applyAnimatableEnvironment(context: Context) { + context.coordinator.apply( + animatableChanges: { + context.coordinator.update(state: state.wrappedValue) + context.coordinator.update(layout: layout, behavior: behavior) + }, + transaction: context.transaction + ) + } +} + +/// A proxy for exposing and controlling a client coordinator object. +/// +/// This proxy is introduced to make the implementation more extensible, rather than directly treating a Coordinator +/// with a lifecycle that spans across FloatingPanelView as a FloatingPanelCoordinator. This object was created to +/// control `FloatingPanelView/state` binding property. +@available(iOS 14, *) +class FloatingPanelCoordinatorProxy { + private let origin: any FloatingPanelCoordinator + private var stateBinding: Binding + + private var subscriptions: Set = Set() + + var proxy: FloatingPanelProxy { origin.proxy } + var controller: FloatingPanelController { origin.controller } + + init( + coordinator: any FloatingPanelCoordinator, + state: Binding + ) { + self.origin = coordinator + self.stateBinding = state + } + + deinit { + for subscription in subscriptions { + subscription.cancel() + } + } + + func setupFloatingPanel( + mainHostingController: UIHostingController
, + contentHostingController: UIHostingController + ) { + origin.setupFloatingPanel( + mainHostingController: mainHostingController, + contentHostingController: contentHostingController + ) + } + + func onUpdate( + context: UIViewControllerRepresentableContext + ) where Representable: UIViewControllerRepresentable { + origin.onUpdate(context: context) + } +} + +@available(iOS 14, *) +extension FloatingPanelCoordinatorProxy { + // MARK: - Layout and behavior updates + + /// Update layout and behavior objects for the specified floating panel. + func update( + layout: (any FloatingPanelLayout)?, + behavior: (any FloatingPanelBehavior)? + ) { + let shouldInvalidateLayout = controller.layout !== layout + + if let layout = layout { + controller.layout = layout + } else { + controller.layout = FloatingPanelBottomLayout() + } + + if shouldInvalidateLayout { + controller.invalidateLayout() + } + + if let behavior = behavior { + controller.behavior = behavior + } else { + controller.behavior = FloatingPanelDefaultBehavior() + } + } +} + +@available(iOS 14, *) +extension FloatingPanelCoordinatorProxy { + // MARK: - State updates + + // Update the state of FloatingPanelController + func update(state: FloatingPanelState?) { + guard let state = state else { return } + controller.move(to: state, animated: false) + } + + /// Start observing ``FloatingPanelController/state`` through the `Core` object. + func observeStateChanges() { + controller.floatingPanel.statePublisher + .sink { [weak self] state in + guard let self = self else { return } + // Needs to update the state binding value on the next run loop cycle to avoid this error. + // > Modifying state during view update, this will cause undefined behavior. + Task { @MainActor in + self.stateBinding.wrappedValue = state + } + }.store(in: &subscriptions) + } +} + +@available(iOS 14, *) +extension FloatingPanelCoordinatorProxy { + // MARK: - Environment updates + + /// Applies animatable environment value changes. + func apply(animatableChanges: @escaping () -> Void, transaction: Transaction) { + /// Returns the default animator object for compatibility with iOS 17 and earlier. + func animateUsingDefaultAnimator(changes: @escaping () -> Void) { + let animator = controller.makeDefaultAnimator() + animator.addAnimations(changes) + animator.startAnimation() + } + + if let animation = transaction.animation, transaction.disablesAnimations == false { + #if compiler(>=6.0) + if #available(iOS 18, *) { + UIView.animate(animation) { + animatableChanges() + } + } else { + animateUsingDefaultAnimator { + animatableChanges() + } + } + #else + animateUsingDefaultAnimator { + animatableChanges() + } + #endif + } else { + animatableChanges() + } + } +} +#endif diff --git a/Sources/SwiftUI/SurfaceAppearance+.swift b/Sources/SwiftUI/SurfaceAppearance+.swift new file mode 100644 index 000000000..8c2b98930 --- /dev/null +++ b/Sources/SwiftUI/SurfaceAppearance+.swift @@ -0,0 +1,80 @@ +// Copyright 2018 the FloatingPanel authors. All rights reserved. MIT license. + +#if canImport(SwiftUI) +import SwiftUI + +@available(iOS 14, *) +extension FloatingPanel.SurfaceAppearance { + /// Creates a transparent surface appearance with customizable borders, corners, and shadows. + /// + /// This utility method makes it easy to create visually appealing panel surfaces with + /// common styling options like borders and shadows. The surface is transparent by default, + /// allowing you to add background effects through your content using SwiftUI views if needed. + /// + /// Example usage: + /// + /// ```swift + /// MainView() + /// .floatingPanel { _ in + /// ZStack { + /// // Your panel content + /// VStack { + /// Text("Panel Title") + /// // ... + /// } + /// .padding() + /// } + /// /// A material effect background within your content + /// .background { + /// GeometryReader { geometry in + /// Rectangle() + /// .fill(.clear) + /// .frame(height: geometry.size.height * 2) + /// .background(.regularMaterial) + /// } + /// } + /// } + /// .floatingPanelSurfaceAppearance( + /// .transparent( + /// borderColor: .secondary.opacity(0.3), + /// borderWidth: 1.0, + /// cornerRadius: 16.0, + /// shadows: [ + /// .init(color: .black, radius: 10, opacity: 0.1, offset: .zero), + /// .init(color: .black, radius: 3, opacity: 0.1, offset: CGSize(width: 0, height: 2)) + /// ] + /// ) + /// ) + /// ``` + /// + /// - Parameters: + /// - borderColor: The color of the border around the panel's edges. Pass `nil` for no border. + /// - borderWidth: The width of the border in points. Defaults to 0.0. + /// - cornerRadius: The radius of the panel's corners in points. Defaults to 8.0. + /// - shadows: An array of `Shadow` objects defining layered shadow effects. + /// Defaults to a single subtle shadow. + /// + /// - Returns: A configured `SurfaceAppearance` instance with the specified styling. + public static func transparent( + borderColor: Color? = nil, + borderWidth: Double = 0.0, + cornerRadius: Double = 8.0, + shadows: [Shadow] = [Shadow()] + ) -> SurfaceAppearance { + let appearance = SurfaceAppearance() + appearance.backgroundColor = .clear + let borderUIColor: UIColor? + if let borderColor { + borderUIColor = UIColor(borderColor) + } else { + borderUIColor = nil + } + appearance.borderColor = borderUIColor + appearance.borderWidth = CGFloat(borderWidth) + appearance.cornerCurve = .continuous + appearance.cornerRadius = cornerRadius + appearance.shadows = shadows + return appearance + } +} +#endif diff --git a/Sources/SwiftUI/View+floatingPanel.swift b/Sources/SwiftUI/View+floatingPanel.swift new file mode 100644 index 000000000..24905a47b --- /dev/null +++ b/Sources/SwiftUI/View+floatingPanel.swift @@ -0,0 +1,64 @@ +// Copyright 2021 the FloatingPanel authors. All rights reserved. MIT license. + +#if canImport(SwiftUI) +import SwiftUI + +@available(iOS 14, *) +extension View { + /// Overlays this view with a floating panel. + /// + /// This modifier is the recommended way to add a floating panel to any SwiftUI view. + /// It creates a `FloatingPanelView` with the current view as the main content and + /// adds your custom content to the floating panel. + /// + /// ```swift + /// ScrollView { + /// LazyVStack { + /// // Main content + /// ForEach(items) { item in + /// ItemView(item) + /// } + /// } + /// } + /// .floatingPanel { proxy in + /// // Panel content + /// DetailView() + /// } + /// ``` + /// + /// You can customize the panel by using additional modifiers: + /// + /// ```swift + /// ContentView() + /// .floatingPanel { proxy in + /// PanelContent() + /// } + /// .floatingPanelLayout(MyCustomLayout()) + /// .floatingPanelBehavior(MyCustomBehavior()) + /// .floatingPanelSurfaceAppearance(MySurfaceAppearance()) + /// ``` + /// + /// - Parameters: + /// - coordinator: A coordinator type that conforms to the ``FloatingPanelCoordinator`` protocol. + /// Defaults to ``FloatingPanelDefaultCoordinator``. Use a custom coordinator for advanced control + /// over panel behavior and events. + /// - action: A closure that is called when events occur in the panel. The event type is defined + /// by the coordinator's associated `Event` type. This parameter is ignored if you use the default + /// coordinator, which doesn't emit events. + /// - content: A closure that returns the content to display in the floating panel. + /// This view builder receives a ``FloatingPanelProxy`` instance that you can use to + /// interact with the panel, such as tracking scroll views or moving the panel programmatically. + public func floatingPanel( + coordinator: T.Type = FloatingPanelDefaultCoordinator.self, + onEvent action: ((T.Event) -> Void)? = nil, + @ViewBuilder content: @escaping (FloatingPanelProxy) -> some View + ) -> some View { + FloatingPanelView( + coordinator: { T.init(action: action ?? { _ in }) }, + main: { self }, + content: content + ) + .ignoresSafeArea() + } +} +#endif diff --git a/Sources/SwiftUI/View+floatingPanelBehavior.swift b/Sources/SwiftUI/View+floatingPanelBehavior.swift new file mode 100644 index 000000000..a41482ae7 --- /dev/null +++ b/Sources/SwiftUI/View+floatingPanelBehavior.swift @@ -0,0 +1,62 @@ +// Copyright 2025 the FloatingPanel authors. All rights reserved. MIT license. + +#if canImport(SwiftUI) +import SwiftUI + +@available(iOS 14, *) +extension EnvironmentValues { + struct BehaviorKey: EnvironmentKey { + static var defaultValue: FloatingPanelBehavior = FloatingPanelDefaultBehavior() + } + + var behavior: FloatingPanelBehavior { + get { self[BehaviorKey.self] } + set { self[BehaviorKey.self] = newValue } + } +} + +@available(iOS 14, *) +extension View { + /// Sets the behavior object controlling the interactive dynamics of floating panels within this view. + /// + /// The behavior object defines how the floating panel responds to user interactions, + /// including: + /// - Momentum and velocity effects during dragging + /// - Position snapping behavior when released + /// - Projection behavior after a swipe gesture + /// - Interaction restrictions for certain positions + /// + /// By default, the panel uses `FloatingPanelDefaultBehavior`, but you can create + /// your own custom behavior by implementing the `FloatingPanelBehavior` protocol: + /// + /// ```swift + /// struct MyCustomBehavior: FloatingPanelBehavior { + /// func shouldProjectMomentum(_ fpc: FloatingPanelController, for proposedTargetPosition: FloatingPanelState) -> Bool { + /// return true + /// } + /// + /// func momentumProjection(from initialVelocity: CGPoint) -> CGPoint { + /// return CGPoint(x: 0, y: initialVelocity.y * 0.5) + /// } + /// } + /// ``` + /// + /// Apply the behavior to your floating panel: + /// + /// ```swift + /// MainView() + /// .floatingPanel { _ in + /// FloatingPanelContent() + /// } + /// .floatingPanelBehavior(MyCustomBehavior()) + /// ``` + /// + /// - Parameter behavior: An object conforming to the `FloatingPanelBehavior` protocol + /// that controls the panel's interactive dynamics, or `nil` to use the default behavior. + public func floatingPanelBehavior( + _ behavior: FloatingPanelBehavior? + ) -> some View { + environment(\.behavior, behavior ?? FloatingPanelDefaultBehavior()) + } +} +#endif diff --git a/Sources/SwiftUI/View+floatingPanelConfiguration.swift b/Sources/SwiftUI/View+floatingPanelConfiguration.swift new file mode 100644 index 000000000..16cc133eb --- /dev/null +++ b/Sources/SwiftUI/View+floatingPanelConfiguration.swift @@ -0,0 +1,102 @@ +// Copyright 2021 the FloatingPanel authors. All rights reserved. MIT license. + +#if canImport(SwiftUI) +import SwiftUI + +@available(iOS 14, *) +extension EnvironmentValues { + struct ContentInsetAdjustmentBehaviorKey: EnvironmentKey { + static var defaultValue: FloatingPanelController.ContentInsetAdjustmentBehavior = .always + } + + var contentInsetAdjustmentBehavior: FloatingPanelController.ContentInsetAdjustmentBehavior { + get { self[ContentInsetAdjustmentBehaviorKey.self] } + set { self[ContentInsetAdjustmentBehaviorKey.self] = newValue } + } + + struct ContentModeKey: EnvironmentKey { + static var defaultValue: FloatingPanelController.ContentMode = .static + } + + var contentMode: FloatingPanelController.ContentMode { + get { self[ContentModeKey.self] } + set { self[ContentModeKey.self] = newValue } + } +} + +@available(iOS 14, *) +extension View { + /// Sets the content mode for floating panels within this view. + /// + /// The content mode controls how the panel's content view is sized and positioned + /// when the panel's position changes. Each mode has different behavior: + /// + /// - `.static`: The content view maintains its current frame regardless of the + /// panel's position. This is the default mode and is suitable for most use cases + /// where the content should remain stable. + /// + /// - `.fitToBounds`: The content view is resized to fit within the panel's bounds + /// at each position. This is useful when you want the content to always fill + /// the available space within the panel. + /// + /// Example usage: + /// + /// ```swift + /// MainView() + /// .floatingPanel { _ in + /// VStack { + /// Text("Panel Content") + /// Image("illustration") + /// } + /// } + /// .floatingPanelContentMode(.fitToBounds) + /// ``` + /// + /// - Parameter contentMode: The content mode to use for the floating panel. + public func floatingPanelContentMode( + _ contentMode: FloatingPanelController.ContentMode + ) -> some View { + environment(\.contentMode, contentMode) + } + + /// Sets the content inset adjustment behavior for floating panels within this view. + /// + /// This modifier controls how the panel adjusts its content insets in relation to + /// the safe area and the panel's position. This is particularly important for + /// scrollable content within the panel. + /// + /// Available behaviors: + /// + /// - `.always`: Always adjust content insets to account for safe areas and panel + /// position. This ensures content is properly inset beneath system bars and panel + /// elements like the grabber handle. This is the default and recommended for most cases. + /// + /// - `.never`: Never adjust content insets. Content will extend to the edges of the + /// panel regardless of safe areas. Use this when you want to manually manage insets + /// or create custom overlay effects. + /// + /// Example usage: + /// + /// ```swift + /// MainView() + /// .floatingPanel { _ in + /// ScrollView { + /// LazyVStack { + /// ForEach(items) { item in + /// ItemRow(item) + /// } + /// } + /// } + /// } + /// .floatingPanelContentInsetAdjustmentBehavior(.always) + /// ``` + /// + /// - Parameter contentInsetAdjustmentBehavior: The content inset adjustment behavior + /// to use for the floating panel. + public func floatingPanelContentInsetAdjustmentBehavior( + _ contentInsetAdjustmentBehavior: FloatingPanelController.ContentInsetAdjustmentBehavior + ) -> some View { + environment(\.contentInsetAdjustmentBehavior, contentInsetAdjustmentBehavior) + } +} +#endif diff --git a/Sources/SwiftUI/View+floatingPanelLayout.swift b/Sources/SwiftUI/View+floatingPanelLayout.swift new file mode 100644 index 000000000..50399c272 --- /dev/null +++ b/Sources/SwiftUI/View+floatingPanelLayout.swift @@ -0,0 +1,71 @@ +// Copyright 2025 the FloatingPanel authors. All rights reserved. MIT license. + +#if canImport(SwiftUI) +import SwiftUI + +@available(iOS 14, *) +extension EnvironmentValues { + struct LayoutKey: EnvironmentKey { + static var defaultValue: FloatingPanelLayout = FloatingPanelBottomLayout() + } + + var layout: FloatingPanelLayout { + get { self[LayoutKey.self] } + set { self[LayoutKey.self] = newValue } + } +} + +@available(iOS 14, *) +extension View { + /// Sets the layout object that defines the position and dimensions of floating panels within this view. + /// + /// The layout object controls critical aspects of the floating panel's appearance: + /// - Available positions (full, half, tip, etc.) and their insets from screen edges + /// - Initial position when the panel first appears + /// - Anchoring behavior and constraints + /// - Layout adaptation for different size classes and device orientations + /// + /// FloatingPanel comes with several built-in layouts: + /// - `FloatingPanelBottomLayout`: Standard bottom-anchored panel (default) + /// + /// You can also create custom layouts by implementing the `FloatingPanelLayout` protocol: + /// + /// ```swift + /// struct MyCustomLayout: FloatingPanelLayout { + /// var position: FloatingPanelPosition { + /// return .bottom + /// } + /// + /// var initialState: FloatingPanelState { + /// return .half + /// } + /// + /// var anchors: [FloatingPanelState: FloatingPanelLayoutAnchoring] { + /// return [ + /// .full: FloatingPanelLayoutAnchor(absoluteInset: 16.0, edge: .top, referenceGuide: .safeArea), + /// .half: FloatingPanelLayoutAnchor(fractionalInset: 0.5, edge: .bottom, referenceGuide: .safeArea), + /// .tip: FloatingPanelLayoutAnchor(absoluteInset: 44.0, edge: .bottom, referenceGuide: .safeArea) + /// ] + /// } + /// } + /// ``` + /// + /// Apply the layout to your floating panel: + /// + /// ```swift + /// MainView() + /// .floatingPanel { _ in + /// FloatingPanelContent() + /// } + /// .floatingPanelLayout(MyCustomLayout()) + /// ``` + /// + /// - Parameter layout: An object conforming to the `FloatingPanelLayout` protocol + /// that defines the panel's position and dimensions, or `nil` to use the default layout. + public func floatingPanelLayout( + _ layout: FloatingPanelLayout? + ) -> some View { + environment(\.layout, layout ?? FloatingPanelBottomLayout()) + } +} +#endif diff --git a/Sources/SwiftUI/View+floatingPanelScrollTracking.swift b/Sources/SwiftUI/View+floatingPanelScrollTracking.swift new file mode 100644 index 000000000..5be422e52 --- /dev/null +++ b/Sources/SwiftUI/View+floatingPanelScrollTracking.swift @@ -0,0 +1,119 @@ +// Copyright 2025 the FloatingPanel authors. All rights reserved. MIT license. + +#if canImport(SwiftUI) +import SwiftUI + +@available(iOS 14, *) +extension View { + /// Automatically tracks scroll views within this view for seamless integration with a floating panel. + /// + /// This modifier automatically detects and tracks a `UIScrollView` instance within your SwiftUI content, + /// linking it with the floating panel for coordinated scrolling behavior. This is essential for + /// creating a smooth user experience when a scrollable view is contained in a floating panel. + /// + /// Example usage: + /// + /// ```swift + /// MainView() + /// .floatingPanel { proxy in + /// ScrollView { + /// VStack(spacing: 20) { + /// ForEach(items) { item in + /// ItemRow(item) + /// } + /// } + /// .padding() + /// } + /// .floatingPanelScrollTracking(proxy: proxy) + /// } + /// ``` + /// + /// For advanced customization, you can provide an onScrollViewDetected closure to access the hosting controller + /// and scroll view directly: + /// + /// ```swift + /// .floatingPanelScrollTracking(proxy: proxy) { scrollView, _ in + /// // Customize scroll view behavior or appearance + /// scrollView.showsVerticalScrollIndicator = false + /// scrollView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: 20, right: 0) + /// } + /// ``` + /// + /// - Parameters: + /// - proxy: The ``FloatingPanelProxy`` instance from the floating panel's content closure. + /// - onScrollViewDetected: Optional closure called when a scroll view is found, allowing for additional customization. + /// The closure receives the detected scroll view and its hosting view controller in that order. + public func floatingPanelScrollTracking( + proxy: FloatingPanelProxy, + onScrollViewDetected: ((UIScrollView, UIHostingController) -> Void)? = nil + ) -> some View { + ScrollViewRepresentable(proxy: proxy, onScrollViewDetected: onScrollViewDetected) { self } + } +} + +@available(iOS 14, *) +private struct ScrollViewRepresentable: UIViewControllerRepresentable where Content: View { + let proxy: FloatingPanelProxy + let onScrollViewDetected: ((UIScrollView, UIHostingController) -> Void)? + @ViewBuilder + let content: () -> Content + + func makeUIViewController(context: Context) -> ScrollViewHostingController { + let vc = ScrollViewHostingController( + rootView: content(), + proxy: proxy, + onScrollViewDetected: onScrollViewDetected + ) + vc.view.backgroundColor = .clear + return vc + } + + func updateUIViewController(_ uiViewController: ScrollViewHostingController, context: Context) { + } + + class ScrollViewHostingController: UIHostingController where V: View { + let proxy: FloatingPanelProxy + let onScrollViewDetected: ((UIScrollView, UIHostingController) -> Void)? + + private weak var detectedScrollView: UIScrollView? + + init( + rootView: V, + proxy: FloatingPanelProxy, + onScrollViewDetected: ((UIScrollView, UIHostingController) -> Void)? + ) { + self.proxy = proxy + self.onScrollViewDetected = onScrollViewDetected + super.init(rootView: rootView) + } + + @MainActor @preconcurrency required dynamic init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewWillLayoutSubviews() { + super.viewWillLayoutSubviews() + if detectedScrollView == nil, + let scrollView = findUIScrollView(in: self.view) + { + proxy.track(scrollView: scrollView) + onScrollViewDetected?(scrollView, self) + detectedScrollView = scrollView + } + } + + func findUIScrollView(in root: UIView?) -> UIScrollView? { + guard let root = root else { return nil } + var queue = ArraySlice([root]) + while !queue.isEmpty { + let view = queue.popFirst() + if view?.isKind(of: UIScrollView.self) ?? false { + return (view as? UIScrollView) + } + queue += view?.subviews ?? [] + } + return nil + } + } +} +#endif diff --git a/Sources/SwiftUI/View+floatingPanelState.swift b/Sources/SwiftUI/View+floatingPanelState.swift new file mode 100644 index 000000000..a43cbee53 --- /dev/null +++ b/Sources/SwiftUI/View+floatingPanelState.swift @@ -0,0 +1,68 @@ +// Copyright 2025 the FloatingPanel authors. All rights reserved. MIT license. + +#if canImport(SwiftUI) +import SwiftUI + +@available(iOS 14, *) +extension EnvironmentValues { + struct StateKey: EnvironmentKey { + static var defaultValue: Binding = .constant(nil) + } + + var state: Binding { + get { self[StateKey.self] } + set { self[StateKey.self] = newValue } + } +} + +@available(iOS 14, *) +extension View { + /// Sets a binding to track and control the floating panel's state. + /// + /// - Important: The timing of changes to this state differs from the timing of + /// ``FloatingPanelController/state`` and ``FloatingPanelControllerDelegate/floatingPanelDidChangeState(_:)``. + /// This state updates slightly later due to differences between UIKit animations and SwiftUI view management. + /// + /// This modifier provides two-way communication with the floating panel: + /// - When the user interacts with the panel, the binding updates to reflect the new state + /// - When you programmatically change the binding value, the panel changes or animates to the new state + /// + /// You can use this binding to: + /// - Respond to state changes when the user interacts with the panel + /// - Programmatically control the panel position with SwiftUI animations + /// - Synchronize the panel state with other parts of your UI + /// + /// Example usage: + /// + /// ```swift + /// struct MainView: View { + /// @State private var panelState: FloatingPanelState? + /// + /// var body: some View { + /// ZStack { + /// Color.orange + /// .ignoresSafeArea() + /// .floatingPanel { _ in + /// ContentView() + /// } + /// .floatingPanelState($panelState) + /// + /// Button("Move to full") { + /// withAnimation(.interactiveSpring) { + /// panelState = .full + /// } + /// } + /// } + /// } + /// } + /// ``` + /// + /// - Parameter state: A binding to a `FloatingPanelState` value that tracks and controls + /// the current state of the floating panel. + public func floatingPanelState( + _ state: Binding + ) -> some View { + environment(\.state, state) + } +} +#endif diff --git a/Sources/SwiftUI/View+floatingPanelSurface.swift b/Sources/SwiftUI/View+floatingPanelSurface.swift new file mode 100644 index 000000000..7f9276fdb --- /dev/null +++ b/Sources/SwiftUI/View+floatingPanelSurface.swift @@ -0,0 +1,88 @@ +// Copyright 2021 the FloatingPanel authors. All rights reserved. MIT license. + +#if canImport(SwiftUI) +import SwiftUI + +@available(iOS 14, *) +extension EnvironmentValues { + struct SurfaceAppearanceKey: EnvironmentKey { + static var defaultValue = SurfaceAppearance() + } + + var surfaceAppearance: SurfaceAppearance { + get { self[SurfaceAppearanceKey.self] } + set { self[SurfaceAppearanceKey.self] = newValue } + } + + struct GrabberHandlePaddingKey: EnvironmentKey { + static var defaultValue: CGFloat = 6.0 + } + + var grabberHandlePadding: CGFloat { + get { self[GrabberHandlePaddingKey.self] } + set { self[GrabberHandlePaddingKey.self] = newValue } + } +} + +@available(iOS 14, *) +extension View { + /// Sets the surface appearance for floating panels within this view. + /// + /// This modifier allows you to fully customize the visual styling of the floating panel's + /// surface, including background color, corner radius, shadows, and borders. + /// + /// Example using a pre-defined appearance: + /// + /// ```swift + /// MainView() + /// .floatingPanel { _ in + /// FloatingPanelContent() + /// } + /// .floatingPanelSurfaceAppearance(.transparent) + /// ``` + /// + /// - Parameter surfaceAppearance: The surface appearance to set for the floating panel. + public func floatingPanelSurfaceAppearance( + _ surfaceAppearance: SurfaceAppearance + ) -> some View { + environment(\.surfaceAppearance, surfaceAppearance) + } + + /// Sets the grabber handle padding for floating panels within this view. + /// + /// This modifier adjusts the vertical spacing between the grabber handle, such as the visual + /// indicator at the top of the bottom positioned panel that users can drag. + /// + /// Adjusting this value can help with: + /// - Visual balance and spacing within the panel + /// - Providing more space for touch interactions with the grabber + /// + /// ```swift + /// MainView() + /// .floatingPanel { _ in + /// VStack(spacing: 0) { + /// Text("Panel Title") + /// .font(.headline) + /// + /// Divider() + /// .padding(.vertical) + /// + /// // Panel content + /// } + /// .padding(.horizontal) + /// } + /// // Add more space between the grabber and content for visual balance + /// .floatingPanelGrabberHandlePadding(16) + /// ``` + /// + /// The default padding is 6.0 points. + /// + /// - Parameter padding: The vertical padding in points between the grabber handle + /// and the panel content. + public func floatingPanelGrabberHandlePadding( + _ padding: CGFloat + ) -> some View { + environment(\.grabberHandlePadding, padding) + } +} +#endif From 71621f51046cea6264d4943bc989977e071b28e1 Mon Sep 17 00:00:00 2001 From: Shin Yamamoto Date: Sun, 11 May 2025 12:57:37 +0900 Subject: [PATCH 25/62] Polish source code of CoreTests a bit --- Tests/CoreTests.swift | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Tests/CoreTests.swift b/Tests/CoreTests.swift index b6f6e6d28..f9926a5c6 100644 --- a/Tests/CoreTests.swift +++ b/Tests/CoreTests.swift @@ -206,8 +206,8 @@ class CoreTests: XCTestCase { func floatingPanel(_ fpc: FloatingPanelController, layoutFor newCollection: UITraitCollection) -> FloatingPanelLayout { layout } func floatingPanel(_ fpc: FloatingPanelController, layoutFor size: CGSize) -> FloatingPanelLayout { layout } } - func _floor(_ alpha: CGFloat) -> CGFloat { - return floor(fpc.backdropView.alpha * 1e+06) / 1e+06 + func round(_ alpha: CGFloat) -> CGFloat { + return (fpc.backdropView.alpha * 1e+06).rounded() / 1e+06 } let timeout = 5.0 @@ -219,20 +219,20 @@ class CoreTests: XCTestCase { fpc.showForTest() fpc.move(to: .full, animated: false) - XCTAssertEqual(_floor(fpc.backdropView.alpha), 0.3) + XCTAssertEqual(round(fpc.backdropView.alpha), 0.3) fpc.move(to: .half, animated: false) XCTAssertEqual(fpc.backdropView.alpha, 0.0) fpc.move(to: .tip, animated: false) - XCTAssertEqual(_floor(fpc.backdropView.alpha), 0.3) + XCTAssertEqual(round(fpc.backdropView.alpha), 0.3) let exp1 = expectation(description: "move to full with animation") fpc.move(to: .full, animated: true) { exp1.fulfill() } wait(for: [exp1], timeout: timeout) - XCTAssertEqual(_floor(fpc.backdropView.alpha), 0.3) + XCTAssertEqual(round(fpc.backdropView.alpha), 0.3) let exp2 = expectation(description: "move to half with animation") fpc.move(to: .half, animated: true) { @@ -250,12 +250,12 @@ class CoreTests: XCTestCase { fpc.contentMode = .fitToBounds XCTAssertEqual(fpc.backdropView.alpha, 0.0) // Must not affect the backdrop alpha by changing the content mode wait(for: [exp3], timeout: timeout) - XCTAssertEqual(_floor(fpc.backdropView.alpha), 0.3) + XCTAssertEqual(round(fpc.backdropView.alpha), 0.3) // Test a size class change of FloatingPanelController.view fpc.move(to: .full, animated: false) - XCTAssertEqual(_floor(fpc.backdropView.alpha), 0.3) + XCTAssertEqual(round(fpc.backdropView.alpha), 0.3) fpc.willTransition(to: UITraitCollection(horizontalSizeClass: .regular), with: MockTransitionCoordinator()) XCTAssertEqual(fpc.backdropView.alpha, 0.0) // Must update the alpha by BackdropTestLayout2 in TestDelegate. @@ -264,7 +264,7 @@ class CoreTests: XCTestCase { fpc.move(to: .full, animated: false) delegate.layout = BackdropTestLayout() fpc.invalidateLayout() - XCTAssertEqual(_floor(fpc.backdropView.alpha), 0.3) + XCTAssertEqual(round(fpc.backdropView.alpha), 0.3) delegate.layout = BackdropTestLayout2() fpc.viewWillTransition(to: CGSize.zero, with: MockTransitionCoordinator()) From 54f1cc17eec5eb5a3348e43df719a399b0444e76 Mon Sep 17 00:00:00 2001 From: Shin Yamamoto Date: Wed, 30 Apr 2025 09:32:59 +0900 Subject: [PATCH 26/62] Take care of file paths including non-ASCII chars in SwiftFormatBuildToolPlugin --- BuildTools/Plugins/swift-format-plugin.swift | 35 +++++++++++--------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/BuildTools/Plugins/swift-format-plugin.swift b/BuildTools/Plugins/swift-format-plugin.swift index 543790168..bbcd0eab0 100644 --- a/BuildTools/Plugins/swift-format-plugin.swift +++ b/BuildTools/Plugins/swift-format-plugin.swift @@ -4,6 +4,7 @@ import PackagePlugin @main struct SwiftFormatBuildToolPlugin: BuildToolPlugin { func createBuildCommands(context: PluginContext, target: Target) async throws -> [Command] { + // Currently the build tool plugin is not supported. return [] } } @@ -11,6 +12,7 @@ struct SwiftFormatBuildToolPlugin: BuildToolPlugin { #if canImport(XcodeProjectPlugin) import XcodeProjectPlugin +/// Formats Swift source files using the `swift format` command and a root configuration file during Xcode builds. extension SwiftFormatBuildToolPlugin: XcodeBuildToolPlugin { // Entry point for creating build commands for targets in Xcode projects. func createBuildCommands(context: XcodePluginContext, target: XcodeTarget) throws -> [Command] { @@ -22,16 +24,17 @@ extension SwiftFormatBuildToolPlugin: XcodeBuildToolPlugin { let xcodeProjectDirectoryURL = URL(fileURLWithPath: context.xcodeProject.directory.string) #endif // Find the code generator tool to run (replace this with the actual one). - print("SwiftFormatBuildToolPlugin -> \(xcodeProjectDirectoryURL.path())") - let configFile = xcodeProjectDirectoryURL.appending(path: ".swift-format") - // Currently check only 'SwiftUI' source code. - let sourceFiles = xcodeProjectDirectoryURL.appending(path: "Sources/SwiftUI") - // let sourceFiles = xcodeProjectDirectoryURL.appending(path: "Sources") - // let testFiles = xcodeProjectDirectoryURL.appending(path: "Tests") - let buildToolsFiles = xcodeProjectDirectoryURL.appending(path: "BuildTools") - let examplesFiles = [ - xcodeProjectDirectoryURL.appending(path: "Examples/SamplesSwiftUI").path() - ] + print("SwiftFormatBuildToolPlugin -> \(xcodeProjectDirectoryURL.filePath)") + let configFile = xcodeProjectDirectoryURL.appending(path: ".swift-format").filePath + let targetFiles = [ + // Currently check only 'SwiftUI' source code in 'Sources' dir. + xcodeProjectDirectoryURL.appending(path: "Sources/SwiftUI"), + // xcodeProjectDirectoryURL.appending(path: "Sources"), + // xcodeProjectDirectoryURL.appending(path: "Tests"), + xcodeProjectDirectoryURL.appending(path: "BuildTools"), + // Currently check only 'SamplesSwiftUI' source code in 'Examples' dir. + xcodeProjectDirectoryURL.appending(path: "Examples/SamplesSwiftUI"), + ].map { $0.filePath } return [ .buildCommand( displayName: "Run swift format(xcode)", @@ -40,16 +43,18 @@ extension SwiftFormatBuildToolPlugin: XcodeBuildToolPlugin { "format", "lint", "--configuration", - configFile.path(), + configFile, "-r", - sourceFiles.path(), - //testFiles.path(), - buildToolsFiles.path(), - ] + examplesFiles, + ] + targetFiles, inputFiles: [], outputFiles: [] ) ] } } + +extension URL { + /// Returns a non–percent-encoded path for use with components containing non-ASCII characters. + var filePath: String { path(percentEncoded: false) } +} #endif From c233c6e1b7443c3313760a2ceb733302830866b9 Mon Sep 17 00:00:00 2001 From: Shin Yamamoto Date: Mon, 5 May 2025 17:18:23 +0900 Subject: [PATCH 27/62] Add .xcodesamplecode.plist --- FloatingPanel.xcworkspace/.xcodesamplecode.plist | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 FloatingPanel.xcworkspace/.xcodesamplecode.plist diff --git a/FloatingPanel.xcworkspace/.xcodesamplecode.plist b/FloatingPanel.xcworkspace/.xcodesamplecode.plist new file mode 100644 index 000000000..5dd5da85f --- /dev/null +++ b/FloatingPanel.xcworkspace/.xcodesamplecode.plist @@ -0,0 +1,5 @@ + + + + + From bb3a0d64377c8f2bf1059d5e374c7568d03ea9f0 Mon Sep 17 00:00:00 2001 From: Shin Yamamoto Date: Sat, 10 May 2025 16:02:11 +0900 Subject: [PATCH 28/62] Move 'assets' folder in 'Documentation' folder --- {assets => Documentation/assets}/maps-landscape.gif | Bin {assets => Documentation/assets}/maps.gif | Bin {assets => Documentation/assets}/stocks.gif | Bin README.md | 6 +++--- 4 files changed, 3 insertions(+), 3 deletions(-) rename {assets => Documentation/assets}/maps-landscape.gif (100%) rename {assets => Documentation/assets}/maps.gif (100%) rename {assets => Documentation/assets}/stocks.gif (100%) diff --git a/assets/maps-landscape.gif b/Documentation/assets/maps-landscape.gif similarity index 100% rename from assets/maps-landscape.gif rename to Documentation/assets/maps-landscape.gif diff --git a/assets/maps.gif b/Documentation/assets/maps.gif similarity index 100% rename from assets/maps.gif rename to Documentation/assets/maps.gif diff --git a/assets/stocks.gif b/Documentation/assets/stocks.gif similarity index 100% rename from assets/stocks.gif rename to Documentation/assets/stocks.gif diff --git a/README.md b/README.md index 941a403f9..4dc072f86 100644 --- a/README.md +++ b/README.md @@ -10,10 +10,10 @@ The user interface displays related content and utilities alongside the main con 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) +![Maps](Documentation/assets/maps.gif) +![Stocks](Documentation/assets/stocks.gif) -![Maps(Landscape)](https://github.com/SCENEE/FloatingPanel/blob/master/assets/maps-landscape.gif) +![Maps(Landscape)](Documentation/assets/maps-landscape.gif) From 73564b66d0772038897dfdc1a53d0cbcba8147ad Mon Sep 17 00:00:00 2001 From: Shin Yamamoto Date: Tue, 6 May 2025 14:47:41 +0900 Subject: [PATCH 29/62] Update documentation for the new SwiftUI APIs * Added 'FloatingPanel SwiftUI API Guide' * Updated README --- .../FloatingPanel 2.0 Migration Guide.md | 2 +- Documentation/FloatingPanel API Guide.md | 603 ++++++++++++++ .../FloatingPanel SwiftUI API Guide.md | 256 ++++++ README.md | 768 ++++-------------- .../FloatingPanel API Guide.md | 1 + .../FloatingPanel SwiftUI API Guide.md | 1 + Sources/FloatingPanel.docc/FloatingPanel.md | 60 +- 7 files changed, 1057 insertions(+), 634 deletions(-) create mode 100644 Documentation/FloatingPanel API Guide.md create mode 100644 Documentation/FloatingPanel SwiftUI API Guide.md create mode 120000 Sources/FloatingPanel.docc/FloatingPanel API Guide.md create mode 120000 Sources/FloatingPanel.docc/FloatingPanel SwiftUI API Guide.md diff --git a/Documentation/FloatingPanel 2.0 Migration Guide.md b/Documentation/FloatingPanel 2.0 Migration Guide.md index 7084efa32..cb650363f 100644 --- a/Documentation/FloatingPanel 2.0 Migration Guide.md +++ b/Documentation/FloatingPanel 2.0 Migration Guide.md @@ -19,7 +19,7 @@ This guide is provided in order to ease the transition of existing applications * __Flexible and explicit layout customization__ * `FloatingPanelLayout` is redesigned. There is no implicit rules to lay out a panel anymore. * __New spring animation without UIViewPropertyAnimator__ - * The new spring animation uses [Numeric springing](http://allenchou.net/2015/04/game-math-precise-control-over-numeric-springing/) which is a very powerful tool for procedural animation. Therefore a library consumer is easy to modify a panel behavior by 2 paramters of the deceleration rate and response time. + * The new spring animation uses [Numeric springing](http://allenchou.net/2015/04/game-math-precise-control-over-numeric-springing/) which is a very powerful tool for procedural animation. Therefore a library consumer is easy to modify a panel behavior by 2 parameters of the deceleration rate and response time. * __Handle the panel position anytime__ * `floatingPanelDidMove(_:)` delegate method is also called while a panel is moving. The method behavior becomes same as `scrollViewDidScroll(_:)` in `UIScrollViewDelegate`. And in the method a library consumer is able to change a panel location. * __Update the removal interaction's invocation__ diff --git a/Documentation/FloatingPanel API Guide.md b/Documentation/FloatingPanel API Guide.md new file mode 100644 index 000000000..1658d2ace --- /dev/null +++ b/Documentation/FloatingPanel API Guide.md @@ -0,0 +1,603 @@ +# Floating Panel API Guide + +## Table of Contents + +
+Click here + +- [Table of Contents](#table-of-contents) +- [View hierarchy](#view-hierarchy) +- [Usage](#usage) + - [Show/Hiding a floating panel in a view with your view hierarchy](#showhiding-a-floating-panel-in-a-view-with-your-view-hierarchy) + - [Scaling the content view when the surface position changes](#scaling-the-content-view-when-the-surface-position-changes) + - [Customizing the layout with `FloatingPanelLayout` protocol](#customizing-the-layout-with-floatingpanellayout-protocol) + - [Changing the initial layout](#changing-the-initial-layout) + - [Updating your panel layout](#updating-your-panel-layout) + - [Supporting your landscape layout](#supporting-your-landscape-layout) + - [Using the intrinsic size of a content in your panel layout](#using-the-intrinsic-size-of-a-content-in-your-panel-layout) + - [Specifying an anchor for each state by an inset of the `FloatingPanelController.view` frame](#specifying-an-anchor-for-each-state-by-an-inset-of-the-floatingpanelcontrollerview-frame) + - [Changing the backdrop alpha](#changing-the-backdrop-alpha) + - [Using custom panel states](#using-custom-panel-states) + - [Customizing the behavior with `FloatingPanelBehavior` protocol](#customizing-the-behavior-with-floatingpanelbehavior-protocol) + - [Modifying your floating panel's interaction](#modifying-your-floating-panels-interaction) + - [Activating the rubber-band effect on panel edges](#activating-the-rubber-band-effect-on-panel-edges) + - [Managing the projection of a pan gesture momentum](#managing-the-projection-of-a-pan-gesture-momentum) + - [Specifying the panel move's boundary](#specifying-the-panel-moves-boundary) + - [Customizing the surface design](#customizing-the-surface-design) + - [Modifying your surface appearance](#modifying-your-surface-appearance) + - [Use a custom grabber handle](#use-a-custom-grabber-handle) + - [Customizing layout of the grabber handle](#customizing-layout-of-the-grabber-handle) + - [Customizing content padding from surface edges](#customizing-content-padding-from-surface-edges) + - [Customizing margins of the surface edges](#customizing-margins-of-the-surface-edges) + - [Customizing gestures](#customizing-gestures) + - [Suppressing the panel interaction](#suppressing-the-panel-interaction) + - [Adding tap gestures to the surface view](#adding-tap-gestures-to-the-surface-view) + - [Interrupting the delegate methods of `FloatingPanelController.panGestureRecognizer`](#interrupting-the-delegate-methods-of-floatingpanelcontrollerpangesturerecognizer) + - [Creating an additional floating panel for a detail](#creating-an-additional-floating-panel-for-a-detail) + - [Moving a position with an animation](#moving-a-position-with-an-animation) + - [Working your contents together with a floating panel behavior](#working-your-contents-together-with-a-floating-panel-behavior) + - [Enabling the tap-to-dismiss action of the backdrop view](#enabling-the-tap-to-dismiss-action-of-the-backdrop-view) + - [Allowing to scroll content of the tracking scroll view in addition to the most expanded state](#allowing-to-scroll-content-of-the-tracking-scroll-view-in-addition-to-the-most-expanded-state) +- [Notes](#notes) + - ['Show' or 'Show Detail' Segues from `FloatingPanelController`'s content view controller](#show-or-show-detail-segues-from-floatingpanelcontrollers-content-view-controller) + - [UISearchController issue](#uisearchcontroller-issue) + +
+ +## View hierarchy + +`FloatingPanelController` manages the views as the following view hierarchy. + +```text +FloatingPanelController.view (FloatingPanelPassThroughView) + ├─ .backdropView (FloatingPanelBackdropView) + └─ .surfaceView (FloatingPanelSurfaceView) + ├─ .containerView (UIView) + │ └─ .contentView (FloatingPanelController.contentViewController.view) + └─ .grabber (FloatingPanelGrabberView) +``` + +## Usage + +### Show/Hiding a floating panel in a view with your view hierarchy + +If you need more control over showing and hiding the floating panel, you can forgo the `addPanel` and `removePanelFromParent` methods. These methods are a convenience wrapper for **FloatingPanel**'s `show` and `hide` methods along with some required setup. + +There are two ways to work with the `FloatingPanelController`: + +1. Add it to the hierarchy once and then call `show` and `hide` methods to make it appear/disappear. +2. Add it to the hierarchy when needed and remove afterwards. + +The following example shows how to add the controller to your `UIViewController` and how to remove it. Make sure that you never add the same `FloatingPanelController` to the hierarchy before removing it. + +**NOTE**: `self.` prefix is not required, nor recommended. It's used here to make it clearer where do the functions used come from. `self` is an instance of a custom UIViewController in your code. + +```swift +// Add the floating panel view to the controller's view on top of other views. +self.view.addSubview(fpc.view) + +// REQUIRED. It makes the floating panel view have the same size as the controller's view. +fpc.view.frame = self.view.bounds + +// In addition, Auto Layout constraints are highly recommended. +// Constraint the fpc.view to all four edges of your controller's view. +// It makes the layout more robust on trait collection change. +fpc.view.translatesAutoresizingMaskIntoConstraints = false +NSLayoutConstraint.activate([ + fpc.view.topAnchor.constraint(equalTo: self.view.topAnchor, constant: 0.0), + fpc.view.leftAnchor.constraint(equalTo: self.view.leftAnchor, constant: 0.0), + fpc.view.rightAnchor.constraint(equalTo: self.view.rightAnchor, constant: 0.0), + fpc.view.bottomAnchor.constraint(equalTo: self.view.bottomAnchor, constant: 0.0), +]) + +// Add the floating panel controller to the controller hierarchy. +self.addChild(fpc) + +// Show the floating panel at the initial position defined in your `FloatingPanelLayout` object. +fpc.show(animated: true) { + // Inform the floating panel controller that the transition to the controller hierarchy has completed. + fpc.didMove(toParent: self) +} +``` + +After you add the `FloatingPanelController` as seen above, you can call `fpc.show(animated: true) { }` to show the panel and `fpc.hide(animated: true) { }` to hide it. + +To remove the `FloatingPanelController` from the hierarchy, follow the example below. + +```swift +// Inform the panel controller that it will be removed from the hierarchy. +fpc.willMove(toParent: nil) + +// Hide the floating panel. +fpc.hide(animated: true) { + // Remove the floating panel view from your controller's view. + fpc.view.removeFromSuperview() + // Remove the floating panel controller from the controller hierarchy. + fpc.removeFromParent() +} +``` + +### Scaling the content view when the surface position changes + +Specify the `contentMode` to `.fitToBounds` if the surface height fits the bounds of `FloatingPanelController.view` when the surface position changes + +```swift +fpc.contentMode = .fitToBounds +``` + +Otherwise, `FloatingPanelController` fixes the content by the height of the top most position. + +> [!NOTE] +> In `.fitToBounds` mode, the surface height changes as following a user interaction so that you have a responsibility to configure Auto Layout constrains not to break the layout of a content view by the elastic surface height. + +### Customizing the layout with `FloatingPanelLayout` protocol + +#### Changing the initial layout + +```swift +class ViewController: UIViewController, FloatingPanelControllerDelegate { + ... { + fpc = FloatingPanelController(delegate: self) + fpc.layout = MyFloatingPanelLayout() + } +} + +class MyFloatingPanelLayout: FloatingPanelLayout { + let position: FloatingPanelPosition = .bottom + let initialState: FloatingPanelState = .tip + let anchors: [FloatingPanelState: FloatingPanelLayoutAnchoring] = [ + .full: FloatingPanelLayoutAnchor(absoluteInset: 16.0, edge: .top, referenceGuide: .safeArea), + .half: FloatingPanelLayoutAnchor(fractionalInset: 0.5, edge: .bottom, referenceGuide: .safeArea), + .tip: FloatingPanelLayoutAnchor(absoluteInset: 44.0, edge: .bottom, referenceGuide: .safeArea), + ] +} +``` + +### Updating your panel layout + +There are 2 ways to update the panel layout. + +1. Manually set `FloatingPanelController.layout` to the new layout object directly. + +```swift +fpc.layout = MyPanelLayout() +fpc.invalidateLayout() // If needed +``` + +Note: If you already set the `delegate` property of your `FloatingPanelController` instance, `invalidateLayout()` overrides the layout object of `FloatingPanelController` with one returned by the delegate object. + +2. Returns an appropriate layout object in one of 2 `floatingPanel(_:layoutFor:)` delegates. + +```swift +class ViewController: UIViewController, FloatingPanelControllerDelegate { + ... + func floatingPanel(_ vc: FloatingPanelController, layoutFor newCollection: UITraitCollection) -> FloatingPanelLayout { + return MyFloatingPanelLayout() + } + + // OR + func floatingPanel(_ vc: FloatingPanelController, layoutFor size: CGSize) -> FloatingPanelLayout { + return MyFloatingPanelLayout() + } +} +``` + +#### Supporting your landscape layout + +```swift +class ViewController: UIViewController, FloatingPanelControllerDelegate { + ... + func floatingPanel(_ vc: FloatingPanelController, layoutFor newCollection: UITraitCollection) -> FloatingPanelLayout { + return (newCollection.verticalSizeClass == .compact) ? LandscapePanelLayout() : FloatingPanelBottomLayout() + } +} + +class LandscapePanelLayout: FloatingPanelLayout { + let position: FloatingPanelPosition = .bottom + let initialState: FloatingPanelState = .tip + let anchors: [FloatingPanelState: FloatingPanelLayoutAnchoring] = [ + .full: FloatingPanelLayoutAnchor(absoluteInset: 16.0, edge: .top, referenceGuide: .safeArea), + .tip: FloatingPanelLayoutAnchor(absoluteInset: 69.0, edge: .bottom, referenceGuide: .safeArea), + ] + + func prepareLayout(surfaceView: UIView, in view: UIView) -> [NSLayoutConstraint] { + return [ + surfaceView.leftAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leftAnchor, constant: 8.0), + surfaceView.widthAnchor.constraint(equalToConstant: 291), + ] + } +} +``` + +#### Using the intrinsic size of a content in your panel layout + +1. Lay out your content View with the intrinsic height size. For example, see "Detail View Controller scene"/"Intrinsic View Controller scene" of [Main.storyboard](Examples/Samples/Sources/Base.lproj/Main.storyboard). The 'Stack View.bottom' constraint determines the intrinsic height. +2. Specify layout anchors using `FloatingPanelIntrinsicLayoutAnchor`. + +```swift +class IntrinsicPanelLayout: FloatingPanelLayout { + let position: FloatingPanelPosition = .bottom + let initialState: FloatingPanelState = .full + let anchors: [FloatingPanelState: FloatingPanelLayoutAnchoring] = [ + .full: FloatingPanelIntrinsicLayoutAnchor(absoluteOffset: 0, referenceGuide: .safeArea), + .half: FloatingPanelIntrinsicLayoutAnchor(fractionalOffset: 0.5, referenceGuide: .safeArea), + ] + ... +} +``` + +> [!WARNING] +> `FloatingPanelIntrinsicLayout` is deprecated on v1. + +#### Specifying an anchor for each state by an inset of the `FloatingPanelController.view` frame + +Use `.superview` reference guide in your anchors. + +```swift +class MyFullScreenLayout: FloatingPanelLayout { + ... + let anchors: [FloatingPanelState: FloatingPanelLayoutAnchoring] = [ + .full: FloatingPanelLayoutAnchor(absoluteInset: 16.0, edge: .top, referenceGuide: .superview), + .half: FloatingPanelLayoutAnchor(fractionalInset: 0.5, edge: .bottom, referenceGuide: .superview), + .tip: FloatingPanelLayoutAnchor(absoluteInset: 44.0, edge: .bottom, referenceGuide: .superview), + ] +} +``` + +> [!WARNING] +> `FloatingPanelFullScreenLayout` is deprecated on v1. + +#### Changing the backdrop alpha + +You can change the backdrop alpha by `FloatingPanelLayout.backdropAlpha(for:)` for each state(`.full`, `.half` and `.tip`). + +For instance, if a panel seems like the backdrop view isn't there on `.half` state, it's time to implement the backdropAlpha API and return a value for the state as below. + +```swift +class MyPanelLayout: FloatingPanelLayout { + func backdropAlpha(for state: FloatingPanelState) -> CGFloat { + switch state { + case .full, .half: return 0.3 + default: return 0.0 + } + } +} +``` + +#### Using custom panel states + +You're able to define custom panel states and use them as the following example. + +```swift +extension FloatingPanelState { + static let lastQuart: FloatingPanelState = FloatingPanelState(rawValue: "lastQuart", order: 750) + static let firstQuart: FloatingPanelState = FloatingPanelState(rawValue: "firstQuart", order: 250) +} + +class FloatingPanelLayoutWithCustomState: FloatingPanelBottomLayout { + override var anchors: [FloatingPanelState: FloatingPanelLayoutAnchoring] { + return [ + .full: FloatingPanelLayoutAnchor(absoluteInset: 0.0, edge: .top, referenceGuide: .safeArea), + .lastQuart: FloatingPanelLayoutAnchor(fractionalInset: 0.75, edge: .bottom, referenceGuide: .safeArea), + .half: FloatingPanelLayoutAnchor(fractionalInset: 0.5, edge: .bottom, referenceGuide: .safeArea), + .firstQuart: FloatingPanelLayoutAnchor(fractionalInset: 0.25, edge: .bottom, referenceGuide: .safeArea), + .tip: FloatingPanelLayoutAnchor(absoluteInset: 20.0, edge: .bottom, referenceGuide: .safeArea), + ] + } +} +``` + +### Customizing the behavior with `FloatingPanelBehavior` protocol + +#### Modifying your floating panel's interaction + +```swift +class ViewController: UIViewController, FloatingPanelControllerDelegate { + ... + func viewDidLoad() { + ... + fpc.behavior = CustomPanelBehavior() + } +} + +class CustomPanelBehavior: FloatingPanelBehavior { + let springDecelerationRate = UIScrollView.DecelerationRate.fast.rawValue + 0.02 + let springResponseTime = 0.4 + func shouldProjectMomentum(_ fpc: FloatingPanelController, to proposedState: FloatingPanelState) -> Bool { + return true + } +} +``` + +> [!WARNING] +> `floatingPanel(_ vc:behaviorFor:)` is deprecated on v1. + +#### Activating the rubber-band effect on panel edges + +```swift +class MyPanelBehavior: FloatingPanelBehavior { + ... + func allowsRubberBanding(for edge: UIRectEdge) -> Bool { + return true + } +} +``` + +#### Managing the projection of a pan gesture momentum + +This allows full projectional panel behavior. For example, a user can swipe up a panel from tip to full nearby the tip position. + +```swift +class MyPanelBehavior: FloatingPanelBehavior { + ... + func shouldProjectMomentum(_ fpc: FloatingPanelController, to proposedState: FloatingPanelState) -> Bool { + return true + } +} +``` + +### Specifying the panel move's boundary + +`FloatingPanelController.surfaceLocation` in `floatingPanelDidMove(_:)` delegate method behaves like `UIScrollView.contentOffset` in `scrollViewDidScroll(_:)`. +As a result, you can specify the boundary of a panel move as below. + +```swift +func floatingPanelDidMove(_ vc: FloatingPanelController) { + if vc.isAttracting == false { + let loc = vc.surfaceLocation + let minY = vc.surfaceLocation(for: .full).y - 6.0 + let maxY = vc.surfaceLocation(for: .tip).y + 6.0 + vc.surfaceLocation = CGPoint(x: loc.x, y: min(max(loc.y, minY), maxY)) + } +} +``` + +> [!WARNING] +> `{top,bottom}InteractionBuffer` property is removed from `FloatingPanelLayout` since v2. + +### Customizing the surface design + +#### Modifying your surface appearance + +```swift +// Create a new appearance. +let appearance = SurfaceAppearance() + +// Define shadows +let shadow = SurfaceAppearance.Shadow() +shadow.color = UIColor.black +shadow.offset = CGSize(width: 0, height: 16) +shadow.radius = 16 +shadow.spread = 8 +appearance.shadows = [shadow] + +// Define corner radius and background color +appearance.cornerRadius = 8.0 +appearance.backgroundColor = .clear + +// Set the new appearance +fpc.surfaceView.appearance = appearance +```` + +#### Use a custom grabber handle + +```swift +let myGrabberHandleView = MyGrabberHandleView() +fpc.surfaceView.grabberHandle.isHidden = true +fpc.surfaceView.addSubview(myGrabberHandleView) +``` + +#### Customizing layout of the grabber handle + +```swift +fpc.surfaceView.grabberHandlePadding = 10.0 +fpc.surfaceView.grabberHandleSize = .init(width: 44.0, height: 12.0) +``` + +> [!NOTE] +> `grabberHandleSize` width and height are reversed in the left/right position. + +#### Customizing content padding from surface edges + +```swift +fpc.surfaceView.contentPadding = .init(top: 20, left: 20, bottom: 20, right: 20) +``` + +#### Customizing margins of the surface edges + +```swift +fpc.surfaceView.containerMargins = .init(top: 20.0, left: 16.0, bottom: 16.0, right: 16.0) +``` + +The feature can be used for these 2 kind panels + +- Facebook/Slack-like panel whose surface top edge is separated from the grabber handle. +- iOS native panel to display AirPods information, for example. + +### Customizing gestures + +#### Suppressing the panel interaction + +You can disable the pan gesture recognizer directly + +```swift +fpc.panGestureRecognizer.isEnabled = false +``` + +Or use this `FloatingPanelControllerDelegate` method. + +```swift +func floatingPanelShouldBeginDragging(_ vc: FloatingPanelController) -> Bool { + return aCondition ? false : true +} +``` + +#### Adding tap gestures to the surface view + +```swift +override func viewDidLoad() { + ... + let surfaceTapGesture = UITapGestureRecognizer(target: self, action: #selector(handleSurface(tapGesture:))) + fpc.surfaceView.addGestureRecognizer(surfaceTapGesture) + surfaceTapGesture.isEnabled = (fpc.position == .tip) +} + +// Enable `surfaceTapGesture` only at `tip` state +func floatingPanelDidChangeState(_ vc: FloatingPanelController) { + surfaceTapGesture.isEnabled = (vc.position == .tip) +} +``` + +#### Interrupting the delegate methods of `FloatingPanelController.panGestureRecognizer` + +If you are set `FloatingPanelController.panGestureRecognizer.delegateProxy` to an object adopting `UIGestureRecognizerDelegate`, it overrides delegate methods of the pan gesture recognizer. + +```swift +class MyGestureRecognizerDelegate: UIGestureRecognizerDelegate { + func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRequireFailureOf otherGestureRecognizer: UIGestureRecognizer) -> Bool { + return false + } +} + +class ViewController: UIViewController { + let myGestureDelegate = MyGestureRecognizerDelegate() + + func setUpFpc() { + .... + fpc.panGestureRecognizer.delegateProxy = myGestureDelegate + } +``` + +### Creating an additional floating panel for a detail + +```swift +override func viewDidLoad() { + // Setup Search panel + self.searchPanelVC = FloatingPanelController() + + let searchVC = SearchViewController() + self.searchPanelVC.set(contentViewController: searchVC) + self.searchPanelVC.track(scrollView: contentVC.tableView) + + self.searchPanelVC.addPanel(toParent: self) + + // Setup Detail panel + self.detailPanelVC = FloatingPanelController() + + let contentVC = ContentViewController() + self.detailPanelVC.set(contentViewController: contentVC) + self.detailPanelVC.track(scrollView: contentVC.scrollView) + + self.detailPanelVC.addPanel(toParent: self) +} +``` + +### Moving a position with an animation + +In the following example, I move a floating panel to full or half position while opening or closing a search bar like Apple Maps. + +```swift +func searchBarCancelButtonClicked(_ searchBar: UISearchBar) { + ... + fpc.move(to: .half, animated: true) +} + +func searchBarTextDidBeginEditing(_ searchBar: UISearchBar) { + ... + fpc.move(to: .full, animated: true) +} +``` + +You can also use a view animation to move a panel. + +```swift +UIView.animate(withDuration: 0.25) { + self.fpc.move(to: .half, animated: false) +} +``` + +### Working your contents together with a floating panel behavior + +```swift +class ViewController: UIViewController, FloatingPanelControllerDelegate { + ... + func floatingPanelWillBeginDragging(_ vc: FloatingPanelController) { + if vc.position == .full { + searchVC.searchBar.showsCancelButton = false + searchVC.searchBar.resignFirstResponder() + } + } + + func floatingPanelWillEndDragging(_ vc: FloatingPanelController, withVelocity velocity: CGPoint, targetState: UnsafeMutablePointer) { + if targetState.pointee != .full { + searchVC.hideHeader() + } + } +} +``` + +### Enabling the tap-to-dismiss action of the backdrop view + +The tap-to-dismiss action is disabled by default. So it needs to be enabled as below. + +```swift +fpc.backdropView.dismissalTapGestureRecognizer.isEnabled = true +``` + +### Allowing to scroll content of the tracking scroll view in addition to the most expanded state + +Just define conditions to allow content scrolling in `floatingPanel(:_:shouldAllowToScroll:in)` delegate method. If the returned value is true, the scroll content scrolls when its scroll position is not at the top of the content. + +```swift +class MyViewController: FloatingPanelControllerDelegate { + ... + + func floatingPanel( + _ fpc: FloatingPanelController, + shouldAllowToScroll trackingScrollView: UIScrollView, + in state: FloatingPanelState + ) -> Bool { + return state == .full || state == .half + } +} +``` + +## Notes + +### 'Show' or 'Show Detail' Segues from `FloatingPanelController`'s content view controller + +'Show' or 'Show Detail' segues from a content view controller will be managed by a view controller(hereinafter called 'master VC') adding a floating panel. Because a floating panel is just a subview of the master VC(except for modality). + +`FloatingPanelController` has no way to manage a stack of view controllers like `UINavigationController`. If so, it would be so complicated and the interface will become `UINavigationController`. This component should not have the responsibility to manage the stack. + +By the way, a content view controller can present a view controller modally with `present(_:animated:completion:)` or 'Present Modally' segue. + +However, sometimes you want to show a destination view controller of 'Show' or 'Show Detail' segue with another floating panel. It's possible to override `show(_:sender)` of the master VC! + +Here is an example. + +```swift +class ViewController: UIViewController { + var fpc: FloatingPanelController! + var secondFpc: FloatingPanelController! + + ... + override func show(_ vc: UIViewController, sender: Any?) { + secondFpc = FloatingPanelController() + + secondFpc.set(contentViewController: vc) + + secondFpc.addPanel(toParent: self) + } +} +``` + +A `FloatingPanelController` object proxies an action for `show(_:sender)` to the master VC. That's why the master VC can handle a destination view controller of a 'Show' or 'Show Detail' segue and you can hook `show(_:sender)` to show a secondary floating panel set the destination view controller to the content. + +It's a great way to decouple between a floating panel and the content VC. + +### UISearchController issue + +`UISearchController` isn't able to be used with `FloatingPanelController` by the system design. + +Because `UISearchController` automatically presents itself modally when a user interacts with the search bar, and then it swaps the superview of the search bar to the view managed by itself while it displays. As a result, `FloatingPanelController` can't control the search bar when it's active, as you can see from [the screen shot](https://github.com/SCENEE/FloatingPanel/issues/248#issuecomment-521263831). diff --git a/Documentation/FloatingPanel SwiftUI API Guide.md b/Documentation/FloatingPanel SwiftUI API Guide.md new file mode 100644 index 000000000..0d5c07944 --- /dev/null +++ b/Documentation/FloatingPanel SwiftUI API Guide.md @@ -0,0 +1,256 @@ +# FloatingPanel SwiftUI API Guide + +## Table of Contents + +
+ +Click here + + +- [Table of Contents](#table-of-contents) +- [Requirements](#requirements) +- [Goals](#goals) +- [Non-Goals](#non-goals) +- [Design Principles](#design-principles) +- [Development and compatibility](#development-and-compatibility) +- [API implementation considerations](#api-implementation-considerations) +- [Supplemental view approach rather than modal presentation](#supplemental-view-approach-rather-than-modal-presentation) +- [Leveraging the view modifiers](#leveraging-the-view-modifiers) +- [`FloatingPanelCoordinator`: The key component](#floatingpanelcoordinator-the-key-component) + - [Core Responsibilities](#core-responsibilities) + - [Default Implementation](#default-implementation) + - [Use Cases](#use-cases) + - [1. Custom Event Handling](#1-custom-event-handling) + - [2. Responding to Environment Changes](#2-responding-to-environment-changes) + - [3. Modal Presentation](#3-modal-presentation) + - [Best Practices](#best-practices) + - [Define Meaningful Events](#define-meaningful-events) + - [Use Lazy Delegate Initialization](#use-lazy-delegate-initialization) + - [Handle Environment Changes Efficiently](#handle-environment-changes-efficiently) + - [Coordinate with SwiftUI Animations](#coordinate-with-swiftui-animations) + +
+ +## Requirements + +- iOS 15 or later +- Xcode 16 or later + +> [!NOTE] +> The SwiftUI API can be used on iOS 14, but it's out of the supported versions. + +## Goals + +1. Build SwiftUI APIs on top of our battle-tested UIKit implementation +2. Enable smooth interoperability between UIKit and SwiftUI components +3. Provide an idiomatic SwiftUI developer experience + +## Non-Goals + +1. Complete reimplementation of FloatingPanel solely using SwiftUI APIs +2. Cover all UIKit-specific customization options through SwiftUI APIs + +## Design Principles + +- APIs designed to maximize user control and flexibility +- FloatingPanel designed as a supplemental view rather than a modal presentation as the same as UIKit implementation. +- Declarative modifiers that follow SwiftUI conventions +- Environment-based configuration patterns +- Seamless integration with SwiftUI's view hierarchy +- Implementation of essential APIs only, with plans to expand based on user requests -- we welcome your feedback! + +## Development and compatibility + +- Built targeting Xcode 16 as the primary development environment + - Maintains backward compatibility with Xcode 14/15 for UIKit support but gradually migrates to full Xcode 16+ features +- iOS version compatibility: + - Our SwiftUI integration builds for iOS 14 or later, but has been primarily tested on iOS 15+ + +## API implementation considerations + +- We've determined that providing an integration API is the optimal approach with enhanced UIKit integration (as of iOS 18) +- Currently not using `@Entry` due to compatibility constraints + +## Supplemental view approach rather than modal presentation + +FloatingPanel has been designed as a supplemental view rather than a modal presentation since its first release in the UIKit implementation. The SwiftUI APIs follow this same principle, allowing users to leverage this library for use cases not covered by Apple's built-in APIs. + +For instance, the SwiftUI API deliberately doesn't provide an `isPresented` binding argument in the `floatingPanel(coordinator:onEvent:content:)` modifier. If you want to hide a floating panel, use the `.hidden` anchor state instead, which enables you to hide a floating panel while keeping the content pre-rendered outside the visible screen area. + +## Leveraging the view modifiers + +The SwiftUI APIs provide a variety of view modifiers. Consider using these modifiers before implementing custom logic in your `FloatingPanelCoordinator` object. + +## `FloatingPanelCoordinator`: The key component + +The `FloatingPanelCoordinator` protocol is a key component in the FloatingPanel's SwiftUI integration, serving as the bridge between SwiftUI's declarative UI framework and FloatingPanel's UIKit-based implementation. + +It manages the connection between the SwiftUI view hierarchy and the underlying `FloatingPanelController`, handling setup, configuration, and event dispatching for floating panels within SwiftUI applications. + +This secion explains the `FloatingPanelCoordinator` protocol in detail, including its purpose, implementation patterns, and common use cases to help you effectively integrate floating panels into your SwiftUI applications. + +### Core Responsibilities + +A `FloatingPanelCoordinator` implementation handles the following key responsibilities: + +1. **Creation and Configuration**: Initializing and configuring the underlying `FloatingPanelController` instance +2. **View Hierarchy Management**: Setting up the relationship between the main SwiftUI view and the panel content view +3. **State Management**: Handling panel state transitions and position changes +4. **Event Handling**: Capturing panel events and dispatching them back to SwiftUI +5. **Environment Changes**: Responding to SwiftUI environment changes and updating the panel accordingly + +### Default Implementation + +The library provides [`FloatingPanelDefaultCoordinator`](Sources/SwiftUI/FloatingPanelCoordinator.swift) as a standard implementation for basic panel integration. + +`FloatingPanelCoordinator` intentionally does not provide default implementations for its required methods. This design choice avoids implicit behavior when handling the `FloatingPanelController`. When implementing a custom coordinator instead of using `FloatingPanelDefaultCoordinator`, users can clearly understand the requirements of the `FloatingPanelCoordinator` protocol and how the `FloatingPanelController` should be managed in their custom implementation. + +### Use Cases + +#### 1. Custom Event Handling + +Define a custom coordinator to handle panel events: + +```swift +struct ContentView: View { + var body: some View { + Color.blue + .ignoresSafeArea() + .floatingPanel( + coordinator: MyPanelCoordinator.self, + onEvent: handlePanelEvent + ) { proxy in + PanelContent(proxy: proxy) + } + } + + func handlePanelEvent(_ event: MyPanelCoordinator.Event) { + switch event { + case .willChangeState(let state): + print("Panel will change to \(state)") + case .didChangeState(let state): + print("Panel changed to \(state)") + } + } +} + +class MyPanelCoordinator: FloatingPanelCoordinator { + enum Event { + case willChangeState(FloatingPanelState) + case didChangeState(FloatingPanelState) + } + + let action: (Event) -> Void + lazy var delegate: FloatingPanelControllerDelegate? = self + let proxy: FloatingPanelProxy + + ... +} + +extension MyPanelCoordinator: FloatingPanelControllerDelegate { + func floatingPanelWillBeginDragging(_ fpc: FloatingPanelController) { + action(.willChangeState(fpc.state)) + } + + func floatingPanelDidChangeState(_ fpc: FloatingPanelController) { + action(.didChangeState(fpc.state)) + } +} +``` + +#### 2. Responding to Environment Changes + +Create a coordinator that responds to SwiftUI environment changes: + +```swift +class EnvironmentAwarePanelCoordinator: FloatingPanelCoordinator { + ... + func onUpdate( + context: UIViewControllerRepresentableContext + ) where Representable: UIViewControllerRepresentable { + // Access environment values and update the panel + let shouldMoveToFullState = context.environment.someCustomValue + if shouldMoveToFullState { + proxy.move(to: .full, animated: true) + } + } +} +``` + +#### 3. Modal Presentation + +Implement a coordinator that presents the panel modally: + +```swift +class ModalPanelCoordinator: FloatingPanelCoordinator { + enum Event { + case dismissed + } + + let action: (Event) -> Void + let proxy: FloatingPanelProxy + lazy var delegate: FloatingPanelControllerDelegate? = nil + + ... + func setupFloatingPanel( + mainHostingController: UIHostingController
, + contentHostingController: UIHostingController + ) where Main: View, Content: View { + // Set up the content + contentHostingController.view.backgroundColor = .clear + controller.set(contentViewController: contentHostingController) + + // Present the panel modally + Task { @MainActor in + controller.isRemovalInteractionEnabled = true + controller.delegate = self + mainHostingController.present(controller, animated: true) + } + } + + ... +} + +extension ModalPanelCoordinator: FloatingPanelControllerDelegate { + func floatingPanelDidEndRemove(_ fpc: FloatingPanelController) { + action(.dismissed) + } +} +``` + +### Best Practices + +#### Define Meaningful Events + +Design your `Event` type to capture meaningful panel interactions that SwiftUI views need to respond to, but avoid over-engineering with too many event types. + +#### Use Lazy Delegate Initialization + +Initialize `delegate` lazily when you want your coordinator to implement `FloatingPanelControllerDelegate`: + +```swift +lazy var delegate: FloatingPanelControllerDelegate? = self +``` + +#### Handle Environment Changes Efficiently + +In your `onUpdate` method, compare environment values with current panel state before making changes to avoid unnecessary updates. + +#### Coordinate with SwiftUI Animations + +Respect SwiftUI's animation context when moving the panel on iOS 18 or later: + +```swift +func onUpdate( + context: UIViewControllerRepresentableContext +) where Representable: UIViewControllerRepresentable { + if #available(iOS 18.0, *) { + let animation = context.transaction.animation ?? .spring(response: 0.25, dampingFraction: 0.9) + UIView.animate(animation) { + proxy.move(to: .full, animated: false) + } + } else { + ... + } +} +``` diff --git a/README.md b/README.md index 4dc072f86..cfdac4f77 100644 --- a/README.md +++ b/README.md @@ -8,709 +8,259 @@ 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.8/documentation/floatingpanel) for more details. - ![Maps](Documentation/assets/maps.gif) ![Stocks](Documentation/assets/stocks.gif) ![Maps(Landscape)](Documentation/assets/maps-landscape.gif) - +## Table of Contents + +
+ +Click here. + +- [Table of Contents](#table-of-contents) - [Features](#features) - [Requirements](#requirements) +- [Documentation](#documentation) - [Installation](#installation) - - [CocoaPods](#cocoapods) - [Swift Package Manager](#swift-package-manager) -- [Getting Started](#getting-started) - - [Add a floating panel as a child view controller](#add-a-floating-panel-as-a-child-view-controller) - - [Present a floating panel as a modality](#present-a-floating-panel-as-a-modality) -- [View hierarchy](#view-hierarchy) -- [Usage](#usage) - - [Show/Hide a floating panel in a view with your view hierarchy](#showhide-a-floating-panel-in-a-view-with-your-view-hierarchy) - - [Scale the content view when the surface position changes](#scale-the-content-view-when-the-surface-position-changes) - - [Customize the layout with `FloatingPanelLayout` protocol](#customize-the-layout-with-floatingpanellayout-protocol) - - [Change the initial layout](#change-the-initial-layout) - - [Update your panel layout](#update-your-panel-layout) - - [Support your landscape layout](#support-your-landscape-layout) - - [Use the intrinsic size of a content in your panel layout](#use-the-intrinsic-size-of-a-content-in-your-panel-layout) - - [Specify an anchor for each state by an inset of the `FloatingPanelController.view` frame](#specify-an-anchor-for-each-state-by-an-inset-of-the-floatingpanelcontrollerview-frame) - - [Change the backdrop alpha](#change-the-backdrop-alpha) - - [Using custome panel states](#using-custome-panel-states) - - [Customize the behavior with `FloatingPanelBehavior` protocol](#customize-the-behavior-with-floatingpanelbehavior-protocol) - - [Modify your floating panel's interaction](#modify-your-floating-panels-interaction) - - [Activate the rubber-band effect on panel edges](#activate-the-rubber-band-effect-on-panel-edges) - - [Manage the projection of a pan gesture momentum](#manage-the-projection-of-a-pan-gesture-momentum) - - [Specify the panel move's boundary](#specify-the-panel-moves-boundary) - - [Customize the surface design](#customize-the-surface-design) - - [Modify your surface appearance](#modify-your-surface-appearance) - - [Use a custom grabber handle](#use-a-custom-grabber-handle) - - [Customize layout of the grabber handle](#customize-layout-of-the-grabber-handle) - - [Customize content padding from surface edges](#customize-content-padding-from-surface-edges) - - [Customize margins of the surface edges](#customize-margins-of-the-surface-edges) - - [Customize gestures](#customize-gestures) - - [Suppress the panel interaction](#suppress-the-panel-interaction) - - [Add tap gestures to the surface view](#add-tap-gestures-to-the-surface-view) - - [Interrupt the delegate methods of `FloatingPanelController.panGestureRecognizer`](#interrupt-the-delegate-methods-of-floatingpanelcontrollerpangesturerecognizer) - - [Create an additional floating panel for a detail](#create-an-additional-floating-panel-for-a-detail) - - [Move a position with an animation](#move-a-position-with-an-animation) - - [Work your contents together with a floating panel behavior](#work-your-contents-together-with-a-floating-panel-behavior) - - [Enabling the tap-to-dismiss action of the backdrop view](#enabling-the-tap-to-dismiss-action-of-the-backdrop-view) - - [Allow to scroll content of the tracking scroll view in addition to the most expanded state](#allow-to-scroll-content-of-the-tracking-scroll-view-in-addition-to-the-most-expanded-state) -- [Notes](#notes) - - ['Show' or 'Show Detail' Segues from `FloatingPanelController`'s content view controller](#show-or-show-detail-segues-from-floatingpanelcontrollers-content-view-controller) - - [UISearchController issue](#uisearchcontroller-issue) + - [Using Xcode](#using-xcode) + - [Using Package.swift](#using-packageswift) + - [CocoaPods](#cocoapods) +- [Getting Started with SwiftUI](#getting-started-with-swiftui) + - [Adding a floating panel within a view](#adding-a-floating-panel-within-a-view) + - [Presenting a floating panel modally within a view](#presenting-a-floating-panel-modally-within-a-view) + - [Displaying multiple panels](#displaying-multiple-panels) + - [Next step](#next-step) +- [Getting Started with UIKit](#getting-started-with-uikit) + - [Adding a floating panel as a child view controller](#adding-a-floating-panel-as-a-child-view-controller) + - [Presenting a floating panel modally](#presenting-a-floating-panel-modally) + - [Next step](#next-step-1) - [Maintainer](#maintainer) - [License](#license) - +
## Features -- [x] Simple container view controller -- [x] Fluid behavior using numeric springing -- [x] Scroll view tracking -- [x] Removal interaction -- [x] Multi panel support -- [x] Modal presentation -- [x] Support for 4 positions (top, left, bottom, right) -- [x] 1 or more magnetic anchors(full, half, tip and more) -- [x] Layout support for all trait environments(i.e. Landscape orientation) -- [x] Common UI elements: surface, backdrop and grabber handle -- [x] Free from common Auto Layout and gesture handling issues -- [x] Compatible with Objective-C +- Simple container view controller +- Fluid behavior using numeric springing +- Scroll view tracking +- Removal interaction +- Multi panel support +- Modal presentation +- Support for 4 positions (top, left, bottom, right) +- 1 or more magnetic anchors(full, half, tip and more) +- Layout support for all trait environments(i.e. Landscape orientation) +- Common UI elements: surface, backdrop and grabber handle +- Free from common Auto Layout and gesture handling issues +- Compatible with Objective-C +- SwiftUI API support Examples can be found here: -- [Examples/Maps](https://github.com/SCENEE/FloatingPanel/tree/master/Examples/Maps) like Apple Maps.app. -- [Examples/Stocks](https://github.com/SCENEE/FloatingPanel/tree/master/Examples/Stocks) like Apple Stocks.app. -- [Examples/Samples](https://github.com/SCENEE/FloatingPanel/tree/master/Examples/Samples) -- [Examples/SamplesObjC](https://github.com/SCENEE/FloatingPanel/tree/master/Examples/SamplesObjC) +- [Maps](Examples/Maps) like Apple Maps.app. +- [Maps-SwiftUI](Examples/Maps-SwiftUI) like Apple Maps.app using SwiftUI. +- [Stocks](Examples/Stocks) like Apple Stocks.app. +- [Samples](Examples/Samples) +- [SamplesObjC](Examples/SamplesObjC) +- [SamplesSwiftUI](Examples/SamplesSwiftUI) ## Requirements -FloatingPanel is written in Swift 5.0+ and compatible with iOS 11.0+. +FloatingPanel is written in Swift 5.0+ and compatible with iOS 12.0+. -## Installation +## Documentation -### CocoaPods - -FloatingPanel is available through [CocoaPods](https://cocoapods.org). To install -it, simply add the following line to your Podfile: +- [API reference on Swift Package Index](https://swiftpackageindex.com/scenee/FloatingPanel/2.8.8/documentation/floatingpanel) +- [FloatingPanel SwiftUI API Guide](/Documentation/FloatingPanel%20SwiftUI%20API%20Guide.md) +- [FloatingPanel API Guide](/Documentation/FloatingPanel%20API%20Guide.md) +- [FloatingPanel 2.0 Migration Guide](/Documentation/FloatingPanel%202.0%20Migration%20Guide.md) -```ruby -pod 'FloatingPanel' -``` +## Installation ### Swift Package Manager -Follow [this doc](https://developer.apple.com/documentation/swift_packages/adding_package_dependencies_to_your_app). - -## Getting Started - -### Add a floating panel as a child view controller - -```swift -import UIKit -import FloatingPanel - -class ViewController: UIViewController, FloatingPanelControllerDelegate { - var fpc: FloatingPanelController! - - override func viewDidLoad() { - super.viewDidLoad() - // Initialize a `FloatingPanelController` object. - fpc = FloatingPanelController() - - // Assign self as the delegate of the controller. - fpc.delegate = self // Optional - - // Set a content view controller. - let contentVC = ContentViewController() - fpc.set(contentViewController: contentVC) - - // Track a scroll view(or the siblings) in the content view controller. - fpc.track(scrollView: contentVC.tableView) - - // Add and show the views managed by the `FloatingPanelController` object to self.view. - fpc.addPanel(toParent: self) - } -} -``` - -### Present a floating panel as a modality - -```swift -let fpc = FloatingPanelController() -let contentVC = ... -fpc.set(contentViewController: contentVC) - -fpc.isRemovalInteractionEnabled = true // Optional: Let it removable by a swipe-down - -self.present(fpc, animated: true, completion: nil) -``` - -You can show a floating panel over UINavigationController from the container view controllers as a modality of `.overCurrentContext` style. - -> [!NOTE] -> FloatingPanelController has the custom presentation controller. If you would like to customize the presentation/dismissal, please see [Transitioning](https://github.com/SCENEE/FloatingPanel/blob/master/Sources/Transitioning.swift). - -## View hierarchy - -`FloatingPanelController` manages the views as the following view hierarchy. - -``` -FloatingPanelController.view (FloatingPanelPassThroughView) - ├─ .backdropView (FloatingPanelBackdropView) - └─ .surfaceView (FloatingPanelSurfaceView) - ├─ .containerView (UIView) - │ └─ .contentView (FloatingPanelController.contentViewController.view) - └─ .grabber (FloatingPanelGrabberView) -``` - -## Usage - -### Show/Hide a floating panel in a view with your view hierarchy - -If you need more control over showing and hiding the floating panel, you can forgo the `addPanel` and `removePanelFromParent` methods. These methods are a convenience wrapper for **FloatingPanel**'s `show` and `hide` methods along with some required setup. - -There are two ways to work with the `FloatingPanelController`: -1. Add it to the hierarchy once and then call `show` and `hide` methods to make it appear/disappear. -2. Add it to the hierarchy when needed and remove afterwards. - -The following example shows how to add the controller to your `UIViewController` and how to remove it. Make sure that you never add the same `FloatingPanelController` to the hierarchy before removing it. - -**NOTE**: `self.` prefix is not required, nor recommended. It's used here to make it clearer where do the functions used come from. `self` is an instance of a custom UIViewController in your code. - -```swift -// Add the floating panel view to the controller's view on top of other views. -self.view.addSubview(fpc.view) - -// REQUIRED. It makes the floating panel view have the same size as the controller's view. -fpc.view.frame = self.view.bounds - -// In addition, Auto Layout constraints are highly recommended. -// Constraint the fpc.view to all four edges of your controller's view. -// It makes the layout more robust on trait collection change. -fpc.view.translatesAutoresizingMaskIntoConstraints = false -NSLayoutConstraint.activate([ - fpc.view.topAnchor.constraint(equalTo: self.view.topAnchor, constant: 0.0), - fpc.view.leftAnchor.constraint(equalTo: self.view.leftAnchor, constant: 0.0), - fpc.view.rightAnchor.constraint(equalTo: self.view.rightAnchor, constant: 0.0), - fpc.view.bottomAnchor.constraint(equalTo: self.view.bottomAnchor, constant: 0.0), -]) - -// Add the floating panel controller to the controller hierarchy. -self.addChild(fpc) - -// Show the floating panel at the initial position defined in your `FloatingPanelLayout` object. -fpc.show(animated: true) { - // Inform the floating panel controller that the transition to the controller hierarchy has completed. - fpc.didMove(toParent: self) -} -``` - -After you add the `FloatingPanelController` as seen above, you can call `fpc.show(animated: true) { }` to show the panel and `fpc.hide(animated: true) { }` to hide it. - -To remove the `FloatingPanelController` from the hierarchy, follow the example below. - -```swift -// Inform the panel controller that it will be removed from the hierarchy. -fpc.willMove(toParent: nil) - -// Hide the floating panel. -fpc.hide(animated: true) { - // Remove the floating panel view from your controller's view. - fpc.view.removeFromSuperview() - // Remove the floating panel controller from the controller hierarchy. - fpc.removeFromParent() -} -``` - -### Scale the content view when the surface position changes +#### Using Xcode -Specify the `contentMode` to `.fitToBounds` if the surface height fits the bounds of `FloatingPanelController.view` when the surface position changes +Just follow [this documentation](https://developer.apple.com/documentation/swift_packages/adding_package_dependencies_to_your_app). -```swift -fpc.contentMode = .fitToBounds -``` - -Otherwise, `FloatingPanelController` fixes the content by the height of the top most position. - -> [!NOTE] -> In `.fitToBounds` mode, the surface height changes as following a user interaction so that you have a responsibility to configure Auto Layout constrains not to break the layout of a content view by the elastic surface height. +#### Using Package.swift -### Customize the layout with `FloatingPanelLayout` protocol - -#### Change the initial layout +In your Package.swift Swift Package Manager manifest, add the following dependency to your dependencies argument: ```swift -class ViewController: UIViewController, FloatingPanelControllerDelegate { - ... { - fpc = FloatingPanelController(delegate: self) - fpc.layout = MyFloatingPanelLayout() - } -} - -class MyFloatingPanelLayout: FloatingPanelLayout { - let position: FloatingPanelPosition = .bottom - let initialState: FloatingPanelState = .tip - let anchors: [FloatingPanelState: FloatingPanelLayoutAnchoring] = [ - .full: FloatingPanelLayoutAnchor(absoluteInset: 16.0, edge: .top, referenceGuide: .safeArea), - .half: FloatingPanelLayoutAnchor(fractionalInset: 0.5, edge: .bottom, referenceGuide: .safeArea), - .tip: FloatingPanelLayoutAnchor(absoluteInset: 44.0, edge: .bottom, referenceGuide: .safeArea), - ] -} +.package(url: "https://github.com/scenee/FloatingPanel", from: "2.8.8"), ``` -### Update your panel layout - -There are 2 ways to update the panel layout. - -1. Manually set `FloatingPanelController.layout` to the new layout object directly. +Add Numerics as a dependency for your target: ```swift -fpc.layout = MyPanelLayout() -fpc.invalidateLayout() // If needed +.target(name: "MyTarget", dependencies: [ + .product(name: "FloatingPanel", package: "FloatingPanel"), + "AnotherModule" +]), ``` -Note: If you already set the `delegate` property of your `FloatingPanelController` instance, `invalidateLayout()` overrides the layout object of `FloatingPanelController` with one returned by the delegate object. - -2. Returns an appropriate layout object in one of 2 `floatingPanel(_:layoutFor:)` delegates. - -```swift -class ViewController: UIViewController, FloatingPanelControllerDelegate { - ... - func floatingPanel(_ vc: FloatingPanelController, layoutFor newCollection: UITraitCollection) -> FloatingPanelLayout { - return MyFloatingPanelLayout() - } - - // OR - func floatingPanel(_ vc: FloatingPanelController, layoutFor size: CGSize) -> FloatingPanelLayout { - return MyFloatingPanelLayout() - } -} -``` +And then add `import FloatingPanel` in your source code. -#### Support your landscape layout +### CocoaPods -```swift -class ViewController: UIViewController, FloatingPanelControllerDelegate { - ... - func floatingPanel(_ vc: FloatingPanelController, layoutFor newCollection: UITraitCollection) -> FloatingPanelLayout { - return (newCollection.verticalSizeClass == .compact) ? LandscapePanelLayout() : FloatingPanelBottomLayout() - } -} +FloatingPanel is available through [CocoaPods](https://cocoapods.org). To install +it, simply add the following line to your Podfile: -class LandscapePanelLayout: FloatingPanelLayout { - let position: FloatingPanelPosition = .bottom - let initialState: FloatingPanelState = .tip - let anchors: [FloatingPanelState: FloatingPanelLayoutAnchoring] = [ - .full: FloatingPanelLayoutAnchor(absoluteInset: 16.0, edge: .top, referenceGuide: .safeArea), - .tip: FloatingPanelLayoutAnchor(absoluteInset: 69.0, edge: .bottom, referenceGuide: .safeArea), - ] - - func prepareLayout(surfaceView: UIView, in view: UIView) -> [NSLayoutConstraint] { - return [ - surfaceView.leftAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leftAnchor, constant: 8.0), - surfaceView.widthAnchor.constraint(equalToConstant: 291), - ] - } -} +```ruby +pod 'FloatingPanel' ``` -#### Use the intrinsic size of a content in your panel layout +## Getting Started with SwiftUI -1. Lay out your content View with the intrinsic height size. For example, see "Detail View Controller scene"/"Intrinsic View Controller scene" of [Main.storyboard](https://github.com/SCENEE/FloatingPanel/blob/master/Examples/Samples/Sources/Base.lproj/Main.storyboard). The 'Stack View.bottom' constraint determines the intrinsic height. -2. Specify layout anchors using `FloatingPanelIntrinsicLayoutAnchor`. +### Adding a floating panel within a view ```swift -class IntrinsicPanelLayout: FloatingPanelLayout { - let position: FloatingPanelPosition = .bottom - let initialState: FloatingPanelState = .full - let anchors: [FloatingPanelState: FloatingPanelLayoutAnchoring] = [ - .full: FloatingPanelIntrinsicLayoutAnchor(absoluteOffset: 0, referenceGuide: .safeArea), - .half: FloatingPanelIntrinsicLayoutAnchor(fractionalOffset: 0.5, referenceGuide: .safeArea), - ] - ... -} -``` - -> [!WARNING] -> `FloatingPanelIntrinsicLayout` is deprecated on v1. - -#### Specify an anchor for each state by an inset of the `FloatingPanelController.view` frame +@State private var layout = MyFloatingPanelLayout() +@State private var state: FloatingPanelState? -Use `.superview` reference guide in your anchors. - -```swift -class MyFullScreenLayout: FloatingPanelLayout { - ... - let anchors: [FloatingPanelState: FloatingPanelLayoutAnchoring] = [ - .full: FloatingPanelLayoutAnchor(absoluteInset: 16.0, edge: .top, referenceGuide: .superview), - .half: FloatingPanelLayoutAnchor(fractionalInset: 0.5, edge: .bottom, referenceGuide: .superview), - .tip: FloatingPanelLayoutAnchor(absoluteInset: 44.0, edge: .bottom, referenceGuide: .superview), - ] +var view: some View { + MainView() + .floatingPanel { proxy in + ScrollView { + VStack(spacing: 20) { + ForEach(items) { item in + ItemRow(item) + } + } + .padding() + } + .floatingPanelScrollTracking(proxy: proxy) + } + .floatingPanelState($state) + .floatingPanelLayout(layout) + .floatingPanelBehavior(MyCustomBehavior()) + .floatingPanelSurfaceAppearance(.transparent) } ``` -> [!WARNING] -> `FloatingPanelFullScreenLayout` is deprecated on v1. - -#### Change the backdrop alpha +### Presenting a floating panel modally within a view -You can change the backdrop alpha by `FloatingPanelLayout.backdropAlpha(for:)` for each state(`.full`, `.half` and `.tip`). - -For instance, if a panel seems like the backdrop view isn't there on `.half` state, it's time to implement the backdropAlpha API and return a value for the state as below. +Please define a custom coordinator object to present a floating panel as a modality. ```swift -class MyPanelLayout: FloatingPanelLayout { - func backdropAlpha(for state: FloatingPanelState) -> CGFloat { - switch state { - case .full, .half: return 0.3 - default: return 0.0 +struct HomeView: View { + var view: some View { + MainView() + .floatingPanel( + coordinator: MyPanelCoordinator.self + ) { proxy in + ... } - } -} -``` - -#### Using custome panel states - -You're able to define custom panel states and use them as the following example. - -```swift -extension FloatingPanelState { - static let lastQuart: FloatingPanelState = FloatingPanelState(rawValue: "lastQuart", order: 750) - static let firstQuart: FloatingPanelState = FloatingPanelState(rawValue: "firstQuart", order: 250) -} - -class FloatingPanelLayoutWithCustomState: FloatingPanelBottomLayout { - override var anchors: [FloatingPanelState: FloatingPanelLayoutAnchoring] { - return [ - .full: FloatingPanelLayoutAnchor(absoluteInset: 0.0, edge: .top, referenceGuide: .safeArea), - .lastQuart: FloatingPanelLayoutAnchor(fractionalInset: 0.75, edge: .bottom, referenceGuide: .safeArea), - .half: FloatingPanelLayoutAnchor(fractionalInset: 0.5, edge: .bottom, referenceGuide: .safeArea), - .firstQuart: FloatingPanelLayoutAnchor(fractionalInset: 0.25, edge: .bottom, referenceGuide: .safeArea), - .tip: FloatingPanelLayoutAnchor(absoluteInset: 20.0, edge: .bottom, referenceGuide: .safeArea), - ] - } -} -``` - -### Customize the behavior with `FloatingPanelBehavior` protocol - -#### Modify your floating panel's interaction - -```swift -class ViewController: UIViewController, FloatingPanelControllerDelegate { - ... - func viewDidLoad() { - ... - fpc.behavior = CustomPanelBehavior() - } + } } -class CustomPanelBehavior: FloatingPanelBehavior { - let springDecelerationRate = UIScrollView.DecelerationRate.fast.rawValue + 0.02 - let springResponseTime = 0.4 - func shouldProjectMomentum(_ fpc: FloatingPanelController, to proposedState: FloatingPanelState) -> Bool { - return true - } -} -``` - -> [!WARNING] -> `floatingPanel(_ vc:behaviorFor:)` is deprecated on v1. - -#### Activate the rubber-band effect on panel edges - -```swift -class MyPanelBehavior: FloatingPanelBehavior { +class MyPanelCoordinator: FloatingPanelCoordinator { ... - func allowsRubberBanding(for edge: UIRectEdge) -> Bool { - return true + func setupFloatingPanel( + mainHostingController: UIHostingController
, + contentHostingController: UIHostingController + ) where Main: View, Content: View { + // Set the delegate object + controller.delegate = delegate + + // Set up the content + contentHostingController.view.backgroundColor = .clear + controller.set(contentViewController: contentHostingController) + + /* =============== HERE ==================== */ + // NOTE: + // Present the floating panel on the next run loop cycle + // to ensure proper view hierarchy setup. + Task { @MainActor in + mainHostingController.present(controller, animated: false) + } } -} -``` - -#### Manage the projection of a pan gesture momentum - -This allows full projectional panel behavior. For example, a user can swipe up a panel from tip to full nearby the tip position. - -```swift -class MyPanelBehavior: FloatingPanelBehavior { ... - func shouldProjectMomentum(_ fpc: FloatingPanelController, to proposedState: FloatingPanelPosition) -> Bool { - return true - } -} -``` - -### Specify the panel move's boundary - -`FloatingPanelController.surfaceLocation` in `floatingPanelDidMove(_:)` delegate method behaves like `UIScrollView.contentOffset` in `scrollViewDidScroll(_:)`. -As a result, you can specify the boundary of a panel move as below. - -```swift -func floatingPanelDidMove(_ vc: FloatingPanelController) { - if vc.isAttracting == false { - let loc = vc.surfaceLocation - let minY = vc.surfaceLocation(for: .full).y - 6.0 - let maxY = vc.surfaceLocation(for: .tip).y + 6.0 - vc.surfaceLocation = CGPoint(x: loc.x, y: min(max(loc.y, minY), maxY)) - } } ``` -> [!WARNING] -> `{top,bottom}InteractionBuffer` property is removed from `FloatingPanelLayout` since v2. - -### Customize the surface design - -#### Modify your surface appearance - -```swift -// Create a new appearance. -let appearance = SurfaceAppearance() - -// Define shadows -let shadow = SurfaceAppearance.Shadow() -shadow.color = UIColor.black -shadow.offset = CGSize(width: 0, height: 16) -shadow.radius = 16 -shadow.spread = 8 -appearance.shadows = [shadow] - -// Define corner radius and background color -appearance.cornerRadius = 8.0 -appearance.backgroundColor = .clear - -// Set the new appearance -fpc.surfaceView.appearance = appearance -```` - -#### Use a custom grabber handle - -```swift -let myGrabberHandleView = MyGrabberHandleView() -fpc.surfaceView.grabberHandle.isHidden = true -fpc.surfaceView.addSubview(myGrabberHandleView) -``` - -#### Customize layout of the grabber handle - -```swift -fpc.surfaceView.grabberHandlePadding = 10.0 -fpc.surfaceView.grabberHandleSize = .init(width: 44.0, height: 12.0) -``` - -> [!NOTE] -> `grabberHandleSize` width and height are reversed in the left/right position. - -#### Customize content padding from surface edges - -```swift -fpc.surfaceView.contentPadding = .init(top: 20, left: 20, bottom: 20, right: 20) -``` - -#### Customize margins of the surface edges - -```swift -fpc.surfaceView.containerMargins = .init(top: 20.0, left: 16.0, bottom: 16.0, right: 16.0) -``` - -The feature can be used for these 2 kind panels - -* Facebook/Slack-like panel whose surface top edge is separated from the grabber handle. -* iOS native panel to display AirPods information, for example. - -### Customize gestures - -#### Suppress the panel interaction - -You can disable the pan gesture recognizer directly - -```swift -fpc.panGestureRecognizer.isEnabled = false -``` - -Or use this `FloatingPanelControllerDelegate` method. - -```swift -func floatingPanelShouldBeginDragging(_ vc: FloatingPanelController) -> Bool { - return aCondition ? false : true -} -``` - -#### Add tap gestures to the surface view - -```swift -override func viewDidLoad() { - ... - let surfaceTapGesture = UITapGestureRecognizer(target: self, action: #selector(handleSurface(tapGesture:))) - fpc.surfaceView.addGestureRecognizer(surfaceTapGesture) - surfaceTapGesture.isEnabled = (fpc.position == .tip) -} - -// Enable `surfaceTapGesture` only at `tip` state -func floatingPanelDidChangeState(_ vc: FloatingPanelController) { - surfaceTapGesture.isEnabled = (vc.position == .tip) -} -``` - -#### Interrupt the delegate methods of `FloatingPanelController.panGestureRecognizer` - -If you are set `FloatingPanelController.panGestureRecognizer.delegateProxy` to an object adopting `UIGestureRecognizerDelegate`, it overrides delegate methods of the pan gesture recognizer. +### Displaying multiple panels + +Multiple floating panels can be displayed in the same view hierarchy. To customize the layout and behavior for each Floating Panel individually, place modifiers directly below each panel. ```swift -class MyGestureRecognizerDelegate: UIGestureRecognizerDelegate { - func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRequireFailureOf otherGestureRecognizer: UIGestureRecognizer) -> Bool { - return false +Color.orange + .ignoresSafeArea() + .floatingPanel( + coordinator: MyPanelCoordinator.self + ) { proxy in + ContentView(proxy: proxy) } -} - -class ViewController: UIViewController { - let myGestureDelegate = MyGestureRecognizerDelegate() - - func setUpFpc() { - .... - fpc.panGestureRecognizer.delegateProxy = myGestureDelegate + .floatingPanelSurfaceAppearance(.transparent()) + .floatingPanel( + coordinator: MyPanelCoordinator.self + ) { proxy in + ContentView(proxy: proxy) } + .floatingPanelSurfaceAppearance(.transparent(cornerRadius: 24)) ``` -### Create an additional floating panel for a detail - -```swift -override func viewDidLoad() { - // Setup Search panel - self.searchPanelVC = FloatingPanelController() - - let searchVC = SearchViewController() - self.searchPanelVC.set(contentViewController: searchVC) - self.searchPanelVC.track(scrollView: contentVC.tableView) - - self.searchPanelVC.addPanel(toParent: self) - - // Setup Detail panel - self.detailPanelVC = FloatingPanelController() - - let contentVC = ContentViewController() - self.detailPanelVC.set(contentViewController: contentVC) - self.detailPanelVC.track(scrollView: contentVC.scrollView) - - self.detailPanelVC.addPanel(toParent: self) -} -``` - -### Move a position with an animation +### Next step -In the following example, I move a floating panel to full or half position while opening or closing a search bar like Apple Maps. +For more details, see the [FloatingPanel SwiftUI API Guide](/Documentation/FloatingPanel%20SwiftUI%20API%20Guide.md) -```swift -func searchBarCancelButtonClicked(_ searchBar: UISearchBar) { - ... - fpc.move(to: .half, animated: true) -} - -func searchBarTextDidBeginEditing(_ searchBar: UISearchBar) { - ... - fpc.move(to: .full, animated: true) -} -``` +## Getting Started with UIKit -You can also use a view animation to move a panel. +### Adding a floating panel as a child view controller ```swift -UIView.animate(withDuration: 0.25) { - self.fpc.move(to: .half, animated: false) -} -``` - -### Work your contents together with a floating panel behavior +import UIKit +import FloatingPanel -```swift class ViewController: UIViewController, FloatingPanelControllerDelegate { - ... - func floatingPanelWillBeginDragging(_ vc: FloatingPanelController) { - if vc.position == .full { - searchVC.searchBar.showsCancelButton = false - searchVC.searchBar.resignFirstResponder() - } - } - - func floatingPanelWillEndDragging(_ vc: FloatingPanelController, withVelocity velocity: CGPoint, targetState: UnsafeMutablePointer) { - if targetState.pointee != .full { - searchVC.hideHeader() - } - } -} -``` - -### Enabling the tap-to-dismiss action of the backdrop view + var fpc: FloatingPanelController! -The tap-to-dismiss action is disabled by default. So it needs to be enabled as below. + override func viewDidLoad() { + super.viewDidLoad() + // Initialize a `FloatingPanelController` object. + fpc = FloatingPanelController() -```swift -fpc.backdropView.dismissalTapGestureRecognizer.isEnabled = true -``` + // Assign self as the delegate of the controller. + fpc.delegate = self // Optional -### Allow to scroll content of the tracking scroll view in addition to the most expanded state + // Set a content view controller. + let contentVC = ContentViewController() + fpc.set(contentViewController: contentVC) -Just define conditions to allow content scrolling in `floatingPanel(:_:shouldAllowToScroll:in)` delegate method. If the returned value is true, the scroll content scrolls when its scroll position is not at the top of the content. + // Track a scroll view(or the siblings) in the content view controller. + fpc.track(scrollView: contentVC.tableView) -```swift -class MyViewController: FloatingPanelControllerDelegate { - ... - - func floatingPanel( - _ fpc: FloatingPanelController, - shouldAllowToScroll trackingScrollView: UIScrollView, - in state: FloatingPanelState - ) -> Bool { - return state == .full || state == .half + // Add and show the views managed by the `FloatingPanelController` object to self.view. + fpc.addPanel(toParent: self) } } ``` -## Notes - -### 'Show' or 'Show Detail' Segues from `FloatingPanelController`'s content view controller - -'Show' or 'Show Detail' segues from a content view controller will be managed by a view controller(hereinafter called 'master VC') adding a floating panel. Because a floating panel is just a subview of the master VC(except for modality). - -`FloatingPanelController` has no way to manage a stack of view controllers like `UINavigationController`. If so, it would be so complicated and the interface will become `UINavigationController`. This component should not have the responsibility to manage the stack. - -By the way, a content view controller can present a view controller modally with `present(_:animated:completion:)` or 'Present Modally' segue. - -However, sometimes you want to show a destination view controller of 'Show' or 'Show Detail' segue with another floating panel. It's possible to override `show(_:sender)` of the master VC! - -Here is an example. +### Presenting a floating panel modally ```swift -class ViewController: UIViewController { - var fpc: FloatingPanelController! - var secondFpc: FloatingPanelController! - - ... - override func show(_ vc: UIViewController, sender: Any?) { - secondFpc = FloatingPanelController() +let fpc = FloatingPanelController() +let contentVC = ... +fpc.set(contentViewController: contentVC) - secondFpc.set(contentViewController: vc) +fpc.isRemovalInteractionEnabled = true // Optional: Let it removable by a swipe-down - secondFpc.addPanel(toParent: self) - } -} +self.present(fpc, animated: true, completion: nil) ``` -A `FloatingPanelController` object proxies an action for `show(_:sender)` to the master VC. That's why the master VC can handle a destination view controller of a 'Show' or 'Show Detail' segue and you can hook `show(_:sender)` to show a secondary floating panel set the destination view controller to the content. - -It's a great way to decouple between a floating panel and the content VC. +You can show a floating panel over UINavigationController from the container view controllers as a modality of `.overCurrentContext` style. -### UISearchController issue +> [!NOTE] +> FloatingPanelController has the custom presentation controller. If you would like to customize the presentation/dismissal, please see [Transitioning](Sources/Transitioning.swift). -`UISearchController` isn't able to be used with `FloatingPanelController` by the system design. +### Next step -Because `UISearchController` automatically presents itself modally when a user interacts with the search bar, and then it swaps the superview of the search bar to the view managed by itself while it displays. As a result, `FloatingPanelController` can't control the search bar when it's active, as you can see from [the screen shot](https://github.com/SCENEE/FloatingPanel/issues/248#issuecomment-521263831). +For more details, see the [FloatingPanel API Guide](/Documentation/FloatingPanel%20API%20Guide.md). ## Maintainer diff --git a/Sources/FloatingPanel.docc/FloatingPanel API Guide.md b/Sources/FloatingPanel.docc/FloatingPanel API Guide.md new file mode 120000 index 000000000..ba9177924 --- /dev/null +++ b/Sources/FloatingPanel.docc/FloatingPanel API Guide.md @@ -0,0 +1 @@ +../../Documentation/FloatingPanel API Guide.md \ No newline at end of file diff --git a/Sources/FloatingPanel.docc/FloatingPanel SwiftUI API Guide.md b/Sources/FloatingPanel.docc/FloatingPanel SwiftUI API Guide.md new file mode 120000 index 000000000..882ec8be3 --- /dev/null +++ b/Sources/FloatingPanel.docc/FloatingPanel SwiftUI API Guide.md @@ -0,0 +1 @@ +../../Documentation/FloatingPanel SwiftUI API Guide.md \ No newline at end of file diff --git a/Sources/FloatingPanel.docc/FloatingPanel.md b/Sources/FloatingPanel.docc/FloatingPanel.md index fe1ea0b15..8972a4415 100644 --- a/Sources/FloatingPanel.docc/FloatingPanel.md +++ b/Sources/FloatingPanel.docc/FloatingPanel.md @@ -4,50 +4,62 @@ Create a user interface to display the related content and utilities alongside t ## Overview - FloatingPanel is a simple and easy-to-use UI component designed for a user interface featured in the Apple Maps, Shortcuts and Stocks app. The user interface displays related content and utilities alongside the main content. - ## Topics -### Essentials +### API Guides + +- +- + +### Creations - ``FloatingPanelController`` - ``FloatingPanelControllerDelegate`` +- ``SwiftUICore/View/floatingPanel(coordinator:onEvent:content:)`` +- ``SwiftUICore/View/floatingPanelScrollTracking(proxy:onScrollViewDetected:)`` +- ``FloatingPanelCoordinator`` +- ``FloatingPanelDefaultCoordinator`` +- ``FloatingPanelProxy`` -### Views +### Layout -- ``SurfaceView`` -- ``SurfaceAppearance`` -- ``BackdropView`` -- ``GrabberView`` +- ``FloatingPanelLayout`` +- ``FloatingPanelBottomLayout`` +- ``FloatingPanelState`` +- ``FloatingPanelPosition`` +- ``SwiftUICore/View/floatingPanelLayout(_:)`` +- ``SwiftUICore/View/floatingPanelContentMode(_:)`` +- ``SwiftUICore/View/floatingPanelContentInsetAdjustmentBehavior(_:)`` -### Gestures +### Behavior -- ``FloatingPanelPanGestureRecognizer`` +- ``FloatingPanelBehavior`` +- ``FloatingPanelDefaultBehavior`` +- ``SwiftUICore/View/floatingPanelBehavior(_:)`` -### Layouts and Anchors +### Layout Properties -- ``FloatingPanelLayout`` -- ``FloatingPanelBottomLayout`` - ``FloatingPanelLayoutAnchoring`` - ``FloatingPanelLayoutAnchor`` - ``FloatingPanelAdaptiveLayoutAnchor`` - ``FloatingPanelIntrinsicLayoutAnchor`` - -### States - -- ``FloatingPanelState`` - -### Positions - -- ``FloatingPanelPosition`` - ``FloatingPanelReferenceEdge`` - ``FloatingPanelLayoutReferenceGuide`` - ``FloatingPanelLayoutContentBoundingGuide`` -### Behaviors +### Appearance + +- ``SurfaceView`` +- ``SurfaceAppearance`` +- ``GrabberView`` +- ``BackdropView`` +- ``SwiftUICore/View/floatingPanelSurfaceAppearance(_:)`` +- ``SwiftUICore/View/floatingPanelGrabberHandlePadding(_:)`` + +### Gesture + +- ``FloatingPanelPanGestureRecognizer`` -- ``FloatingPanelBehavior`` -- ``FloatingPanelDefaultBehavior`` From ebfdfee809a962bfca3d95741e792152d6046a4e Mon Sep 17 00:00:00 2001 From: Shin Yamamoto Date: Fri, 9 May 2025 23:14:56 +0900 Subject: [PATCH 30/62] ci: update example builds --- .github/workflows/ci.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3d3360d38..4f7bd3485 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -81,6 +81,8 @@ jobs: - example: "Maps-SwiftUI" - example: "Stocks" - example: "Samples" + - example: "SamplesObjC" + - example: "SamplesSwiftUI" steps: - uses: actions/checkout@v4 - name: Building ${{ matrix.example }} From 7a7502dad81184ec32c7dba815257a34846c8f11 Mon Sep 17 00:00:00 2001 From: Shin Yamamoto Date: Fri, 9 May 2025 23:39:17 +0900 Subject: [PATCH 31/62] ci: fix swift 5.9 and 5.10 builds --- BuildTools/Package.swift | 2 +- BuildTools/Package@swift-5.9.swift | 20 ++++++++++++++++++++ BuildTools/Plugins/swift-format-plugin.swift | 8 ++++---- 3 files changed, 25 insertions(+), 5 deletions(-) create mode 100644 BuildTools/Package@swift-5.9.swift diff --git a/BuildTools/Package.swift b/BuildTools/Package.swift index 3545ea127..07a67c3e8 100644 --- a/BuildTools/Package.swift +++ b/BuildTools/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version: 5.9 +// swift-tools-version: 6.0 import PackageDescription diff --git a/BuildTools/Package@swift-5.9.swift b/BuildTools/Package@swift-5.9.swift new file mode 100644 index 000000000..1efcbce5d --- /dev/null +++ b/BuildTools/Package@swift-5.9.swift @@ -0,0 +1,20 @@ +// swift-tools-version: 5.9 +// This file is used when the 'built-tools' package is built by Xcode 15 or earlier. + +import PackageDescription + +let package = Package( + name: "build-tools", + products: [ + .plugin( + name: "swift-format-plugin", + targets: ["swift-format-plugin"] + ) + ], + targets: [ + .plugin( + name: "swift-format-plugin", + capability: .buildTool() + ) + ] +) diff --git a/BuildTools/Plugins/swift-format-plugin.swift b/BuildTools/Plugins/swift-format-plugin.swift index bbcd0eab0..18d0eed7d 100644 --- a/BuildTools/Plugins/swift-format-plugin.swift +++ b/BuildTools/Plugins/swift-format-plugin.swift @@ -19,10 +19,6 @@ extension SwiftFormatBuildToolPlugin: XcodeBuildToolPlugin { #if swift(>=6.0) let swift = try context.tool(named: "swift").url let xcodeProjectDirectoryURL = context.xcodeProject.directoryURL - #else - let swift = try context.tool(named: "swift").path - let xcodeProjectDirectoryURL = URL(fileURLWithPath: context.xcodeProject.directory.string) - #endif // Find the code generator tool to run (replace this with the actual one). print("SwiftFormatBuildToolPlugin -> \(xcodeProjectDirectoryURL.filePath)") let configFile = xcodeProjectDirectoryURL.appending(path: ".swift-format").filePath @@ -50,6 +46,10 @@ extension SwiftFormatBuildToolPlugin: XcodeBuildToolPlugin { outputFiles: [] ) ] + #else + // Skip running `swift format`; this subcommand ships with the Swift 6 compiler. + return [] + #endif } } From ca8926b02da3448f16c4bc8be58d11602011d4bf Mon Sep 17 00:00:00 2001 From: Shin Yamamoto Date: Sat, 10 May 2025 21:39:45 +0900 Subject: [PATCH 32/62] ci: use Xcode 16.3 and iOS 18.4 --- .github/workflows/ci.yml | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4f7bd3485..332d32223 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,7 +19,7 @@ jobs: matrix: include: - swift: "5" - xcode: "16.2" + xcode: "16.3" runs-on: macos-15 - swift: "5.10" xcode: "15.4" @@ -43,8 +43,8 @@ jobs: fail-fast: false matrix: include: - - os: "18.2" - xcode: "16.2" + - os: "18.4" + xcode: "16.3" sim: "iPhone 16 Pro" parallel: NO # Stop random test job failures runs-on: macos-15 @@ -72,7 +72,7 @@ jobs: example: runs-on: macos-15 env: - DEVELOPER_DIR: /Applications/Xcode_16.2.app/Contents/Developer + DEVELOPER_DIR: /Applications/Xcode_16.3.app/Contents/Developer strategy: fail-fast: false matrix: @@ -86,7 +86,6 @@ 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 \ @@ -100,20 +99,20 @@ jobs: strategy: fail-fast: false matrix: - xcode: ["16.2", "15.4"] + xcode: ["16.3", "15.4"] platform: [iphoneos, iphonesimulator] arch: [x86_64, arm64] exclude: - platform: iphoneos arch: x86_64 include: - # 18.2 + # 18.4 - platform: iphoneos - xcode: "16.2" - sys: "ios18.2" + xcode: "16.3" + sys: "ios18.4" - platform: iphonesimulator - xcode: "16.2" - sys: "ios18.2-simulator" + xcode: "16.3" + sys: "ios18.4-simulator" # 17.2 - platform: iphoneos xcode: "15.4" @@ -155,7 +154,7 @@ jobs: cocoapods: runs-on: macos-15 env: - DEVELOPER_DIR: /Applications/Xcode_16.2.app/Contents/Developer + DEVELOPER_DIR: /Applications/Xcode_16.3.app/Contents/Developer steps: - uses: actions/checkout@v4 - name: "CocoaPods: pod lib lint" From 184c868df83521b821c14d1db8630ee25436eea6 Mon Sep 17 00:00:00 2001 From: Shin Yamamoto Date: Sat, 10 May 2025 12:47:52 +0900 Subject: [PATCH 33/62] Add CHANGELOG --- CHANGELOG.md | 137 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 137 insertions(+) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 000000000..287e3b95c --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,137 @@ +# ChangeLog + +## [3.0.0](https://github.com/scenee/FloatingPanel/releases/tag/3.0.0) + +### Breaking Changes + +- The minimum deployment target is now **iOS 13.0**. +- Dropped support for building with **Xcode 13.4.1**. + +### Added + +- Introduced new SwiftUI APIs. +- Added `Documentation/FloatingPanel SwiftUI API Guide.md`. +- Added `Documentation/FloatingPanel API Guide.md`, migrating from `README.md`. +- Added `Examples/SamplesSwiftUI` example app. +- Added `FloatingPanelControllerDelegate/floatingPanel(_:animatorForMovingTo:)`. +- Added partial `swift-format` support via the BuildTools plugin package. + **Limitation:** Formatting currently applies only to the source code for the + new SwiftUI API and the `SamplesSwiftUI` example app. +- Enabled README preview mode in Xcode via `.xcodesamplecode.plist`. + +### Changed + +- Updated `README.md` to cover the new SwiftUI APIs. +- Moved UIKit-specific details to `Documentation/FloatingPanel API Guide.md`. +- Updated DocC documentation for the new SwiftUI APIs. +- Moved the `assets` folder. + +## [2.8.8](https://github.com/scenee/FloatingPanel/releases/tag/2.8.8) + +### Bugfixes + +- Allowed slight deviation when checking for anchor position. +- Addressed #661 issue since v2.8.0 (#662) + +## [2.8.7](https://github.com/scenee/FloatingPanel/releases/tag/2.8.7) + +:warning: [NOTICE] This release contains a regression. Please use v2.8.8 instead. + +### Bugfixes + +- Disallow interrupting the panel interaction while bouncing over the most + expanded state (#652) +- Reset initialScrollOffset after the attracting animation ends (#659) + +## [2.8.6](https://github.com/scenee/FloatingPanel/releases/tag/2.8.6) + +### Bugfixes + +- Fix doc comment errors (#643) + +## [2.8.5](https://github.com/scenee/FloatingPanel/releases/tag/2.8.5) + +### Bugfixes + +- Replaced fatal errors in transitionDuration delegate methods (#642) + +## [2.8.4](https://github.com/scenee/FloatingPanel/releases/tag/2.8.4) + +### Bugfixes + +- Fixed an inappropriate condition to determine scrolling content (#633) + +## [2.8.3](https://github.com/scenee/FloatingPanel/releases/tag/2.8.3) + +### Bugfixes + +- Fix the scroll tracking of WKWebView on iOS 17.4 (#630) +- Fix a broken panel layout with a compositional collection view (#634) +- Fix a compilation error in Xcode 16 by @WillBishop (#636) + +## [2.8.2](https://github.com/scenee/FloatingPanel/releases/tag/2.8.2) + +### New features + +- Enabled to define and use a subclass object of BackdropView (#617) + +### Improvements + +- Fixed the scroll locking behavior by @futuretap (#615) +- Supported Xcode 15.2 on the GitHub Actions (#619) + +### Bugfixes + +- Added a possible fix for #586 +- Fixed a bug that state was not changed property after v2.8.1 + +## [2.8.1](https://github.com/scenee/FloatingPanel/releases/tag/2.8.1) + +- Fixed an invalid behavior after switching to a new layout object (#611) + +## [2.8.0](https://github.com/scenee/FloatingPanel/releases/tag/2.8.0) + +### Breaking changes + +- The minimum deployment target of this library became iOS 11.0 on this release. + +### New features + +- Added the new delegate method, `floatingPanel(_:shouldAllowToScroll:in:)`. + +### Improvements + +- Enabled content scrolling in non-expanded states (#455) + +### Bugfixes + +- Fixed CGFloat.rounded(by:) for a floating point error +- Fixed scroll offset reset when moving in grabber area +- Fixed a panel not moving when picked up in certain area +- Fixed errors of offset value from a state position + +## [2.7.0](https://github.com/scenee/FloatingPanel/releases/tag/2.7.0) + +### Breaking changes + +- Calls the `floatingPanelDidMove` delegate method at the end of the move + interaction. +- Calls the `floatingPanelDidEndDragging` delegate method after + `FloatingPanelController.state` changes when `willAttract` is `false`. +- Sets `isAttracting` to `true` even when moving between states by + `FloatingPanelController.move(to:animated:completion)` except for moves from + or to `.hidden`. +- Do not reset the scroll offset of its tracking scroll view when a user moves a + panel outside its scroll view or on the navigation bar above it. + +## Improvements + +- Added `FloatingPanelPanGestureRecognizer.delegateOrigin` to allow to access + the default delegate implementations (It's useful when using `delegateProxy`). + +## Bugfixes + +- Retains scroll view position while moving between states (#587) +- Fixed invalid scroll offsets after moving between states +- Calls `floatingPanelWillRemove` delegate method when a panel is removed from a + window From d5177203c08e01a7ffe07c8cccf3d19123db89d2 Mon Sep 17 00:00:00 2001 From: Shin Yamamoto Date: Mon, 12 May 2025 20:42:58 +0900 Subject: [PATCH 34/62] Version 3.0.0 --- FloatingPanel.podspec | 2 +- README.md | 4 ++-- Sources/Info.plist | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/FloatingPanel.podspec b/FloatingPanel.podspec index eda4d0d84..a97a55011 100644 --- a/FloatingPanel.podspec +++ b/FloatingPanel.podspec @@ -1,7 +1,7 @@ Pod::Spec.new do |s| s.name = "FloatingPanel" - s.version = "2.8.8" + s.version = "3.0.0" 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 cfdac4f77..27f1675e7 100644 --- a/README.md +++ b/README.md @@ -74,7 +74,7 @@ FloatingPanel is written in Swift 5.0+ and compatible with iOS 12.0+. ## Documentation -- [API reference on Swift Package Index](https://swiftpackageindex.com/scenee/FloatingPanel/2.8.8/documentation/floatingpanel) +- [API reference on Swift Package Index](https://swiftpackageindex.com/scenee/FloatingPanel/3.0.0/documentation/floatingpanel) - [FloatingPanel SwiftUI API Guide](/Documentation/FloatingPanel%20SwiftUI%20API%20Guide.md) - [FloatingPanel API Guide](/Documentation/FloatingPanel%20API%20Guide.md) - [FloatingPanel 2.0 Migration Guide](/Documentation/FloatingPanel%202.0%20Migration%20Guide.md) @@ -92,7 +92,7 @@ Just follow [this documentation](https://developer.apple.com/documentation/swift In your Package.swift Swift Package Manager manifest, add the following dependency to your dependencies argument: ```swift -.package(url: "https://github.com/scenee/FloatingPanel", from: "2.8.8"), +.package(url: "https://github.com/scenee/FloatingPanel", from: "3.0.0"), ``` Add Numerics as a dependency for your target: diff --git a/Sources/Info.plist b/Sources/Info.plist index 173b005c4..0e076a854 100644 --- a/Sources/Info.plist +++ b/Sources/Info.plist @@ -15,7 +15,7 @@ CFBundlePackageType FMWK CFBundleShortVersionString - 2.8.8 + 3.0.0 CFBundleVersion $(CURRENT_PROJECT_VERSION) From ac94f3dcbe53961e5832a0fe9d4f49906fd7eb89 Mon Sep 17 00:00:00 2001 From: Shin Yamamoto Date: Wed, 14 May 2025 11:54:53 +0900 Subject: [PATCH 35/62] Modify the top page of the DocC --- Sources/FloatingPanel.docc/FloatingPanel.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Sources/FloatingPanel.docc/FloatingPanel.md b/Sources/FloatingPanel.docc/FloatingPanel.md index 8972a4415..b9fabb295 100644 --- a/Sources/FloatingPanel.docc/FloatingPanel.md +++ b/Sources/FloatingPanel.docc/FloatingPanel.md @@ -24,11 +24,15 @@ The user interface displays related content and utilities alongside the main con - ``FloatingPanelDefaultCoordinator`` - ``FloatingPanelProxy`` +### State + +- ``FloatingPanelState`` +- ``SwiftUICore/View/floatingPanelState(_:)`` + ### Layout - ``FloatingPanelLayout`` - ``FloatingPanelBottomLayout`` -- ``FloatingPanelState`` - ``FloatingPanelPosition`` - ``SwiftUICore/View/floatingPanelLayout(_:)`` - ``SwiftUICore/View/floatingPanelContentMode(_:)`` From 4996e6cf3bbd1b866e70dd5f6b8cc6fc49319cae Mon Sep 17 00:00:00 2001 From: Shin Yamamoto Date: Wed, 30 Jul 2025 12:43:54 +0900 Subject: [PATCH 36/62] Improve CoreTests a bit --- Tests/CoreTests.swift | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/Tests/CoreTests.swift b/Tests/CoreTests.swift index f9926a5c6..9ff25247a 100644 --- a/Tests/CoreTests.swift +++ b/Tests/CoreTests.swift @@ -206,10 +206,8 @@ class CoreTests: XCTestCase { func floatingPanel(_ fpc: FloatingPanelController, layoutFor newCollection: UITraitCollection) -> FloatingPanelLayout { layout } func floatingPanel(_ fpc: FloatingPanelController, layoutFor size: CGSize) -> FloatingPanelLayout { layout } } - func round(_ alpha: CGFloat) -> CGFloat { - return (fpc.backdropView.alpha * 1e+06).rounded() / 1e+06 - } + let acc = 0.000_001 let timeout = 5.0 let delegate = TestDelegate() @@ -219,27 +217,27 @@ class CoreTests: XCTestCase { fpc.showForTest() fpc.move(to: .full, animated: false) - XCTAssertEqual(round(fpc.backdropView.alpha), 0.3) + XCTAssertEqual(fpc.backdropView.alpha, 0.3, accuracy: acc) fpc.move(to: .half, animated: false) - XCTAssertEqual(fpc.backdropView.alpha, 0.0) + XCTAssertEqual(fpc.backdropView.alpha, 0.0, accuracy: acc) fpc.move(to: .tip, animated: false) - XCTAssertEqual(round(fpc.backdropView.alpha), 0.3) + XCTAssertEqual(fpc.backdropView.alpha, 0.3, accuracy: acc) let exp1 = expectation(description: "move to full with animation") fpc.move(to: .full, animated: true) { exp1.fulfill() } wait(for: [exp1], timeout: timeout) - XCTAssertEqual(round(fpc.backdropView.alpha), 0.3) + XCTAssertEqual(fpc.backdropView.alpha, 0.3, accuracy: acc) let exp2 = expectation(description: "move to half with animation") fpc.move(to: .half, animated: true) { exp2.fulfill() } wait(for: [exp2], timeout: timeout) - XCTAssertEqual(fpc.backdropView.alpha, 0.0) + XCTAssertEqual(fpc.backdropView.alpha, 0.0, accuracy: acc) // Test a content mode change of FloatingPanelController @@ -250,12 +248,12 @@ class CoreTests: XCTestCase { fpc.contentMode = .fitToBounds XCTAssertEqual(fpc.backdropView.alpha, 0.0) // Must not affect the backdrop alpha by changing the content mode wait(for: [exp3], timeout: timeout) - XCTAssertEqual(round(fpc.backdropView.alpha), 0.3) + XCTAssertEqual(fpc.backdropView.alpha, 0.3, accuracy: acc) // Test a size class change of FloatingPanelController.view fpc.move(to: .full, animated: false) - XCTAssertEqual(round(fpc.backdropView.alpha), 0.3) + XCTAssertEqual(fpc.backdropView.alpha, 0.3, accuracy: acc) fpc.willTransition(to: UITraitCollection(horizontalSizeClass: .regular), with: MockTransitionCoordinator()) XCTAssertEqual(fpc.backdropView.alpha, 0.0) // Must update the alpha by BackdropTestLayout2 in TestDelegate. @@ -264,7 +262,7 @@ class CoreTests: XCTestCase { fpc.move(to: .full, animated: false) delegate.layout = BackdropTestLayout() fpc.invalidateLayout() - XCTAssertEqual(round(fpc.backdropView.alpha), 0.3) + XCTAssertEqual(fpc.backdropView.alpha, 0.3, accuracy: acc) delegate.layout = BackdropTestLayout2() fpc.viewWillTransition(to: CGSize.zero, with: MockTransitionCoordinator()) From 7a9f450e13460e063538ef9bd446a24068d6fe4f Mon Sep 17 00:00:00 2001 From: Shin Yamamoto Date: Wed, 13 Aug 2025 00:10:21 +0900 Subject: [PATCH 37/62] ci: fix the build environments --- .github/workflows/ci.yml | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 332d32223..b06653786 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,7 +19,7 @@ jobs: matrix: include: - swift: "5" - xcode: "16.3" + xcode: "16.4" runs-on: macos-15 - swift: "5.10" xcode: "15.4" @@ -43,8 +43,8 @@ jobs: fail-fast: false matrix: include: - - os: "18.4" - xcode: "16.3" + - os: "18.5" + xcode: "16.4" sim: "iPhone 16 Pro" parallel: NO # Stop random test job failures runs-on: macos-15 @@ -93,33 +93,37 @@ jobs: -sdk iphonesimulator swiftpm: - runs-on: macos-15 + runs-on: ${{ matrix.runs-on }} env: DEVELOPER_DIR: /Applications/Xcode_${{ matrix.xcode }}.app/Contents/Developer strategy: fail-fast: false matrix: - xcode: ["16.3", "15.4"] + xcode: ["16.4", "15.4"] platform: [iphoneos, iphonesimulator] arch: [x86_64, arm64] exclude: - platform: iphoneos arch: x86_64 include: - # 18.4 + # 18.5 - platform: iphoneos - xcode: "16.3" - sys: "ios18.4" + xcode: "16.4" + sys: "ios18.5" + runs-on: macos-15 - platform: iphonesimulator - xcode: "16.3" - sys: "ios18.4-simulator" + xcode: "16.4" + sys: "ios18.5-simulator" + runs-on: macos-15 # 17.2 - platform: iphoneos xcode: "15.4" sys: "ios17.2" + runs-on: macos-14 - platform: iphonesimulator xcode: "15.4" sys: "ios17.2-simulator" + runs-on: macos-14 steps: - uses: actions/checkout@v4 - name: "Swift Package Manager build" From 25cfe0bf13332e98ea933e9b833058682a773704 Mon Sep 17 00:00:00 2001 From: Shin Yamamoto Date: Tue, 12 Aug 2025 23:55:25 +0900 Subject: [PATCH 38/62] Fix the pod spec for the SwiftUI APIs --- FloatingPanel.podspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FloatingPanel.podspec b/FloatingPanel.podspec index a97a55011..7f766128b 100644 --- a/FloatingPanel.podspec +++ b/FloatingPanel.podspec @@ -13,7 +13,7 @@ The new interface displays the related contents and utilities in parallel as a u s.platform = :ios, "13.0" s.source = { :git => "https://github.com/scenee/FloatingPanel.git", :tag => s.version.to_s } - s.source_files = "Sources/*.swift" + s.source_files = "Sources/**/*.swift" s.swift_version = '5.0' s.framework = "UIKit" From a2646dd2704656fa5a679af68953249a70c77f40 Mon Sep 17 00:00:00 2001 From: Shin Yamamoto Date: Sun, 17 Aug 2025 16:37:43 +0900 Subject: [PATCH 39/62] Version 3.0.1 --- FloatingPanel.podspec | 2 +- README.md | 4 ++-- Sources/Info.plist | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/FloatingPanel.podspec b/FloatingPanel.podspec index 7f766128b..d42fc058c 100644 --- a/FloatingPanel.podspec +++ b/FloatingPanel.podspec @@ -1,7 +1,7 @@ Pod::Spec.new do |s| s.name = "FloatingPanel" - s.version = "3.0.0" + s.version = "3.0.1" s.summary = "FloatingPanel is a clean and easy-to-use UI component of a floating panel interface." s.description = <<-DESC FloatingPanel is a clean and easy-to-use UI component for a new interface introduced in Apple Maps, Shortcuts and Stocks app. diff --git a/README.md b/README.md index 27f1675e7..f92c3594e 100644 --- a/README.md +++ b/README.md @@ -74,7 +74,7 @@ FloatingPanel is written in Swift 5.0+ and compatible with iOS 12.0+. ## Documentation -- [API reference on Swift Package Index](https://swiftpackageindex.com/scenee/FloatingPanel/3.0.0/documentation/floatingpanel) +- [API reference on Swift Package Index](https://swiftpackageindex.com/scenee/FloatingPanel/3.0.1/documentation/floatingpanel) - [FloatingPanel SwiftUI API Guide](/Documentation/FloatingPanel%20SwiftUI%20API%20Guide.md) - [FloatingPanel API Guide](/Documentation/FloatingPanel%20API%20Guide.md) - [FloatingPanel 2.0 Migration Guide](/Documentation/FloatingPanel%202.0%20Migration%20Guide.md) @@ -92,7 +92,7 @@ Just follow [this documentation](https://developer.apple.com/documentation/swift In your Package.swift Swift Package Manager manifest, add the following dependency to your dependencies argument: ```swift -.package(url: "https://github.com/scenee/FloatingPanel", from: "3.0.0"), +.package(url: "https://github.com/scenee/FloatingPanel", from: "3.0.1"), ``` Add Numerics as a dependency for your target: diff --git a/Sources/Info.plist b/Sources/Info.plist index 0e076a854..014c4f384 100644 --- a/Sources/Info.plist +++ b/Sources/Info.plist @@ -15,7 +15,7 @@ CFBundlePackageType FMWK CFBundleShortVersionString - 3.0.0 + 3.0.1 CFBundleVersion $(CURRENT_PROJECT_VERSION) From ef058d45b936173261e6576fb0b13f27e301c287 Mon Sep 17 00:00:00 2001 From: Shin Yamamoto Date: Fri, 3 Oct 2025 15:32:58 +0900 Subject: [PATCH 40/62] ci: remove builds using deprecated 'macos-13' machine --- .github/workflows/ci.yml | 38 ++------------------------------------ 1 file changed, 2 insertions(+), 36 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b06653786..8153d5bc6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,12 +24,6 @@ jobs: - swift: "5.10" xcode: "15.4" runs-on: macos-14 - - swift: "5.9" - xcode: "15.2" - runs-on: macos-13 - - swift: "5.8" - xcode: "14.3.1" - runs-on: macos-13 steps: - uses: actions/checkout@v4 - name: Building in Swift ${{ matrix.swift }} @@ -53,11 +47,6 @@ jobs: sim: "iPhone 15 Pro" parallel: NO # Stop random test job failures runs-on: macos-14 - - os: "16.4" - xcode: "14.3.1" - sim: "iPhone 14 Pro" - parallel: NO # Stop random test job failures - runs-on: macos-13 steps: - uses: actions/checkout@v4 - name: Testing in iOS ${{ matrix.os }} @@ -72,7 +61,7 @@ jobs: example: runs-on: macos-15 env: - DEVELOPER_DIR: /Applications/Xcode_16.3.app/Contents/Developer + DEVELOPER_DIR: /Applications/Xcode_16.4.app/Contents/Developer strategy: fail-fast: false matrix: @@ -132,33 +121,10 @@ jobs: --sdk "$(xcrun --sdk ${{ matrix.platform }} --show-sdk-path)" \ -Xswiftc "-target" -Xswiftc "${{ matrix.arch }}-apple-${{ matrix.sys }}" - swiftpm_old: - runs-on: ${{ matrix.runs-on }} - env: - DEVELOPER_DIR: /Applications/Xcode_${{ matrix.xcode }}.app/Contents/Developer - strategy: - fail-fast: false - matrix: - include: - # 16.4 - - target: "x86_64-apple-ios16.4-simulator" - xcode: "14.3.1" - runs-on: macos-13 - - target: "arm64-apple-ios16.4-simulator" - xcode: "14.3.1" - runs-on: macos-13 - steps: - - uses: actions/checkout@v4 - - name: "Swift Package Manager build" - run: | - swift build \ - -Xswiftc "-sdk" -Xswiftc "`xcrun --sdk iphonesimulator --show-sdk-path`" \ - -Xswiftc "-target" -Xswiftc "${{ matrix.target }}" - cocoapods: runs-on: macos-15 env: - DEVELOPER_DIR: /Applications/Xcode_16.3.app/Contents/Developer + DEVELOPER_DIR: /Applications/Xcode_16.4.app/Contents/Developer steps: - uses: actions/checkout@v4 - name: "CocoaPods: pod lib lint" From 77b558498f556d39c97c9ce45d885579f53154c5 Mon Sep 17 00:00:00 2001 From: Will Bishop <17292672+WillBishop@users.noreply.github.com> Date: Thu, 23 Oct 2025 11:24:34 +1030 Subject: [PATCH 41/62] Add support for UICornerConfiguration in iOS 26 (#664) --- Sources/SurfaceView.swift | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/Sources/SurfaceView.swift b/Sources/SurfaceView.swift index 508d4e73f..40589815a 100644 --- a/Sources/SurfaceView.swift +++ b/Sources/SurfaceView.swift @@ -45,6 +45,14 @@ public class SurfaceAppearance: NSObject { } }() + @available(iOS 26.0, *) + public var cornerConfiguration: UICornerConfiguration? { + get { _cornerConfiguration as? UICornerConfiguration } + set { _cornerConfiguration = newValue } + } + + private var _cornerConfiguration: Any? + /// The radius to use when drawing the top rounded corners. /// /// `self.contentView` is masked with the top rounded corners automatically on iOS 11 and later. @@ -376,6 +384,11 @@ public class SurfaceView: UIView { } private func updateCornerRadius() { + if #available(iOS 26.0, *), let cornerConfiguration = appearance.cornerConfiguration { + containerView.cornerConfiguration = cornerConfiguration + containerView.layer.masksToBounds = true + return + } containerView.layer.cornerRadius = appearance.cornerRadius guard containerView.layer.cornerRadius != 0.0 else { containerView.layer.masksToBounds = false From 2ec94cf00b2efdf3a1e9a8534fff73a162cbbf61 Mon Sep 17 00:00:00 2001 From: Shin Yamamoto Date: Thu, 23 Oct 2025 09:52:04 +0900 Subject: [PATCH 42/62] ci: add iOS 26 to test matrix --- .github/workflows/ci.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8153d5bc6..3eda50457 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -37,6 +37,11 @@ jobs: fail-fast: false matrix: include: + - os: "26.0" + xcode: "26.0.1" + sim: "iPhone 16 Pro" + parallel: NO # Stop random test job failures + runs-on: macos-15 - os: "18.5" xcode: "16.4" sim: "iPhone 16 Pro" From aa5e372c3d6c41335f51ecaad621b31563df1b6b Mon Sep 17 00:00:00 2001 From: Shin Yamamoto Date: Thu, 23 Oct 2025 10:21:17 +0900 Subject: [PATCH 43/62] Pass CoreTests.test_initial_scroll_offset_reset() Since iOS 26, UIGestureRecognizer.state is not able to change even if it's assigned a new value programmatically. Therefore, I created MockPanGestureRecognizer to change the state property in testing. --- Sources/Core.swift | 4 ++-- Tests/CoreTests.swift | 18 ++++++++++++++++++ 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/Sources/Core.swift b/Sources/Core.swift index ee8de6737..2d19ac791 100644 --- a/Sources/Core.swift +++ b/Sources/Core.swift @@ -52,7 +52,7 @@ class Core: NSObject, UIGestureRecognizerDelegate { } private(set) var statePublisher: CurrentValueSubject = .init(.hidden) - let panGestureRecognizer: FloatingPanelPanGestureRecognizer + var panGestureRecognizer: FloatingPanelPanGestureRecognizer let panGestureDelegateRouter: FloatingPanelPanGestureRecognizer.DelegateRouter var isRemovalInteractionEnabled: Bool = false @@ -1261,7 +1261,7 @@ class Core: NSObject, UIGestureRecognizerDelegate { } /// A gesture recognizer that looks for panning (dragging) gestures in a panel. -public final class FloatingPanelPanGestureRecognizer: UIPanGestureRecognizer { +public class FloatingPanelPanGestureRecognizer: UIPanGestureRecognizer { /// The gesture starting location in the surface view which it is attached to. fileprivate var initialLocation: CGPoint = .zero private weak var floatingPanel: Core! // Core has this gesture recognizer as non-optional diff --git a/Tests/CoreTests.swift b/Tests/CoreTests.swift index 9ff25247a..9dfda5e30 100644 --- a/Tests/CoreTests.swift +++ b/Tests/CoreTests.swift @@ -958,7 +958,22 @@ class CoreTests: XCTestCase { } func test_initial_scroll_offset_reset() { + class MockPanGestureRecognizer: FloatingPanelPanGestureRecognizer { + var _view: UIView? + override var view: UIView? { + set { _view = newValue } + get { _view } + } + var _state: UIGestureRecognizer.State = .possible + override var state: UIGestureRecognizer.State { + set { _state = newValue } + get { _state } + } + } let fpc = FloatingPanelController() + let mockGesture = MockPanGestureRecognizer() + mockGesture.view = fpc.floatingPanel.surfaceView + fpc.floatingPanel.panGestureRecognizer = mockGesture let scrollView = UIScrollView() fpc.layout = FloatingPanelBottomLayout() fpc.track(scrollView: scrollView) @@ -966,6 +981,7 @@ class CoreTests: XCTestCase { fpc.move(to: .full, animated: false) + fpc.panGestureRecognizer.state = .began fpc.floatingPanel.handle(panGesture: fpc.panGestureRecognizer) @@ -978,6 +994,8 @@ class CoreTests: XCTestCase { scrollView.setContentOffset(expect, animated: false) + XCTAssertEqual(expect, scrollView.contentOffset) + fpc.move(to: .half, animated: true) waitRunLoop(secs: 1.0) From c5291d7d58145da662ed1fecc79fac383512e56f Mon Sep 17 00:00:00 2001 From: Shin Yamamoto Date: Thu, 23 Oct 2025 10:51:02 +0900 Subject: [PATCH 44/62] Fix a build error on Xcode 16 --- Sources/SurfaceView.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Sources/SurfaceView.swift b/Sources/SurfaceView.swift index 40589815a..1c02ab904 100644 --- a/Sources/SurfaceView.swift +++ b/Sources/SurfaceView.swift @@ -45,11 +45,13 @@ public class SurfaceAppearance: NSObject { } }() + #if swift(>=6.2) @available(iOS 26.0, *) public var cornerConfiguration: UICornerConfiguration? { get { _cornerConfiguration as? UICornerConfiguration } set { _cornerConfiguration = newValue } } + #endif private var _cornerConfiguration: Any? @@ -384,11 +386,13 @@ public class SurfaceView: UIView { } private func updateCornerRadius() { + #if swift(>=6.2) if #available(iOS 26.0, *), let cornerConfiguration = appearance.cornerConfiguration { containerView.cornerConfiguration = cornerConfiguration containerView.layer.masksToBounds = true return } + #endif containerView.layer.cornerRadius = appearance.cornerRadius guard containerView.layer.cornerRadius != 0.0 else { containerView.layer.masksToBounds = false From 5fff51bdb7446015f091b3a07e74bc58a105358b Mon Sep 17 00:00:00 2001 From: Shin Yamamoto Date: Thu, 23 Oct 2025 15:54:19 +0900 Subject: [PATCH 45/62] Fix compilation conditions at c5291d7 --- Sources/SurfaceView.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/SurfaceView.swift b/Sources/SurfaceView.swift index 1c02ab904..dcfefe975 100644 --- a/Sources/SurfaceView.swift +++ b/Sources/SurfaceView.swift @@ -45,7 +45,7 @@ public class SurfaceAppearance: NSObject { } }() - #if swift(>=6.2) + #if compiler(>=6.2) @available(iOS 26.0, *) public var cornerConfiguration: UICornerConfiguration? { get { _cornerConfiguration as? UICornerConfiguration } @@ -386,7 +386,7 @@ public class SurfaceView: UIView { } private func updateCornerRadius() { - #if swift(>=6.2) + #if compiler(>=6.2) if #available(iOS 26.0, *), let cornerConfiguration = appearance.cornerConfiguration { containerView.cornerConfiguration = cornerConfiguration containerView.layer.masksToBounds = true From c5da510d7d50c93475fd09553724ba7c4a302e8b Mon Sep 17 00:00:00 2001 From: Shin Yamamoto Date: Wed, 5 Nov 2025 18:51:01 +0900 Subject: [PATCH 46/62] Enable to build it for iOS 12 --- .../SamplesSwiftUI/UseCases/MainView.swift | 4 ++++ FloatingPanel.xcodeproj/project.pbxproj | 6 +++--- Sources/Core.swift | 18 ++++++++++++++++-- Sources/SwiftUI/FloatingPanelView.swift | 2 +- 4 files changed, 24 insertions(+), 6 deletions(-) diff --git a/Examples/SamplesSwiftUI/SamplesSwiftUI/UseCases/MainView.swift b/Examples/SamplesSwiftUI/SamplesSwiftUI/UseCases/MainView.swift index d0cd5b062..faedc8ac8 100644 --- a/Examples/SamplesSwiftUI/SamplesSwiftUI/UseCases/MainView.swift +++ b/Examples/SamplesSwiftUI/SamplesSwiftUI/UseCases/MainView.swift @@ -3,6 +3,7 @@ import FloatingPanel import SwiftUI import UIKit +import os.log struct MainView: View { @State private var panelLayout: FloatingPanelLayout? = MyFloatingPanelLayout() @@ -20,6 +21,9 @@ struct MainView: View { .floatingPanelSurfaceAppearance(.transparent()) .floatingPanelLayout(panelLayout) .floatingPanelState($panelState) + .onChange(of: panelState) { newValue in + Logger().debug("Panel state changed: \(newValue ?? .hidden)") + } VStack(spacing: 32) { Button("Move to full") { diff --git a/FloatingPanel.xcodeproj/project.pbxproj b/FloatingPanel.xcodeproj/project.pbxproj index 7d9486d09..96e7c1dec 100644 --- a/FloatingPanel.xcodeproj/project.pbxproj +++ b/FloatingPanel.xcodeproj/project.pbxproj @@ -523,7 +523,7 @@ ENABLE_USER_SCRIPT_SANDBOXING = YES; INFOPLIST_FILE = Sources/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -556,7 +556,7 @@ ENABLE_USER_SCRIPT_SANDBOXING = YES; INFOPLIST_FILE = Sources/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -694,7 +694,7 @@ ENABLE_USER_SCRIPT_SANDBOXING = YES; INFOPLIST_FILE = Sources/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", diff --git a/Sources/Core.swift b/Sources/Core.swift index 2d19ac791..659ca72c0 100644 --- a/Sources/Core.swift +++ b/Sources/Core.swift @@ -1,6 +1,8 @@ // Copyright 2018-Present Shin Yamamoto. All rights reserved. MIT license. +#if canImport(Combine) import Combine +#endif import UIKit import os.log @@ -50,7 +52,13 @@ class Core: NSObject, UIGestureRecognizerDelegate { } } } - private(set) var statePublisher: CurrentValueSubject = .init(.hidden) + + @available(iOS 13.0, *) + private(set) var statePublisher: CurrentValueSubject? { + get { _statePublisher as? CurrentValueSubject } + set { _statePublisher = newValue } + } + private var _statePublisher: Any? var panGestureRecognizer: FloatingPanelPanGestureRecognizer let panGestureDelegateRouter: FloatingPanelPanGestureRecognizer.DelegateRouter @@ -100,6 +108,10 @@ class Core: NSObject, UIGestureRecognizerDelegate { super.init() + if #available(iOS 13.0, *) { + statePublisher = .init(.hidden) + } + panGestureRecognizer.set(floatingPanel: self) surfaceView.addGestureRecognizer(panGestureRecognizer) panGestureRecognizer.addTarget(self, action: #selector(handle(panGesture:))) @@ -262,7 +274,9 @@ class Core: NSObject, UIGestureRecognizerDelegate { layoutAdapter.activateLayout(for: target, forceLayout: true) backdropView.alpha = getBackdropAlpha(for: target) adjustScrollContentInsetIfNeeded() - statePublisher.send(target) + if #available(iOS 13.0, *) { + statePublisher?.send(target) + } } private func getBackdropAlpha(for target: FloatingPanelState) -> CGFloat { diff --git a/Sources/SwiftUI/FloatingPanelView.swift b/Sources/SwiftUI/FloatingPanelView.swift index 972a150c8..d7ba4d113 100644 --- a/Sources/SwiftUI/FloatingPanelView.swift +++ b/Sources/SwiftUI/FloatingPanelView.swift @@ -235,7 +235,7 @@ extension FloatingPanelCoordinatorProxy { /// Start observing ``FloatingPanelController/state`` through the `Core` object. func observeStateChanges() { - controller.floatingPanel.statePublisher + controller.floatingPanel.statePublisher? .sink { [weak self] state in guard let self = self else { return } // Needs to update the state binding value on the next run loop cycle to avoid this error. From 411276df5240f5176bc02db093c4d752da648fbf Mon Sep 17 00:00:00 2001 From: Shin Yamamoto Date: Wed, 5 Nov 2025 19:33:33 +0900 Subject: [PATCH 47/62] Add CoreTests.test_statePublisher --- Tests/CoreTests.swift | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/Tests/CoreTests.swift b/Tests/CoreTests.swift index 9dfda5e30..3057430be 100644 --- a/Tests/CoreTests.swift +++ b/Tests/CoreTests.swift @@ -1,6 +1,9 @@ // Copyright 2018 the FloatingPanel authors. All rights reserved. MIT license. import XCTest +#if canImport(Combine) +import Combine +#endif @testable import FloatingPanel class CoreTests: XCTestCase { @@ -1022,6 +1025,45 @@ class CoreTests: XCTestCase { XCTAssertEqual(fpc.surfaceLocation(for: .full).y, fpc.surfaceLocation.y) XCTAssertEqual(delegate.willAttract, false) } + + @available(iOS 13.0, *) + func test_statePublisher() throws { + let fpc = FloatingPanelController() + fpc.showForTest() + + XCTAssertEqual(fpc.state, .half) + + // Verify statePublisher is available on iOS 13+ + XCTAssertNotNil(fpc.floatingPanel.statePublisher) + + var receivedStates: [FloatingPanelState] = [] + var cancellables = Set() + + // Subscribe to statePublisher + fpc.floatingPanel.statePublisher? + .sink { state in + receivedStates.append(state) + } + .store(in: &cancellables) + + // The initial state should be emitted first + XCTAssertEqual(receivedStates, [.half]) + + // Move to .full + fpc.move(to: .full, animated: false) + XCTAssertEqual(fpc.state, .full) + XCTAssertEqual(receivedStates, [.half, .full]) + + // Move to .tip + fpc.move(to: .tip, animated: false) + XCTAssertEqual(fpc.state, .tip) + XCTAssertEqual(receivedStates, [.half, .full, .tip]) + + // Move back to .half + fpc.move(to: .half, animated: false) + XCTAssertEqual(fpc.state, .half) + XCTAssertEqual(receivedStates, [.half, .full, .tip, .half]) + } } private class FloatingPanelLayout3Positions: FloatingPanelTestLayout { From 3c670b4d5e805fcecc695413ce081e02882f749a Mon Sep 17 00:00:00 2001 From: Shin Yamamoto Date: Wed, 5 Nov 2025 19:42:02 +0900 Subject: [PATCH 48/62] ci: fix a job error --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3eda50457..476c69ab7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -37,7 +37,7 @@ jobs: fail-fast: false matrix: include: - - os: "26.0" + - os: "26.0.1" xcode: "26.0.1" sim: "iPhone 16 Pro" parallel: NO # Stop random test job failures From edcf3ea11d0f6d704e1f0acca93313f4fb2f5a3c Mon Sep 17 00:00:00 2001 From: Shin Yamamoto Date: Tue, 11 Nov 2025 13:09:05 +0900 Subject: [PATCH 49/62] Add 'Star History Chart' at the bottom of README --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index f92c3594e..09b159537 100644 --- a/README.md +++ b/README.md @@ -269,3 +269,5 @@ Shin Yamamoto | [@scenee](https://twitter.com/scenee) ## License FloatingPanel is available under the MIT license. See the LICENSE file for more info. + +[![Star History Chart](https://api.star-history.com/svg?repos=scenee/FloatingPanel&type=date&legend=top-left)](https://www.star-history.com/#scenee/FloatingPanel&type=date&legend=top-left) From 5222c056c666934dbece1c19e5701e06b7ded858 Mon Sep 17 00:00:00 2001 From: Shin Yamamoto Date: Tue, 11 Nov 2025 18:50:47 +0900 Subject: [PATCH 50/62] Set minimum iOS version to v12 --- FloatingPanel.podspec | 3 ++- Package.swift | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/FloatingPanel.podspec b/FloatingPanel.podspec index d42fc058c..7f3ef63bc 100644 --- a/FloatingPanel.podspec +++ b/FloatingPanel.podspec @@ -11,7 +11,7 @@ The new interface displays the related contents and utilities in parallel as a u s.author = "Shin Yamamoto" s.social_media_url = "https://x.com/scenee" - s.platform = :ios, "13.0" + s.platform = :ios, "12.0" s.source = { :git => "https://github.com/scenee/FloatingPanel.git", :tag => s.version.to_s } s.source_files = "Sources/**/*.swift" s.swift_version = '5.0' @@ -20,3 +20,4 @@ The new interface displays the related contents and utilities in parallel as a u s.license = { :type => "MIT", :file => "LICENSE" } end + diff --git a/Package.swift b/Package.swift index 8f86014a4..d6b6c5c65 100644 --- a/Package.swift +++ b/Package.swift @@ -6,7 +6,7 @@ import PackageDescription let package = Package( name: "FloatingPanel", platforms: [ - .iOS(.v13) + .iOS(.v12) ], products: [ // Products define the executables and libraries produced by a package, and make them visible to other packages. @@ -25,3 +25,4 @@ let package = Package( ], swiftLanguageVersions: [.version("5")] ) + From 41d75802452e3fc6bf3a888ae83d122fd0e4ef18 Mon Sep 17 00:00:00 2001 From: Shin Yamamoto Date: Tue, 11 Nov 2025 19:54:43 +0900 Subject: [PATCH 51/62] Version 3.1.0 --- CHANGELOG.md | 22 ++++++++++++++++++++++ FloatingPanel.podspec | 2 +- README.md | 4 ++-- Sources/Info.plist | 2 +- 4 files changed, 26 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 287e3b95c..76622238a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,26 @@ # ChangeLog +## [3.1.0](https://github.com/scenee/FloatingPanel/releases/tag/3.1.0) + +### Added + +- Added support for configuring `UICornerConfiguration` when running on iOS 26 so surfaces can adopt the latest corner styles (#664). +- Expanded Core test coverage with new cases for scroll offset resets and `statePublisher` delivery to guard against regressions. + +### Changed + +- **Restored compatibility with iOS 12** by lowering the deployment target across the project, Swift Package manifest, and CocoaPods spec, and by making Combine usage conditional (#674). + +## [3.0.1](https://github.com/scenee/FloatingPanel/releases/tag/3.0.1) + +### Changed + +- Refined the DocC documentation landing page for the new SwiftUI APIs. + +### Fixed + +- Corrected the CocoaPods spec so the SwiftUI sources are packaged properly. + ## [3.0.0](https://github.com/scenee/FloatingPanel/releases/tag/3.0.0) ### Breaking Changes @@ -135,3 +156,4 @@ - Fixed invalid scroll offsets after moving between states - Calls `floatingPanelWillRemove` delegate method when a panel is removed from a window + diff --git a/FloatingPanel.podspec b/FloatingPanel.podspec index 7f3ef63bc..c72028c77 100644 --- a/FloatingPanel.podspec +++ b/FloatingPanel.podspec @@ -1,7 +1,7 @@ Pod::Spec.new do |s| s.name = "FloatingPanel" - s.version = "3.0.1" + s.version = "3.1.0" 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 09b159537..8567d3a42 100644 --- a/README.md +++ b/README.md @@ -74,7 +74,7 @@ FloatingPanel is written in Swift 5.0+ and compatible with iOS 12.0+. ## Documentation -- [API reference on Swift Package Index](https://swiftpackageindex.com/scenee/FloatingPanel/3.0.1/documentation/floatingpanel) +- [API reference on Swift Package Index](https://swiftpackageindex.com/scenee/FloatingPanel/3.1.0/documentation/floatingpanel) - [FloatingPanel SwiftUI API Guide](/Documentation/FloatingPanel%20SwiftUI%20API%20Guide.md) - [FloatingPanel API Guide](/Documentation/FloatingPanel%20API%20Guide.md) - [FloatingPanel 2.0 Migration Guide](/Documentation/FloatingPanel%202.0%20Migration%20Guide.md) @@ -92,7 +92,7 @@ Just follow [this documentation](https://developer.apple.com/documentation/swift In your Package.swift Swift Package Manager manifest, add the following dependency to your dependencies argument: ```swift -.package(url: "https://github.com/scenee/FloatingPanel", from: "3.0.1"), +.package(url: "https://github.com/scenee/FloatingPanel", from: "3.1.0"), ``` Add Numerics as a dependency for your target: diff --git a/Sources/Info.plist b/Sources/Info.plist index 014c4f384..e68a44b4d 100644 --- a/Sources/Info.plist +++ b/Sources/Info.plist @@ -15,7 +15,7 @@ CFBundlePackageType FMWK CFBundleShortVersionString - 3.0.1 + 3.1.0 CFBundleVersion $(CURRENT_PROJECT_VERSION) From 913cd1b95bab68cbed0715c3ff4351862575b31f Mon Sep 17 00:00:00 2001 From: Shin Yamamoto Date: Wed, 5 Nov 2025 21:45:20 +0900 Subject: [PATCH 52/62] Support dynamic content updates in SwiftUI Store content hosting controller reference and update it dynamically in updateUIViewController(), allowing panel content to change without recreation. Resolved #672 --- .../SamplesSwiftUI/UseCases/MainView.swift | 48 ++++++++++++++----- Sources/SwiftUI/FloatingPanelView.swift | 20 ++++++++ 2 files changed, 56 insertions(+), 12 deletions(-) diff --git a/Examples/SamplesSwiftUI/SamplesSwiftUI/UseCases/MainView.swift b/Examples/SamplesSwiftUI/SamplesSwiftUI/UseCases/MainView.swift index faedc8ac8..e993db22c 100644 --- a/Examples/SamplesSwiftUI/SamplesSwiftUI/UseCases/MainView.swift +++ b/Examples/SamplesSwiftUI/SamplesSwiftUI/UseCases/MainView.swift @@ -6,26 +6,28 @@ import UIKit import os.log struct MainView: View { + enum CardContent: String, CaseIterable, Identifiable { + case list + case detail + + var id: String { rawValue } + } @State private var panelLayout: FloatingPanelLayout? = MyFloatingPanelLayout() @State private var panelState: FloatingPanelState? + @State private var selectedContent: CardContent = .list var body: some View { ZStack { Color.orange .ignoresSafeArea() - .floatingPanel( - coordinator: MyPanelCoordinator.self - ) { proxy in - ContentView(proxy: proxy) - } - .floatingPanelSurfaceAppearance(.transparent()) - .floatingPanelLayout(panelLayout) - .floatingPanelState($panelState) - .onChange(of: panelState) { newValue in - Logger().debug("Panel state changed: \(newValue ?? .hidden)") - } - VStack(spacing: 32) { + Picker("type", selection: $selectedContent) { + ForEach(CardContent.allCases) { + type in + Text(type.rawValue).tag(type) + } + } + .pickerStyle(.segmented) Button("Move to full") { withAnimation(.interactiveSpring) { panelState = .full @@ -46,8 +48,29 @@ struct MainView: View { Text("Switch to My layout") } } + Spacer() + } + } + .floatingPanel( + coordinator: MyPanelCoordinator.self + ) { proxy in + switch selectedContent { + case .list: + ContentView(proxy: proxy) + case .detail: + VStack { + Text("off") + .padding(.top, 32) + Spacer() + } } } + .floatingPanelSurfaceAppearance(.transparent()) + .floatingPanelLayout(panelLayout) + .floatingPanelState($panelState) + .onChange(of: panelState) { newValue in + Logger().debug("Panel state changed: \(newValue ?? .hidden)") + } } } @@ -104,3 +127,4 @@ class MyFloatingPanelLayout: FloatingPanelLayout { #Preview("MainView") { MainView() } + diff --git a/Sources/SwiftUI/FloatingPanelView.swift b/Sources/SwiftUI/FloatingPanelView.swift index d7ba4d113..b11e2aeb9 100644 --- a/Sources/SwiftUI/FloatingPanelView.swift +++ b/Sources/SwiftUI/FloatingPanelView.swift @@ -110,7 +110,11 @@ struct FloatingPanelView: UIViewControllerRep _ uiViewController: UIHostingController, context: Context ) { + uiViewController.rootView = main + + context.coordinator.updateContent(content(context.coordinator.proxy)) context.coordinator.onUpdate(context: context) + applyEnvironment(context: context) applyAnimatableEnvironment(context: context) } @@ -160,6 +164,9 @@ class FloatingPanelCoordinatorProxy { private var subscriptions: Set = Set() + // Store a reference to the content hosting controller for dynamic updates + private weak var contentHostingController: UIViewController? + var proxy: FloatingPanelProxy { origin.proxy } var controller: FloatingPanelController { origin.controller } @@ -181,12 +188,25 @@ class FloatingPanelCoordinatorProxy { mainHostingController: UIHostingController
, contentHostingController: UIHostingController ) { + // Store the content hosting controller reference + self.contentHostingController = contentHostingController + origin.setupFloatingPanel( mainHostingController: mainHostingController, contentHostingController: contentHostingController ) } + /// Updates the content of the floating panel with new content. + func updateContent(_ newContent: Content) { + guard + let hostingController = contentHostingController as? UIHostingController + else { + return + } + hostingController.rootView = newContent + } + func onUpdate( context: UIViewControllerRepresentableContext ) where Representable: UIViewControllerRepresentable { From 2ba4badf54ce10548082e7aba6dce7ddedb173ea Mon Sep 17 00:00:00 2001 From: Shin Yamamoto Date: Sun, 23 Nov 2025 20:28:00 +0900 Subject: [PATCH 53/62] Improve SamplesSwiftUI --- .../SamplesSwiftUI/UseCases/MainView.swift | 15 ++++++++---- .../SamplesSwiftUI/Views/BackgroundView.swift | 23 +++++++++++++++++++ .../SamplesSwiftUI/Views/ContentView.swift | 9 +++----- 3 files changed, 37 insertions(+), 10 deletions(-) create mode 100644 Examples/SamplesSwiftUI/SamplesSwiftUI/Views/BackgroundView.swift diff --git a/Examples/SamplesSwiftUI/SamplesSwiftUI/UseCases/MainView.swift b/Examples/SamplesSwiftUI/SamplesSwiftUI/UseCases/MainView.swift index e993db22c..35412bb32 100644 --- a/Examples/SamplesSwiftUI/SamplesSwiftUI/UseCases/MainView.swift +++ b/Examples/SamplesSwiftUI/SamplesSwiftUI/UseCases/MainView.swift @@ -58,11 +58,19 @@ struct MainView: View { case .list: ContentView(proxy: proxy) case .detail: - VStack { - Text("off") - .padding(.top, 32) + HStack { + Spacer() + VStack { + Text("Detail content") + .padding(.top, 32) + Spacer() + } Spacer() } + .padding() + .background { + BackgroundView() + } } } .floatingPanelSurfaceAppearance(.transparent()) @@ -127,4 +135,3 @@ class MyFloatingPanelLayout: FloatingPanelLayout { #Preview("MainView") { MainView() } - diff --git a/Examples/SamplesSwiftUI/SamplesSwiftUI/Views/BackgroundView.swift b/Examples/SamplesSwiftUI/SamplesSwiftUI/Views/BackgroundView.swift new file mode 100644 index 000000000..0f9968eb3 --- /dev/null +++ b/Examples/SamplesSwiftUI/SamplesSwiftUI/Views/BackgroundView.swift @@ -0,0 +1,23 @@ +import SwiftUI + +struct BackgroundView: View { + var body: some View { + GeometryReader { geometry in + Rectangle() + .fill(.clear) + .frame(height: geometry.size.height * 2) + .backgroundEffect() + } + } +} + +extension View { + @ViewBuilder + fileprivate func backgroundEffect() -> some View { + if #available(iOS 26, *) { + self.glassEffect(.regular, in: .rect) + } else { + self.background(.regularMaterial) + } + } +} diff --git a/Examples/SamplesSwiftUI/SamplesSwiftUI/Views/ContentView.swift b/Examples/SamplesSwiftUI/SamplesSwiftUI/Views/ContentView.swift index d7d01e96c..8db4c47c3 100644 --- a/Examples/SamplesSwiftUI/SamplesSwiftUI/Views/ContentView.swift +++ b/Examples/SamplesSwiftUI/SamplesSwiftUI/Views/ContentView.swift @@ -15,6 +15,7 @@ struct ContentView: View { .frame(maxWidth: .infinity, alignment: .leading) .frame(height: 60) .background(.clear) + .padding(.horizontal) } } } @@ -28,6 +29,7 @@ struct ContentView: View { .frame(maxWidth: .infinity, alignment: .leading) .frame(height: 60) .background(.clear) + .padding(.horizontal) } } } @@ -38,12 +40,7 @@ struct ContentView: View { } // Prevent revealing underlying content at the bottom of the panel when the panel is moving beyond its fully‑expanded position. .background { - GeometryReader { geometry in - Rectangle() - .fill(.clear) - .frame(height: geometry.size.height * 2) - .background(.regularMaterial) - } + BackgroundView() } } } From 7cd414945bbe53f1ec22cdf246bee637e31077ab Mon Sep 17 00:00:00 2001 From: Shin Yamamoto Date: Thu, 27 Nov 2025 16:32:23 +0900 Subject: [PATCH 54/62] ci: use Xcode 26.0.1 for all builds --- .github/workflows/ci.yml | 74 ++++++++++++++++++++-------------------- 1 file changed, 37 insertions(+), 37 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 476c69ab7..e73ca60e2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,13 +1,13 @@ name: ci -on: +on: push: - branches: + branches: - master pull_request: - branches: - - '*' - workflow_dispatch: + branches: + - "*" + workflow_dispatch: jobs: build: @@ -40,33 +40,33 @@ jobs: - os: "26.0.1" xcode: "26.0.1" sim: "iPhone 16 Pro" - parallel: NO # Stop random test job failures + parallel: NO # Stop random test job failures runs-on: macos-15 - os: "18.5" xcode: "16.4" sim: "iPhone 16 Pro" - parallel: NO # Stop random test job failures + parallel: NO # Stop random test job failures runs-on: macos-15 - os: "17.5" xcode: "15.4" sim: "iPhone 15 Pro" - parallel: NO # Stop random test job failures + parallel: NO # Stop random test job failures runs-on: macos-14 steps: - - uses: actions/checkout@v4 - - name: Testing in iOS ${{ matrix.os }} - run: | - xcodebuild clean test \ - -workspace FloatingPanel.xcworkspace \ - -scheme FloatingPanel \ - -destination 'platform=iOS Simulator,OS=${{ matrix.os }},name=${{ matrix.sim }}' \ - -parallel-testing-enabled '${{ matrix.parallel }}' + - uses: actions/checkout@v4 + - name: Testing in iOS ${{ matrix.os }} + run: | + xcodebuild clean test \ + -workspace FloatingPanel.xcworkspace \ + -scheme FloatingPanel \ + -destination 'platform=iOS Simulator,OS=${{ matrix.os }},name=${{ matrix.sim }}' \ + -parallel-testing-enabled '${{ matrix.parallel }}' timeout-minutes: 20 example: runs-on: macos-15 env: - DEVELOPER_DIR: /Applications/Xcode_16.4.app/Contents/Developer + DEVELOPER_DIR: /Applications/Xcode_26.0.1.app/Contents/Developer strategy: fail-fast: false matrix: @@ -78,13 +78,13 @@ jobs: - example: "SamplesObjC" - example: "SamplesSwiftUI" steps: - - uses: actions/checkout@v4 - - name: Building ${{ matrix.example }} - run: | - xcodebuild clean build \ - -workspace FloatingPanel.xcworkspace \ - -scheme ${{ matrix.example }} \ - -sdk iphonesimulator + - uses: actions/checkout@v4 + - name: Building ${{ matrix.example }} + run: | + xcodebuild clean build \ + -workspace FloatingPanel.xcworkspace \ + -scheme ${{ matrix.example }} \ + -sdk iphonesimulator swiftpm: runs-on: ${{ matrix.runs-on }} @@ -93,7 +93,7 @@ jobs: strategy: fail-fast: false matrix: - xcode: ["16.4", "15.4"] + xcode: ["26.0.1", "16.4", "15.4"] platform: [iphoneos, iphonesimulator] arch: [x86_64, arm64] exclude: @@ -119,20 +119,20 @@ jobs: sys: "ios17.2-simulator" runs-on: macos-14 steps: - - uses: actions/checkout@v4 - - name: "Swift Package Manager build" - run: | - xcrun swift build \ - --sdk "$(xcrun --sdk ${{ matrix.platform }} --show-sdk-path)" \ - -Xswiftc "-target" -Xswiftc "${{ matrix.arch }}-apple-${{ matrix.sys }}" + - uses: actions/checkout@v4 + - name: "Swift Package Manager build" + run: | + xcrun swift build \ + --sdk "$(xcrun --sdk ${{ matrix.platform }} --show-sdk-path)" \ + -Xswiftc "-target" -Xswiftc "${{ matrix.arch }}-apple-${{ matrix.sys }}" cocoapods: runs-on: macos-15 env: - DEVELOPER_DIR: /Applications/Xcode_16.4.app/Contents/Developer + DEVELOPER_DIR: /Applications/Xcode_26.0.1.app/Contents/Developer steps: - - uses: actions/checkout@v4 - - name: "CocoaPods: pod lib lint" - run: pod lib lint --allow-warnings --verbose - - name: "CocoaPods: pod spec lint" - run: pod spec lint --allow-warnings --verbose + - uses: actions/checkout@v4 + - name: "CocoaPods: pod lib lint" + run: pod lib lint --allow-warnings --verbose + - name: "CocoaPods: pod spec lint" + run: pod spec lint --allow-warnings --verbose From c954ce36214ed2f7faf80eb9e9a445303e083aa6 Mon Sep 17 00:00:00 2001 From: Shin Yamamoto Date: Thu, 27 Nov 2025 16:43:34 +0900 Subject: [PATCH 55/62] Fix shadow on iOS 26 --- Sources/Extensions.swift | 69 +++++++++++++++++++++++++++++++++++++++ Sources/SurfaceView.swift | 47 ++++++++++++++++++++++++-- 2 files changed, 113 insertions(+), 3 deletions(-) diff --git a/Sources/Extensions.swift b/Sources/Extensions.swift index 8d635855d..d9128c386 100644 --- a/Sources/Extensions.swift +++ b/Sources/Extensions.swift @@ -214,4 +214,73 @@ extension UIBezierPath { cornerRadii: CGSize(width: cornerRadius, height: cornerRadius)) } + + #if compiler(>=6.2) + @available(iOS 26.0, *) + static func path(roundedRect rect: CGRect, view: UIView) -> UIBezierPath { + // Apply inset to hide the gap between UIBezierPath's circular arcs + // and UICornerConfiguration's corner curves. + let rect = rect.insetBy(dx: 4, dy: 4) + + // Query the effective corner radius from the view using the new iOS 26 API + // This properly handles UICornerConfiguration including .fixed, .dynamic, and .continuous + let topLeadingRadius = view.effectiveRadius(corner: .topLeft) + let topTrailingRadius = view.effectiveRadius(corner: .topRight) + let bottomLeadingRadius = view.effectiveRadius(corner: .bottomLeft) + let bottomTrailingRadius = view.effectiveRadius(corner: .bottomRight) + + // Otherwise, create a path with individual corner radii + let path = UIBezierPath() + let minX = rect.minX + let minY = rect.minY + let maxX = rect.maxX + let maxY = rect.maxY + + // Start from top left, after the corner + path.move(to: CGPoint(x: minX + topLeadingRadius, y: minY)) + + // Top edge and top-right corner + path.addLine(to: CGPoint(x: maxX - topTrailingRadius, y: minY)) + if topTrailingRadius > 0 { + path.addArc(withCenter: CGPoint(x: maxX - topTrailingRadius, y: minY + topTrailingRadius), + radius: topTrailingRadius, + startAngle: -0.5 * .pi, + endAngle: 0, + clockwise: true) + } + + // Right edge and bottom-right corner + path.addLine(to: CGPoint(x: maxX, y: maxY - bottomTrailingRadius)) + if bottomTrailingRadius > 0 { + path.addArc(withCenter: CGPoint(x: maxX - bottomTrailingRadius, y: maxY - bottomTrailingRadius), + radius: bottomTrailingRadius, + startAngle: 0, + endAngle: .pi * 0.5, + clockwise: true) + } + + // Bottom edge and bottom-left corner + path.addLine(to: CGPoint(x: minX + bottomLeadingRadius, y: maxY)) + if bottomLeadingRadius > 0 { + path.addArc(withCenter: CGPoint(x: minX + bottomLeadingRadius, y: maxY - bottomLeadingRadius), + radius: bottomLeadingRadius, + startAngle: .pi * 0.5, + endAngle: .pi, + clockwise: true) + } + + // Left edge and top-left corner + path.addLine(to: CGPoint(x: minX, y: minY + topLeadingRadius)) + if topLeadingRadius > 0 { + path.addArc(withCenter: CGPoint(x: minX + topLeadingRadius, y: minY + topLeadingRadius), + radius: topLeadingRadius, + startAngle: .pi, + endAngle: .pi * 1.5, + clockwise: true) + } + + path.close() + return path + } + #endif } diff --git a/Sources/SurfaceView.swift b/Sources/SurfaceView.swift index dcfefe975..08b1264cf 100644 --- a/Sources/SurfaceView.swift +++ b/Sources/SurfaceView.swift @@ -359,8 +359,23 @@ public class SurfaceView: UIView { let spread = shadow.spread let shadowRect = containerView.frame.insetBy(dx: -spread, dy: -spread) - let shadowPath = UIBezierPath.path(roundedRect: shadowRect, + + // Create shadow path based on corner configuration or corner radius + let shadowPath: UIBezierPath + #if compiler(>=6.2) + if #available(iOS 26.0, *), appearance.cornerConfiguration != nil { + // Use UIView.effectiveRadius(corner:) API for iOS 26+ + // This properly queries the actual corner radius from UICornerConfiguration + shadowPath = UIBezierPath.path(roundedRect: shadowRect, view: containerView) + } else { + shadowPath = UIBezierPath.path(roundedRect: shadowRect, appearance: appearance) + } + #else + shadowPath = UIBezierPath.path(roundedRect: shadowRect, + appearance: appearance) + #endif + shadowLayer.shadowPath = shadowPath.cgPath shadowLayer.shadowColor = shadow.color.cgColor shadowLayer.shadowOffset = shadow.offset @@ -369,17 +384,43 @@ public class SurfaceView: UIView { shadowLayer.shadowOpacity = shadow.opacity let mask = CAShapeLayer() - let path = UIBezierPath.path(roundedRect: containerView.frame, - appearance: appearance) + + // Create mask path based on corner configuration or corner radius + let path: UIBezierPath + #if compiler(>=6.2) + if #available(iOS 26.0, *), appearance.cornerConfiguration != nil { + // Use UIView.effectiveRadius(corner:) API for iOS 26+ + // This properly queries the actual corner radius from UICornerConfiguration + path = UIBezierPath.path(roundedRect: containerView.frame, view: containerView) + } else { + path = UIBezierPath.path(roundedRect: containerView.frame, + appearance: appearance) + } + #else + path = UIBezierPath.path(roundedRect: containerView.frame, + appearance: appearance) + #endif + let size = window?.bounds.size ?? CGSize(width: 1000.0, height: 1000.0) path.append(UIBezierPath(rect: layer.bounds.insetBy(dx: -size.width, dy: -size.height))) mask.fillRule = .evenOdd mask.path = path.cgPath + + #if compiler(>=6.2) + if #available(iOS 26.0, *), appearance.cornerConfiguration != nil { + // Corner curve is handled by UICornerConfiguration + } else if #available(iOS 13.0, *) { + containerView.layer.cornerCurve = appearance.cornerCurve + mask.cornerCurve = appearance.cornerCurve + } + #else if #available(iOS 13.0, *) { containerView.layer.cornerCurve = appearance.cornerCurve mask.cornerCurve = appearance.cornerCurve } + #endif + shadowLayer.mask = mask } CATransaction.commit() From bf15ba77003be06bdebf0b610ff5c55ee499e53d Mon Sep 17 00:00:00 2001 From: Shin Yamamoto Date: Thu, 27 Nov 2025 20:07:56 +0900 Subject: [PATCH 56/62] examples: fix animation glitch on iOS 26 --- Examples/Maps-SwiftUI/Maps/MapsApp.swift | 97 ++++++++++++------------ 1 file changed, 50 insertions(+), 47 deletions(-) diff --git a/Examples/Maps-SwiftUI/Maps/MapsApp.swift b/Examples/Maps-SwiftUI/Maps/MapsApp.swift index aa1b459d1..325b67e97 100644 --- a/Examples/Maps-SwiftUI/Maps/MapsApp.swift +++ b/Examples/Maps-SwiftUI/Maps/MapsApp.swift @@ -1,7 +1,7 @@ // Copyright 2021 the FloatingPanel authors. All rights reserved. MIT license. -import SwiftUI import FloatingPanel +import SwiftUI @main struct MapsApp: App { @@ -24,12 +24,12 @@ struct MapsApp: App { final class MapPanelCoordinator: FloatingPanelCoordinator { enum Event {} - let action: (Event) -> () + let action: (Event) -> Void let proxy: FloatingPanelProxy private lazy var delegate: FloatingPanelControllerDelegate? = self - init(action: @escaping (Event) -> ()) { + init(action: @escaping (Event) -> Void) { self.action = action self.proxy = .init(controller: FloatingPanelController()) } @@ -42,51 +42,54 @@ final class MapPanelCoordinator: FloatingPanelCoordinator { contentHostingController.ignoresKeyboardSafeArea() if #available(iOS 16, *) { - // Set the delegate object - controller.delegate = delegate - - // Set up the content - contentHostingController.view.backgroundColor = nil - controller.set(contentViewController: contentHostingController) - - // Show the panel - controller.addPanel(toParent: mainHostingController, animated: false) - } else { - // NOTE: Fix floating panel content view constraints (#549) - // This issue happens on iOS 15 or earlier. - - // Set the delegate object - controller.delegate = delegate - - // Set up the content - contentHostingController.view.backgroundColor = nil - let contentWrapperViewController = UIViewController() - contentWrapperViewController.view.addSubview(contentHostingController.view) - contentWrapperViewController.addChild(contentHostingController) - contentHostingController.didMove(toParent: contentWrapperViewController) - controller.set(contentViewController: contentWrapperViewController) - - // Show the panel - controller.addPanel(toParent: mainHostingController, animated: false) - - contentHostingController.view.translatesAutoresizingMaskIntoConstraints = false - let bottomConstraint = contentHostingController.view.bottomAnchor.constraint( - equalTo: contentWrapperViewController.view.bottomAnchor - ) - bottomConstraint.priority = .defaultHigh - NSLayoutConstraint.activate([ - contentHostingController.view.topAnchor.constraint( - equalTo: contentWrapperViewController.view.topAnchor - ), - contentHostingController.view.leadingAnchor.constraint( - equalTo: contentWrapperViewController.view.leadingAnchor - ), - contentHostingController.view.trailingAnchor.constraint( - equalTo: contentWrapperViewController.view.trailingAnchor - ), - bottomConstraint - ]) + if #unavailable(iOS 26) { + // Set the delegate object + controller.delegate = delegate + + // Set up the content + contentHostingController.view.backgroundColor = nil + controller.set(contentViewController: contentHostingController) + + // Show the panel + controller.addPanel(toParent: mainHostingController, animated: false) + return + } } + + // NOTE: Fix floating panel content view constraints (#549) + // This issue happens on iOS 15 or earlier, and iOS 26 or later. + + // Set the delegate object + controller.delegate = delegate + + // Set up the content + contentHostingController.view.backgroundColor = nil + let contentWrapperViewController = UIViewController() + contentWrapperViewController.view.addSubview(contentHostingController.view) + contentWrapperViewController.addChild(contentHostingController) + contentHostingController.didMove(toParent: contentWrapperViewController) + controller.set(contentViewController: contentWrapperViewController) + + // Show the panel + controller.addPanel(toParent: mainHostingController, animated: false) + + contentHostingController.view.translatesAutoresizingMaskIntoConstraints = false + let bottomConstraint = contentHostingController.view.bottomAnchor.constraint( + equalTo: contentWrapperViewController.view.bottomAnchor + ) + bottomConstraint.priority = .defaultHigh + NSLayoutConstraint.activate([ + contentHostingController.view.topAnchor.constraint( + equalTo: contentWrapperViewController.view.topAnchor + ), + contentHostingController.view.leadingAnchor.constraint( + equalTo: contentWrapperViewController.view.leadingAnchor + ), + contentHostingController.view.trailingAnchor.constraint( + equalTo: contentWrapperViewController.view.trailingAnchor + ), + bottomConstraint, + ]) } func onUpdate( From 7844fac6a477a7fbca12c1d6214deb3984517f85 Mon Sep 17 00:00:00 2001 From: Shin Yamamoto Date: Fri, 28 Nov 2025 17:54:37 +0900 Subject: [PATCH 57/62] Version 3.2.0 --- CHANGELOG.md | 16 ++++++++++++++++ FloatingPanel.podspec | 2 +- README.md | 6 +++--- Sources/Info.plist | 2 +- 4 files changed, 21 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 76622238a..cec166aaf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,21 @@ # ChangeLog +## [3.2.0](https://github.com/scenee/FloatingPanel/releases/tag/3.2.0) + +### Added + +- Added support for dynamic content updates in SwiftUI (#675). + +### Fixed + +- Fixed shadow rendering issues on iOS 26. +- Fixed animation glitches in example apps on iOS 26. + +### Changed + +- Updated CI to use Xcode 26.0.1 for all builds. +- Improved SamplesSwiftUI example app. + ## [3.1.0](https://github.com/scenee/FloatingPanel/releases/tag/3.1.0) ### Added diff --git a/FloatingPanel.podspec b/FloatingPanel.podspec index c72028c77..6cc5fbb59 100644 --- a/FloatingPanel.podspec +++ b/FloatingPanel.podspec @@ -1,7 +1,7 @@ Pod::Spec.new do |s| s.name = "FloatingPanel" - s.version = "3.1.0" + s.version = "3.2.0" 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 8567d3a42..d2799c153 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,6 @@ [![Swift 5](https://img.shields.io/badge/swift-5-orange.svg?style=flat)](https://swift.org/) [![Platform](https://img.shields.io/cocoapods/p/FloatingPanel.svg)](https://cocoapods.org/pods/FloatingPanel) [![Version](https://img.shields.io/cocoapods/v/FloatingPanel.svg)](https://cocoapods.org/pods/FloatingPanel) -![GitHub Workflow Status (with branch)](https://img.shields.io/github/actions/workflow/status/scenee/FloatingPanel/ci.yml?branch=master) # FloatingPanel @@ -74,7 +73,7 @@ FloatingPanel is written in Swift 5.0+ and compatible with iOS 12.0+. ## Documentation -- [API reference on Swift Package Index](https://swiftpackageindex.com/scenee/FloatingPanel/3.1.0/documentation/floatingpanel) +- [API reference on Swift Package Index](https://swiftpackageindex.com/scenee/FloatingPanel/3.2.0/documentation/floatingpanel) - [FloatingPanel SwiftUI API Guide](/Documentation/FloatingPanel%20SwiftUI%20API%20Guide.md) - [FloatingPanel API Guide](/Documentation/FloatingPanel%20API%20Guide.md) - [FloatingPanel 2.0 Migration Guide](/Documentation/FloatingPanel%202.0%20Migration%20Guide.md) @@ -92,7 +91,7 @@ Just follow [this documentation](https://developer.apple.com/documentation/swift In your Package.swift Swift Package Manager manifest, add the following dependency to your dependencies argument: ```swift -.package(url: "https://github.com/scenee/FloatingPanel", from: "3.1.0"), +.package(url: "https://github.com/scenee/FloatingPanel", from: "3.2.0"), ``` Add Numerics as a dependency for your target: @@ -271,3 +270,4 @@ Shin Yamamoto | [@scenee](https://twitter.com/scenee) FloatingPanel is available under the MIT license. See the LICENSE file for more info. [![Star History Chart](https://api.star-history.com/svg?repos=scenee/FloatingPanel&type=date&legend=top-left)](https://www.star-history.com/#scenee/FloatingPanel&type=date&legend=top-left) + diff --git a/Sources/Info.plist b/Sources/Info.plist index e68a44b4d..fa57ac954 100644 --- a/Sources/Info.plist +++ b/Sources/Info.plist @@ -15,7 +15,7 @@ CFBundlePackageType FMWK CFBundleShortVersionString - 3.1.0 + 3.2.0 CFBundleVersion $(CURRENT_PROJECT_VERSION) From 612082ca6863ac0b8ce266cf498d5427f09124e4 Mon Sep 17 00:00:00 2001 From: Shin Yamamoto Date: Fri, 28 Nov 2025 18:01:51 +0900 Subject: [PATCH 58/62] Remove badges from README --- README.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/README.md b/README.md index d2799c153..10db4d26f 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,3 @@ -[![Swift 5](https://img.shields.io/badge/swift-5-orange.svg?style=flat)](https://swift.org/) -[![Platform](https://img.shields.io/cocoapods/p/FloatingPanel.svg)](https://cocoapods.org/pods/FloatingPanel) -[![Version](https://img.shields.io/cocoapods/v/FloatingPanel.svg)](https://cocoapods.org/pods/FloatingPanel) - # FloatingPanel FloatingPanel is a simple and easy-to-use UI component designed for a user interface featured in Apple Maps, Shortcuts and Stocks app. From 9e8fd344ac2465114d7049c199de3d3ea3baeac3 Mon Sep 17 00:00:00 2001 From: Shin Yamamoto Date: Fri, 28 Nov 2025 18:10:37 +0900 Subject: [PATCH 59/62] ci: fix 'swiftpm' job --- .github/workflows/ci.yml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e73ca60e2..97dc9f036 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -100,6 +100,15 @@ jobs: - platform: iphoneos arch: x86_64 include: + # 26.0.1 + - platform: iphoneos + xcode: "26.0.1" + sys: "ios26.0" + runs-on: macos-15 + - platform: iphonesimulator + xcode: "26.0.1" + sys: "ios26.0-simulator" + runs-on: macos-15 # 18.5 - platform: iphoneos xcode: "16.4" From cd135b3fb6aa89cc71ffdb88fe44e793f314066f Mon Sep 17 00:00:00 2001 From: Shin Yamamoto Date: Thu, 15 Jan 2026 20:49:55 +0900 Subject: [PATCH 60/62] Fix scroll offset reset due to floating-point precision errors (#677) When the panel is already at a boundary position, floating-point precision errors on some devices (like iPhone 17 Pro Max) can cause unexpected offset resets in the position calculation. This commit addresses the layout issue where fractional values caused incorrect position evaluation. Based on the initial fix and sample project provided by @0x1306a94. Co-authored-by: 0x1306a94 --- Sources/Layout.swift | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Sources/Layout.swift b/Sources/Layout.swift index 2118e4c67..bcce8cba4 100644 --- a/Sources/Layout.swift +++ b/Sources/Layout.swift @@ -306,7 +306,7 @@ class LayoutAdapter { } func surfaceLocation(for state: FloatingPanelState) -> CGPoint { - let pos = position(for: state).rounded(by: surfaceView.fp_displayScale) + let pos = position(for: state) switch layout.position { case .top, .bottom: return CGPoint(x: 0.0, y: pos) @@ -316,6 +316,10 @@ class LayoutAdapter { } func position(for state: FloatingPanelState) -> CGFloat { + return _position(for: state).rounded(by: surfaceView.fp_displayScale) + } + + private func _position(for state: FloatingPanelState) -> CGFloat { let bounds = vc.view.bounds let anchor = layout.anchors[state] ?? self.hiddenAnchor From 6a343c8a830ee1d377c05218c13db5b60ba07c5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B6ren=20Gade?= Date: Fri, 16 Jan 2026 00:49:41 +0100 Subject: [PATCH 61/62] Perform view updates during scroll tracking (#679) When we enable floatingPanelScrollTracking on our view inside of the floating panel, some view updates are performed (i.e., SwiftUI views are being instantiated) but no rendering (i.e., body evaluations) occurs. Example code: ```swift content .floatingPanel( coordinator: ContentPanelCoordinator.self, onEvent: onEvent ) { proxy in panelContent .floatingPanelScrollTracking(proxy: proxy) // scroll tracking breaks view updates } ``` We tracked this down to the scroll tracking, and it seems that the performing of view updates inside of the ScrollViewRepresentable was missing. This seems similar to #675. --- Sources/SwiftUI/View+floatingPanelScrollTracking.swift | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Sources/SwiftUI/View+floatingPanelScrollTracking.swift b/Sources/SwiftUI/View+floatingPanelScrollTracking.swift index 5be422e52..f288618df 100644 --- a/Sources/SwiftUI/View+floatingPanelScrollTracking.swift +++ b/Sources/SwiftUI/View+floatingPanelScrollTracking.swift @@ -68,7 +68,11 @@ private struct ScrollViewRepresentable: UIViewControllerRepresentable w return vc } - func updateUIViewController(_ uiViewController: ScrollViewHostingController, context: Context) { + func updateUIViewController( + _ uiViewController: ScrollViewHostingController, + context: Context + ) { + uiViewController.rootView = content() } class ScrollViewHostingController: UIHostingController where V: View { From ee2c6fee991c8b2d15c6e2db2b883065becf18e2 Mon Sep 17 00:00:00 2001 From: Shin Yamamoto Date: Sat, 24 Jan 2026 11:40:26 +0900 Subject: [PATCH 62/62] Version 3.2.1 --- CHANGELOG.md | 7 +++++++ FloatingPanel.podspec | 2 +- README.md | 4 ++-- Sources/Info.plist | 2 +- 4 files changed, 11 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cec166aaf..0b4c9741a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # ChangeLog +## [3.2.1](https://github.com/scenee/FloatingPanel/releases/tag/3.2.1) + +### Fixed + +- Fixed scroll offset reset due to floating-point precision errors (#677). +- Fixed view updates during scroll tracking (#679). + ## [3.2.0](https://github.com/scenee/FloatingPanel/releases/tag/3.2.0) ### Added diff --git a/FloatingPanel.podspec b/FloatingPanel.podspec index 6cc5fbb59..9d48e8608 100644 --- a/FloatingPanel.podspec +++ b/FloatingPanel.podspec @@ -1,7 +1,7 @@ Pod::Spec.new do |s| s.name = "FloatingPanel" - s.version = "3.2.0" + s.version = "3.2.1" s.summary = "FloatingPanel is a clean and easy-to-use UI component of a floating panel interface." s.description = <<-DESC FloatingPanel is a clean and easy-to-use UI component for a new interface introduced in Apple Maps, Shortcuts and Stocks app. diff --git a/README.md b/README.md index 10db4d26f..ba6a7e8ed 100644 --- a/README.md +++ b/README.md @@ -69,7 +69,7 @@ FloatingPanel is written in Swift 5.0+ and compatible with iOS 12.0+. ## Documentation -- [API reference on Swift Package Index](https://swiftpackageindex.com/scenee/FloatingPanel/3.2.0/documentation/floatingpanel) +- [API reference on Swift Package Index](https://swiftpackageindex.com/scenee/FloatingPanel/3.2.1/documentation/floatingpanel) - [FloatingPanel SwiftUI API Guide](/Documentation/FloatingPanel%20SwiftUI%20API%20Guide.md) - [FloatingPanel API Guide](/Documentation/FloatingPanel%20API%20Guide.md) - [FloatingPanel 2.0 Migration Guide](/Documentation/FloatingPanel%202.0%20Migration%20Guide.md) @@ -87,7 +87,7 @@ Just follow [this documentation](https://developer.apple.com/documentation/swift In your Package.swift Swift Package Manager manifest, add the following dependency to your dependencies argument: ```swift -.package(url: "https://github.com/scenee/FloatingPanel", from: "3.2.0"), +.package(url: "https://github.com/scenee/FloatingPanel", from: "3.2.1"), ``` Add Numerics as a dependency for your target: diff --git a/Sources/Info.plist b/Sources/Info.plist index fa57ac954..d50b42082 100644 --- a/Sources/Info.plist +++ b/Sources/Info.plist @@ -15,7 +15,7 @@ CFBundlePackageType FMWK CFBundleShortVersionString - 3.2.0 + 3.2.1 CFBundleVersion $(CURRENT_PROJECT_VERSION)