侧边栏壁纸
博主头像
colo

欲买桂花同载酒

  • 累计撰写 1823 篇文章
  • 累计收到 0 条评论

实现高性能无限滚动列表,支持动态内容高度和预加载

2025-12-12 / 0 评论 / 4 阅读

题目

实现高性能无限滚动列表,支持动态内容高度和预加载

信息

  • 类型:问答
  • 难度:⭐⭐⭐

考点

SwiftUI视图更新机制, 列表性能优化, 异步数据加载, 自定义布局, 动态内容高度计算

快速回答

实现高性能无限滚动列表的关键点:

  • 使用LazyVStack替代List避免默认视图回收机制限制
  • 通过GeometryReaderPreferenceKey动态计算内容高度
  • 实现onAppear触发预加载逻辑,结合Task处理异步数据
  • 使用EquatableView优化视图更新,避免不必要的重绘
  • 添加滚动偏移监听实现提前加载和内存管理
## 解析

核心挑战与解决方案

在SwiftUI中实现高性能无限滚动列表需要解决:

  1. 动态高度计算:内容高度不固定导致滚动跳跃
  2. 内存管理:大量数据导致内存溢出
  3. 异步加载阻塞:滚动卡顿
  4. 精确预加载:平衡性能和用户体验

完整实现方案

// 1. 数据模型
struct DynamicItem: Identifiable, Equatable {
    let id = UUID()
    var content: String
    var calculatedHeight: CGFloat = 0 // 存储计算高度
}

// 2. 高度偏好键
struct HeightPreferenceKey: PreferenceKey {
    static var defaultValue: [UUID: CGFloat] = [:]
    static func reduce(value: inout [UUID: CGFloat], nextValue: () -> [UUID: CGFloat]) {
        value.merge(nextValue()) { $1 }
    }
}

// 3. 主视图
struct InfiniteScrollView: View {
    @StateObject private var viewModel = InfiniteScrollViewModel()
    @State private var visibleItems: Set<UUID> = []

    var body: some View {
        ScrollView {
            LazyVStack(spacing: 0) {
                ForEach(viewModel.items) { item in
                    DynamicRow(content: item.content)
                        .equatable() // 优化重绘
                        .background(
                            GeometryReader { proxy in
                                Color.clear
                                    .preference(
                                        key: HeightPreferenceKey.self,
                                        value: [item.id: proxy.size.height]
                                    )
                            }
                        )
                        .onAppear {
                            // 触发加载和高度更新
                            visibleItems.insert(item.id)
                            viewModel.loadMoreIfNeeded(currentItem: item)

                            // 内存优化:移除屏幕外项目
                            if visibleItems.count > 50 {
                                visibleItems.removeFirst()
                            }
                        }
                }
            }
            .onPreferenceChange(HeightPreferenceKey.self) { heights in
                viewModel.updateHeights(heights)
            }
        }
        .frame(maxHeight: .infinity)
        .onAppear { viewModel.initialLoad() }
    }
}

// 4. 动态行视图
struct DynamicRow: View, Equatable {
    let content: String
    static func == (lhs: Self, rhs: Self) -> Bool {
        lhs.content == rhs.content
    }

    var body: some View {
        Text(content)
            .padding(16)
            .frame(maxWidth: .infinity, alignment: .leading)
            .background(Color.gray.opacity(0.1))
    }
}

// 5. 视图模型
class InfiniteScrollViewModel: ObservableObject {
    @Published var items: [DynamicItem] = []
    private var currentPage = 0
    private var isLoading = false

    func initialLoad() {
        loadPage(page: 0)
    }

    func loadMoreIfNeeded(currentItem: DynamicItem) {
        guard let index = items.firstIndex(where: { $0.id == currentItem.id }),
              index > items.count - 5,
              !isLoading else { return }

        currentPage += 1
        loadPage(page: currentPage)
    }

    private func loadPage(page: Int) {
        isLoading = true
        Task {
            // 模拟网络请求
            let newItems = await fetchData(page: page)
            await MainActor.run {
                items.append(contentsOf: newItems)
                isLoading = false
            }
        }
    }

    func updateHeights(_ heights: [UUID: CGFloat]) {
        for (id, height) in heights {
            if let index = items.firstIndex(where: { $0.id == id }) {
                items[index].calculatedHeight = height
            }
        }
    }

    private func fetchData(page: Int) async -> [DynamicItem] {
        // 实际项目替换为真实网络请求
        return (0..<20).map { _ in
            DynamicItem(content: String(repeating: "随机内容 ", count: .random(in: 5...50)))
        }
    }
}

关键技术解析

  • LazyVStack vs ListList在iOS 16+有改进,但LazyVStack提供更灵活的高度控制
  • 动态高度计算GeometryReader获取实际渲染高度,通过PreferenceKey回传数据
  • 预加载策略:当滚动到倒数第5项时触发加载,避免过早/过晚加载
  • 内存优化:跟踪可见项ID,移除屏幕外项引用(实际数据保留)
  • EquatableView:防止内容未变化时重绘

最佳实践

  1. 分页策略:实现lastItemIndex - threshold触发加载,阈值根据内容高度动态调整
  2. 错误处理:添加加载状态和错误重试机制
  3. 占位符:加载时显示骨架屏避免布局跳动
  4. 缓存策略:使用NSCache缓存计算过的高度
  5. Combine优化:使用Debounce处理快速滚动事件

常见错误

错误后果解决方案
直接使用List动态高度支持差,内存回收不可控改用LazyVStack或自定义UICollectionView包装
未实现高度缓存滚动时反复计算导致卡顿将计算高度存储在数据模型中
同步加载数据主线程阻塞导致滚动卡顿确保数据加载在Task中异步执行
无可见项跟踪内存无限增长最终崩溃实现屏幕外项卸载机制

扩展知识

  • UIKit集成:复杂场景可包装UICollectionView利用UICollectionViewCompositionalLayout
  • DiffableDataSource:处理大规模数据更新时保证性能
  • SwiftUI-Introspect:第三方库访问底层UIScrollView实现精确控制
  • Prefetching API:iOS 15+使用contentMarginsprefetch增强体验