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