Las interfaces de las aplicaciones para sistemas Apple ya no pueden diseñarse pensando únicamente en un conjunto reducido de tamaños de pantalla. En iPad las ventanas se pueden redimensionar libremente y, con iOS 27, las aplicaciones de iPhone compiladas con el SDK más reciente también pueden cambiar de tamaño cuando se ejecutan en un iPad o mediante iPhone Mirroring en el Mac.
Esto hace que decisiones tradicionales como comprobar el modelo del dispositivo, el userInterfaceIdiom o incluso la orientación de la interfaz sean cada vez menos apropiadas para decidir cómo organizar una vista. Lo importante ya no es tanto en qué dispositivo se ejecuta la aplicación, sino cuánto espacio tiene realmente disponible en ese momento.
SwiftUI ofrece varias herramientas para resolver este problema: ViewThatFits, containerRelativeFrame, las size classes o una implementación personalizada de Layout. Sin embargo, hay situaciones en las que necesitamos observar directamente la geometría resuelta de una vista y convertirla en información que nuestra interfaz pueda utilizar. Para esos casos existe onGeometryChange(for:of:action:).
Qué es onGeometryChange
onGeometryChange permite observar la geometría de una vista después de que SwiftUI haya realizado el cálculo de layout.
Su firma actual es conceptualmente similar a esta:
func onGeometryChange<T>(
for type: T.Type,
of transform: @escaping @Sendable (GeometryProxy) -> T,
action: @escaping (T) -> Void
) -> some View where T: Equatable, T: Sendable
El modificador tiene dos fases claramente separadas.
En primer lugar, el bloque transform recibe un GeometryProxy. Desde ahí podemos consultar propiedades como:
sizesafeAreaInsetsframe(in:)bounds(of:)
Pero la idea no es guardar directamente toda esa geometría. El objetivo consiste en transformarla en el dato más sencillo que realmente necesita nuestra interfaz.
Después, SwiftUI compara ese resultado con el anterior. Como el tipo debe conformar a Equatable, el bloque action solo se ejecuta cuando el valor transformado cambia.
Esta diferencia es fundamental.
No observes un tamaño si solo necesitas una decisión
Imaginemos una barra de herramientas para un editor. Cuando dispone de suficiente anchura queremos mostrar los botones con texto, mientras que en espacios pequeños queremos utilizar únicamente iconos.
Podríamos almacenar continuamente el ancho completo de la vista:
@State private var availableWidth: CGFloat = 0
.onGeometryChange(for: CGFloat.self) { geometry in
geometry.size.width
} action: { width in
availableWidth = width
}
Funciona, pero cada pequeño cambio de tamaño actualiza el estado. Durante el redimensionado de una ventana podrían generarse muchas modificaciones aunque la interfaz no necesitase hacer nada distinto.
Es mejor convertir la geometría directamente en una decisión semántica:
struct EditorToolbar: View {
@State private var isCompact = false
var body: some View {
HStack {
Button {
saveDocument()
} label: {
if isCompact {
Image(systemName: "square.and.arrow.down")
} else {
Label("Guardar", systemImage: "square.and.arrow.down")
}
}
Button {
duplicateDocument()
} label: {
if isCompact {
Image(systemName: "plus.square.on.square")
} else {
Label("Duplicar", systemImage: "plus.square.on.square")
}
}
Spacer()
Button {
showInspector()
} label: {
if isCompact {
Image(systemName: "sidebar.right")
} else {
Label("Inspector", systemImage: "sidebar.right")
}
}
}
.padding()
.onGeometryChange(for: Bool.self) { geometry in
geometry.size.width < 420
} action: { isCompact in
self.isCompact = isCompact
}
}
private func saveDocument() {}
private func duplicateDocument() {}
private func showInspector() {}
}
Aunque la anchura cambie de 700 a 650, 600 o 500 puntos, el resultado del bloque de transformación sigue siendo false. SwiftUI no necesita ejecutar el bloque action hasta que se cruza realmente el umbral de 420 puntos.
La geometría puede cambiar decenas de veces. El estado de nuestra aplicación, sin embargo, solo cambia cuando existe una diferencia relevante para la interfaz.
Podemos devolver algo más expresivo que un Bool
Para interfaces algo más complejas, limitarse a un valor booleano puede quedarse corto. El resultado puede ser cualquier tipo Equatable y Sendable.
Por ejemplo, podemos definir varios niveles de densidad:
enum ControlDensity: Equatable, Sendable {
case compact
case regular
case expanded
}
Y derivarlos directamente de la anchura disponible:
.onGeometryChange(for: ControlDensity.self) { geometry in
switch geometry.size.width {
case ..<360:
return .compact
case ..<700:
return .regular
default:
return .expanded
}
} action: { density in
self.density = density
}
Con este patrón dejamos de propagar números de píxeles o puntos por la aplicación y trabajamos con estados que representan decisiones reales de diseño.
Además de reducir actualizaciones, el código resulta más sencillo de probar y razonar.
Evitar ciclos entre geometría y estado
Hay una cuestión especialmente importante: el estado que modificamos en action no debería alterar la misma geometría utilizada para calcularlo de una manera que pueda hacer oscilar el resultado.
Imaginemos que una vista activa un margen adicional cuando su anchura baja de cierto valor. Si ese margen cambia a su vez la anchura medida y hace que vuelva a superar el umbral, podemos construir accidentalmente un ciclo:
- La vista mide menos de 400 puntos.
- Actualizamos el estado.
- El nuevo estado cambia el layout.
- La vista pasa a medir más de 400 puntos.
- Volvemos a actualizar el estado.
- El layout regresa al estado anterior.
SwiftUI vuelve a calcular sus vistas de manera reactiva, por lo que este tipo de dependencia circular puede provocar actualizaciones repetidas o interfaces inestables.
Una buena regla consiste en observar la geometría de un contenedor cuyo tamaño venga determinado desde fuera y utilizar el resultado para reorganizar su contenido interno.
El bloque de transformación debe ser sencillo
La geometría puede cambiar con mucha frecuencia. Esto es especialmente evidente durante el redimensionado interactivo de una ventana o dentro de un ScrollView.
Aunque SwiftUI solo ejecute action cuando cambia el valor resultante, el bloque transform sí necesita reevaluarse para poder realizar esa comparación.
Por tanto, conviene evitar cálculos costosos:
// ❌ Demasiado trabajo dentro de la transformación
.onGeometryChange(for: LayoutMode.self) { geometry in
let configuration = calculateComplexConfiguration(
width: geometry.size.width,
height: geometry.size.height
)
return configuration
} action: { mode in
layoutMode = mode
}
La transformación debería limitarse normalmente a operaciones sencillas sobre la geometría:
// ✅ Convertir geometría en información mínima
.onGeometryChange(for: Bool.self) { geometry in
geometry.size.width < 500
} action: { compact in
isCompact = compact
}
En los SDK actuales, además, el bloque de transformación está marcado como @Sendable. Apple ha explicado que SwiftUI puede ejecutar determinados cálculos geométricos fuera del hilo principal como optimización. Esto refuerza la idea de que la transformación debe ser una función pura: recibir geometría, calcular un valor y devolverlo, sin modificar estado ni depender de efectos secundarios.
onGeometryChange frente a GeometryReader
Durante años, uno de los patrones habituales para obtener el tamaño de una vista consistía en introducir un GeometryReader, publicar la medida mediante una PreferenceKey y escuchar después los cambios desde un nivel superior de la jerarquía.
El resultado podía acabar teniendo bastante código repetitivo como una receta:
struct WidthPreferenceKey: PreferenceKey {
static var defaultValue: CGFloat = 0
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
value = nextValue()
}
}
Después había que colocar un GeometryReader, escribir la preferencia y observarla con onPreferenceChange.
Ese patrón sigue teniendo casos de uso, especialmente cuando necesitamos propagar información desde una vista hija hacia un ancestro. Pero si lo único que queremos es reaccionar a la geometría resuelta de una vista, onGeometryChange expresa la intención de forma mucho más directa.
También evita utilizar GeometryReader únicamente como mecanismo de medición. Esto es interesante porque GeometryReader participa en el layout y su comportamiento puede afectar a la propuesta de tamaño recibida por otras vistas si se introduce en un lugar inadecuado.
onGeometryChange, en cambio, funciona como observación de una geometría ya resuelta.
Antes de observar geometría, comprueba si SwiftUI ya tiene una herramienta mejor
Que onGeometryChange sea cómodo no significa que deba convertirse en la primera solución para cualquier diseño adaptable.
Si únicamente queremos seleccionar la primera variante que cabe en el espacio disponible, ViewThatFits suele ser más declarativo:
ViewThatFits(in: .horizontal) {
HStack {
playbackControls
volumeControls
}
VStack {
playbackControls
volumeControls
}
}
Aquí no existe estado adicional, no observamos ninguna medida y no definimos umbrales manualmente. SwiftUI simplemente selecciona la primera alternativa que cabe.
De forma similar, containerRelativeFrame resulta apropiado cuando una vista debe dimensionarse en relación con su contenedor, y una implementación personalizada de Layout es normalmente una solución más robusta cuando necesitamos medir y colocar varias subvistas de manera coordinada.
onGeometryChange encaja mejor cuando nuestra interfaz necesita derivar una información concreta de la geometría resuelta y reaccionar a ella.
Geometría dentro de un ScrollView
Un ScrollView es un caso especialmente sensible porque la geometría puede cambiar en cada fotograma mientras el usuario desplaza contenido.
Apple insiste en evitar actualizaciones grandes de estado en respuesta a cada modificación geométrica. El mismo principio vuelve a ser válido: reducir primero la geometría al dato mínimo necesario.
Por ejemplo, si solo necesitamos saber si un encabezado ha abandonado la zona visible, es mejor producir un booleano que almacenar continuamente su coordenada vertical:
.onGeometryChange(for: Bool.self) { geometry in
geometry.frame(in: .scrollView).minY < 0
} action: { hasScrolledPastHeader in
self.hasScrolledPastHeader = hasScrolledPastHeader
}
En las versiones recientes de SwiftUI también existen APIs más específicas para desplazamiento, como onScrollGeometryChange, onScrollVisibilityChange y onScrollPhaseChange. Cuando el problema pertenece específicamente al comportamiento de un ScrollView, estas herramientas suelen expresar mejor la intención que una observación geométrica genérica.
Por qué esta API es más relevante con iOS 27
El cambio hacia interfaces completamente redimensionables hace que la lógica basada en el tipo de dispositivo tengan todavía menos sentido.
Una aplicación ejecutándose como iPhone puede disponer de una ventana mucho más ancha de lo que históricamente asociábamos a un teléfono. Del mismo modo, una aplicación de iPad puede encontrarse en una ventana muy estrecha.
Apple recomienda basar las decisiones de layout en el espacio disponible, utilizar size classes para categorías generales y recurrir a la geometría local cuando necesitamos un control más preciso.
Esto encaja perfectamente con la filosofía de SwiftUI: describir cómo debería reaccionar la interfaz a su entorno en lugar de intentar deducir ese entorno a partir de un modelo de dispositivo concreto.
Disponibilidad
Aunque onGeometryChange empezó a recibir más visibilidad con las APIs de geometría introducidas alrededor de iOS 18, Apple la hizo retrocompatible hasta iOS 16, iPadOS 16 y macOS 13. Esto permite adoptar el patrón incluso en proyectos cuyo deployment target todavía no se encuentre en las versiones más recientes del sistema.
No ocurre lo mismo con todas las APIs relacionadas con scroll introducidas posteriormente, por lo que conviene comprobar individualmente su disponibilidad cuando se quiera sustituir una solución genérica por otra más especializada.
Conclusión
onGeometryChange cubre un hueco muy concreto dentro de SwiftUI: permite convertir la geometría resuelta de una vista en un valor de estado útil sin introducir un GeometryReader únicamente para medir.
La clave para utilizarlo correctamente no consiste en observar más geometría, sino precisamente en observar menos. En lugar de almacenar continuamente tamaños, posiciones o rectángulos completos, conviene transformar esos datos en la mínima información semántica que necesita la interfaz: un Bool, un nivel de densidad, un estado de visibilidad o cualquier pequeño tipo Equatable.
Cuanto más redimensionables se vuelven las aplicaciones para iPhone, iPad y Mac, más importante resulta diseñar en función del espacio real disponible. onGeometryChange es una herramienta útil para aquellos casos en los que las abstracciones de layout de más alto nivel no son suficientes, siempre que la medición permanezca barata, estable y desacoplada del estado que modifica.