diff --git a/Examples/Maps-SwiftUI/Maps/MapsApp.swift b/Examples/Maps-SwiftUI/Maps/MapsApp.swift index aa1b459d..325b67e9 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( diff --git a/Sources/Extensions.swift b/Sources/Extensions.swift index 8d635855..d9128c38 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 dcfefe97..08b1264c 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()