同心圓角
巢狀圓角形狀只有在它們共享圓角中心時看起來才正確:內部半徑等於外部半徑減去兩者之間的間距。弄錯的話,圓角看起來會外擴(內側比外側更圓)或擠壓(內側比外側更方)。Apple 的三種形狀類型:
- 固定 — 恆定的半徑(
RoundedRectangle(cornerRadius:))。 - 膠囊形 — 半徑等於高度的一半;本質上就是同心(按鈕、徽章、藥丸形狀)。
- 同心 — 半徑 = 父層半徑 − 內邊距,由系統計算。
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(container − inset, minimum)。如果容器對於內容的內邊距來說太緊,請提高容器的半徑 — 而不是降低最小值。isUniform: true使每個角落具有相同的半徑(浮在地圖一邊附近的卡片);false僅使靠近容器角落的角落變為同心(Apple 的 sheets)。- 如果沒有
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, *)),並使用高對比填色:一個容器、一個內含一個內邊距的內容、同一個內容單獨使用的方式,以及一個較大容器內的浮動卡片。圓角應明顯共享中心;單獨使用的方式則顯示最小值。