当数据集较大时,SwiftUI List 显示操作(leading/trailing, contextMenu)极其缓慢。

4

我在使用 SwiftUI 的 List 显示大量数据时遇到了性能问题。我创建了一个演示应用程序来展示使用 500,000 个 String 数据集的问题,并显示一个跟随操作,当操作其中一个字符串时,CPU 占用率会达到 100%,不可用。我还尝试将 UITableView 包装成 SwiftUI 组件,使用同样的数据集(500,000个 String),并且跟随操作会立即显示。

是否有任何方法可以提高 SwiftUI List 的速度,或者这只是该框架的限制?

我已经将测试两种实现方式变得非常容易,只需更改名为 listKind 的变量,下面是示例代码:

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

这是在模拟器上还是真机上? - Jordan D
@JordanD 这种情况在模拟器和真实设备(iPhone 11 Pro)上都会发生。 - Xavi Moll
1个回答

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

Instruments

即使List已经优化以避免创建不可见的行,它仍然具有直接UITableView实现所不具备的元数据开销。使用ScrollView/LazyVStack组合来演示List的开销过高。我不建议这种替代方案(除了视觉差异外,随着列表向下滚动,它将会崩溃),但由于它不进行更改跟踪,因此它也将具有相当快的初始显示。

感谢您抽出时间对代码进行分析。您知道是否有任何方法可以防止更改跟踪以加快速度吗?(我可以放弃任何动画,我只会在显示≥100K行的列表上使用可能的修复程序) - Xavi Moll
我不知道有什么绕过它的方法。房间里的大象是这个问题:你真的需要一个包含100K+行的列表吗?相反,您可以通过在用户滚动时扩展项目数组来进行延迟加载列表。但是,如果用户实际上滚动了100k行(很可能更快),他们将遭受巨大的UI延迟。 - Eric Shieh
我知道这样的长列表并不常见,但在AppKit/UIKit中由于适当的单元格重用和不加载屏幕上不存在的内容,它们一直表现良好。我正在开发一个处理大量数据的应用程序,所有这些数据都可以进行搜索,并且理想情况下,所有内容都应该能够一次性显示出来。我需要调查一下是否使用Core Data并在NSFetchRequest上设置适当的“fetchBatchSize”可以改善情况(我不确定它是否适用于SwiftUI的@FetchRequest,我会在到达时发布我的发现)。无论如何,感谢您对此进行研究! - Xavi Moll
似乎这是一个常见的话题,“List”在处理大量数据时表现不佳:https://kean.blog/post/not-list - Xavi Moll

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接