Communitygithub.com

Esquinas concéntricas en SwiftUI

Esquinas concéntricas en SwiftUI (iOS 26+): ConcentricRectangle, containerShape, Edge.Corner.Style.concentric(minimum:) y una escala de esquinas reutilizable para que formas anidadas (una foto en una tarjeta, una tarjeta flotando en un mapa, una miniatura en esa tarjeta, una imagen hero cerca del borde de la pantalla) compartan sus centros de esquina en lugar de verse apretadas o ensanchadas. Úsalo al elegir radios de esquina, anidar vistas redondeadas, adoptar formas de vidrio líquido, corregir esquinas que se ven "mal" dentro de contenedores o construir un sistema de diseño con tokens de radio. Basado en WWDC25 "Conoce el nuevo sistema de diseño" y el SDK de iOS 26.

¿Qué es Esquinas concéntricas en SwiftUI?

Esta habilidad aborda el desafío visual de las esquinas en formas redondeadas anidadas en SwiftUI. Cuando una vista redondeada (como una tarjeta) contiene otra vista redondeada (como una foto), las esquinas pueden verse mal si los radios no se calculan correctamente: la esquina interior puede parecer más redondeada (ensanchada) o más cuadrada (apretada) que la exterior. La solución consiste en compartir los centros de las esquinas, donde el radio interior es igual al radio exterior menos el espacio entre ellas. La habilidad presenta tres tipos de formas proporcionadas por Apple: Fixed (radio constante), Capsule (radio igual a la mitad de la altura) y Concentric (el sistema calcula el radio como el del padre menos el padding). Se centra en la API de iOS 26 (y macOS 26), incluyendo `ContainerShape` para que un contenedor declare su forma y `ConcentricRectangle` para el contenido anidado, que acepta un estilo `.concentric(minimum:)` con un radio mínimo. Además, ofrece una escala de esquinas reutilizable para derivar contenedores a partir de los contenidos, eligiendo un radio interior y añadiendo padding para los contenedores. Esto asegura consistencia en todo un sistema de diseño, desde fotos y tarjetas pequeñas hasta mapas y tarjetas flotantes. La habilidad también cubre consideraciones importantes como la uniformidad de las esquinas (`isUniform`), el comportamiento sin un `containerShape` (usando las esquinas de la ventana/pantalla), la compatibilidad con versiones anteriores (iOS 18) y la verificación con renders fuera de pantalla.

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

Preguntar en tu IA favorita

Abre un nuevo chat con esta habilidad de agente ya precargada.

Documentación

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: true da a cada esquina el mismo radio (una tarjeta flotando cerca de un borde de un mapa); false hace 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.
  • ConcentricRectangle no es una RoundedRectangularShape: no puede ser un contenedor. Un contenedor que a su vez está anidado (una tarjeta en un mapa) declara su radio resultante como un RoundedRectangle (radio del mapa − inserción) para sus propios hijos.
  • matchedTransitionSource { $0.clipShape(…) } solo acepta RoundedRectangle — dale el radio habitual del contenido.
  • Antes de iOS 26: recurre a RoundedRectangle con el radio mínimo. containerShape en un RoundedRectangle todavía compila (la sobrecarga más antigua de InsettableShape).

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 un minimum sensato.
  • 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.

Individual skills in this repo

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

Skills relacionados