I’ve a protracted residence feed constructed as a SwiftUI ScrollView containing a VStack of sections. The final part is paginated when the consumer scrolls close to the underside, I fetch the following web page and append rows to that part’s information array.
As soon as there’s a considerable amount of content material above the paginated part, scrolling slowly via the paginated part causes the seen rows to shift/exchange beneath the finger when a brand new web page is appended as if the content material is “refreshing” whereas stationary. The scroll offset itself doesn’t reset (I logged it, it climbs monotonically), however the seen rows visibly leap.
The leap solely seems when the content material above the paginated part is tall (in my actual app these are a number of horizontally-scrolling suggestion carousels). With little content material above, it isn’t noticeable. Changing the outer VStack with LazyVStack reduces/hides the leap, which makes me suspect it is associated to how content material measurement is re-measured on append.
-
Is that this a recognized
ScrollViewconduct whencontentSizegrows throughout scroll? -
Is there a supported strategy to preserve the seen content material anchored throughout an append (e.g.
scrollPosition,defaultScrollAnchor,.geometryGroup()), with out migrating the entire display toRecord? -
Does
LazyVStack“fixing” it point out the foundation trigger is raring content-size re-measurement, or is it simply masking the symptom?
import SwiftUI
struct ContentView: View {
// Simulates tall content material above the paginated part
let headerBlocks = Array(0..<8)
@State personal var objects: [Int] = Array(0..<20)
@State personal var isLoading = false
var physique: some View {
ScrollView {
VStack(spacing: 0) {
// --- Tall content material above (the set off situation) ---
ForEach(headerBlocks, id: .self) { block in
// A horizontal carousel, like a "suggestions" row
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 12) {
ForEach(0..<10, id: .self) { _ in
RoundedRectangle(cornerRadius: 12)
.fill(.grey.opacity(0.3))
.body(width: 160, peak: 120)
}
}
.padding(.horizontal)
}
.body(peak: 130)
.padding(.vertical, 8)
}
// --- Paginated vertical part ---
LazyVStack(spacing: 16) {
ForEach(objects, id: .self) { merchandise in
RoundedRectangle(cornerRadius: 12)
.fill(.blue.opacity(0.2))
.body(peak: 90)
.overlay(Textual content("Merchandise (merchandise)"))
.onAppear {
// Set off close to the top
if merchandise == objects.rely - 2 {
loadMore()
}
}
}
}
.padding()
}
}
}
personal func loadMore() {
guard !isLoading else { return }
isLoading = true
// Simulate community append
DispatchQueue.essential.asyncAfter(deadline: .now() + 0.3) {
let subsequent = objects.rely
objects.append(contentsOf: subsequent..<(subsequent + 20))
isLoading = false
}
}
}
