Hello 👋
I ran into a SwiftUI lifecycle gotcha while debugging a List with .refreshable
I share the code used to reproduce the issue
@Observable
final class CounterModel: Identifiable {
let id: String
var title: String
var value: Int
init(id: String, title: String, value: Int = 0) {
self.id = id
self.title = title
self.value = value
}
deinit {
print("deinit", title)
}
}
@Observable
final class ObservableCountersStore {
var counters: [CounterModel] = [
.init(id: "1", title: "A"),
.init(id: "2", title: "B"),
.init(id: "3", title: "C")
]
func refresh() async {
try? await Task.sleep(nanoseconds: 300_000_000)
counters = [.init(id: "4", title: "D")]
}
}
struct ObservableCountersListView: View {
@State private var store = ObservableCountersStore()
var body: some View {
List {
ForEach(store.counters) { counter in
ObservableCounterRow(counter: counter)
}
}
.refreshable {
await store.refresh()
}
}
}
struct ObservableCounterRow: View {
let counter: CounterModel
var body: some View {
Text(counter.title)
}
}
Observation:
After calling refresh(), only some of the previous CounterModel
only one CounterModel is deallocated immediately.
Others are retained
This doesn’t look like a leak, but it made me realize that passing observable
reference types directly into List rows leads to non-deterministic object
lifetimes, especially with .refreshable.
Posting this as a gotcha — curious if this matches intended behavior or if others have run into the same thing.