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
相关产品推荐
相关产品推荐

