SwiftUI List未实现视图实例化懒加载?如何实现真正懒加载?
核心问题说明
SwiftUI的List官方宣称的“懒加载”仅针对渲染阶段(即仅绘制可见区域的内容),但和UIKit中UITableView/UICollectionView的“实例化懒加载”(仅创建可见cell实例)不是同一概念。你的测试代码中所有Row结构体被提前初始化、body被计算,正是因为SwiftUI的视图树构建逻辑:当你在List的闭包里直接返回Row(value: el)时,SwiftUI会遍历整个数据源数组,提前创建所有对应的View结构体实例并计算body,这和List的渲染懒加载机制无关。
而LazyVStack的设计则完全不同——它会延迟创建视图实例,只有当视图即将进入可见区域时,才会初始化对应的View结构体并计算body,这才是你期望的“实例化层面的懒加载”。
解决方案
方案1:用LazyVStack + ScrollView替代List
如果不需要List的原生交互(如滑动删除、下拉刷新),直接用LazyVStack包裹在ScrollView中即可实现实例化懒加载:
struct ContentView: View { var elements: [String] = (0..<100000).map(\.description) var body: some View { ScrollView { LazyVStack { ForEach(elements, id: \.self) { el in Row(value: el) .id(el) } } } } } struct Row: View, Equatable { var value: String init(value: String) { print("Row.init \(value)") self.value = value } var body: some View { let _ = print("Row.body \(value)") Text(value) } }
运行后会发现,只有可见区域的Row会被初始化和计算body,滚动到新区域时才会创建新的实例。
方案2:封装UIKit列表组件(保留原生交互)
如果需要List的原生交互特性,可通过UIViewRepresentable封装UIKit的UITableView,完全控制cell的实例化逻辑:
struct UIKitListView: UIViewRepresentable { var elements: [String] func makeUIView(context: Context) -> UITableView { let tableView = UITableView() tableView.register(UITableViewCell.self, forCellReuseIdentifier: "Cell") tableView.dataSource = context.coordinator return tableView } func updateUIView(_ uiView: UITableView, context: Context) { uiView.reloadData() } func makeCoordinator() -> Coordinator { Coordinator(self) } class Coordinator: NSObject, UITableViewDataSource { var parent: UIKitListView init(_ parent: UIKitListView) { self.parent = parent } func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { parent.elements.count } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) cell.textLabel?.text = parent.elements[indexPath.row] print("Cell created for \(parent.elements[indexPath.row])") return cell } } } // 使用示例 struct ContentView: View { var elements: [String] = (0..<100000).map(\.description) var body: some View { UIKitListView(elements: elements) } }
这种方式完全遵循UIKit的cell复用机制,仅创建可见区域的cell实例,实现真正的实例化懒加载。
补充说明
iOS上的List底层虽基于UITableView,但SwiftUI的视图构建逻辑与UIKit的cell复用是分离的:SwiftUI会提前构建所有View结构体,再将可见的View同步到UIKit的cell中,因此你会看到所有Row被初始化,但只有可见的才会被渲染到屏幕上。这就是为什么iOS(包括iOS 18及以下版本)也存在该问题。
内容的提问来源于stack exchange,提问作者John

