I have a long home feed built as a SwiftUI ScrollView containing a VStack of sections. The last section is paginated when the user scrolls near the bottom, I fetch the next page and append rows to that section’s data array.
Once there is a large amount of content above the paginated section, scrolling slowly through the paginated section causes the visible rows to shift/replace under the finger when a new page is appended as if the content is “refreshing” while stationary. The scroll offset itself does not reset (I logged it, it climbs monotonically), but the visible rows visibly jump.
The jump only appears when the content above the paginated section is tall (in my real app these are several horizontally-scrolling recommendation carousels). With little content above, it’s not noticeable. Replacing the outer VStack with LazyVStack reduces/hides the jump, which makes me suspect it’s related to how content size is re-measured on append.
-
Is this a known
ScrollViewbehavior whencontentSizegrows during scroll? -
Is there a supported way to keep the visible content anchored across an append (e.g.
scrollPosition,defaultScrollAnchor,.geometryGroup()), without migrating the whole screen toList? -
Does
LazyVStack“fixing” it indicate the root cause is eager content-size re-measurement, or is it just masking the symptom?
import SwiftUI
struct ContentView: View {
// Simulates tall content above the paginated section
let headerBlocks = Array(0..<8)
@State private var items: [Int] = Array(0..<20)
@State private var isLoading = false
var body: some View {
ScrollView {
VStack(spacing: 0) {
// --- Tall content above (the trigger condition) ---
ForEach(headerBlocks, id: \.self) { block in
// A horizontal carousel, like a "recommendations" row
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 12) {
ForEach(0..<10, id: \.self) { _ in
RoundedRectangle(cornerRadius: 12)
.fill(.gray.opacity(0.3))
.frame(width: 160, height: 120)
}
}
.padding(.horizontal)
}
.frame(height: 130)
.padding(.vertical, 8)
}
// --- Paginated vertical section ---
LazyVStack(spacing: 16) {
ForEach(items, id: \.self) { item in
RoundedRectangle(cornerRadius: 12)
.fill(.blue.opacity(0.2))
.frame(height: 90)
.overlay(Text("Item \(item)"))
.onAppear {
// Trigger near the end
if item == items.count - 2 {
loadMore()
}
}
}
}
.padding()
}
}
}
private func loadMore() {
guard !isLoading else { return }
isLoading = true
// Simulate network append
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
let next = items.count
items.append(contentsOf: next..<(next + 20))
isLoading = false
}
}
}