swiftui

SwiftUI - .scrollPosition Modifier doesn't update binding Attribute


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")
        }
    }
}


Solution

  • 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
                }
            }
    }