Communitygithub.com

Concentric Corners(同心圆角)

在 SwiftUI 中实现嵌套圆角视图的完美同心圆角效果,适用于 iOS 26+ / macOS 26+,基于 WWDC25 新设计系统。

Concentric Corners(同心圆角) 是什么?

该技能提供了一套完整且可复用的 SwiftUI 圆角解决方案,确保嵌套的圆角视图(如卡片中的照片、地图上浮动的卡片及其缩略图)的角部中心点对齐,避免出现“外扩”(里侧圆角比外侧大)或“内缩”(内侧圆角过于方直)的视觉问题。它引入了 `ConcentricRectangle`、`containerShape` 及 `Edge.Corner.Style.concentric(minimum:)` 等新 API,系统会自动计算内层圆角半径(外层半径减去两视图间距),并提供最小半径保底。技能还包含一套可复用的角部比例系统,建议从最内层的照片或小卡片开始定义基础半径,然后向外层层加 padding 得到容器半径。支持 iOS 26 新 Shape 类型,并为 iOS 26 以下版本提供 `RoundedRectangle` 回退方案。附带验证清单和渲染测试方法,帮助开发者确保嵌套圆角的视觉一致性。

兼容平台~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/gtokman/skills/tree/HEAD/swiftui-concentric-corners

在你喜欢的 AI 中提问

打开一个已预加载此 Agent Skill 的新对话。

文档

同心圆角

嵌套的圆角形状只有共享它们的角部中心点时看起来才正确:内侧半径等于外侧半径减去两者之间的间距。如果搞错了,角部会看起来 外扩(内角比外角更圆)或 内缩(内角比外角方得多)。Apple 的三种形状类型:

  • Fixed(固定) — 恒定半径(RoundedRectangle(cornerRadius:))。
  • Capsule(胶囊) — 半径是高度的一半;本质上是同心的(按钮、徽章、药丸形状)。
  • Concentric(同心) — 半径 = 父级半径 − 内边距,由系统计算得出。

API(iOS 26, macOS 26)

// 容器声明其形状。必须是 RoundedRectangularShape:
// RoundedRectangle, UnevenRoundedRectangle, Rectangle, Capsule, Circle。
card.containerShape(RoundedRectangle(cornerRadius: 28, style: .continuous))

// 内部内容与其保持同心,绝不会低于最小值(内容是独立或远离容器角部时的半径)。
ConcentricRectangle(corners: .concentric(minimum: .fixed(16)), isUniform: false)
  • .concentric(minimum:) 是一个 下限,而不是目标:半径 = max(容器 − 内间距, 最小值)。如果容器对于内容的 padding 来说太紧,请增大容器的半径——而不是降低最小值。
  • isUniform: true 使每个角具有相同的半径(例如在地图的一个边缘附近浮动的卡片);false 则仅使靠近容器角部的角为同心(Apple 的 sheet 样式)。
  • 如果没有 containerShape,同心形状会针对窗口/屏幕角部进行解析——因此全屏视图的顶部内容可以跟随设备的边框。
  • ConcentricRectangle 不是 RoundedRectangularShape:它不能作为容器使用。本身被嵌套的容器(例如地图上的卡片)需要将其 结果 半径声明为 RoundedRectangle(地图半径 − 内间距),以供其自身的子视图使用。
  • matchedTransitionSource { $0.clipShape(…) } 只接受 RoundedRectangle——请传入内容通常的半径。
  • 在 iOS 26 之前:回退到使用最小半径的 RoundedRectangle。对 RoundedRectangle 使用 containerShape 仍然可以编译(旧的 InsettableShape 重载)。

可复用的角部比例方案

从内层推导外层,而不是反过来:为照片和小卡片选择一个 内侧 半径,并使每个容器的半径为 内侧半径 + 内间距。这样任何距离容器一个内间距的内容都会精确地得到 内侧半径。

enum Corners {
    static let inner: CGFloat = 16          // 照片、小卡片
    static let map: CGFloat = 28            // 地图和大型媒体
    static let mapInset: CGFloat = 12       // 地图上的卡片 → 16

    static func container(padding: CGFloat) -> CGFloat { inner + padding }   // 12 pt → 28

    static func rect(_ radius: CGFloat) -> RoundedRectangle {
        RoundedRectangle(cornerRadius: radius, style: .continuous)
    }

    /// 嵌套在容器中的内容:同心,永不低于 `minimum`。
    static func nested(minimum: CGFloat, uniform: Bool = false) -> AnyShape {
        if #available(iOS 26, *) {
            AnyShape(ConcentricRectangle(corners: .concentric(minimum: .fixed(minimum)), isUniform: uniform))
        } else {
            AnyShape(rect(minimum))
        }
    }
}

用法:

// 一张卡片:根据其内间距得到的半径,声明为容器。
let shape = Corners.rect(max(requested, Corners.container(padding: padding)))
VStack { photo }.padding(padding)
    .background(shape.fill(.surface))
    .containerShape(shape)

// 任何位置的照片:与它的容器保持同心,独立时为 16。
let photo = Corners.nested(minimum: Corners.inner)
photo.fill(.placeholder).overlay { image }.clipShape(photo)

// 地图上带有玻璃效果卡片,卡片内包含缩略图。
map.clipShape(Corners.rect(Corners.map)).containerShape(Corners.rect(Corners.map))
    .overlay(alignment: .bottom) {
        HStack { Corners.nested(minimum: 10).fill(…).frame(width: 64, height: 64) … }
            .padding(12)
            .glassEffect(.regular, in: Corners.nested(minimum: Corners.map - Corners.mapInset, uniform: true))
            .containerShape(Corners.rect(Corners.map - Corners.mapInset))
            .padding(Corners.mapInset)
    }

检查清单

  • 每个容纳圆角内容的容器都声明了 .containerShape(_:)。
  • 嵌套内容使用 ConcentricRectangle(或辅助方法),并设置了合理的 minimum。
  • 容器半径 ≥ 内容半径 + 内间距;绝不在小的容器内部放入更大的半径。
  • 裁剪、背景、玻璃效果和点击测试(contentShape)都使用相同的形状。
  • 按钮、徽章和药丸形状使用 Capsule;在手机上注意屏幕边缘的额外边距。
  • 已放置 iOS 18 的回退方案。

验证方法

在 macOS 26+ 上离屏渲染(ImageRenderer,在辅助方法的副本中使用 #available(iOS 26, macOS 26, *)),并使用高对比度填充:一个容器、一个内容(垫一个内间距)、同一内容独立显示、以及一个放在更大容器中的浮动卡片。角部应明显共享中心点;独立显示的内容则展示最小值。

Individual skills in this repo

This repo contains 1 individual skill — each has its own dedicated page.

相关技能