Reading Concentric Corner Radii with GeometryProxy in SwiftUI


Greetings, traveler!

SwiftUI can create shapes whose corners follow the curvature of their container. ConcentricRectangle handles this automatically, but sometimes you need the calculated radius values themselves.

In iOS 27, GeometryProxy provides two APIs for this:

var concentricCornerRadii: RectangleCornerRadii? { get }

func concentricCornerRadii(
    in frame: CGRect
) -> RectangleCornerRadii?

They let you use SwiftUI’s concentric corner calculations in custom shapes, drawing code, animations, and layouts. The APIs were added in the iOS 27 beta cycle.

What concentric corners are

Two rounded rectangles have concentric corners when their curves share the same center point.

Consider a card placed inside a rounded container. Giving both shapes the same corner radius will not produce matching curves because the inner card is smaller and positioned farther from the outer edge.

The inner radius needs to account for that distance. In simplified terms, SwiftUI calculates it by subtracting the distance between the corresponding corners from the container’s radius.

inner radius = container radius - corner distance

Each corner is calculated separately. A view may therefore receive different values for its top-leading, top-trailing, bottom-leading, and bottom-trailing corners.

SwiftUI clamps the result when necessary. Corners far enough from the container’s curved area receive a radius of zero. The radius also cannot exceed the maximum supported by the view’s size.

Reading the radii of a view

The concentricCornerRadii property calculates values for the bounds represented by the current GeometryProxy.

struct ConcentricCard: View {
    var body: some View {
        GeometryReader { geometry in
            if let radii = geometry.concentricCornerRadii {
                UnevenRoundedRectangle(
                    cornerRadii: radii,
                    style: .continuous
                )
                .fill(.blue)
            }
        }
        .containerShape(
            .rect(cornerRadius: 48)
        )
    }
}

The result is optional because SwiftUI may not be able to resolve a suitable rounded container shape.

RectangleCornerRadii contains four CGFloat values:

radii.topLeading
radii.topTrailing
radii.bottomLeading
radii.bottomTrailing

This makes it possible to pass the result to UnevenRoundedRectangle, construct a custom Path, or use the values in a Canvas.

Calculating radii for a specific frame

The property version works with the bounds associated with the proxy. The method version calculates radii for any rectangle expressed in the proxy’s local coordinate space:

let radii = geometry.concentricCornerRadii(
    in: itemFrame
)

This is useful when one GeometryReader manages several movable or manually positioned elements.

For example, a draggable rectangle can update its corner radii as its frame moves around the screen. When its top-leading corner approaches the top-leading corner of the container, that corner begins to receive a larger radius. The other corners can remain square.

Moving the rectangle away from the curved area reduces the radius until it reaches zero.

Building a draggable demo

The following example places a draggable rectangle inside a full-screen GeometryReader. Its shape updates as it approaches the screen corners.

import SwiftUI

struct ConcentricCornerRadiiDemo: View {
    private let itemSize = CGSize(
        width: 180,
        height: 130
    )

    @State private var position: CGPoint?
    @State private var dragStartPosition: CGPoint?

    var body: some View {
        GeometryReader { geometry in
            let currentPosition =
                position ?? initialPosition(in: geometry.size)

            let itemFrame = CGRect(
                x: currentPosition.x - itemSize.width / 2,
                y: currentPosition.y - itemSize.height / 2,
                width: itemSize.width,
                height: itemSize.height
            )

            let radii =
                geometry.concentricCornerRadii(in: itemFrame)
                ?? RectangleCornerRadii()

            ZStack {
                Color(.systemBackground)

                UnevenRoundedRectangle(
                    cornerRadii: radii,
                    style: .continuous
                )
                .fill(.blue.gradient)
                .overlay {
                    radiusLabels(radii)
                }
                .frame(
                    width: itemSize.width,
                    height: itemSize.height
                )
                .position(currentPosition)
                .gesture(
                    dragGesture(
                        from: currentPosition,
                        containerSize: geometry.size
                    )
                )
            }
            .frame(
                maxWidth: .infinity,
                maxHeight: .infinity
            )
        }
        .ignoresSafeArea()
    }

    private func radiusLabels(
        _ radii: RectangleCornerRadii
    ) -> some View {
        VStack(spacing: 4) {
            Text("Drag me")
                .font(.headline)

            Text(
                """
                TL: \(radii.topLeading, format: .number.precision(.fractionLength(1)))
                TR: \(radii.topTrailing, format: .number.precision(.fractionLength(1)))
                BL: \(radii.bottomLeading, format: .number.precision(.fractionLength(1)))
                BR: \(radii.bottomTrailing, format: .number.precision(.fractionLength(1)))
                """
            )
            .font(.caption.monospacedDigit())
        }
        .foregroundStyle(.white)
    }

    private func dragGesture(
        from currentPosition: CGPoint,
        containerSize: CGSize
    ) -> some Gesture {
        DragGesture()
            .onChanged { value in
                if dragStartPosition == nil {
                    dragStartPosition = currentPosition
                }

                guard let dragStartPosition else {
                    return
                }

                let proposedPosition = CGPoint(
                    x: dragStartPosition.x
                        + value.translation.width,
                    y: dragStartPosition.y
                        + value.translation.height
                )

                position = clamped(
                    proposedPosition,
                    to: containerSize
                )
            }
            .onEnded { _ in
                dragStartPosition = nil
            }
    }

    private func initialPosition(
        in containerSize: CGSize
    ) -> CGPoint {
        CGPoint(
            x: containerSize.width / 2,
            y: containerSize.height / 2
        )
    }

    private func clamped(
        _ position: CGPoint,
        to containerSize: CGSize
    ) -> CGPoint {
        let halfWidth = itemSize.width / 2
        let halfHeight = itemSize.height / 2

        return CGPoint(
            x: min(
                max(position.x, halfWidth),
                containerSize.width - halfWidth
            ),
            y: min(
                max(position.y, halfHeight),
                containerSize.height - halfHeight
            )
        )
    }
}

The rectangle stays inside the container, which makes the effect easier to inspect. Its frame changes during the drag, so concentricCornerRadii(in:) returns new values on every layout update.

At the center of the screen, all four values will usually be zero. Near a screen corner, the corresponding value increases and the rectangle follows that corner’s curvature.

Testing with an explicit container shape

The root container can derive its shape from the current window or presentation. That behavior depends on where the view is displayed.

For a predictable test, define the container shape yourself:

GeometryReader { geometry in
    DemoContent(geometry: geometry)
}
.containerShape(
    .rect(
        cornerRadius: 64,
        style: .continuous
    )
)

Any concentric calculation inside this hierarchy will use the specified rounded rectangle as its reference container. The same mechanism is used by ConcentricRectangle and other container-relative shapes.

This is also useful when building nested cards, panels, widgets, or custom presentation components. The inner geometry does not need to know the container’s fixed radius. It receives values calculated for its actual position.

Using onGeometryChange

A GeometryProxy is also available inside onGeometryChange, so a view can extract the radii without placing its content directly inside a GeometryReader.

struct CornerAwareView: View {
    @State private var radii: RectangleCornerRadii?

    var body: some View {
        Color.blue
            .onGeometryChange(
                for: RectangleCornerRadii?.self
            ) { geometry in
                geometry.concentricCornerRadii
            } action: { newValue in
                radii = newValue
            }
    }
}

This approach makes sense when the values need to be stored in state or passed to code outside the view’s drawing closure.

Avoid updating state unless the geometry value has changed. Geometry callbacks can run frequently during layout, scrolling, resizing, and interactive movement.

Can it expose the screen corner radius?

A full-screen view may receive radii derived from the display or window container. This gives you geometry that matches the visible screen corners without relying on private APIs or a hardcoded device table.

It should not be treated as a hardware specification API, though.

concentricCornerRadii describes the current view relative to its current container. That container may be:

  • a device screen
  • a resizable iPad window
  • a sheet
  • a popover
  • a custom view with containerShape(_:)
  • a macOS window

The values can also differ between corners. Apple notes that window controls and other system UI may affect corner geometry on platforms such as iPadOS and macOS.

For UI layout, this distinction is useful. The view receives the radius it should use now, rather than a physical value tied to a specific device model.

When to use this API

Use ConcentricRectangle when you only need to draw a rectangle that follows its container. It already performs the calculation and rendering.

Use concentricCornerRadii when you need the values themselves. Common cases include:

  • constructing a custom Path
  • drawing with Canvas
  • changing content based on proximity to a container corner
  • animating individual corners
  • applying the same geometry to several drawing operations
  • calculating radii for a movable frame

The new GeometryProxy API fills the gap between automatic concentric shapes and fully custom drawing. SwiftUI still owns the geometry calculation, while your code decides how to use the result.