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: truedonne à chaque corner le même rayon (une carte flottant près d'un bord d'une carte) ;falserend 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. ConcentricRectanglen'est pas uneRoundedRectangularShape: 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 unRoundedRectangle(rayon de la carte − retrait) pour ses propres enfants.matchedTransitionSource { $0.clipShape(…) }n'accepte queRoundedRectangle— donnez-lui le rayon usuel du contenu.- Avant iOS 26 : revenez à
RoundedRectangleau rayon minimum.containerShapesur unRoundedRectanglecompile toujours (la surcharge plus ancienneInsettableShape).
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 unminimumraisonnable. - 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.