Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

SwiftUI - .scrollPosition Modifier doesn't update binding Attribute

Tags:

swiftui

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

like image 680
Götz Avatar asked Nov 27 '25 00:11

Götz


2 Answers

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
            }
        }
}
like image 192
rob mayoff Avatar answered Nov 28 '25 12:11

rob mayoff


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)

like image 20
Nikolay Suvandzhiev Avatar answered Nov 28 '25 14:11

Nikolay Suvandzhiev