Para que una fila de una lista de SwiftUI refleje los cambios realizados en un elemento ObservableObject, pásalo a una vista de fila con @ObservedObject y declara sus propiedades mutables como publicadas. Esta guía crea un pequeño editor de etiquetas para Mac y explica las funciones distintas de las actualizaciones de la colección, la observación de las filas y la identidad.

¿Por qué la fila no cambia cuando modifico un elemento?

Considera un almacén con una propiedad @Published var items: [LabelItem], donde cada LabelItem es una clase. Añadir un elemento modifica el arreglo. Asignar un título nuevo a un elemento existente modifica el objeto al que se hace referencia. Son mutaciones distintas: los arreglos de Swift son tipos de valor, mientras que las instancias de clase tienen semántica de referencia. Un arreglo de referencias a clases sigue conteniendo referencias a instancias compartidas. Tipos de valor y de referencia en Swift.

La consecuencia práctica es que publicar el arreglo no establece automáticamente la observación de cada propiedad de todos los objetos que contiene. La guía de migración de Apple señala como una ventaja del nuevo sistema Observation el seguimiento de colecciones de objetos. Si ya tienes una implementación de ObservableObject, establece una conexión propia entre cada fila y el objeto que muestra. Migrar del protocolo Observable Object a la macro Observable.

Una forma útil de diagnosticar el problema es distinguir dos responsabilidades: la vista contenedora observa qué elementos pertenecen a la lista; cada fila observa el contenido mutable de su elemento. Apple documenta @ObservedObject como el envoltorio para un ObservableObject recibido cuyos cambios publicados deben actualizar la vista receptora. ObservedObject.

Crear un ejemplo completo para macOS con una fila observada

Usa esta versión si tu app ya usa ObservableObject de Combine, incluso si es compatible con versiones de macOS anteriores a la 14. El ejemplo requiere macOS 11 o posterior porque su propietario usa @StateObject, disponible desde macOS 11, según Apple. Usa una versión actual del compilador Swift de Xcode. StateObject.

Crea un destino de app para macOS con la interfaz de SwiftUI. Reemplaza el archivo de entrada generado de la app por el programa completo que aparece a continuación y elimina la vista de contenido generada si ya no la necesitas. Mantén exactamente una declaración @main en el destino. Todas las etiquetas del ejemplo son ficticias y las interacciones se realizan dentro de la propia ventana de la app.

import SwiftUI
import Combine
import Foundation

@MainActor
final class LabelItem: ObservableObject, Identifiable {
    let id = UUID()
    @Published var title: String
    @Published var isEnabled: Bool

    init(title: String, isEnabled: Bool = true) {
        self.title = title
        self.isEnabled = isEnabled
    }
}

@MainActor
final class LabelStore: ObservableObject {
    @Published var items: [LabelItem] = [
        LabelItem(title: "Amber"),
        LabelItem(title: "Cobalt", isEnabled: false)
    ]

    func reviseFirstItem() {
        guard let first = items.first else { return }
        first.title = first.title == "Amber" ? "Copper" : "Amber"
        first.isEnabled.toggle()
    }

    func addItem() {
        items.append(LabelItem(title: "New label"))
    }

    func removeLastItem() {
        guard !items.isEmpty else { return }
        items.removeLast()
    }
}

@MainActor
struct LabelRow: View {
    @ObservedObject var item: LabelItem

    var body: some View {
        HStack {
            TextField("Label", text: $item.title)
                .textFieldStyle(.roundedBorder)
                .frame(minWidth: 120)

            Text(item.title)
                .frame(minWidth: 100, alignment: .leading)

            Toggle("Enabled", isOn: $item.isEnabled)
        }
        .padding(.vertical, 4)
    }
}

@MainActor
struct LabelListView: View {
    @StateObject private var store = LabelStore()

    var body: some View {
        VStack(alignment: .leading, spacing: 12) {
            HStack {
                Button("Change first label") {
                    store.reviseFirstItem()
                }
                .disabled(store.items.isEmpty)

                Button("Add label") {
                    store.addItem()
                }

                Button("Remove last") {
                    store.removeLastItem()
                }
                .disabled(store.items.isEmpty)
            }

            Text("Rows: \(store.items.count)")

            List(store.items) { item in
                LabelRow(item: item)
            }
        }
        .padding()
        .frame(minWidth: 540, minHeight: 300)
    }
}

@main
@MainActor
struct LabelRowsApp: App {
    var body: some Scene {
        WindowGroup {
            LabelListView()
        }
    }
}

El almacén es propietario de las instancias de los elementos. LabelListView crea y conserva el almacén mediante @StateObject; las filas reciben instancias existentes mediante @ObservedObject. Apple explica que un objeto de estado persiste durante la vida de la identidad de su contenedor declarante y que, por lo general, un objeto observado debe ser una entrada, no un valor predeterminado recién creado. StateObject, ObservedObject.

El campo de texto y el interruptor usan enlaces a las propiedades publicadas del elemento. El texto contiguo muestra el mismo título, lo que permite comprobar visualmente que la edición y la visualización comparten un único modelo. @MainActor mantiene las operaciones del modelo y de la interfaz de este ejemplo en el actor principal, cuyo ejecutor Apple describe como equivalente a la cola principal de GCD. ObservedObject, MainActor.

Este programa se revisó comparándolo con las API documentadas, pero no se compiló aquí: el entorno disponible no tiene un conjunto de herramientas de Swift. Compílalo y ejecútalo en Xcode para verificar su comportamiento con tu destino de implementación.

Verificar por separado los cambios de elementos y de la colección

Sigue esta secuencia para distinguir las dos rutas de actualización. Los resultados indicados son los esperados según el programa; no corresponden a mediciones de pruebas realizadas.

  1. Inicia la app. Debería mostrar dos filas, con las etiquetas iniciales Amber y Cobalt.
  2. Haz clic en Change first label. La primera fila debería mostrar Copper y su interruptor Enabled debería cambiar. El número de filas debería seguir siendo dos.
  3. Edita el campo Label de la primera fila. El texto contiguo debería mostrar el mismo título.
  4. Haz clic en Add label. Debería aparecer una tercera fila llamada New label y aumentar el número de elementos.
  5. Haz clic en Remove last hasta que la lista quede vacía. Los botones para quitar elementos y cambiar la primera etiqueta deberían quedar desactivados; el botón para añadir una etiqueta debería seguir disponible.

El segundo paso resulta especialmente útil porque la mutación se origina en la acción del botón de la vista contenedora. Modifica el elemento existente sin reemplazar el arreglo. Por tanto, si la fila se actualiza correctamente, se comprueba directamente la ruta de observación del elemento. Los pasos de inserción y eliminación ponen a prueba por separado el arreglo publicado del almacén.

Si la inserción funciona, pero los cambios de título no, revisa la declaración de la propiedad en la fila que lee el título. En esta arquitectura, debe recibir el elemento mediante @ObservedObject, y el título mutable debe ser @Published. La publicación corresponde a la propiedad que lleva ese envoltorio; no es una declaración recursiva para todo un grafo de objetos. Published, ObservedObject.

Diagnosticar los problemas restantes en un orden útil

Confirmar que quien modifica el elemento y la fila comparten la misma instancia

Sigue el objeto que se pasa a LabelRow(item:) y el objeto que modifica la acción. Crear otro LabelItem con el mismo título genera una instancia distinta. Del mismo modo, copiar el título al estado local de la fila crea otro valor que tendrás que mantener sincronizado. Para este editor, enlázalo directamente al elemento compartido, salvo que necesites un flujo deliberado de edición de borrador y confirmación. Esto se deriva de la distinción documentada en Swift entre copiar valores y compartir referencias a clases. Tipos de valor y de referencia en Swift.

Mantener estable la identidad mientras cambia el contenido

El ejemplo asigna a cada elemento un identificador inmutable almacenado. ForEach, que sirve de base para la construcción habitual de listas dinámicas, requiere elementos identificables o un identificador explícito. La identidad permite a SwiftUI asociar el contenido generado con el elemento de datos correspondiente a través de las actualizaciones. No sustituye la conexión de observación de la fila. ForEach.

Evita añadir un .id(UUID()) cambiante como mecanismo habitual de actualización. Apple documenta que cambiar la identidad de una vista restablece su estado, incluido el estado del foco. Recrear una fila para mostrar un título editado introduce un cambio de ciclo de vida aparte de la edición del contenido; primero establece la ruta de observación. StateObject.

Tratar los resúmenes del padre como consumidores adicionales

Supongamos que añades encima de la lista un recuento de elementos habilitados. En el diseño heredado que se muestra aquí, observar LabelStore no hace que el padre observe automáticamente el publicador isEnabled de cada elemento. Por tanto, corregir las filas por sí solo no establece las dependencias necesarias para ese resumen.

Elige explícitamente cómo se actualizará: agrega en el almacén las notificaciones de los elementos, usa elementos de tipo valor cuyas ediciones se escriban de vuelta en la colección publicada o adopta Observation si el destino de implementación lo permite. Son opciones de arquitectura derivadas de la distinción entre la colección y sus elementos; el ejemplo solo implementa la observación directa de las filas. La guía de migración de Apple describe la capacidad del nuevo sistema para seguir colecciones y las propiedades que una vista lee realmente. Migrar del protocolo Observable Object a la macro Observable.

Si implementas la agregación basada en publicadores, ten en cuenta el momento de las notificaciones. Apple especifica que @Published publica durante willSet: un suscriptor recibe el valor entrante antes de que cambie la propiedad almacenada. Si vuelves a leer la propiedad dentro de esa devolución de llamada, puede que obtengas su valor anterior. Usa el valor emitido al hacer el cálculo a partir de ese evento. Published.

¿Cuándo conviene pasar a Observation en macOS 14 o posterior?

Si el destino mínimo de implementación es macOS 14 o posterior, SwiftUI admite el sistema Observation. Con un modelo @Observable, SwiftUI hace seguimiento de las lecturas de propiedades observables durante la evaluación de la vista. La guía de Apple muestra específicamente una colección de objetos observables cuyas etiquetas individuales se actualizan cuando cambian. Administrar los datos del modelo en tu app.

Para migrar este ejemplo, cambia tanto LabelItem como LabelStore para que usen @Observable con import Observation, elimina sus conformidades a ObservableObject y quita @Published. Cambia la propiedad del almacén propietario de @StateObject a @State. Como esta fila contiene controles editables, cambia el envoltorio del elemento recibido de @ObservedObject a @Bindable. Estos cambios siguen la ruta de migración documentada por Apple. Migrar del protocolo Observable Object a la macro Observable.

Una fila de solo lectura puede recibir un elemento observable mediante una propiedad almacenada ordinaria y leer su título en body. @Bindable proporciona los enlaces necesarios para controles como TextField y Toggle; cumple una función distinta de @ObservedObject. No envuelvas con @ObservedObject un modelo que solo usa el nuevo sistema Observation: su restricción genérica requiere ObservableObject. Bindable, ObservedObject.

La decisión es sencilla: conserva la solución de la fila observada para reparar un modelo de Combine existente y mantener su rango actual de versiones de macOS compatibles; considera Observation cuando macOS 14 sea una versión mínima aceptable y el seguimiento de dependencias a nivel de propiedad se ajuste al resto de la interfaz.

¿Qué cambia si la fila incluye una vista de texto de AppKit?

El ejemplo de SwiftUI usa controles estándar y no requiere una integración personalizada con TextKit. Si tu propia fila ya incluye una vista de AppKit mediante NSViewRepresentable, comprueba un segundo límite después de confirmar la observación del modelo: Apple indica que debes actualizar la configuración de la vista de AppKit en updateNSView(_:context:) cuando cambie el estado correspondiente de SwiftUI. Crear la vista nativa una sola vez no implementa las actualizaciones posteriores de su configuración. updateNSView(_:context:).

Si incluyes un NSTextView, distingue esa sincronización del modelo a la vista del diseño del texto. Apple describe NSTextView como la interfaz del sistema de texto de AppKit y documenta una opción de compatibilidad cuando se accede explícitamente a su propiedad layoutManager en macOS 12 o posterior. Evita introducir cambios en el motor de diseño mientras diagnosticas una conexión de observación que falta en una fila. NSTextView.

Para el editor de etiquetas, la solución concreta queda completa en la fila: conserva el almacén, publica las propiedades editables del elemento y observa cada elemento recibido donde se muestra. Verifica una mutación del elemento sin cambiar la colección y, después, verifica la inserción y la eliminación. Esa secuencia permite identificar claramente qué conexión requiere atención.

Fuentes