View: el protocolo base de SwiftUI

¿Qué es View?

View es el protocolo base que debe implementar cualquier componente que tenga una representación visual. Al ser un protocolo no podemos declararlo directamente. Puedes consultar aquí la documentación oficial.

SwiftUI basa todo su funcionamiento en este protocolo. Por lo que, como hemos indicado anteriormente, cualquier elemento visual deberá implementar este protocolo. Dicho protocolo tiene la siguiente definición:

public protocol View {

    /// The type of view representing the body of this view.
    ///
    /// When you create a custom view, Swift infers this type from your
    /// implementation of the required `body` property.
    associatedtype Body : View

    /// The content and behavior of the view.
    var body: Self.Body { get }
}

Esta definición nos impide utilizar el protocolo View como un tipo de retorno, ya que consta de un tipo asociado Body que debe ser conocido antes de poder usar dicho protocolo. Este tipo Body es de tipo View y, concretamente, es igual a la vista que se devuelve con el parámetro body.

Para entender esto hay que probar el siguiente código:

struct ContentView: View {
    var body: View {
        Text("Hello world!")
    }
}

Este código devolverá el siguiente error: Protocol 'View' can only be used as a generic constraint because it has Self or associated type requirements. Lo que indica este error es que no se conoce los tipos asociados que contiene el protocolo View, concretamente el tipo asociado Body. Una forma de resolver este problema sería especificando concretamente el tipo de retorno.

//Bad solution
struct ContentView: View {
    var body: Text {
        Text("Hello world!")
    }
}

Este sería un mal ejemplo de cómo resolver el problema. Si lo hacemos de esta forma tendremos muchos problemas cuando tengamos una vista compleja, ya que tendremos que indicar todos los tipos anidados uno dentro de otros.

//Bad solution
struct ContentView: View {
    var body: Group {
        Group {
            Text("Hello world!")
        }
    }
}

Esta solución es inviable ya que nos obligaría a mantener relación entre el tipo devuelto y su implementación y si cambiásemos la implementación tendríamos que cambiar el tipo devuelto. Si aún no estáis convencidos probad a declarar una vista más compleja e intentar indicar el tipo devuelto para el parámetro body.

Para resumir, no podemos usar el genérico View porque necesitamos definir el tipo asociado y la solución anteriormente propuesta tampoco es viable. ¿Qué podemos hacer? Para este caso se han desarrollado los tipos opacos.

Tipos opacos

Los tipos opacos permiten ocultar el tipo concreto de dato que se devuelve de una función para que tenga soporte a los protocolos. Esto quiere decir que a efectos de implementación podremos usar un protocolo como tipo devuelto, pero en tiempo de compilación el compilador conocerá el tipo concreto que se devuelve. Esto obliga a que todos los métodos de una función devuelvan siempre el mismo tipo en todos los return que esta contenga.

En el caso de SwiftUI esto es muy útil, ya que usar un tipo opaco en la variable body deja al compilador indicar el tipo de retorno real de la variable, definiendo el tipo asociado del protocolo View (como hemos hecho en las dos malas implementaciones anteriores), por lo que nos permitiría usar el protocolo View como tipo devuelto y de esta forma será el propio compilador quien infiera el tipo real que se está devolviendo. El uso de los tipos opacos se realiza declarando el tipo de retorno como some . Para este ejemplo el resultado sería el siguiente:

struct ContentView: View {
    var body: some View {
        Group {
            Text("Hello world!")
        }
    }
}

Solo es posible usar los tipos opacos para indicar un return de una variable o una función. No podremos usarlo en un closure.

Asignando estilos

Una vez que hemos visto la lógica del protocolo View tenemos que saber cómo personalizar su aspecto. El protocolo View cuenta con diferentes extensiones que permiten modificar el aspecto visual de la vista a la que se asigna el valor y a sus hijos. Esto último es importante, ya que muchos de los estilos que asignamos a una vista se propagan en cadena a todas las vistas que esta contenga.

Para asignar estilos tenemos que usar los modificadores (funciones) del propio componente declarado y los definidos en la documentación oficial sobre el protocolo View. El orden de asignación siempre debería ser:

  • Modificadores del propio componente.
  • Modificadores del protocolo View.

Estos modificadores siempre devuelven el mismo tipo de la vista o el tipo genérico View, por lo que es importante que para usar los modificadores propios de su tipo se haga antes de usar un modificador que devuelva View. Desde ese momento solo podremos usar los métodos de View porque habremos perdido la referencia del tipo original.

Es muy importante saber que el orden en el que asignamos los estilos afecta al resultado final. No es lo mismo asignar el frame y después del background que hacerlo al revés. El resultado cambia totalmente y nos proporciona una nueva forma de asignar estilos:

frame + background.

Text("Hello, World!")
    .frame(width: 300, height: 100, alignment: .center)
    .background(Color.red)

background + frame

Text("Hello, World!")
    .background(Color.red)
    .frame(width: 300, height: 100, alignment: .center)

Doble background sobre un Text

Text("Hello, World!")
    .padding()
    .background(Color.yellow)
    .frame(width: 300, height: 100, alignment: .center)
    .background(Color.red)
    .clipShape(Capsule())

También es muy útil la creación de estilos propios donde se aplican varios modificadores a la vez y nos permite aplicarlos fácilmente a las vistas a través de una única función. Para estos casos se declarará una extensión de la clase deseada y se implementarán los métodos necesarios.

extension View {
    public func textFieldLogin() -> some View {
        return self
            .textFieldStyle(RoundedBorderTextFieldStyle())
            .background(Color.white)
            .frame(height: 40)
            .cornerRadius(5)
    }
}

Modificadores comunes para View

A continuación explicaremos los modificadores más comunes que podemos usar en cualquier View, pero puedes consultar todos los modificadores en la documentación oficial.

accentColor

Permite modificar el color de resalto de la vista y de los componentes que esta contenga. Muchos componentes de SwiftUI utilizan este color como el color predeterminado.

Group {
    Button("Hello, World!") { }
    Slider(value: .constant(5), in: 0...10)
}
.accentColor(.purple)

background

Permite insertar un View detrás de la propia vista. Por lo general lo usaremos para ‘setear’ un color de fondo.

Text("Hello, World!")
    .background(Color.yellow)

Pero podemos insertar cualquier elemento que implemente el protocolo View, como una imagen u otra vista.

Text("Hello, World!")
    .background(
        Ellipse()
            .fill(Color.gray).padding(.all, -10)
    )
Text("Hello, World!")
    .padding(.all, 5)
    .background(
        Image("image_backgorund").resizable()
    )

blur

Permite aplicar el efecto blur a una vista.

Text("Hello, World!")
    .background(
        Ellipse()
            .fill(Color.gray).padding(.all, -10)
    ).blur(radius: 3)

border

Permite aplicar un borde a la vista. La definición de este modificador es la siguiente:

@inlinable public func border(_ content: S, width: CGFloat = 1) -> some View where S : ShapeStyle

El parámetro ShapeStyle puede ser cualquier componente que implemente dicho protocolo. Se pueden consultar quién implementa este protocolo en la documentación oficial.

Text("Hello, world!")
    .padding()
    .border(Color.black, width: 5)
Text("Hello, world!")
    .padding()
    .border(LinearGradient(gradient: Gradient(colors: [Color.red, Color.blue]), startPoint: .leading, endPoint: .trailing), width: 5)

clipShape

Permite indicar una forma geométrica con los límites pintar la vista. Esta propiedad permite indicar un tipo Shape que hará que toda la vista que esté fuera de sus límites no se pinte. Existen varias implementaciones del tipo Shape que podemos consultar en la documentación oficial.

Button("Hello, World") {
    //Do something
}
.padding()
.background(Color.black)
.foregroundColor(.white)
.clipShape(Capsule())

contentShape

Permite indicar la superficie que responderá a los eventos de pulsación. Por lo general la pulsación afecta a toda la vista, pero solamente si esa vista es visible (tiene algún color de fondo por ejemplo).

VStack {
    Image("sdos_office")
        .resizable()
        .scaledToFit()
        .frame(width: 100, height: 100, alignment: .center)
    Spacer()
        .frame(height: 50)
    Text("Hello, World!")
}
.contentShape(Rectangle())
.onTapGesture {
    print("Execute and watch log")
}
.padding()
.border(Color.black, width: 1)

En este caso nuestro Spacer es transparente, por lo que sin el modificador contentShape al pulsar en la superficie que ocupa ese Spacer (50 px entre la foto y el texto) no se mostraría el mensaje de log por consola (Para probar este código ejecutar en el simulador o un dispositivo).

cornerRadius

Permite indicar el radio de curvatura de la vista. Todo lo que esté fuera de los límites definidos por esta propiedad no serán visibles.

Button("Hello, World") {
    //Do something
}
.padding()
.background(Color.black)
.foregroundColor(.white)
.cornerRadius(10.0)

disabled

Desactiva el elemento para impedir que se pueda interaccionar con él.

Button("Hello, World") {
    //Do something
}
.disabled(true)

foreground

Permite indicar el color de los elementos que se muestran. Por lo general lo usaremos para modificar el color del texto.

Button("Hello, World") {
    //Do something
}
.foregroundColor(.red)

frame

Permite indicar el tamaño de la vista. Existen dos métodos frame con diferentes parámetros:

@inlinable public func frame(width: CGFloat? = nil, height: CGFloat? = nil, alignment: Alignment = .center) -> some View
@inlinable public func frame(minWidth: CGFloat? = nil, idealWidth: CGFloat? = nil, maxWidth: CGFloat? = nil, minHeight: CGFloat? = nil, idealHeight: CGFloat? = nil, maxHeight: CGFloat? = nil, alignment: Alignment = .center) -> some View

Este método permite ‘setear’ el ancho y alto con diferentes variantes y la alineación del contenido. Todos sus parámetros tienen valores por defecto, por lo que solo es necesario indicar aquellos que queramos sobrescribir.

Button("Hello, World") {
    //Do something
}
.frame(width: 250)
.padding()
.background(Color.black)
.foregroundColor(.white)
.cornerRadius(10.0)
VStack(spacing: 30) {
    Text("Hello, World!")
        .frame(height: 200)
        .background(Color.yellow)
    Text("Bye, World!")
        .frame(maxWidth: .infinity)
        .background(Color.green)
}
.frame(maxHeight: .infinity, alignment: .topTrailing)
.background(Color.red)

Para los parámetros maxHeight y maxWidth podemos usar el valor .infinity, que indicará que el contenido deberá usar toda la pantalla de la que disponga.

offset

Permite indicar un desplazamiento en píxeles a la vista.

Group {
    Button("Hello, World") {
        //Do something
    }
    .padding()
    .background(Color.black)
    .foregroundColor(.white)
    .cornerRadius(10.0)
    .offset(x: 10.0, y: 10.0)
}
.background(Color.red)

opacity

Permite indicar la opacidad de la vista.

Group {
    Button("Hello, World") {
        //Do something
    }
    .padding()
    .background(Color.black)
    .foregroundColor(.white)
    .cornerRadius(30.0)
    .opacity(0.7)
}
.background(Color.red)
.padding()

overlay

Permite añadir un nuevo View sobre la vista

Image(systemName: "folder")
    .font(.system(size: 55, weight: .thin))
    .overlay(Text("❤️")
                .offset(y: -5)
             , alignment: .bottom)

padding

Es muy común usarlo para añadir un borde redondeado para una vista. Hay varias formas de indicar el padding:

Text("Hello, World!")
    .padding()
    .background(Color.black)
    .foregroundColor(.white)
Text("Hello, World!")
    .padding(3)
    .background(Color.black)
    .foregroundColor(.white)
Text("Hello, World!")
    .padding(.all, 30)
    .background(Color.black)
    .foregroundColor(.white)
Text("Hello, World!")
    .padding([.leading, .trailing], 30)
    .background(Color.black)
    .foregroundColor(.white)

rotationEffect 

Permite indicar una rotación para la vista.

Text("Hello, World!")
    .padding()
    .background(Color.black)
    .foregroundColor(.white)
    .rotationEffect(.init(degrees: 90))

shadow

Permite indicar un efecto de sombra para la vista.

Text("Hello, World!")
    .padding()
    .shadow(color: .red, radius: 2, x: 2, y: 5)

Ciclo de vida

SwiftUI tiene un ciclo de vida mucho más simple al que nos proporcionaba UIKit. Solo existen dos métodos equivalentes a los proporcionados por UIKit.

onAppear

Closure que será invocado cuando la vista se muestre en pantalla.

Group {
    Text("Hello, World!")
        .onDisappear {
            print("Text Disappear")
        }
}.onDisappear {
    print("Group Disappear")
}

onDisappear

Closure que será invocado cuando la vista desaparezca de la pantalla.

Group {
    Text("Hello, World!")
        .onDisappear {
            print("Text Disappear")
        }
}.onDisappear {
    print("Group Disappear")
}

Para ver los mensajes de log debes ejecutar la aplicación en el simulador o en un dispositivo.

Gestos para View

onTapGesture

Añade un gesto que permite responder a la pulsación sobre la vista.  Si no se indica un parámetro, responde a una pulsación.

Text("Hello, World!")
    .onTapGesture {
        print("Tap gesture")
    }

También se puede indicar un número de pulsaciones para que se active.

Text("Hello, World!")
    .onTapGesture(count: 3) {
        print("Tap gesture")
    }

onLongPressGesture

Añade un gesto que permite responder a una pulsación larga sobre la vista.

Text("Hello, World!")
    .onLongPressGesture {
        print("Long gesture")
    }

También da la posibilidad de indicar el tiempo hasta que se active.

Text("Hello, World!")
    .onLongPressGesture(minimumDuration: 2) {
        print("Long gesture")
    }

Ejemplo

Puedes encontrar este ejemplo en https://github.com/SDOSLabs/SwiftUI-Test bajo el apartado View.

¡Esperamos que te haya resultado muy útil nuestro nuevo post sobre SwiftUI y que lo pongas en práctica muy pronto!

Rafael Fernández,
iOS Tech Lider