0

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.

Instrument Leak Test

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.

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.