Posts

Dependencias de datos en SwiftUI: cuándo se vuelve a evaluar body

Arturo Rivas Arias

En SwiftUI, que cambie un dato no implica necesariamente que se vuelva a evaluar el body de todas las vistas que lo reciben. Tampoco una nueva evaluación equivale por sí sola a dibujar de nuevo toda la interfaz. SwiftUI mantiene un grafo de dependencias, calcula de nuevo las ramas afectadas y decide después qué cambios deben reflejarse en pantalla.

La parte menos evidente es que no todas las entradas crean la misma clase de dependencia. Una propiedad almacenada, un @Binding, un valor del entorno y una propiedad de un modelo @Observable pueden contener información equivalente, pero provocan actualizaciones distintas.

Las propiedades almacenadas forman parte del valor de la vista

Una vista es un valor. Cuando la vista padre genera una nueva instancia, SwiftUI examina sus propiedades almacenadas para decidir si debe volver a evaluar su contenido. Con tipos de valor, la comparación alcanza los valores que contienen; con tipos de referencia, lo relevante es si se ha recibido la misma instancia.

Esto significa que una propiedad puede provocar reevaluación aunque body no la utilice:

struct StorageGauge: View {
    let usedBytes: Int64
    let accountName: String
    let lastSync: Date // No se utiliza en body

    var body: some View {
        VStack(alignment: .leading) {
            Text(accountName)
            Text(usedBytes, format: .byteCount(style: .file))
        }
    }
}

Si la vista padre crea otro StorageGauge con una fecha diferente, el valor de la vista ha cambiado aunque el resultado visual sea idéntico. Eliminar entradas que ya no se usan o extraer componentes más pequeños reduce este tipo de evaluaciones. Apple recomienda precisamente limitar cada vista a los datos que realmente necesita.

Las clases sin observación presentan el caso contrario. Modificar una propiedad de la misma instancia no cambia la referencia almacenada, por lo que SwiftUI no tiene una señal para actualizar la interfaz. Sustituir la instancia sí puede producirla.

Los bloques de código son otra entrada especial. SwiftUI no puede determinar de forma fiable si dos bloques representan la misma operación. Si el padre vuelve a ejecutar su body y crea de nuevo un bloque que pasa a un hijo, ese hijo puede evaluarse otra vez aunque sus demás datos no hayan cambiado. No es necesario eliminar todas las acciones de las vistas, pero conviene no arrastrarlas por puntos de la jerarquía que no las necesitan, especialmente si una medición confirma que nuestra app tiene problemas con las reevaluaciones.

@State y @Binding: la lectura establece la dependencia

@State conserva un valor en almacenamiento administrado por SwiftUI y ligado a la identidad de la vista. @Binding ofrece acceso de lectura y escritura a ese mismo almacenamiento sin convertirse en su propietario.

A diferencia de una propiedad almacenada normal, un estado o un enlace se convierte en dependencia cuando se lee su wrappedValue durante la evaluación de body. Si un @Binding solo se modifica desde la acción de un botón y nunca se consulta para construir la interfaz, ese cambio no necesita evaluar de nuevo la vista que contiene el botón.

Hay un matiz importante con las ramas condicionales. Si la vista muestra una sección que lee el enlace, la dependencia se registra en ese momento. Aunque una evaluación posterior oculte la sección, la dependencia permanece mientras se conserve la identidad de la vista. Para evitar que una dependencia ya registrada siga activando una rama grande, suele ser mejor mover la sección que consume el valor a una vista independiente.

El entorno suscribe por declaración

@Environment y @FocusedValue se comportan de otra forma: declarar la propiedad suscribe la vista al valor correspondiente. Si el valor cambia, SwiftUI puede evaluar body aunque la propiedad ya no aparezca en él.

Es fácil conservar un @Environment olvidado después de una refactorización. Aunque parezca inofensivo, mantiene una dependencia con un dato que puede cambiar con frecuencia, como el tamaño dinámico del texto, la escena activa o una selección propagada por la jerarquía. Quitar estas dependencias cuando dejan de utilizarse es una optimización pequeña, pero prácticamente gratuita.

Observation ajusta la dependencia a cada propiedad

La macro @Observable cambia la granularidad. SwiftUI registra las propiedades concretas del modelo que se leen durante cada evaluación de body, no una suscripción general a toda la instancia.

import Observation
import SwiftUI

@Observable
final class DownloadModel {
    var fileName = "Manual.pdf"
    var progress = 0.0
}

struct DownloadTitle: View {
    let model: DownloadModel

    var body: some View {
        Text(model.fileName)
    }
}

struct DownloadScreen: View {
    @State private var model = DownloadModel()

    var body: some View {
        VStack {
            DownloadTitle(model: model)
            ProgressView(value: model.progress)

            Button("Avanzar") {
                model.progress += 0.1
            }
        }
    }
}

DownloadScreen depende de progress, pero DownloadTitle solo depende de fileName. Cambiar el progreso no obliga a evaluar el body del título. Apple mostró este seguimiento por acceso al presentar Observation en SwiftUI, y la propuesta SE-0395 explica cómo la macro implementa las lecturas y mutaciones.

Además, Observation recalcula estas dependencias en cada evaluación. Si una propiedad observable se lee dentro de una condición que después deja de ejecutarse, deja de ser una dependencia. Esta precisión dinámica es distinta del comportamiento acumulativo de @State y @Binding.

Con ObservableObject, @StateObject, @ObservedObject y @EnvironmentObject, la suscripción es más amplia. Cualquier emisión de objectWillChange, incluida la generada por una propiedad @Published, invalida las vistas suscritas aunque estas no lean la propiedad modificada. Por eso migrar modelos adecuados a Observation puede reducir trabajo, además de simplificar las declaraciones.

Un mapa rápido de dependencias

Entrada de la vistaCuándo puede provocar otra evaluación de body
Propiedad almacenadaCuando cambia el valor recibido o se sustituye la referencia, aunque no se use en body
@State / @BindingDespués de que el valor se haya leído en body; la dependencia permanece con la identidad de la vista
@Environment / @FocusedValueCuando cambia el valor al que la declaración está suscrita, aunque ya no se lea
Modelo @ObservableCuando cambia una propiedad leída en la evaluación vigente de body
ObservableObject y sus envoltoriosCuando el objeto emite objectWillChange, con independencia de la propiedad usada

Medir antes de dividir la interfaz

Una evaluación adicional de una vista pequeña suele ser irrelevante. El problema aparece cuando se repite con frecuencia, arrastra una jerarquía grande o ejecuta filtros, formateos, asignaciones y otras operaciones costosas dentro de body.

Durante la depuración se puede detener la ejecución dentro de body y lanzar expression Self._printChanges() desde LLDB. El resultado ofrece una explicación aproximada de la dependencia que activó la evaluación. Al comenzar por guion bajo, es una API de diagnóstico sin estabilidad garantizada y no debe permanecer en el código de producción.

El objetivo no es conseguir que body se ejecute una única vez, sino que cada vista declare solo las entradas que necesita y que su evaluación resulte barata. Componentes con límites claros, modelos @Observable cuando encajen con los sistemas mínimos y mediciones sobre un caso real suelen aportar mucho más que cualquier microoptimización aislada.