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