Konzentrische Ecken
Geschachtelte abgerundete Formen sehen richtig aus, wenn sie ihre Eckenmittelpunkte teilen: Der Innenradius ist der Außenradius minus dem Abstand zwischen ihnen. Wird dies falsch gemacht, sehen die Ecken aufgeweitet (innen runder als außen) oder eingeklemmt (innen deutlich eckiger) aus. Die drei Formtypen von Apple:
- Fest – ein konstanter Radius (
RoundedRectangle(cornerRadius:)). - Kapsel – der Radius ist die Hälfte der Höhe; von Natur aus konzentrisch (Buttons, Badges, Pills).
- Konzentrisch – Radius = Elternradius − Padding, vom System berechnet.
Die API (iOS 26, macOS 26)
// Der Container deklariert seine Form. Muss ein RoundedRectangularShape sein:
// RoundedRectangle, UnevenRoundedRectangle, Rectangle, Capsule, Circle.
card.containerShape(RoundedRectangle(cornerRadius: 28, style: .continuous))
// Der Inhalt darin ist konzentrisch dazu, nie unter dem Minimum (sein Radius, wenn
// er eigenständig oder weit von einer Containerecke entfernt ist).
ConcentricRectangle(corners: .concentric(minimum: .fixed(16)), isUniform: false)
.concentric(minimum:)ist eine Untergrenze, kein Ziel: Radius = max(Container − Einzug, Minimum). Wenn der Container zu eng für das Padding des Inhalts ist, erhöhe den Radius des Containers – senke nicht das Minimum.isUniform: truegibt jeder Ecke denselben Radius (eine Karte, die nahe einer Kante einer Karte schwebt);falsemacht nur die Ecken konzentrisch, die nahe den Containerecken liegen (Apples Sheets).- Ohne
containerShapewerden konzentrische Formen gegen die Fenster-/Displayecken aufgelöst – so kann der obere Inhalt einer Vollbildansicht der Geräteabrundung folgen. ConcentricRectangleist keinRoundedRectangularShape: Es kann kein Container sein. Ein Container, der selbst geschachtelt ist (eine Karte auf einer Karte), deklariert seinen resultierenden Radius alsRoundedRectangle(Kartenradius − Einzug) für seine eigenen Kinder.matchedTransitionSource { $0.clipShape(…) }akzeptiert nurRoundedRectangle– gib ihm den üblichen Radius des Inhalts.- Vor iOS 26: Fallback auf
RoundedRectanglemit dem Mindestradius.containerShapeauf einemRoundedRectanglekompiliert trotzdem (die ältereInsettableShape-Überladung).
Eine wiederverwendbare Ecken-Skala
Leite Container aus Inhalten ab, nicht umgekehrt: Wähle einen inneren Radius für Fotos und kleine Karten, und mache jeden Container zu inner + padding. Dann kommt alles, was ein Padding entfernt ist, exakt bei inner an.
enum Corners {
static let inner: CGFloat = 16 // Fotos, kleine Karten
static let map: CGFloat = 28 // Karten und große Medien
static let mapInset: CGFloat = 12 // Karten auf einer Karte → 16
static func container(padding: CGFloat) -> CGFloat { inner + padding } // 12 pt → 28
static func rect(_ radius: CGFloat) -> RoundedRectangle {
RoundedRectangle(cornerRadius: radius, style: .continuous)
}
/// Inhalt, der in einem Container geschachtelt ist: konzentrisch, nie unter `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))
}
}
}
Verwendung:
// Eine Karte: Radius aus ihrem Padding, als Container deklariert.
let shape = Corners.rect(max(requested, Corners.container(padding: padding)))
VStack { photo }.padding(padding)
.background(shape.fill(.surface))
.containerShape(shape)
// Ein Foto überall: konzentrisch mit dem, was es hält, eigenständig 16.
let photo = Corners.nested(minimum: Corners.inner)
photo.fill(.placeholder).overlay { image }.clipShape(photo)
// Eine Karte mit einer schwebenden Glaskarte, die ein Thumbnail enthält.
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)
}
Checkliste
- Jeder Container, der abgerundeten Inhalt enthält, deklariert
.containerShape(_:). - Geschachtelter Inhalt verwendet
ConcentricRectangle(oder eine Hilfsfunktion) mit einem sinnvollenminimum. - Containerradius ≥ Inhaltsradius + Padding; niemals ein größerer Radius innerhalb eines kleineren.
- Clip, Hintergrund, Glas und Hit-Testing (
contentShape) verwenden dieselbe Form. - Kapseln für Buttons, Badges und Pills; zusätzlicher Rand vom Bildschirmrand auf Telefonen.
- Fallback für iOS 18 vorhanden.
Überprüfung
Rendere offscreen auf macOS 26+ (ImageRenderer, #available(iOS 26, macOS 26, *) in einer Kopie der Hilfsfunktion) mit kontrastreichen Füllungen: ein Container, Inhalt ein Padding entfernt, derselbe Inhalt eigenständig und eine schwebende Karte in einem größeren Container. Die Ecken sollten sichtbar die Mittelpunkte teilen; die eigenständige zeigt das Minimum.