SwiftUI ScrollView shuffles visible content when appending paginated items mid-scroll
11:22 21 May 2026

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.

  1. Is this a known ScrollView behavior when contentSize grows during scroll?

  2. Is there a supported way to keep the visible content anchored across an append (e.g. scrollPosition, defaultScrollAnchor, .geometryGroup()), without migrating the whole screen to List?

  3. 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
        }
    }
}
ios swift swiftui vstack lazyvstack