Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 50 additions & 47 deletions Examples/Maps-SwiftUI/Maps/MapsApp.swift
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Copyright 2021 the FloatingPanel authors. All rights reserved. MIT license.

import SwiftUI
import FloatingPanel
import SwiftUI

@main
struct MapsApp: App {
Expand All @@ -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())
}
Expand All @@ -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<Representable>(
Expand Down
69 changes: 69 additions & 0 deletions Sources/Extensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
47 changes: 44 additions & 3 deletions Sources/SurfaceView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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()
Expand Down