SwiftUI: componente Shape

Qué es Shape

Shape es un protocolo de SwiftUI que define formas geométricas 2D que pueden ser usadas en las vistas. Los componentes que implementan este protocolo ocupan todo el tamaño disponible en pantalla, por lo que es común definir su frame con el modificador frame o de forma indirecta al estar incluido en otro componente.

Aquí podéis consultar la documentación oficial

El uso común de componentes que implementan el protocolo Shape suele ser uno de los siguientes:

  • Mostrar una forma geométrica en la vista.
  • Recortar una vista ya existente (como un Text o Image) para que, en vez de ser cuadrada (forma por defecto), tenga una forma geométrica determinada (como, por ejemplo, una imagen redondeada del avatar de un usuario).

SwiftUI tiene por defecto varias implementaciones del protocolo Shape, como pueden ser:

Capsule

Capsule()
    .fill(Color.blue)
    .frame(width: 200, height: 70)

Circle

Circle()
    .fill(Color.blue)
    .frame(width: 70, height: 70)

Ellipse

Ellipse()
    .fill(Color.blue)
    .frame(width: 200, height: 100)

Rectangle

Rectangle()
    .fill(Color.blue)
    .frame(width: 200, height: 70)

RoundedRectangle

RoundedRectangle(cornerRadius: 25)
    .fill(Color.blue)
    .frame(width: 200, height: 70)

Los ejemplos que vemos en las capturas anteriores los veremos con los modificadores que se explicarán a continuación.

Modificadores comunes para Shape

El protocolo Shape incluye nuevos modificadores propios adicionales a los de View. Siempre que queramos hacer uso de ellos primero deberán usarse los de Shape y luego los de View, ya que al usar los de View perdemos la referencia de Shape.

Los modificadores de View podemos consultarlos en el siguiente enlace.

fill

Permite rellenar el contenedor con un color o gradiente.

Capsule()
    .fill(Color.blue)
    .frame(width: 200, height: 70)

stroke

Permite definir el borde del contenedor con un color o gradiente.

Capsule()
    .stroke(LinearGradient(gradient: Gradient(colors: [Color.orange, Color.green]), startPoint: .leading, endPoint: .trailing), lineWidth: 5)
    .frame(width: 200, height: 70)

También permite aplicar un estilo al borde para conseguir diferentes efectos.

Capsule()
    .stroke(Color.red, style: StrokeStyle(lineWidth: 5, dash: [10]))
    .frame(width: 200, height: 70)

trim

Permite recortar el contenedor.

Circle()
    .trim(from: 0, to: 0.5)
    .fill(Color.blue)
    .frame(width: 200, height: 70)

Cómo modificar la forma de un View con clipShape

Todas las vistas de SwiftUI tienen a su disposición el modificador clipShape que permite indicar un Shape que indica los límites visibles de la vista, lo que permitirá conseguir un efecto por el que cualquier View tenga la forma del Shape indicado.

Text("Hello, World!")
    .font(.title)
    .padding()
    .background(Color.blue)
    .foregroundColor(.white)
    .clipShape(Capsule())

En este ejemplo se ha indicado Capsule, pero podríamos modificarlo por otro Shape como Ellipse.

Cómo modificar la forma de un View con clipShape y añadir un borde con stroke

Como en el apartado anterior, para dar forma a un View tenemos que usar el modificador clipShape para definir los límites de la vista. En cambio, para añadir un borde hay que usar el modificador overlay y añadir un nuevo Shape del mismo tipo que en clipShape, pero añadiéndole el modificador stroke con la configuración deseada.

Text("Hello, World!")
    .font(.title)
    .padding()
    .background(Color.blue)
    .foregroundColor(.white)
    .clipShape(Capsule())
    .overlay(
        Capsule()
            .stroke(LinearGradient(gradient: Gradient(colors: [Color.orange, Color.green]), startPoint: .leading, endPoint: .trailing), lineWidth: 5)
    )

Cómo animar el borde de un Shape

Modificando un poco el ejemplo anterior podemos conseguir que el borde que hemos añadido tenga una animación por la que se vaya rellenando poco a poco. Para conseguir este efecto usaremos el modificador trim sobre el Shape añadido en el overlay, usando como parámetros dos variables de estado que se irán modificando a partir de un Timer.

La vista se suscribirá a los cambios del Timer a través del modificador onReceive, permitiéndonos modificar los valores de las variables de estado a los que deseemos para conseguir el efecto.

struct ContentView: View {
    private var timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
    @State private var trimFrom: CGFloat = 0
    @State private var trimTo: CGFloat = 0
    
    var body: some View {
        Text("Hello, World!")
            .font(.title)
            .padding()
            .background(Color.blue)
            .foregroundColor(.white)
            .clipShape(Capsule())
            .overlay(
                Capsule()
                    .trim(from: trimFrom, to: trimTo)
                    .stroke(LinearGradient(gradient: Gradient(colors: [Color.yellow, Color.purple]), startPoint: .leading, endPoint: .trailing), lineWidth: 5)
            )
            .onReceive(timer) { _ in
                withAnimation {
                    let increment: CGFloat = 0.2
                    if trimFrom == 0 && trimTo != 1 {
                        trimTo += increment
                        if trimTo > 1 {
                            trimTo = 1
                        }
                    } else if trimTo == 1 && trimFrom != 1 {
                        trimFrom += increment
                        if trimFrom > 1 {
                            trimFrom = 1
                        }
                    } else if trimTo == 1 && trimFrom == 1 {
                        trimFrom = 0
                        trimTo = 0
                    }
                }
            }
    }
}

Es importante que la modificación de las variables de estado se encuentre dentro del bloque withAnimation para conseguir que el cambio de valores sea animado.

Ejemplo

Puedes encontrar este ejemplo en github.com bajo el apartado Shape.

Rafael Fernández,
iOS Tech Lider