Reputation: 287
I'm facing performance problems using a SwiftUI List
with lots of data. I've created a demo app just to showcase the problem with 500_000 String
s and to show a trailing action for one of them, the CPU hits 100% for a few seconds, which is totally unusable. I also went ahead and wrapped UITableView
to use it on SwiftUI using the same dataset (the same half a million String
s) and the trailing action is displayed instantly.
Is there any way to speed things up on the SwiftUI List
or is this just a limitation of the framework?
I've made it easy to test both implementations by just changing the var named listKind
, here's the sample code:
import SwiftUI
@main
struct LargeListPerformanceProblemApp: App {
var body: some Scene {
WindowGroup {
NavigationView {
ContentView().navigationBarTitleDisplayMode(.inline)
}
}
}
}
enum ListKind {
case slow
case fast
}
struct ContentView: View {
var listKind = ListKind.slow
var items: [String]
init() {
self.items = (0...500_000).map { "Item \($0)" }
}
var body: some View {
switch listKind {
case .slow:
List {
ForEach(items, id: \.self) { item in
Text(item).swipeActions(edge: .trailing, allowsFullSwipe: true) {
Button("Print") {
let _ = print("Tapped")
}
}
}
}.navigationTitle("Slow (SwiftUI List)")
case .fast:
FastList(items: self.items)
.navigationTitle("Fast (UITableView Wrapper)")
}
}
}
// UITableView wrapper
struct FastList: UIViewRepresentable {
let items: [String]
init(items: [String]) {
self.items = items
}
func makeUIView(context: Context) -> UITableView {
let tableView = UITableView(frame: .zero, style: .insetGrouped)
tableView.dataSource = context.coordinator
tableView.delegate = context.coordinator
return tableView
}
func updateUIView(_ uiView: UITableView, context: Context) {
uiView.reloadData()
}
func makeCoordinator() -> Coordinator {
Coordinator(items: items)
}
class Coordinator: NSObject, UITableViewDataSource, UITableViewDelegate {
var items: [String]
init(items: [String]) {
self.items = items
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
self.items.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = UITableViewCell(style: .default, reuseIdentifier: nil)
cell.textLabel?.text = self.items[indexPath.row]
return cell
}
func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
let printAction = UIContextualAction(style: .normal, title: "Print") { _, _, block in
print("Tapped")
block(true)
}
return UISwipeActionsConfiguration(actions: [printAction])
}
}
}
Upvotes: 5
Views: 1752
Reputation: 817
Profiling in instruments it appears that List creates meta data for every item in order to track changes (for things like insert/delete animations).
So even though List is optimized to avoid creating invisible rows, it still has the meta data overhead that the direct UITableView implementation doesn't incur.
One other demonstration of the overhead of List is to instead use a ScrollView/LazyVStack combination. I don't recommend this as an alternative (in addition to the visual differences, it will blow up as you scroll down the list), but because it doesn't do change tracking, it too will have a fairly quick initial display.
Upvotes: 3