SwiftUI Group Lab: Track Visible Items, Not Scroll Offset

|

Notes from a SwiftUI Group Lab Q&A — a summary of what the panel said, not my own take.

Treat a scroll view’s content offset as an implementation detail — with lazy stacks it’s only estimated from the heights of off-screen views and carries no reliable semantic meaning. Trigger UI changes off the views actually on screen instead.

scrollPosition tracks which item is currently visible by ID, not by offset — so it stays relative to your data model rather than a pixel value:

struct FeedView: View {
    @State private var position = ScrollPosition(idType: Item.ID.self)

    var body: some View {
        ScrollView {
            LazyVStack {
                ForEach(items) { item in
                    ItemRow(item: item).id(item.id)
                }
            }
        }
        .scrollPosition($position)
    }
}

For visibility-based triggers — analytics, impression tracking — onScrollVisibilityChange(threshold:) fires when a view crosses a percentage-visible threshold, instead of you computing that from offsets yourself:

ItemRow(item: item)
    .onScrollVisibilityChange(threshold: 0.8) { isVisible in
        if isVisible { analytics.logImpression(item) }
    }

onGeometryChange and scroll transitions round out the toolkit: the former replaces GeometryReader hacks for reading frame changes, the latter offloads scroll-driven visual effects to the system and is often cheaper than computing them by hand from offset.

Infinite scrolling, per the panel, doesn’t need any offset math either — put a sentinel view at the end of the list and let its onAppear trigger the next page fetch:

LazyVStack {
    ForEach(items) { item in
        ItemRow(item: item)
    }
    Color.clear
        .frame(height: 1)
        .onAppear { Task { await loadMore() } }
}

Takeaway: stop reasoning about scroll views in terms of content offset. Whatever you’re after — revealing a view at some scroll depth, tracking impressions, or paging in more data — there’s an API keyed to the views on screen instead.

← Back to DevLog
rss facebook twitter github youtube mail spotify lastfm instagram linkedin google google-plus pinterest medium vimeo stackoverflow reddit quora