In my project I have two LazyVGrids inside the same ScrollView, which are dynamically loading content: pages of the first grid are requested every time the user is reaching the end, and when all the elements are loaded, this process is repeated with the second one. The problem is that when I go down the content of the second grid, the scroll makes a jump that puts me back to the end of the first one (much higher than where I was). From what I’ve seen I think it’s not that the scroll actually jumps, but that for some reason the size of the first grid suddenly increases to a completely wrong one.
The bug occurs on iOS 16.4, with Xcode 15.4, and I’ve managed to reproduce it in a separate project of only 100 lines:
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
primaryCell()
.onAppear {
loadMorePrimaryItemsIfNeeded(currentItem: item)
}
}
}
}
VStack {
LazyVGrid(columns: [GridItem(spacing: 5, alignment: .topLeading),
GridItem(alignment: .topLeading)],
alignment: .leading,
spacing: .zero) {
ForEach(secondaryItems, id: .self) { item in
secondaryCell()
.onAppear {
loadMoreSecondaryItemsIfNeeded(currentItem: item)
}
}
}
.background(Color.gray)
}
}
.onAppear {
loadMorePrimaryItems()
}
}
var header: some View {
Text("This is a header")
.frame(width: UIScreen.main.bounds.size.width, height: 100)
.background(Color.red)
}
func primaryCell() -> some View {
VStack(spacing: .zero) {
Color.green
.frame(width: UIScreen.main.bounds.size.width, height: Bool.random() ? 30 : 200)
Text(randomText())
}
}
func secondaryCell() -> some View {
VStack(spacing: .zero) {
Color.blue
.frame(width: (UIScreen.main.bounds.size.width - 5) / 2, height: Bool.random() ? 300 : 200)
Text(randomText())
}
}
func randomText() -> String {
return Bool.random() ? "Hello, world!" : "This is an example of a longer text that can be used in our SwiftUI application."
}
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 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
}
}
func loadMorePrimaryItemsIfNeeded(currentItem: Int) {
if primaryItems.last == currentItem && primaryItems.count < 100 {
loadMorePrimaryItems()
}
}
func loadMoreSecondaryItemsIfNeeded(currentItem: Int) {
if primaryItems.count == 100 {
if secondaryItems.last == currentItem || secondaryItems.isEmpty {
loadMoreSecondaryItems()
}
}
}
}
Here’s a video to better understand the problem: