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

SwiftUI中单个ScrollView嵌套两个LazyVGrid的滚动跳转问题

ScrollView嵌套两个动态加载LazyVGrid时滚动跳转问题

问题详情

同一ScrollView内包含两个LazyVGrid,均实现动态加载逻辑:

  • 滚动至第一个网格底部时加载下一页,直至加载完100条内容
  • 第一个网格加载完成后,滚动至第二个网格底部时加载其内容

问题现象:滚动第二个网格内容时,ScrollView会突然跳回第一个网格的末尾位置,推测是第一个LazyVGrid的尺寸计算出现异常。

环境:iOS 16.4、Xcode 15.4

复现代码

import SwiftUI

struct ContentView: View {
    @State private var primaryItems = [Int]()
    @State private var secondaryItems = [Int]()
    @State private var primaryLoading = false
    @State private var secondaryLoading = false
    
    var body: some View {
        ScrollView {
            LazyVGrid(columns: [GridItem(alignment: .topLeading)],
                      alignment: .leading,
                      spacing: .zero,
                      pinnedViews: .sectionHeaders) {
                Section(header: header) {
                    ForEach(primaryItems, id: \.self) { item in
                        cellView(width: UIScreen.main.bounds.size.width)
                    }
                    Color.clear
                        .frame(height: 1)
                        .onAppear {
                            loadMorePrimaryItems()
                        }
                }
            }
            LazyVGrid(columns: [GridItem(spacing: 5, alignment: .topLeading),
                                GridItem(alignment: .topLeading)],
                      alignment: .leading,
                      spacing: .zero) {
                ForEach(secondaryItems, id: \.self) { item in
                    cellView(isSecondary: true, width: (UIScreen.main.bounds.size.width - 5) / 2)
                }
                Color.clear
                    .frame(height: 1)
                    .onAppear {
                        loadMoreSecondaryItems()
                    }
            }
            .background(Color.gray)
        }
    }
    
    var header: some View {
        Text("This is a header")
            .frame(width: UIScreen.main.bounds.size.width, height: 100)
            .background(Color.red)
    }
    
    func cellView(isSecondary: Bool = false, width: CGFloat) -> some View {
        VStack(spacing: .zero) {
            (isSecondary ? Color.blue : Color.green)
                .frame(width: width, height:  200)
            
            Text("Hello, world!")
        }
    }
    
    func loadMorePrimaryItems() {
        guard !primaryLoading && primaryItems.count < 100 else { return }
        primaryLoading = true
        
        DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
            let startIndex = primaryItems.count
            let endIndex = min(startIndex + 20, 100)
            let newItems = Array(startIndex..<endIndex)
            primaryItems.append(contentsOf: newItems)
            primaryLoading = false
            // Start loading secondary items if primary items reached 100
            if primaryItems.count == 100 {
                loadMoreSecondaryItems()
            }
        }
    }
    
    func loadMoreSecondaryItems() {
        guard !secondaryLoading, primaryItems.count == 100 else { return }
        secondaryLoading = true
        
        DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
            let startIndex = secondaryItems.count
            let newItems = Array(startIndex..<startIndex+20)
            secondaryItems.append(contentsOf: newItems)
            secondaryLoading = false
        }
    }
}

问题演示

问题演示动图

解决方案

方案1:设置第一个LazyVGrid的布局优先级

给第一个LazyVGrid添加.layoutPriority(1),让SwiftUI优先计算其布局,避免第二个网格加载时干扰其高度计算,这是最简洁的修复方式:

LazyVGrid(columns: [GridItem(alignment: .topLeading)],
          alignment: .leading,
          spacing: .zero,
          pinnedViews: .sectionHeaders) {
    // 原有内容
}
.layoutPriority(1) // 添加此行

方案2:使用ScrollViewReader稳定滚动位置

通过ScrollViewReader记录当前滚动位置,在加载内容后手动恢复,避免位置跳转:

@State private var lastScrollID: String? = nil

var body: some View {
    ScrollViewReader { proxy in
        ScrollView {
            // 第一个LazyVGrid给底部标记添加ID
            LazyVGrid(columns: [GridItem(alignment: .topLeading)],
                      alignment: .leading,
                      spacing: .zero,
                      pinnedViews: .sectionHeaders) {
                Section(header: header) {
                    ForEach(primaryItems, id: \.self) { item in
                        cellView(width: UIScreen.main.bounds.size.width)
                    }
                    Color.clear
                        .frame(height: 1)
                        .id("primary-bottom")
                        .onAppear {
                            loadMorePrimaryItems()
                        }
                }
            }
            
            // 第二个LazyVGrid加载时记录并恢复位置
            LazyVGrid(columns: [GridItem(spacing: 5, alignment: .topLeading),
                                GridItem(alignment: .topLeading)],
                      alignment: .leading,
                      spacing: .zero) {
                ForEach(secondaryItems, id: \.self) { item in
                    cellView(isSecondary: true, width: (UIScreen.main.bounds.size.width - 5) / 2)
                }
                Color.clear
                    .frame(height: 1)
                    .onAppear {
                        // 记录当前可见位置的ID
                        lastScrollID = proxy.currentAnchor?.id as? String
                        loadMoreSecondaryItems()
                    }
                    .onDisappear {
                        if let id = lastScrollID {
                            proxy.scrollTo(id, anchor: .bottom)
                        }
                    }
            }
            .background(Color.gray)
        }
    }
}

方案3:移除第一个LazyVGrid的固定表头(业务允许时)

如果不需要固定表头,移除.pinnedViews(.sectionHeaders)参数可直接解决布局计算异常问题:

LazyVGrid(columns: [GridItem(alignment: .topLeading)],
          alignment: .leading,
          spacing: .zero) { // 移除pinnedViews参数
    // 原有内容
}

内容的提问来源于stack exchange,提问作者Jaime Alcántara Arnela

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.20 17:49:56