4

I have a SwiftUI app which displays large lists of 1000 to 5000 items. I noticed that on macOS displaying such a long list has very bad performance. It takes several seconds for SwiftUI to render the list. This is independent of the complexity of the row views. Even if the rows are only Text() views.

On iOS, however, the same list would render almost instantaneously.

My code for the list view looks like this:

struct WordList: View {
    
    @EnvironmentObject var store: Store
    @State var selectedWord: RankedWord? = nil

    var body: some View {
        List(selection: $selectedWord) {
            ForEach(store.words) { word in
                HStack {
                    Text("\(word.rank)")
                    Text(word.word)
                }
                .tag(word)
            }
        }
    }
}

Does anybody know some tricks to speed this up? Or is this a general problem on macOS 12 and we need to hope Apple improves this in the next major OS update?

I have also created a very simple sample app to test and demonstrate the list performance and of which the code above is taken from. You can browse / download it on GitHub

Update for Ventura

List performance on Ventura has significantly improved over Monterey. So no additional optimization might be necessary.

codingFriend1
  • 6,487
  • 6
  • 45
  • 67

3 Answers3

3

If you want to stay with List because you need all this nice features like selection, reordering, easy drag & drop... you will have to help SwiftUI estimate the total height of your list by having a fixed size for your rows. (I think this is the same in UIKit where performance will significantly improve if you are able to estimate the row height for each entry.)

So in your example, modify your row code as follows:

HStack {
    Text("\(word.rank)")
    Text(word.word)
}
.frame(width: 500, height: 15, alignment: .leading)
.tag(word)

I know it is an ugly solution because it doesn't dynamically adjust to the font size but it reduces the rendering time on my M1 Max based Mac from 2s down to 0.3s for a list of 10,000 words.

pd95
  • 1,999
  • 1
  • 20
  • 33
  • 1
    I think this is the best answer until Apple applies performance improvements on a system level. I had actually tried setting a fixed height. Yet it actually leads to significant performance improvements (about 5-fold) but only when *width and height* are set fixed. Setting the width is of course quite restrictive. I now use `GeometryReader` to set it depending on the List view. But be warned: this makes resizing the window laggy. Regarding the height: `ScaledMetric` at least preserves accessibility and dynamic font sizing. – codingFriend1 May 04 '22 at 16:46
1

List seems to be not lazy on macOS. But you can use Table which is lazy, and supports single or multiple selection:

struct WordList_mac: View {
    
    @EnvironmentObject var store: Store
    @State var selectedWord: RankedWord.ID? = nil

    var body: some View {

        Table(store.words, selection: $selectedWord) {
            TableColumn("Rank") { Text("\($0.rank)") }
            TableColumn("Word", value: \.word)

        }
    }
}
ChrisR
  • 9,523
  • 1
  • 8
  • 26
  • 2
    I tested with `Table` of 10 columns where the data is a mix of `String`, `Int`, and `Date` and I use `.formatted()` for formatting them inside a `Text` view. The performance is very slow when scrolling or resizing the window if the number of rows is more than about 150. CPU usage spikes to 100% on my Intel Mac. – alobaili May 04 '22 at 15:18
  • Thanks! This is a great alternative to List and in fact Table is highly performant. Also it is possible to use custom views in the columns. However, there will always be a column heading and it does not replace List in every use case. – codingFriend1 May 04 '22 at 16:48
  • @alobaili how is it with the new xcode beta? – aehlke Jul 04 '22 at 20:36
  • 1
    @aehlke I switched to an M1 Mac and I don't use beta versions of Xcode unfortunately. For my app I switched to AppKit at the time of writing my first comment. When macOS Ventura comes out I will test SwiftUI's `Table` again and see if there're any new optimizations. – alobaili Jul 05 '22 at 21:42
-2

What you wrote is fully generic code with say 5,000 user interface elements. A good old UITableView will handle this easily - it is one UI element instead of 5,000, and it creates reusable cells just for rows of the table that are visible on the screen (say 30 on an iPad, instead of 5,000.

gnasher729
  • 51,477
  • 5
  • 75
  • 98
  • 3
    Thanks, but this is not really an answer to the question. I want to use a SwiftUI solution so I don't need to to implement both, NSTableView and UITableView, for a multi-platform app. – codingFriend1 May 01 '22 at 08:32
  • Under the hood SwiftUI is using `NSTableView` and cell re-use, but as the content of the cells is not necessarily of fixed height, `NSTableView` is querying each row for its height which makes SwiftUI evaluate the body of each row over and over. – pd95 May 03 '22 at 18:31