Creando UI al estilo Tinder en SwiftUI
by Jorge Acosta, Senior Developer
Mi objetivo es mostrar cómo crear interfaces complejas, es decir, que tengan micro-interacciones, interacciones con gestos, animaciones, etc.
En esta instancia mostrare como montar la interface central de Tinder, donde vas Swipeando los rostros de los pretendientes dando like o unlike
Asumiré que tienes nociones sobre Swift y que ya sabes que es SwiftUI, por tanto no explicare detalles del lenguaje y no profundizare sobre SwiftUI, por lo demás ya existen un montón de posts ahi afuera que desarrollan las nociones básica de este maravilloso Framework desarrollado por apple.
Lo primero que haremos sera revisar nuestro prototipo y extraeremos sus colores y assets principales, los agregaremos a nuestro Assets.xcassets con sus correspondientes nombres.

Construyendo el Header
SwiftUI nos presenta una forma sencilla de trabajar con gradientes: LinearGradient, RadialGradient y AngularGradient. En esta ocasión trabajaremos con Linear gradient, que es una struct que nos pide un Gradient y dos UnitPoint, inicial y final.
Gradient nos pide un array de colores, que en nuestro caso sera el del gradiente izquierdo y derecho descritos anteriormente
Gradient(colors: [Color("left_gradient_color"), Color("rigth_gradient_color")])
Un UnitPoint es un punto, imagina un plano cartesiano con las coordenadas X y Y. Puedes inicializar asi:
UnitPoint(x: 0.0, y: 0.0)
Pero tambien puedes acceder a las constantes
UnitPoint.bottom
UnitPoint.bottomLeading
UnitPoint.bottomTrailing
UnitPoint.center
UnitPoint.leading
UnitPoint.top
UnitPoint.topLeading
UnitPoint.topTrailing
UnitPoint.trailing
UnitPoint.zero
Si quieres un Gradiente que vaya de izquierda a derecha utilizarías el UnitPoint.leading y el UnitPoint.trailing, generalmente estas constantes te servirán para la mayoría de los casos en los que quieras utilizar gradientes, sin embargo siempre podrás inicializar un UnitPoint con un punto arbitrario en el plano, para lograr algo exactamente como lo que estas buscando.

import SwiftUI
struct GradientHeader: View {
var body: some View {
VStack{
Text("Discover")
.font(.system(size: 30))
.bold()
.foregroundColor(.white)
.offset(x:25, y: 25)
}
.frame(minWidth:0, maxWidth: .infinity, alignment: .leading)
.frame(height: 180)
.background(LinearGradient(gradient: Gradient(colors: [Color("left_gradient_color"),Color("rigth_gradient_color")]), startPoint: .leading, endPoint: .trailing))
}
}
El resultado del código anterior nos dará como resultado lo siguiente

Botones
Los botones en SwiftUI son muy simples de declarar
Button("Click Me"){
//tap action
}
.frame(width:60, height:60)
Otra forma de declararlo
Button(action:{
//tap action
}){
Text("Click Me")
}
.frame(width:60, height:60)
La forma anterior nos permite agregar mas cosas, como Images, Stacks, etc
En esta imagen mostramos la modificación de un botón con cada linea de código que agrega un nuevo modificador a la vista.

Button("Click Me"){
//tap action
}
.frame(width:60, height:60)
.background(Color.white)
.mask(Circle())
.shadow(color: Color("Shadow"), radius: 4, x: 0, y: 5)
Una forma más avanzada de ordenar nuestros botones, es generar unos estilos derivados de ButtonStyle, así podremos inyectar estilos de manera fácil a nuestros botones solo referenciando sus respectivos nombres de estilos.
//button+styles.swift
import Foundation
import SwiftUI
public struct Rounded : ButtonStyle {
public func body(configuration: Button<Self.Label>, isPressed: Bool) -> some View {
configuration
.background(Color.white)
.mask(Circle())
.shadow(color: Color("Shadow"), radius: 4, x: 0, y: 5)
}
}
public struct Big : ButtonStyle {
public func body(configuration: Button<Self.Label>, isPressed: Bool) -> some View {
configuration
.frame(width: 60, height: 60)
}
}
extension StaticMember where Base : ButtonStyle {
public static var rounded: Rounded.Member {
StaticMember<Rounded>(Rounded())
}
public static var big: Big.Member {
StaticMember<Big>(Big())
}
}
Aislando nuestros estilos como se muestran anteriormente, nos queda una composición de nuestro botón de manera mas limpia, con la aventaja añadida que podemos combinar estilos, aquí podemos ver como combinamos el estilo de un botón grande con el de un botón redondeado.
//Crea un nuevo Documento SwiftUI
import SwiftUI
struct ContentView: View {
var body: some View {
Button("Click Me!"){
//Tap Code!!
}
.buttonStyle(.rounded)
.buttonStyle(.big)
}
}
Una vez que hemos codeado nuestro botón, crearemos una Vista que contenga todos nuestros botones, intercalando los botones pequeños y grandes como muestra nuestro prototipo, los botones de los costados tienen un pequeño desfase hacia arriba.
//Buttons.swift
import SwiftUI
struct Buttons: View {
var body: some View {
HStack(alignment: .center){
Button(action:{}){
Image("redo")
.resizable()
.frame(width: 22, height: 22)
.foregroundColor(Color("redo"))
}
.buttonStyle(.rounded)
.buttonStyle(.small)
.offset(y:-14.0)
// Agregar los otros 4 botones, intercalando .small y .big
// solo los botones de los costados tienen offset y : -14
}
.padding(.bottom, 35)
.frame(minWidth:0, maxWidth:.infinity)
}
}
El resultado de generar los botones de esta manera

Construcción de card draggables

Offset
El offset es considerado el desplazamiento desde nuestra posición original, ya sea que nuestra vista este dentro de un VStack, HStack o ZStack; el offset desplazara nuestra vista en el eje X o en el eje Y, considerando su posición original, es decir que si a nuestra vista le seteamos un offset de (0,0) esta simplemente permanecera en su lugar, consideramos un offset X positivo un desplazamiento a la derecha de nuestra pantalla y uno negativo un desplazamiento a la izquierda de nuestra pantalla. Por otro lado el offset Y positivo se desplazara hacia abajo de nuestra pantalla y uno negativo hacia arriba de nuestra pantalla.

el evento dragging nos devolverá un vector (x, y) que indica la dirección que tomo nuestro dedo al arrastrar una vista. Utilizaremos este desplazamiento para sumarlo al offset de nuestra vista, si hacemos esto sumaremos esa interacción del drag, entonces si hacemos drag de nuestra vista y la desplazamos unos 100 pixeles a la derecha, el offset nuevo de nuestra vista sera de (100,0) desde su posición original.

el modificador gesture espera un objeto tipo Gesture, nosotros le pasaremos un DragGesture con 2 eventos: onChanged, qué nos indica que nuestro gesto esta cambiando, y onEnded que nuestro evento termino, es decir, que se dejo de hacer dragging.
el evento onChanged se ejecuta cada cierta cantidad de Frames en nuestra app, entonces lo que haremos es ir sumando esa diferencia de nuestro desplazamiento a un offset general (como vemos en el siguiente ejemplo).
Tenemos que considerar otro factor, que es el parámetro startLocation, que es él desde el borde superior izquierdo al punto que esta haciendo dragging (como muestra la figura anterior).
Lo que haremos es restart el startLocation (x,y) a location, esto hará que nuestra vista se fije de forma consistente al punto al que estamos haciendo dragging, si solo consideráramos location para esto, la vista comenzaría a moverse anclada desde la esquina superior izquierdo.
import SwiftUI
struct Card: View {
@State var offset = CGPoint(x: 0.0, y: 0.0)
var body: some View {
VStack{
Image("paloma_mami")
}
.background(Color.white)
.cornerRadius(10)
.shadow(color: Color("Shadow"), radius: 4, x: 0, y: 5)
.gesture(
DragGesture()
.onChanged{ value in
self.offset.x += value.location.x - value.startLocation.x
self.offset.y += value.location.y - value.startLocation.y
}
.onEnded{ _ in
withAnimation {
self.offset = CGPoint(x: 0.0, y: 0.0)
}
}
)
.offset(x: self.offset.x, y: self.offset.y)
}
}
Una forma muy elegante de ordenar y encapsular nuestro código, son los ViewModifier
//Draggable.swift
import Foundation
import SwiftUI
struct DraggableView : ViewModifier {
@State var offset = CGPoint(x: 0.0, y: 0.0)
func body(content: Content) -> some View {
content.gesture(
DragGesture()
.onChanged{ value in
self.offset.x += value.location.x - value.startLocation.x
self.offset.y += value.location.y - value.startLocation.y
}
.onEnded{ _ in
withAnimation {
self.offset = CGPoint(x: 0.0, y: 0.0)
}
}
)
.offset(x: self.offset.x, y: self.offset.y)
}
}
extension View {
func draggable() -> some View {
modifier(DraggableView())
}
}
Conclusión
Desarrollar vistas con SwiftUI es realmente sencillo, los desarrolladores Swift agradecerán la forma coherente que tiene SwiftUI de utilizar la potencia del lenguaje con sus nuevas características. Una cosa interesante de observar es lo parecido con react native, a los desarrolladores que vengan de este mundo, les resultara muy familiar la forma en que se construyen las interfaces; Quizás sea una excelente oportunidad para explorar el mundo del desarrollo nativo.
Se agradece cualquier critica que puedan dejar, ademas de compartir el post con desarrolladores interesados en el desarrollo Mobile.
Anexare el proyecto de Github con los contenidos del tutorial:
Tag con el avance de este post