Cómo posicionar vistas en espiral usando el protocolo Layout

SwiftUI ofrece diversas herramientas para organizar vistas. Para estructuras verticales tenemos VStack, para listas usamos List, y para superponer vistas están ZStack y HStack, entre otras. No obstante, estas vistas pueden limitar la flexibilidad en el posicionamiento. Para una mayor personalización, podemos crear nuestras propias vistas implementando el protocolo Layout.

Cómo usar el protocolo Layout

El protocolo Layout nos permite definir vistas personalizadas con un control preciso sobre la disposición de sus subcomponentes. A continuación, aprenderemos a crear una vista que dispone círculos en forma de espiral.

Creando SpiralLayout

Iniciamos con una estructura SpiralLayout que implementa Layout. Esta estructura requiere dos métodos: sizeThatFits(proposal:subviews:cache:) y placeSubviews(in:proposal:subviews:cache:):

struct SpiralLayout: Layout {
func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize {
}
func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) {
}
}

Primero implementamos sizeThatFits(proposal:subviews:cache:), este calcula y retorna el tamaño del contenedor aceptando el espacio propuesto o usando un valor por defecto (CGSize(width: 10, height: 10)).

func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize {
proposal.replacingUnspecifiedDimensions()
}

Luego, implementamos placeSubviews(in:proposal:subviews:cache:):

func placeSubviews(
in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews,
cache: inout Void
) {
// 1
let radius = min(bounds.size.width, bounds.size.height) / 2
// 2
for (index, subview) in subviews.enumerated() {
// 3
let progress = Double(index) / Double(subviews.count)
// 4
let currentRadius = radius * (1 - progress)
let angle = progress * 4 * .pi
// 5
let xPos = cos(angle) * currentRadius
let yPos = sin(angle) * currentRadius
// 6
let point = CGPoint(x: bounds.midX + xPos, y: bounds.midY + yPos)
subview.place(at: point, anchor: .center, proposal: .unspecified)
}
}

En este código:

  1. Calcula el radio máximo del espiral tomando la mitad de la dimensión más pequeña de la pantalla.
  2. Recorre cada subvista dentro de subviews.
  3. Para cada vista calculamos el progreso basado en la posición de la vista en el arreglo y el total de suvistas.
  4. Calcula el ángulo de la subvista actual. progress define la fracción de una rotación completa, y al multiplicarlo por 4 * .pi se obtienen hasta 2 rotaciones completas (720°).
  5. Convierte ángulo y radio en coordenadas (x,y).
  6. Posiciona la vista en la posición calculada con respecto al centro.

El código completo para SpiralLayout:

import SwiftUI
struct SpiralLayout: Layout {
func sizeThatFits(
proposal: ProposedViewSize, subviews: Subviews, cache: inout Void
) -> CGSize {
proposal.replacingUnspecifiedDimensions()
}
func placeSubviews(
in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews,
cache: inout Void
) {
let radius = min(bounds.size.width, bounds.size.height) / 2
for (index, subview) in subviews.enumerated() {
let progress = Double(index) / Double(subviews.count)
// Decrease radius as we go inward
let currentRadius = radius * (1 - progress)
let angle = progress * 4 * .pi // 2 full rotations
let xPos = cos(angle) * currentRadius
let yPos = sin(angle) * currentRadius
let point = CGPoint(x: bounds.midX + xPos, y: bounds.midY + yPos)
subview.place(at: point, anchor: .center, proposal: .unspecified)
}
}
}

Utilizando SpiralLayout

import SwiftUI
struct ContentView: View {
// 1
@State private var count = 25
var body: some View {
VStack(spacing: 50) {
// 2
SpiralLayout {
// 3
ForEach(0..<count, id: \.self) { _ in
// 4
Circle()
.frame(width: 35)
.padding(5)
.foregroundStyle(.green)
}
}
.frame(width: 200, height: 200)
// 5
Button("Add a circle") {
withAnimation {
count += 1
}
}
}
}
}

En este código:

  1. Creamos una variable de estado count que contiene la cantidad de subvistas (círculos) a mostrar.
  2. Utilizamos SpiralLayout, la vista que creamos al principio de este tutorial.
  3. Usamos ForEach para iterar desde 0 hasta el valor almacenado en count.
  4. Declaramos una vista Circle para cada iteración del ForEach.
  5. El botón incrementa count y, al usar withAnimation, las vistas aparecen de manera animada.
Vista posicionada en espiral

Si presionamos el Add a circle podemos ver como más círculos son agregados a la espiral:

Vista posicionada en espiral animada

Comparte este artículo

Subscríbete a nuestro Newsletter

Mantente al día en el mundo de las aplicaciones móviles con nuestro blog especializado.

Artículos semanales

Todas las semanas artículos nuevos sobre el mundo de las aplicaciones móviles.

No spam

No te enviaremos spam, solo contenido de calidad. Puedes darte de baja cuando quieras.

Contenido de calidad

Nada de contenido generado de manera automática usando ChatGPT.

Recomendaciones

Tips indispensables sobre mejores prácticas y metodologías.

© 2025 AsyncLearn