20

I try to set a ScrollView to the top most element with a button. Therefor the new .scrollPosition() modifier should come in handy. I can't get it to work tho. In the following example the scrolling works when clicking on an item or the button at the bottom of the stack. But when scrolling the value is not updated. You can reproduce the behaviour by scrolling to the bottom, click the button "Scroll to top" and scroll back down. The second time, nothing happens because the value is still on 1. Am I doing something wrong or is my expectation of the modifier wrong? The documentation says so: https://developer.apple.com/documentation/swiftui/view/scrollposition(id:anchor:)

//
//  ContentView.swift
//  Test
//
//  Created by Sebastian Götte on 23.09.23.
//

import SwiftUI

struct ContentView: View {
    
    @State private var scrollViewPosition: Int?
    
    var body: some View {
        ScrollView(.vertical) {
            VStack(spacing: 32) {
                ForEach(1..<21) { item in
                    Text("I am item no. \(item)")
                        .id(item)
                    
                        .frame(maxWidth: .infinity)
                        .padding()
                        .background(.purple)
                        .cornerRadius(4)
                    
                        .onTapGesture {
                            withAnimation {
                                scrollViewPosition = item
                            }
                        }
                }
                
                Button("Scroll to top", action: {
                    withAnimation {
                        scrollViewPosition = 1
                    }
                })
            }
            .scrollTargetLayout()
            .scrollTargetBehavior(.viewAligned)
            .padding()
        }
        .scrollPosition(id: $scrollViewPosition, anchor: .top)
        
        .onChange(of: scrollViewPosition) { oldValue, newValue in
        print(newValue ?? "No value set")
        }
    }
}

3
  • 1
    You are using a VStack in the ScrollView. You need to use a LazyVStack. Commented Sep 23, 2023 at 18:08
  • 1
    @Yrb Thanks for the answer, I wasn't aware you needed to use a lazy stack for scrollPosition to get updated. I couldn't find it documented anywhere. Actually, I have used an HStack with scrollPosition bound to an Int and it was working - each stack was manually assigned an Int id. Would appreciate some more insight into this. Commented Jul 16, 2024 at 21:27
  • See Rob's answer below. Commented Jul 17, 2024 at 19:51

3 Answers 3

26

If you change VStack to LazyVStack, your code works.

However, you might not be able to use LazyVStack for some reason. For example, in my app, I use a custom Layout inside my ScrollView.

If you cannot use LazyVStack, then (in my testing on iOS 17.0.1), using the .id modifier prevents SwiftUI from updating the scrollPosition binding. Instead of using .id, you need to rely on ForEach's behavior of assigning an id to each of its subviews. This also means you need to use a version of ForEach that takes an id: parameter, or provide it with a collection of Identifiable-conforming values.

So, to make your example work, change your ForEach loop to this:

ForEach(1..<21, id: \.self) { item in
    Text("I am item no. \(item)")
        // .id modifier removed
        .frame(maxWidth: .infinity)
        .padding()
        .background(.purple)
        .cornerRadius(4)
        .onTapGesture {
            withAnimation {
                scrollViewPosition = item
            }
        }
}
Sign up to request clarification or add additional context in comments.

2 Comments

thank you very much, that works. ScrollPosition seams also to be very picky with the id Type, i tried UUID as an id but that doesn't seam to work. Strings and Ints work fine tho
I had the same issue, and it turned out that even if you have ID a few levels down the hierarchy of the scrollable item, it still leads to the position update problem. Thank you for publishing this discovery.
14

If someone arrived here from a Google search, check that you've added .scrollTargetLayout() to the relevant view inside the ScrollView definition.

As per Apple's docs:

ScrollView {
    LazyVStack {
        ForEach(items) { item in
            ItemView(item)
        }
    }
    .scrollTargetLayout()
}
.scrollPosition(id: $scrolledID)

(I know this is not an answer to the original question, but it might be useful for people searching for scrollPosition troubleshooting)

2 Comments

Thank you so much! This solved a very frustrating issue of mine
OMG! Solved my day! Thanks!
2

Not sure if entirely relevant, but I found that:

  • when I was scrolling to a position using scrollTo
  • updated the scroll position in code The scrollPosition value would not update.

My solution was to add scrollPosition = newValue.id.

Hope someone else can replicate / validate!

1 Comment

Yes, I also found when programmatically navigating using ScrollViewProxy the scrollPosition binding isn't updated. Setting it manually when requesting the scroll doesn't seem to lead to any side effects.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.