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

SwiftUI中ScrollView嵌套多个LazyVGrid引发滚动异常的问题排查及临时解决方案

解决ScrollView嵌套多个LazyVGrid时的滚动跳变问题

最近我开发了一款按日期展示缩略图集合的App,原本的设计是每天的缩略图对应一个独立的LazyVGrid,把所有日期的LazyVGrid放到VStack里再嵌套进ScrollView。但实际跑起来才发现,这种看似可行的结构会引发滚动时的异常行为。

问题现象

运行初始代码后,滚动视图时会出现ScrollView跳变的情况,窗口越窄、列表越长,这个问题就越明显。但如果只在ScrollView里放单个LazyVGrid,就完全不会有问题。我试过调整视图的数量,结果随机性很强——有时候能正常跑一会儿,有时候一启动就复现,但只要把窗口调窄,最终肯定会出问题。后来我把VStack去掉了,问题还是存在,而且iOS端也能复现这个情况。

排查过程

  • Edit 1:按照建议把数据改成了Identifiable类型,但问题依旧。
  • Edit 4:终于找到点规律——当网格的最后一行有空余空间时就会出问题,如果所有行都填满了,运行就完全正常。

初始代码示例

import SwiftUI
struct ContentView: View {
 var body: some View {
 ScrollView {
 VStack {
 Grid(400, color: .yellow)
 Grid(300, color: .blue)
 Grid(300, color: .red)
 }
 }
 }
}
struct Item: Identifiable {
 var id: UUID = UUID()
 var num: Int
}
struct Grid: View {
 @State var items = [Item]()
 let itemsCount: Int
 let color: Color
 init(_ itemsCount: Int, color: Color) {
 self.itemsCount = itemsCount
 self.color = color
 }
 var body: some View {
 LazyVGrid(columns: [GridItem(.adaptive(minimum: 128.0, maximum: 128.0))], spacing: 5 ) {
 ForEach(items) { i in
 Text(String(i.num))
 .frame(width: 128, height:96)
 .background(self.color)
 }
 }
 .onAppear {
 for i in 1...itemsCount {
 self.items.append(Item(num:i))
 }
 }
 }
}

临时解决方案:填充占位Tile

既然找到了问题出在最后一行的空余空间,那思路就很明确了——用占位Tile把最后一行填满,让每一个LazyVGrid的所有行都是完整的。这里需要监听视图宽度来计算需要多少个占位Tile,以下是完整可运行的代码:

struct ContentView: View {
 var body: some View {
 ScrollView {
 VStack {
 Grid(400, color: .yellow)
 Grid(299, color: .blue)
 Grid(299, color: .red)
 }
 }
 }
}
// 自定义PreferenceKey,用于监听视图宽度变化
private struct SizePreferenceKey: PreferenceKey {
 static var defaultValue: CGFloat = 0
 static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {}
}
extension View {
 func readSize(onChange: @escaping (CGFloat) -> Void) -> some View {
 background(
 GeometryReader { geometryProxy in
 Color.clear
 .preference(key: SizePreferenceKey.self, value: geometryProxy.size.width)
 }
 )
 .onPreferenceChange(SizePreferenceKey.self, perform: onChange)
 }
}
struct Item: Identifiable {
 var id: UUID = UUID()
 var num: Int
}
struct Filler: Identifiable {
 var id: UUID = UUID()
}
struct Grid: View {
 @State var items = [Item]()
 @State var width: CGFloat = 0 // 存储当前视图的宽度
 let itemsCount: Int
 let color: Color
 let hSpacing = CGFloat(5) // 统一管理水平间距,避免多处硬编码
 init(_ itemsCount: Int, color: Color) {
 self.itemsCount = itemsCount
 self.color = color
 }
 var body: some View {
 // 监听视图宽度,更新width状态
 GeometryReader { geometryProxy in
 Color.clear.readSize(onChange: { s in
 self.width = s
 })
 }
 // 核心的LazyVGrid部分
 LazyVGrid(columns: [GridItem(.adaptive(minimum: 128.0, maximum: 128.0),spacing: hSpacing)], spacing: 5) {
 ForEach(items) { i in
 Text(String(i.num))
 .frame(width: 128, height:96)
 .background(self.color)
 }
 // 添加占位Tile填充最后一行的空隙
 ForEach(self.fillers(items.count)) { _ in
 Rectangle()
 .size(CGSize(width: 128, height: 96))
 .fill(Color.gray)
 }
 }
 .onAppear {
 for i in 1...itemsCount {
 self.items.append(Item(num:i))
 }
 }
 }
 // 计算需要的占位Tile数量
 func fillers(_ total: Int) -> [Filler] {
 guard width > 0 else { return [] }
 var columnsf = CGFloat(0)
 // 算出当前宽度下能容纳的列数
 while (columnsf * 128) + (columnsf - 1) * hSpacing <= width {
 columnsf += 1
 }
 let columns = Int(columnsf - 1)
 let lastRowTiles = CGFloat(total).truncatingRemainder(dividingBy: CGFloat(columns))
 let neededFillers = columns - Int(lastRowTiles)
 // 只有当最后一行有空隙时才添加占位
 guard lastRowTiles > 0, neededFillers > 0 else { return [] }
 return Array(repeating: Filler(), count: neededFillers)
 }
}
struct ContentView_Previews: PreviewProvider {
 static var previews: some View {
 ContentView()
 }
}

不得不说这是我写过的最粗糙的临时方案,但确实能解决滚动跳变的问题。分享出来是希望能给遇到同样问题的开发者提供一个思路,也期待有更优雅的解决方案出现。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.29 09:27:40