Esquinas concéntricas
Las formas redondeadas anidadas se ven correctas cuando comparten sus centros de esquina: el radio interior es el radio exterior menos el espacio entre ellas. Si se hace mal, las esquinas se ven ensanchadas (interior más redondeado que exterior) o apretadas (interior mucho más cuadrado). Los tres tipos de forma de Apple:
- Fija — un radio constante (
RoundedRectangle(cornerRadius:)). - Cápsula — el radio es la mitad de la altura; concéntrica por naturaleza (botones, insignias, píldoras).
- Concéntrica — radio = radio del padre − padding, calculado por el sistema.
La API (iOS 26, macOS 26)
// El contenedor declara su forma. Debe ser una RoundedRectangularShape:
// RoundedRectangle, UnevenRoundedRectangle, Rectangle, Capsule, Circle.
card.containerShape(RoundedRectangle(cornerRadius: 28, style: .continuous))
// El contenido interior es concéntrico con ella, nunca por debajo del mínimo (su radio cuando
// está solo o lejos de una esquina de contenedor).
ConcentricRectangle(corners: .concentric(minimum: .fixed(16)), isUniform: false)
.concentric(minimum:)es un suelo, no un objetivo: radio = max(contenedor − inserción, mínimo). Si el contenedor es demasiado ajustado para el padding del contenido, aumenta el radio del contenedor, no bajes el mínimo.isUniform: trueda a cada esquina el mismo radio (una tarjeta flotando cerca de un borde de un mapa);falsehace que solo las esquinas cercanas a las esquinas del contenedor sean concéntricas (las hojas de Apple).- Sin
containerShape, las formas concéntricas se resuelven contra las esquinas de la ventana/pantalla — así que el contenido superior de una vista a pantalla completa puede seguir el bisel del dispositivo. ConcentricRectangleno es unaRoundedRectangularShape: no puede ser un contenedor. Un contenedor que a su vez está anidado (una tarjeta en un mapa) declara su radio resultante como unRoundedRectangle(radio del mapa − inserción) para sus propios hijos.matchedTransitionSource { $0.clipShape(…) }solo aceptaRoundedRectangle— dale el radio habitual del contenido.- Antes de iOS 26: recurre a
RoundedRectanglecon el radio mínimo.containerShapeen unRoundedRectangletodavía compila (la sobrecarga más antigua deInsettableShape).
Una escala de esquinas reutilizable
Deriva los contenedores a partir de los contenidos, no al revés: elige un radio interior para
fotos y tarjetas pequeñas, y haz que cada contenedor sea interior + padding. Entonces cualquier cosa que esté a un padding de distancia resultará exactamente interior.
enum Corners {
static let inner: CGFloat = 16 // fotos, tarjetas pequeñas
static let map: CGFloat = 28 // mapas y medios grandes
static let mapInset: CGFloat = 12 // tarjetas en un mapa → 16
static func container(padding: CGFloat) -> CGFloat { inner + padding } // 12 pt → 28
static func rect(_ radius: CGFloat) -> RoundedRectangle {
RoundedRectangle(cornerRadius: radius, style: .continuous)
}
/// Contenido anidado en un contenedor: concéntrico, nunca por debajo 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))
}
}
}
Uso:
// Una tarjeta: radio desde su padding, declarado como el contenedor.
let shape = Corners.rect(max(requested, Corners.container(padding: padding)))
VStack { photo }.padding(padding)
.background(shape.fill(.surface))
.containerShape(shape)
// Una foto en cualquier lugar: concéntrica con lo que la contenga, 16 por sí sola.
let photo = Corners.nested(minimum: Corners.inner)
photo.fill(.placeholder).overlay { image }.clipShape(photo)
// Un mapa con una tarjeta de vidrio flotante que contiene una miniatura.
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)
}
Lista de verificación
- Cada contenedor que tenga contenido redondeado declara
.containerShape(_:). - El contenido anidado usa
ConcentricRectangle(o un ayudante) con unminimumsensato. - Radio del contenedor ≥ radio del contenido + padding; nunca un radio mayor dentro de uno menor.
- Clip, fondo, vidrio y prueba de impacto (
contentShape) usan la misma forma. - Cápsulas para botones, insignias y píldoras; margen extra del borde de la pantalla en teléfonos.
- Retroceso a iOS 18 implementado.
Verificación
Renderiza fuera de pantalla en macOS 26+ (ImageRenderer, #available(iOS 26, macOS 26, *) en una copia
del ayudante) con rellenos de alto contraste: un contenedor, contenido a un padding de distancia, el mismo contenido
solo, y una tarjeta flotante en un contenedor más grande. Las esquinas deben compartir visiblemente los centros;
la que está sola muestra el mínimo.