SwiftUI: detect the beginning of a View using scrollPosition in a V/HStack

Hello,

I want to detect when a ScrollView is scrolled at the top of a specific View in a LazyVStack. Here is the code I use:

struct ContentView: View {
    @State private var scrollID: Int?
    
    var body: some View {
        HStack {
            VStack {
                Text(scrollID?.formatted() ?? "Unknown")
                
                Button("Go") {
                    withAnimation {
                        scrollID = 7
                    }
                }
                
                Divider()
                
                ScrollView {
                    LazyVStack(spacing: 300) {
                        ForEach(0...100, id: \.self) { int in
                            Text(int.formatted())
                                    .frame(maxWidth: .infinity)
                                    .background(.red)
                        }
                    }
                    .scrollTargetLayout()
                }
                .scrollPosition(id: $scrollID, anchor: .top)
            }
        }
    }
}

As I specify a top anchor, I was expecting to see the scrollID binding being updated when the red Text View is at the top of the ScrollView. But I noticed that scrollPosition updates the binding way before the red Text View is positioned at the top of the ScrollView, which is not what I want.

In this image, you can see the binding is already at one even though there is a lot of space between the View and the top of the ScrollView. Maybe the Stack spacing is taken into account?

And manually setting the binding scroll at the position I want, just above the red Text for 7, which makes me think the views IDs are correct.

Is my understanding wrong about this modifier?

How can I detect the top (beginning) of the View?

(If this is a SwiftUI bug, I filed #FB13811349)

Hi @alpennec,

I took a look at your code and the .scrollPosition documentation. I'd say this is expected behavior due to:

You can provide an anchor to this modifier to both: Influence which view the system chooses as the view whose identity value will update the providing binding as the scroll view scrolls.

Control the alignment of the view when scrolling to a view when writing a new binding value.

For example, providing a value of bottom will prefer to have the bottom-most view chosen and prefer to scroll to views aligned to the bottom.

Since you have a top anchor, the LazyVStack spacing after the top-most view is included in the next view as it's "top" (eg: If it starts at 0, the moment the 0 line is scrolled off the screen, the spacing below becomes part of the next view).

One rather creative workaround for this could be looking at using the .contentMargins modifier to set the edge inset for the top so that when the correct offset is hit, the top of your view is aligned with the true top of the container. You will have to play around with numbers and spacing for this to work.

The feedback report is helpful, thank you for filing it and posting the number.

Thanks for your answer.

Unfortunately, I'm not sure how to interpret it. Maybe my question was not clear enough and I badly explained what I want.

I don't want to only detect the top of the red Text("0") view (i.e. the top of the scrollView), I want to detect the top/bottom of every red Text("...") view, without the spacing being taken into account.

So when scrolling down, I want the binding to be updated only when a red background is at the top of the visible scroll view. In the example I provide, the binding is updated to 5 soon after I scroll 4, where 5 is at the middle of the screen.

And when scrolling down, I want the binding to be updated only when a red background appears at the top. In my example, the binding is updated to 8 when 9 is still almost at the top of the screen.

The thing I find strange is that setting the binding to 7 scroll at the position I want: just over the red Text("7") view. As if this was considered the top of a target View.

Why scrolling provides a different result? Why would the spacing or margin be taken into account when scrolling but not when specifying manually a position?

@alpennec ,

I'm sorry, my answer may not have been clear either! I agree with you that this behavior is inconsistent and that filing a bug report was a good idea. Spacing is only taken into account when scrolling, as you had noticed.

My idea for a workaround is what I had posted above.

@alpennec

there are now a few modifiers that may help as of WWDC24. I saw these and thought it may really apply to your use case.

Check out .onScrollVisibilityChange , it may be what you're looking for.

The video "What's new in SwiftUI" at 16:22 has more ScrollView info.

There is also a .top so you can scroll to the top edge.

SwiftUI: detect the beginning of a View using scrollPosition in a V/HStack
 
 
Q