Communitygithub.com

swiftui-concentric-corners

SwiftUI(iOS 26+)における同心円状の角:ConcentricRectangle、containerShape、Edge.Corner.Style.concentric(minimum:)、および再利用可能な角のスケール。ネストされたシェイプ(カード内の写真、マップ上のカード、そのカード内のサムネイル、画面端近くのヒーロー画像)が、角の中心を共有することで、つまんだりフレアしたりするように見えないようにします。角の半径を選択するとき、丸いビューをネストするとき、Liquid Glassのシェイプを採用するとき、コンテナ内で「ずれている」ように見える角を修正するとき、またはデザインシステムの半径トークンを構築するときに使用します。WWDC25「新しいデザインシステムを知る」およびiOS 26 SDKに基づいています。

swiftui-concentric-corners とは?

Concentric corners(同心円状の角)は、SwiftUI アプリケーションにおいて、ネストされた角丸シェイプが視覚的に調和するように設計されたスキルです。このスキルの中核となる考え方は、内側の半径と外側の半径の差がパディングに等しい場合に、角の中心が一致するというものです。これにより、内側の角が外側よりも丸く見える「フレア」や、逆に内側の角が不自然に四角くなる「ピンチ」といった問題を解決します。このスキルは iOS 26 / macOS 26 以降で利用可能な `ConcentricRectangle`、`.concentric(minimum:)`、`containerShape(_:)` などの新しい API を活用します。開発者は、内側の要素(例:写真、サムネイル)に最小半径を設定し、外側のコンテナ(例:カード、シート)の半径を内側の半径 + パディングとして宣言することで、一貫性のある視覚的階層を構築できます。また、`isUniform` フラグを使用して、すべての角を均等にするか、コンテナの角に近い角のみを同心円状にするかを制御できます。このスキルは、`Corners` 列挙型で定義された再利用可能な補助関数も提供し、プロジェクト全体で一貫した半径トークンを管理できます。対応プラットフォームは iOS 26 および macOS 26 以上で、iOS 18 以前へのフォールバックもコード例で示されています。

対応~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/gtokman/skills/tree/HEAD/swiftui-concentric-corners

お気に入りのAIに質問する

このエージェントスキルを事前に読み込んだ状態で新しいチャットを開きます。

ドキュメント

同心円状の角

ネストされた丸いシェイプは、角の中心を共有していると正しく見えます。つまり、内側の半径は外側の半径からそれらの間のスペースを引いたものです。これを間違えると、角はフレア(内側が外側より丸い)またはピンチ(内側がかなり四角い)に見えます。Apple の3つのシェイプタイプ:

  • 固定 — 一定の半径(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:) は下限であり、目標ではありません:radius = max(container − inset, minimum)。コンテナがコンテンツのパディングに対してきつすぎる場合は、コンテナの半径を上げてください — 最小値を下げないでください。
  • isUniform: true はすべての角に同じ半径を与えます(マップの片端近くに浮かぶカード)。false にすると、コンテナの角に近い角のみが同心円状になります(Apple のシート)。
  • containerShape がない場合、同心円状のシェイプはウィンドウ/ディスプレイの角に対して解決されます。つまり、全画面ビューの上部コンテンツはデバイスのベゼルに追従できます。
  • ConcentricRectangle は RoundedRectangularShape ではありません。そのため、コンテナになることはできません。それ自体がネストされているコンテナ(マップ上のカード)は、自身の子のために 結果として得られる 半径を RoundedRectangle(map radius − inset)として宣言します。
  • matchedTransitionSource { $0.clipShape(…) } は RoundedRectangle のみを受け入れます — コンテンツの通常の半径を指定してください。
  • iOS 26 より前:最小半径の RoundedRectangle にフォールバックします。RoundedRectangle 上の containerShape は引き続きコンパイルされます(古い InsettableShape オーバーロード)。

再利用可能な角のスケール

コンテンツからコンテナを導出します。その逆ではありません。写真や小さなカードには1つの内側半径を選び、すべてのコンテナを 内側 + パディング にします。そうすると、パディングの内側にあるものはすべて正確に 内側 の半径になります。

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(_:) を宣言している。
  • ネストされたコンテンツは、適切な minimum を持つ ConcentricRectangle(またはヘルパー)を使用している。
  • コンテナの半径 ≥ コンテンツの半径 + パディング。小さい半径の中に大きい半径が決してない。
  • クリップ、背景、ガラス効果、ヒットテスト(contentShape)はすべて同じシェイプを使用している。
  • ボタン、バッジ、ピルにはカプセルを使用。携帯電話では画面端からの余分なマージン。
  • 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.

関連スキル