Communitygithub.com

SwiftUI 同心圓角

在 SwiftUI(iOS 26+)中實作同心圓角:使用 ConcentricRectangle、containerShape、Edge.Corner.Style.concentric(minimum:) 以及可重複使用的圓角比例,讓巢狀形狀(例如卡片中的照片、地圖上的卡片、卡片中的縮圖、靠近螢幕邊緣的主視覺)共享圓角中心,避免產生擠壓或外擴的效果。適用於選擇圓角半徑、巢狀圓角視圖、採用 Liquid Glass 形狀、修正容器內「看起來不協調」的圓角,或建立設計系統的半徑 token。基於 WWDC25「認識新的設計系統」與 iOS 26 SDK。

SwiftUI 同心圓角 是什麼?

此技能專注於在 SwiftUI(iOS 26+)中實作同心圓角,解決巢狀圓角視覺上不協調的問題。它引入了 ConcentricRectangle、containerShape 修飾器以及 Edge.Corner.Style.concentric(minimum:) 等 API,確保內層形狀的半徑自動等於外層容器半徑減去內邊距,從而呈現完美的同心圓角效果。最重要的是,它提供了一個可重複使用的圓角比例設計模式,建議開發者從「最內層」內容的半徑開始定義,然後透過為每個容器加上內邊距來推導外層容器的半徑,這樣任何內含一個內邊距間距的內容都會精確地繼承內層半徑。此技能支援在 iOS 26 及 macOS 26 以上的平台使用,並提供了 iOS 18 的向下相容方案。它詳細說明了 `ConcentricRectangle` 不是 `RoundedRectangularShape`,因此不能作為容器;若容器本身是巢狀的(例如地圖上的卡片),需要將計算後的結果半徑宣告為一個新的 `RoundedRectangle` 供其子視圖使用。此外,`matchedTransitionSource` 僅接受 `RoundedRectangle`,需傳入內容的常規半徑。

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

在你喜歡的 AI 中提問

開啟一個已預先載入此 Agent Skill 的新對話。

說明文件

同心圓角

巢狀圓角形狀只有在它們共享圓角中心時看起來才正確:內部半徑等於外部半徑減去兩者之間的間距。弄錯的話,圓角看起來會外擴(內側比外側更圓)或擠壓(內側比外側更方)。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, *)),並使用高對比填色:一個容器、一個內含一個內邊距的內容、同一個內容單獨使用的方式,以及一個較大容器內的浮動卡片。圓角應明顯共享中心;單獨使用的方式則顯示最小值。

Individual skills in this repo

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

相關技能