Communitygithub.com

swiftui-concentric-corners

Coins concentriques en SwiftUI (iOS 26+) : ConcentricRectangle, containerShape, Edge.Corner.Style.concentric(minimum:) et une échelle de coins réutilisable afin que les formes imbriquées — une photo dans une carte, une carte flottant sur une carte, une vignette dans cette carte, une image de héros près du bord de l'écran — partagent leurs centres de coins au lieu de sembler pincées ou évasées. À utiliser lors du choix de rayons de coins, de l'imbrication de vues arrondies, de l'adoption des formes Liquid Glass, de la correction de coins qui semblent « faux » à l'intérieur de conteneurs, ou de la construction des tokens de rayon d'un système de design. Basé sur la WWDC25 « Découvrez le nouveau système de design » et le SDK iOS 26.

Qu'est-ce que swiftui-concentric-corners ?

Cette compétence fournit une implémentation complète des coins concentriques en SwiftUI pour iOS 26+ et macOS 26+. Elle résout le problème courant où des formes arrondies imbriquées (comme une photo dans une carte, une carte sur une carte, ou une vignette) semblent pincées ou évasées lorsque leurs coins ne partagent pas le même centre. L'API introduit `ConcentricRectangle`, `containerShape` avec `.concentric(minimum:)`, et une échelle de coins réutilisable qui permet de dériver les rayons de conteneurs à partir des rayons intérieurs en ajoutant simplement le padding. Elle fonctionne sur toutes les plateformes Apple supportant le nouveau SDK et inclut un fallback pour iOS 18. La compétence détaille les trois types de formes (fixe, capsule, concentrique), illustre une vérification visuelle des coins, et propose une checklist pour assurer une application correcte dans les designs complexes.

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

Demander à votre IA préférée

Ouvre une nouvelle conversation avec cette compétence d'agent déjà préchargée.

Documentation

Coins concentriques

Les formes arrondies imbriquées paraissent correctes lorsqu'elles partagent leurs centres de coins : le rayon intérieur est le rayon extérieur moins l'espace entre elles. Si c'est mal fait, les coins semblent évasés (intérieur plus arrondi que l'extérieur) ou pincés (intérieur beaucoup plus carré). Les trois types de formes d'Apple :

  • Fixé — un rayon constant (RoundedRectangle(cornerRadius:)).
  • Capsule — le rayon est la moitié de la hauteur ; concentrique par nature (boutons, badges, pilules).
  • Concentrique — rayon = rayon du parent − padding, calculé par le système.

L'API (iOS 26, macOS 26)

// Le conteneur déclare sa forme. Doit être une RoundedRectangularShape :
// RoundedRectangle, UnevenRoundedRectangle, Rectangle, Capsule, Circle.
card.containerShape(RoundedRectangle(cornerRadius: 28, style: .continuous))

// Le contenu à l'intérieur est concentrique avec lui, jamais en dessous du minimum (son rayon lorsqu'il
// est seul ou loin d'un coin de conteneur).
ConcentricRectangle(corners: .concentric(minimum: .fixed(16)), isUniform: false)
  • .concentric(minimum:) est un plancher, pas une cible : rayon = max(conteneur − retrait, minimum). Si le conteneur est trop serré pour le padding du contenu, augmentez le rayon du conteneur — ne diminuez pas le minimum.
  • isUniform: true donne à chaque corner le même rayon (une carte flottant près d'un bord d'une carte) ; false rend seulement les coins près des coins du conteneur concentriques (les feuilles d'Apple).
  • Sans containerShape, les formes concentriques se résolvent par rapport aux coins de la fenêtre/de l'écran — ainsi le contenu supérieur d'une vue plein écran peut suivre le bord de l'appareil.
  • ConcentricRectangle n'est pas une RoundedRectangularShape : elle ne peut pas être un conteneur. Un conteneur qui est lui-même imbriqué (une carte sur une carte) déclare son rayon résultant comme un RoundedRectangle (rayon de la carte − retrait) pour ses propres enfants.
  • matchedTransitionSource { $0.clipShape(…) } n'accepte que RoundedRectangle — donnez-lui le rayon usuel du contenu.
  • Avant iOS 26 : revenez à RoundedRectangle au rayon minimum. containerShape sur un RoundedRectangle compile toujours (la surcharge plus ancienne InsettableShape).

Une échelle de coins réutilisable

Dérivez les conteneurs à partir des contenus, pas l'inverse : choisissez un rayon intérieur pour les photos et les petites cartes, et faites de chaque conteneur intérieur + padding. Ainsi, tout ce qui est à un padding de distance sort exactement à intérieur.

enum Corners {
    static let inner: CGFloat = 16          // photos, petites cartes
    static let map: CGFloat = 28            // cartes et grands médias
    static let mapInset: CGFloat = 12       // cartes sur une carte → 16

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

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

    /// Contenu imbriqué dans un conteneur : concentrique, jamais en dessous de `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))
        }
    }
}

Utilisation :

// Une carte : rayon à partir de son padding, déclaré comme le conteneur.
let shape = Corners.rect(max(requested, Corners.container(padding: padding)))
VStack { photo }.padding(padding)
    .background(shape.fill(.surface))
    .containerShape(shape)

// Une photo n'importe où : concentrique avec ce qui la contient, 16 toute seule.
let photo = Corners.nested(minimum: Corners.inner)
photo.fill(.placeholder).overlay { image }.clipShape(photo)

// Une carte avec une carte en verre flottante contenant une vignette.
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)
    }

Liste de vérification

  • Chaque conteneur qui contient du contenu arrondi déclare .containerShape(_:).
  • Le contenu imbriqué utilise ConcentricRectangle (ou un assistant) avec un minimum raisonnable.
  • Rayon du conteneur ≥ rayon du contenu + padding ; jamais un rayon plus grand à l'intérieur d'un plus petit.
  • Le clip, l'arrière-plan, le verre et le test de hit (contentShape) utilisent tous la même forme.
  • Capsules pour boutons, badges et pilules ; marge supplémentaire du bord de l'écran sur les téléphones.
  • Fallback iOS 18 en place.

Vérification

Rendu hors écran sur macOS 26+ (ImageRenderer, #available(iOS 26, macOS 26, *) dans une copie de l'assistant) avec des remplissages à fort contraste : un conteneur, un contenu à un padding à l'intérieur, le même contenu seul, et une carte flottante dans un conteneur plus grand. Les coins doivent visiblement partager les centres ; celui seul montre le minimum.

Individual skills in this repo

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

Skills associés