当数据集很大时,SwiftUI 列表在显示操作前导/尾随、上下文菜单方面非常慢

问题描述

我在使用包含大量数据的 SwiftUI List 时遇到了性能问题。我创建了一个演示应用程序只是为了展示 500_000 String 秒的问题并显示其中一个的尾随操作,cpu 达到 100% 几秒钟,这是完全无法使用的。我还继续包装 UITableView 以在 SwiftUI 上使用相同的数据集(相同的 50 万个 String)使用它,并立即显示尾随操作。

有什么方法可以加快 SwiftUI List 上的速度,还是这只是框架的限制?

我只需更改名为 listKind 的 var 即可轻松测试这两种实现,示例代码如下:

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])
        }
    }
}

解决方法

在工具中进行分析,似乎 List 为每个项目创建元数据以跟踪更改(例如插入/删除动画)。

Instruments

因此,即使 List 经过优化以避免创建不可见的行,它仍然具有直接 UITableView 实现不会产生的元数据开销。

List 开销的另一个演示是改为使用 ScrollView/LazyVStack 组合。我不建议将此作为替代方案(除了视觉差异之外,当您向下滚动列表时,它会爆炸),但由于它不进行更改跟踪,因此它也将具有相当快的初始显示。