El coste oculto de los valores por defecto inestables en el Environment de SwiftUI
Arturo Rivas Arias
SwiftUI utiliza el los entornos para propagar configuraciones y dependencias a través de la jerarquía de vistas. Valores del sistema como locale, colorScheme o dynamicTypeSize llegan por esta vía, pero también podemos definir los nuestros propios mediante el macro @Entry.
La sintaxis es tan sencilla que resulta fácil pasar por alto un detalle importante: el valor por defecto no debe cambiar cada vez que SwiftUI lo consulta. Si la expresión crea una instancia nueva en cada acceso, una dependencia que parecía constante puede provocar reevaluaciones innecesarias de las vistas.
Xcode 27 avisa de este problema cuando detecta una clase creada directamente en la declaración de @Entry. No se trata de una cuestión estética ni de una nueva regla arbitraria del compilador. El aviso señala una dependencia inestable que puede multiplicar el trabajo de SwiftUI y acabar afectando a la fluidez de la interfaz.
Qué hace realmente @Entry
Antes de la llegada de @Entry, crear un valor personalizado exigía declarar un tipo conforme a EnvironmentKey, proporcionar su valor por defecto y añadir una propiedad calculada a EnvironmentValues. El macro elimina ese código repetitivo:
final class ThumbnailRepository: Sendable {
private let session: URLSession
init(session: URLSession = .shared) {
self.session = session
}
func data(for url: URL) async throws -> Data {
let (data, _) = try await session.data(from: url)
return data
}
}
extension EnvironmentValues {
@Entry var thumbnailRepository = ThumbnailRepository()
}
El código parece correcto: cualquier vista puede obtener el repositorio y la aplicación puede sustituirlo en producción, en una preview o durante los tests. Sin embargo, Xcode 27 muestra un aviso porque la expresión ThumbnailRepository() construye una referencia nueva cada vez que se utiliza el valor de respaldo.
El acceso a la propiedad generado por @Entry se comporta como un getter calculado. Cuando no existe un valor inyectado en la jerarquía, SwiftUI recurre a ese getter y evalúa la expresión por defecto. Como dos instancias de una clase tienen identidades diferentes aunque su configuración sea idéntica, el framework interpreta que el entorno ha cambiado.
Por qué una referencia nueva provoca más actualizaciones
SwiftUI registra las dependencias que una vista lee al evaluar su propiedad body. Cuando cambia el estado o el entorno, utiliza esa información para decidir qué partes de la jerarquía necesitan volver a calcularse.
Imaginemos una fila de una tabla que solo depende del repositorio de miniaturas:
struct TrailRow: View {
let trail: Trail
@Environment(\.thumbnailRepository)
private var thumbnailRepository
var body: some View {
let _ = Self._printChanges()
Label(trail.name, systemImage: "mountain.2")
}
}
En un nivel superior existe otro valor del entorno completamente independiente:
enum MapDetail: Equatable {
case reduced
case complete
}
extension EnvironmentValues {
@Entry var mapDetail = MapDetail.complete
}
struct TrailList: View {
let trails: [Trail]
@State private var detail = MapDetail.complete
var body: some View {
List(trails) { trail in
TrailRow(trail: trail)
}
.environment(\.mapDetail, detail)
.toolbar {
Button("Cambiar detalle") {
detail = detail == .complete ? .reduced : .complete
}
}
}
}
TrailRow no lee mapDetail, por lo que cambiar este valor no debería obligarla a recalcular su body. El problema aparece cuando SwiftUI comprueba las dependencias de entorno de la fila: al volver a consultar thumbnailRepository, obtiene una instancia diferente. Para el framework, el repositorio ha cambiado y la actualización de la fila pasa a ser necesaria.
Una única reevaluación adicional rara vez será perceptible. La situación cambia si la pantalla contiene decenas de filas, cada body realiza un trabajo que no es sencillo o el cambio se produce con frecuencia durante un desplazamiento o una animación. Apple recuerda en su documentación que una acumulación de actualizaciones rápidas también puede superar el tiempo disponible para preparar un frame y producir tirones, aunque ninguna actualización individual sea especialmente lenta.
Almacenar una referencia estable
Cuando existe un comportamiento por defecto válido, la solución más directa consiste en crear la instancia una sola vez y hacer que @Entry se limite a devolverla:
private let defaultThumbnailRepository = ThumbnailRepository()
extension EnvironmentValues {
@Entry var thumbnailRepository = defaultThumbnailRepository
}
El getter sigue ejecutándose cuando falta un valor inyectado, pero todas las consultas devuelven ahora la misma referencia. SwiftUI puede comprobar su identidad, determinar que la dependencia no ha cambiado y evitar la reevaluación de las vistas que la leen.
Otra opción es guardar el valor estable como propiedad estática del propio tipo:
extension ThumbnailRepository {
static let defaultValue = ThumbnailRepository()
}
extension EnvironmentValues {
@Entry var thumbnailRepository = ThumbnailRepository.defaultValue
}
Ambas variantes resuelven la inestabilidad. La elección depende de si esa instancia predeterminada forma parte de la interfaz del tipo o es un detalle privado de la integración con SwiftUI.
Un valor opcional cuando la inyección es obligatoria
No todas las dependencias deberían disponer de una implementación por defecto. Si el repositorio necesita credenciales, una configuración específica o un almacenamiento proporcionado por la aplicación, inventar un valor de respaldo puede ocultar un error de lógica.
En ese caso es preferible declarar la entrada como opcional:
extension EnvironmentValues {
@Entry var thumbnailRepository: ThumbnailRepository?
}
La raíz de la aplicación crea la dependencia una sola vez y la inyecta en la jerarquía:
@main
struct HikingApp: App {
private let thumbnailRepository = ThumbnailRepository()
var body: some Scene {
WindowGroup {
ContentView()
.environment(
\.thumbnailRepository,
thumbnailRepository
)
}
}
}
Las vistas consumidoras deben manejar explícitamente su ausencia. Puede resultar algo más verboso, pero el tipo refleja mejor el contrato real: SwiftUI no garantiza que el valor exista y es responsabilidad del punto de composición proporcionarlo.
También puede ser útil contar con una implementación no operativa pero estable para previews o componentes reutilizables. Esa alternativa tiene sentido cuando no realizar ninguna acción. Si la ausencia impide que la pantalla funcione correctamente, usar un opcional o una validación temprana durante la composición resuelven mejor el problema.
Las clases no son el único caso inestable
El nuevo diagnóstico de Xcode se centra en las clases porque su identidad hace que el fallo sea especialmente fácil de detectar, pero la regla es más amplia: evaluar repetidamente la expresión por defecto debe producir el mismo valor efectivo.
Estos valores también son inestables:
extension EnvironmentValues {
@Entry var requestID = UUID()
@Entry var loadedAt = Date()
}
Cada acceso genera un identificador o instante diferente. Ninguno representa un valor por defecto estable, aunque ambos sean tipos por valor.
Una estructura tampoco soluciona automáticamente el problema si contiene una referencia recién creada:
class ResultCache {
// ...
}
struct SearchDependencies {
let cache: ResultCache
}
extension EnvironmentValues {
@Entry var searchDependencies = SearchDependencies(
cache: ResultCache()
)
}
La estructura se copia por valor, pero el ResultCache interno sigue teniendo una identidad nueva en cada evaluación. Mover toda la expresión a almacenamiento estable evita el mismo problema:
private let defaultSearchDependencies = SearchDependencies(
cache: ResultCache()
)
extension EnvironmentValues {
@Entry var searchDependencies = defaultSearchDependencies
}
Por el contrario, literales constantes, casos de un enum, nil y estructuras compuestas únicamente por valores deterministas suelen ser buenos valores predeterminados. No es imprescindible que el tipo sea Equatable; lo importante es que la expresión no cere un estado efectivo distinto en cada consulta.
Inyectar siempre el valor también evita el síntoma, pero no corrige el diseño
La inestabilidad solo aparece cuando una vista utiliza el valor de respaldo. Si la aplicación inyecta una instancia por encima de todas las vistas consumidoras, SwiftUI reutiliza esa referencia y no evalúa la expresión predeterminada.
Aun así, mantener un valor por defecto inestable deja abierta la puerta a que el problema reaparezca en una preview, un test, una pantalla secundaria o una nueva rama de la jerarquía que olvide realizar la inyección. El aviso de Xcode invita a corregir la declaración y no solo el camino que se ejecuta actualmente.
El entorno, además, distribuye una dependencia pero no debería utilizarse para ocultar quién es su propietario. Los servicios y modelos con identidad deben crearse en un punto de la composición con un ciclo de vida claro. Después pueden propagarse mediante environment, pero su creación accidental dentro del valor por defecto hace que ese ciclo de vida dependa de un detalle de implementación del framework.
Cómo comprobar las actualizaciones innecesarias
Durante el desarrollo, Self._printChanges() permite registrar en la consola por qué SwiftUI ha vuelto a evaluar una vista. Es una herramienta de diagnóstico útil para ejemplos pequeños y para confirmar que una dependencia de entorno está invalidando el body.
Cuando el problema aparece en una interfaz real, la plantilla de SwiftUI incluida en Instruments ofrece una imagen más completa. Su pista de actualizaciones permite localizar vistas cuyo body se ejecuta con demasiada frecuencia, mientras que el grafo de causa y efecto relaciona el cambio original con las actualizaciones resultantes. De esta forma se puede comparar una captura antes y después de estabilizar el valor predeterminado en lugar de confiar únicamente en la percepción visual.
Conviene probar también previews y rutas de navegación que no partan de la raíz principal. Son precisamente los lugares donde es más probable que una vista recurra sin querer al valor predeterminado.
Una regla sencilla para @Entry
Cada declaración debería responder a una pregunta: si SwiftUI evalúa esta expresión varias veces, ¿obtendrá siempre el mismo valor?
Si la respuesta es negativa, hay tres soluciones posibles:
- Mover la instancia a almacenamiento estable cuando existe un valor por defecto válido.
- Utilizar
nilcuando la ausencia de valor forma parte del contrato. - Crear e inyectar la dependencia desde el propietario con un ciclo de vida explícito cuando su presencia es obligatoria.
El aviso de Xcode 27 convierte en visible un fallo que hasta ahora podía permanecer escondido detrás de listas que actualizaban demasiado, animaciones con pequeños tirones o un body de una vista que se ejecutaban sin una causa aparente. Mantener estables los valores predeterminados permite que el grafo de dependencias de SwiftUI represente cambios reales y que el framework descarte trabajo que no necesita hacer.