I'm trying to implement a SwiftUI Table and users would be able to add/define items (rows) and properties (columns). I expect there to be large amounts of items. For persistence, I'm hoping to use SwiftData.
Item and Property are defined using @Observable (to be switched to @Model). The items' properties are stored in a dictionary, referencing the Property.ID, so that they can be expanded by the user.
What I've been struggling with is the increasing memory consumption when I scroll up and down the table (I have other features, e.g. cell selection, text editing, using @Observable objects which seem to amplify the memory problem but I suspect its the same cause).
Here's a minimal reproducible code with arbitrary 300 items and 5 properties. I've also been experimenting with different data structures/views which are commented out.
import SwiftUI
// MARK: - Item
@Observable class Item: Identifiable {
// struct Item: Identifiable {
var id: String
var index: Int
var properties: [Property.ID: String]
var propA: String
var propB: String
var propC: String
var propD: String
var propE: String
init(
id: String = UUID().uuidString,
index: Int,
properties: [Property.ID: String] = [:],
propA: String = "",
propB: String = "",
propC: String = "",
propD: String = "",
propE: String = ""
) {
self.id = id
self.index = index
self.properties = properties
self.propA = propA
self.propB = propB
self.propC = propC
self.propD = propD
self.propE = propE
}
static var items: [Item] = (0..<300).map {
Item(
index: $0,
properties: [
Property.propA.id: "PropA-\($0)",
Property.propB.id: "PropB-\($0)",
Property.propC.id: "PropC-\($0)",
Property.propD.id: "PropD-\($0)",
Property.propE.id: "PropE-\($0)",
],
propA: "PropA-\($0)",
propB: "PropB-\($0)",
propC: "PropC-\($0)",
propD: "PropD-\($0)",
propE: "PropE-\($0)"
)
}
}
// MARK: - Property
@Observable class Property: Identifiable {
// struct Property: Identifiable {
var id: String
var label: String
init(id: String = UUID().uuidString, label: String) {
self.id = id
self.label = label
}
static var propA = Property(id: "propA", label: "PropA")
static var propB = Property(id: "propB", label: "PropB")
static var propC = Property(id: "propC", label: "PropC")
static var propD = Property(id: "propD", label: "PropD")
static var propE = Property(id: "propE", label: "PropE")
static var props: [Property] = [propA,propB,propC,propD,propE]
}
View:
struct TableView: View {
@State private var items: [Item] = Item.items
@State private var props: [Property] = Property.props
var body: some View {
Table(items) {
TableColumnForEach(props) { prop in
TableColumn(prop.label) { item in
Text(item.properties[prop.id] ?? "")
}
}
}
// Table(items) {
// TableColumn("PropA", value: \.propA)
// TableColumn("PropB", value: \.propB)
// TableColumn("PropC", value: \.propC)
// TableColumn("PropD", value: \.propD)
// TableColumn("PropE", value: \.propE)
// }
// List(items) { item in
// HStack {
// ForEach(props) { prop in
// Text(item.properties[prop.id] ?? "")
// }
// }
// }
// List(items) { item in
// HStack {
// Text(item.propA)
// Text(item.propB)
// Text(item.propC)
// Text(item.propD)
// Text(item.propE)
// }
// }
}
}
For this example, the memory starts at ~30MB, every scroll to the bottom+top the memory increases without coming back down, and ~12 scroll cycles already gets to ~100MB with no limit (if I include other features ~6 scroll cycles = 100MB which is unbearable). I tried closing the simulation app and opening again but the memory remains high.
Each of these changes seems to alleviate the problem a little: switching either/both model to struct, using static columns instead of dictionary, using a List+HStack.
The problem goes away completely for Table if I use struct Item and static columns. But what's interesting is that for List, I can use Observable Item + static columns, there's also no problem, which leads me to believe using observable objects for large list/table is ok (I also don't see anywhere in documentation suggesting it is not ok use observable objects vs struct in this scenario).
Dictionary seems to cause problem in either scenario, but I don't understand why (i don't see any where that can cause reference cycle?). Instruments show there are leaks but I'm not sure where its coming from.
I also understand Table uses NSTableView (as does List) and it reuses views which is why I chose Table rather than e.g. LazyVGrid for large amounts of data, but I'm not sure whether this reusing behaviour has anything to do with the memory problem.
Any help is appreciated.
