11.1 C
Canberra
Monday, May 25, 2026

ios – SwiftUI ScrollView shuffles seen content material when appending paginated objects mid-scroll


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.

  1. Is that this a recognized ScrollView conduct when contentSize grows throughout scroll?

  2. 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 to Record?

  3. 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
        }
    }
}

Related Articles

LEAVE A REPLY

Please enter your comment!
Please enter your name here

[td_block_social_counter facebook="tagdiv" twitter="tagdivofficial" youtube="tagdiv" style="style8 td-social-boxed td-social-font-icons" tdc_css="eyJhbGwiOnsibWFyZ2luLWJvdHRvbSI6IjM4IiwiZGlzcGxheSI6IiJ9LCJwb3J0cmFpdCI6eyJtYXJnaW4tYm90dG9tIjoiMzAiLCJkaXNwbGF5IjoiIn0sInBvcnRyYWl0X21heF93aWR0aCI6MTAxOCwicG9ydHJhaXRfbWluX3dpZHRoIjo3Njh9" custom_title="Stay Connected" block_template_id="td_block_template_8" f_header_font_family="712" f_header_font_transform="uppercase" f_header_font_weight="500" f_header_font_size="17" border_color="#dd3333"]
- Advertisement -spot_img

Latest Articles