gswierczynski
gswierczynski

Reputation: 4173

iOS SwiftUI List is not lazy

I read everywhere that List is supposed to be Lazy on iOS but following snippet seems to contradict it

import SwiftUI

struct Item {
    var id: Int
}

let items = (1...30000)
    .map { v in
        Item(id: v)
    }

struct ItemRow:View{
    let item: Item
    init(item: Item){
        self.item = item
        print("init #\(item.id)")
    }
    var body: some View{
        Text(String(item.id))
    }
}

struct ContentView: View {
    var body: some View {
//        ScrollView {
//            LazyVStack {
//                ForEach(items, id: \.id) { item in
//                    ItemRow(item: item)
//                }
//            }
//        }
        List(items, id: \.id) { item in
            ItemRow(item: item)
        }
    }
}

This prints out 30000 times twice (new empty project). Also checked that ItemRow's body is also called for all 30k items immediately.

Am I missing something?

Upvotes: 1

Views: 134

Answers (2)

Kevin
Kevin

Reputation: 56129

TL;DR wrap all the rows in a VStack or similar container.

At a high level, laziness requires a List to know from its Content type that all iterations will result in the same number of cells, and how many that is. Evidently the algorithm to do this only crawls known concrete classes and does not check the View.Body associated type on custom views.

List will be lazy if and only if its body conforms to the above. If the List body contains one or more ForEach loops, either directly or nested in Section, the List itself will be greedily iterated but the ForEach(es) will be lazy iff their bodies meet the same requirements.

An extensive but probably not comprehensive list of acceptable views:

Container views, regardless of contents

  • HStack / VStack / ZStack
  • LazyHStack / LazyVStack
  • LazyHGrid / LazyVGrid
  • Grid
  • Table
  • ScrollView
  • Probably nested Lists, I didn't check.

Leaf/Control views

Certain transparent/helper views if and only if their content meets the requirements, e.g.

  • TupleView (implicit wrapper for multiple views)
  • Group (only groups for purpose of modifiers; is NOT a container.)
  • ScrollReader
  • GeometryReader

Upvotes: 1

malhal
malhal

Reputation: 30746

It calls body to figure out how many Views there are per row. It needs to do that to calculate the size of the List. It is much faster if you have constant number of Views per row, i.e. avoid any any ifs. It isn't a big deal, View structs are just values like Ints, negligible performance-wise. As long as you don't do anything slow in body, e.g. accidentally init a heap object or a sort. This was actually recently covered at WWDC 2023 https://developer.apple.com/videos/play/wwdc2023/10160?time=806

To speed it up you can just do:

List(items) { item in
    ItemRow(itemID: item.id) // since you only need the id it wont call body if another property of item is changed.
}

or

List(items) { item in
    Text(item.id, format: .number)
    Text(item.text)
}

It's best to only pass in the data Views need to keep them small and fast. E.g. in the last example it avoids the pointless wrapper ItemRow.

Upvotes: 0

Related Questions