Reputation: 4173
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
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
List
s, I didn't check.Leaf/Control views
Text
Image
Button
Color
Circle
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
Reputation: 30746
It calls body
to figure out how many View
s 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 if
s. It isn't a big deal, View
structs are just values like Int
s, 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