@State inside a class does nothing. For iOS 17+, mark the class @Observable and own it with @State; for older targets, use ObservableObject, @Published, and @StateObject.Why this happens
append() is working. The array really does get a new element. The problem is that SwiftUI has no reason to redraw the List, because nothing it observes has changed.
In the first example, items is a @State property on the View struct. SwiftUI knows how to watch that property, so mutating it invalidates the view and the list refreshes.
In the second example, items is a property on a plain class. Putting @State on a property inside a class does not make the class observable. @State is a SwiftUI property wrapper designed for view state, not a general-purpose observation mechanism for model objects. The class also has no publisher or observation tracking that would tell SwiftUI the array changed.
There is a second, smaller problem: private var test = TestObject() is a stored property on the view struct. SwiftUI recreates view structs often, so the object identity is not guaranteed to stay stable. The model object should be owned by SwiftUI state, not created as a plain stored property.
How to fix it
1. Use @Observable on iOS 17 and later
If your deployment target is iOS 17 or newer, the cleanest fix is the Observation framework. Mark the model class with @Observable, remove @State from its properties, and hold the instance with @State in the view.
import SwiftUI
import Observation
@Observable
final class TestObject {
var items: [String] = ["item 1"]
}
struct ContentView: View {
@State private var test = TestObject()
@State private var c = 1
var body: some View {
VStack {
List(test.items, id: \.self) { item in
Text(item)
}
Button("Add") {
c += 1
test.items.append("item \(c)")
}
}
}
}
When test.items.append(...) runs, the @Observable macro has instrumented the items property. SwiftUI tracks the read of test.items in body, so the mutation triggers a refresh.
Do not put @State on items inside TestObject. The class should expose plain stored properties, and the view should own the object with @State.
2. Use ObservableObject and @Published for older targets
If you support iOS 13 through iOS 16, use Combine-based observation instead. Conform the class to ObservableObject, mark the array with @Published, and own the instance with @StateObject.
import SwiftUI
import Combine
final class TestObject: ObservableObject {
@Published var items: [String] = ["item 1"]
}
struct ContentView: View {
@StateObject private var test = TestObject()
@State private var c = 1
var body: some View {
VStack {
List(test.items, id: \.self) { item in
Text(item)
}
Button("Add") {
c += 1
test.items.append("item \(c)")
}
}
}
}
@Published sends a change notification before the property’s new value is set. ObservableObject turns that into a SwiftUI update. @StateObject creates and owns the object for the lifetime of the view.
If the object is created outside the view and passed in, use @ObservedObject instead:
struct ChildView: View {
@ObservedObject var test: TestObject
var body: some View {
List(test.items, id: \.self) { item in
Text(item)
}
}
}
Use @StateObject when the view creates the object. Use @ObservedObject when the view receives an object it does not own.
3. Remove NSObject unless you actually need it
Neither fix requires NSObject. SwiftUI observation works with plain Swift classes. Inheriting from NSObject adds Objective-C runtime behavior that is irrelevant here and can make the model harder to reason about.
If some other API requires NSObject, keep it, but do not rely on it for SwiftUI updates. The observation mechanism is @Observable or ObservableObject, not NSObject.
4. Check where the object is stored
A view struct is recreated frequently. This is fragile:
private var test = TestObject()
SwiftUI may preserve the object in simple cases, but it is not the supported ownership model. Use @State for an @Observable object, or @StateObject for an ObservableObject. That gives SwiftUI a stable storage location and a clear ownership rule.
How to prevent it
- Use
@Observablefor reference-type models when your minimum target is iOS 17 or later. - On older targets, conform model classes to
ObservableObjectand mark mutable published state with@Published. - Never put
@Stateinside a model class. Keep@Statein views or otherDynamicPropertytypes. - Own model objects with
@Stateor@StateObject; use@ObservedObjectonly for instances passed into the view. - Drop
NSObjectfrom SwiftUI model classes unless another framework requires it. - If the list still does not update, confirm the view reads the observable property directly in
body, not through a cached local copy.
FAQ
Why does append() work but the list stays the same?
Because append() mutates the array, but SwiftUI only redraws when observable state changes. A plain class does not publish changes, so SwiftUI has no signal to refresh the List.
Can I keep @State inside my model class?
No. @State is a SwiftUI property wrapper for view state. Inside a class it does not create observation and does not notify SwiftUI. Use @Observable or @Published instead.
Do I need to inherit from NSObject?
No. SwiftUI observation does not require NSObject. Remove it unless another API specifically needs Objective-C runtime behavior.
Parts of this guide are adapted from content licensed under CC BY-SA 4.0; this article is shared under the same license.
