You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

SwiftUI水平ScrollView滚动添加子视图的性能优化问题

问题分析与优化方案

针对你遇到的水平无限滚动追加数据时的跳帧问题,以下是具体的排查思路和优化方向:

1. 替换基于索引范围的ForEach,改用实际数据数组

你当前用0...fetcher.lastIndexToShow(section)作为ForEach数据源,且以索引作为id。这种方式会导致每次lastIndexToShow变化时,SwiftUI需要重新计算整个序列的视图差异,哪怕只是新增一个元素。改用实际的items数组作为数据源,结合Identifiable协议让SwiftUI精准识别新增项:

// 先确保你的Game类型遵循Identifiable
struct Game: Identifiable {
    let id: String // 替换为你的唯一标识字段
    // 其他属性
}

// 修改ScrollView代码
ScrollView(.horizontal) {
    LazyHStack(spacing: 10) {
        ForEach(fetcher.items[section] ?? []) { item in
            Group {
                if let index = fetcher.items[section]?.firstIndex(of: item), index < 30 {
                    ItemCard(item: item)
                } else {
                    Rectangle()
                        .frame(width: 400, height: 220)
                        .opacity(1)
                        .padding(10)
                }
            }
            .onAppear {
                // 仅在当前项是倒数第5个时触发加载,避免频繁请求
                guard let items = fetcher.items[section], 
                      let currentIndex = items.firstIndex(of: item),
                      currentIndex >= items.count - 5,
                      !fetcher.isLoading[section] else { return }
                try? fetcher.fetchNewPageIfNeeded(section: section, index: currentIndex)
            }
        }
        // 添加加载状态占位,替代透明矩形填充
        if fetcher.isLoading[section] == true {
            ProgressView()
                .frame(width: 400, height: 220)
        }
    }
}

2. 批量加载数据,减少Published变量更新频率

你当前每次仅追加单个元素(append(contentsOf: [res.data[0]])),会导致items频繁触发Published更新,每次更新都会触发视图重绘。改成批量加载整页数据,大幅减少更新次数:

// 在Fetcher中添加加载状态标记
class Fetcher: ObservableObject {
    @Published var items: [String: [Game]] = [:]
    @Published var isLoading: [String: Bool] = [:]
    // 其他属性和方法
}

// 修改fetchNextPage方法
func fetchNextPage(section: String) throws {
    // 避免重复请求
    guard !fetcher.isLoading[section] else { return }
    fetcher.isLoading[section] = true
    
    Task { @MainActor in
        do {
            guard let res: ApiResponse<Game> = try await fetch(url: url) else {
                fetcher.isLoading[section] = false
                return
            }
            if !res.data.isEmpty {
                // 批量追加整页数据
                items[section]?.append(contentsOf: res.data)
            }
            fetcher.isLoading[section] = false
        } catch {
            fetcher.isLoading[section] = false
            // 这里可以添加错误处理逻辑
        }
    }
}

3. 缩小ObservableObject的更新粒度

当前items是字典类型,某个section的数组更新时,SwiftUI会认为整个字典发生变化,可能触发不必要的父视图重绘。可以将每个section的数据单独包装成ObservableObject,缩小重绘范围:

// 单个section的数据模型
class SectionData: ObservableObject {
    @Published var items: [Game] = []
    @Published var isLoading = false
    
    func fetchNextPage(url: URL) async throws {
        guard !isLoading else { return }
        isLoading = true
        defer { isLoading = false }
        
        guard let res: ApiResponse<Game> = try await fetch(url: url) else { return }
        if !res.data.isEmpty {
            items.append(contentsOf: res.data)
        }
    }
}

// 修改Fetcher
class Fetcher: ObservableObject {
    @Published var sections: [String: SectionData] = [:]
    
    func sectionData(for section: String) -> SectionData {
        if let existing = sections[section] {
            return existing
        }
        let newData = SectionData()
        sections[section] = newData
        return newData
    }
}

// 使用示例
let sectionData = fetcher.sectionData(for: section)
ScrollView(.horizontal) {
    LazyHStack(spacing: 10) {
        ForEach(sectionData.items) { item in
            Group {
                if let index = sectionData.items.firstIndex(of: item), index < 30 {
                    ItemCard(item: item)
                } else {
                    Rectangle()
                        .frame(width: 400, height: 220)
                        .opacity(1)
                        .padding(10)
                }
            }
            .onAppear {
                guard let index = sectionData.items.firstIndex(of: item),
                      index >= sectionData.items.count - 5,
                      !sectionData.isLoading else { return }
                Task {
                    try? await sectionData.fetchNextPage(url: yourTargetUrl)
                }
            }
        }
        if sectionData.isLoading {
            ProgressView()
                .frame(width: 400, height: 220)
        }
    }
}

4. 移除冗余视图层级与任务触发

你当前给每个索引视图都添加了.task修饰符,滚动时会触发大量并发任务,增加主线程压力。改用.onAppear仅在特定条件下触发加载,同时移除不必要的ZStack(直接用Group包裹条件视图即可):

// 简化后的视图结构
ForEach(sectionData.items) { item in
    Group {
        // 条件渲染逻辑
    }
    .onAppear {
        // 加载触发逻辑
    }
}

5. 手动维护滚动偏移(可选)

如果以上优化后仍有跳帧,可能是SwiftUI更新视图时重新计算了ScrollView内容尺寸导致偏移变化。可以通过GeometryReader和PreferenceKey手动保存并恢复滚动偏移:

@State private var scrollOffset: CGFloat = 0

// 定义偏好键
struct ScrollOffsetKey: PreferenceKey {
    static var defaultValue: CGFloat = 0
    static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {}
}

// 使用示例
ScrollView(.horizontal) {
    LazyHStack(spacing: 10) {
        // 内容视图
    }
    .background(
        GeometryReader { proxy in
            Color.clear
                .preference(key: ScrollOffsetKey.self, value: proxy.frame(in: .named("horizontalScroll")).minX)
        }
    )
}
.coordinateSpace(name: "horizontalScroll")
.onPreferenceChange(ScrollOffsetKey.self) { offset in
    scrollOffset = offset
}
.onReceive(sectionData.$items) { _ in
    // 数据更新后恢复偏移
    DispatchQueue.main.async {
        // 结合ScrollViewReader实现精准滚动恢复
        // scrollViewProxy.scrollTo(...)
    }
}

内容的提问来源于stack exchange,提问作者ssantos

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.26 10:20:56