SwiftUI: componente GeometryReader
Qué es GeometryReader
El componente GeometryReader
es un contenedor de vistas que permite acceder a su tamaño y posición. De esta forma podemos alinear correctamente elementos, igualar tamaños de vistas, proporcionar animaciones basadas en la posición y cualquier cosa que requiera trabajar con los tamaños y posiciones en pantalla.
Aquí podéis consultar la documentación oficial
Para crear el componente solo necesitaremos embeber en su ViewBuilder
las vistas sobre las que queramos controlar los tamaños:
GeometryReader{ geometry in Text("Hello, World!") .frame(width: geometry.size.width) .background(Color.yellow.opacity(0.3)) }
El ViewBuilder
de inicialización tiene un parámetro GeometryProxy
que contiene la información sobre la posición y tamaño del área que ocupa el GeometryReader
. Este tamaño puede ser relativo al contenedor en el que se encuentra o a toda la pantalla donde se presenta. Se puede saber estos tamaños accediendo a la propiedad geometry.frame
y usando el valor .local
o .global
dependiendo del caso.
struct ContentView: View { var body: some View { GeometryReader{ geometry in VStack (spacing: 10) { Text("Coordinates") Divider() VStack(spacing: 5) { Text("Global minX: (geometry.frame(in: .global).minX)") Text("Global maxX: (geometry.frame(in: .global).maxX)") } .background(Color.yellow.opacity(0.5)) Divider() .background(Rectangle().fill(Color.yellow.opacity(0.3))) .background(Color.yellow.opacity(0.3)) VStack(spacing: 5) { Text("Local minX: (geometry.frame(in: .local).minX)") Text("Local maxX: (geometry.frame(in: .local).maxX)") } .background(Color.orange.opacity(0.5)) } } .frame(width: 200, height: 200) .background(Color.blue.opacity(0.2)) } }
Modificadores comunes para GeometryReader
El componente GeometryReader
comparte los mismos métodos de personalización que el componente View
y pueden ser consultados en el siguiente enlace.
Cómo ocupar todo el tamaño de pantalla disponible con GeometryReader
Podemos combinar GeomteryReader
con otros componentes de SwiftUI
para conseguir nuevos efectos que de otra forma serían imposible. Por ejemplo, podemos posicionar elementos en un HStack
en el que queramos que queramos que todos los elementos tengan el mismo ancho para que ocupen toda la pantalla disponible.
Con GeometryReader
tendremos la información del ancho disponible en pantalla y solamente tendremos que dividirla entre los elementos a mostrar.
GeometryReader { geometry in HStack(spacing: 0) { Text("Left") .frame(width: geometry.size.width / 3, height: 60) .background(Color.blue.opacity(0.3)) Text("Middle") .frame(width: geometry.size.width / 3, height: 60) .background(Color.orange.opacity(0.3)) Text("Right") .frame(width: geometry.size.width / 3, height: 60) .background(Color.red.opacity(0.3)) } }
Cómo alinear vistas con GeometryReader y PreferenceKey
La maquetación de una aplicación es algo muy importante y hacer que todos sus componentes estén completamente alineados es esencial para conseguir un resultado profesional.
Por ejemplo, vamos a pintar un formulario con varias opciones para que el usuario pueda introducir sus datos. Para ello vamos a usar el siguiente código:
struct ContentView: View { var body: some View { Form { HStack { Text("Name") TextField("", text: .constant("")) .textFieldStyle(RoundedBorderTextFieldStyle()) } HStack { Text("Surname") TextField("", text: .constant("")) .textFieldStyle(RoundedBorderTextFieldStyle()) } HStack { Text("Address") TextField("", text: .constant("")) .textFieldStyle(RoundedBorderTextFieldStyle()) } HStack { Text("DNI") TextField("", text: .constant("")) .textFieldStyle(RoundedBorderTextFieldStyle()) } } } }
Visualmente se verá de la siguiente forma:
Como se ve, el resultado no es estético y no tiene forma de formulario en condiciones. Idealmente deberíamos alinear los TextField
para que todos empiecen en la misma posición, dando una sensación de uniformidad mucho más agradable a la vista. Para conseguir este efecto vamos a combinar GeometryReader
y PreferenceKey
:
- Con
GeometryReader
vamos a recoger el valor del ancho de cadaText
de forma que nos quedaremos con el de mayor tamaño. - Con
PreferenceKey
vamos a notificar cada vez que cambie el tamaño de unText
. A través de esta notificación vamos a actualizar el frame de losText
haciendo que todos tenga el mismo ancho, consiguiendo el efecto de alineación que buscamos.
Para conseguir esto tendremos que crear las siguientes estructuras:
struct WidthPreferenceKey: PreferenceKey { typealias Value = CGFloat static var defaultValue: CGFloat = 0 static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) { } }
WidthPreferenceKey
es el PreferenceKey
que almacenará y notificará del valor CGFloat
correspondiente al ancho de cada Text
struct WidthBackground: View { var body: some View { GeometryReader { geometry in Color.clear .preference(key: WidthPreferenceKey.self, value: geometry.size.width) } .scaledToFill() } }
WidthBackground
se usará para conocer el ancho exacto de cada Text
y se lo tendrá que enviar a WidthPreferenceKey
a través de la siguiente línea:
.preference(key: WidthPreferenceKey.self, value: geometry.size.width)
De esta forma ya estaría todo listo para actualizar el código del formulario anterior para que reciba los parámetros que notifica WidthPreferenceKey
y fije el ancho de cada Text
.
struct ContentView: View { @State var maxLabelWidth: CGFloat = 0 var body: some View { Form { HStack { Text("Name") .frame(width: maxLabelWidth, alignment: .leading) .background(WidthBackground()) TextField("", text: .constant("")) .textFieldStyle(RoundedBorderTextFieldStyle()) } HStack { Text("Surname") .frame(width: maxLabelWidth, alignment: .leading) .background(WidthBackground()) TextField("", text: .constant("")) .textFieldStyle(RoundedBorderTextFieldStyle()) } HStack { Text("Address") .frame(width: maxLabelWidth, alignment: .leading) .background(WidthBackground()) TextField("", text: .constant("")) .textFieldStyle(RoundedBorderTextFieldStyle()) } HStack { Text("DNI") .frame(width: maxLabelWidth, alignment: .leading) .background(WidthBackground()) TextField("", text: .constant("")) .textFieldStyle(RoundedBorderTextFieldStyle()) } } .onPreferenceChange(WidthPreferenceKey.self) { if $0 > self.maxLabelWidth { self.maxLabelWidth = $0 } } } }
Tl y como se ve el modificador onPreferenceChange
indica que debe escuchar los cambios que ocurran en WidthPreferenceKey
. De esta forma almacenamos el valor más grande en la variable maxLabelWidth
, que también usamos para definir el ancho de cada Text
.
Es importante que cada Text
tenga el modificador .background(WidthBackground())
ya que esta es la forma en la que indicamos que Text
se tienen que tener en cuenta para la alineación.
Con este código conseguimos el resultado esperado.
Cómo crear animaciones con GeometryReader
Podemos ayudarnos del componente GeometryReader
para realizar animaciones basadas en la posición actual de las vistas.
Por ejemplo, podemos conseguir añadir el modificador scaleEffect
para que al hacer scroll en un listado de textos consigamos que el que ocupe la posición central se vea más grande que los demás.
Para ello usaremos dos GeometryReader
: uno global para conocer el tamaño total de pantalla y otro especifico de cada Text
para conocer su posición en cada momento. De esta forma seremos capaces de determinar dónde se encuentra cada Text
con respecto a toda la pantalla y podremos aplicar los cálculos necesarios para conseguir aplicar el factor de escala correcto en cada caso.
struct ContentView: View { let colors: [Color] = [.blue, .red, .orange, .yellow, .green, .purple] var body: some View { Group { GeometryReader { geometryParent in ScrollView { ForEach(0..<50) { index in GeometryReader { geometry in Text("Index (index)") .font(.title2) .frame(width: geometryParent.size.width) .background(colors
Para realizar el calculo obtenemos el alto completo de la pantalla y lo dividimos entre dos para obtener el punto medio, que es donde queremos que la escala sea más grande geometryParent.size.height / 2
.
Después obtenemos la posición actual del Text
con respecto a toda la pantalla para saber qué escala hay que aplicarle geometry.frame(in: .global).minY
. Con estos dos datos podemos aplicar la función ratioScale
que se encargará de devolver el valor correspondiente.
Ejemplo
Puedes encontrar este ejemplo en https://github.com/SDOSLabs/SwiftUI-Test bajo el apartado GeometryReader.
Rafael Fernández,
iOS Tech Lider