In iOS app written with SwiftUI I need a ScrollView to scroll to certain object that has certain ID. I'm using scrollTo function of ScrollViewReader to do this and it works while ScrollView is not dragged by the user. But if user drag the ScrollView then that function is not working until ScrollView stops. Any idea how to make it scroll while the ScrollView is still scrolling after users drag gesture?
struct TestScrollView: View {
@Namespace var topID
@Namespace var bottomID
var body: some View {
ScrollViewReader { proxy in
VStack(spacing: 10) {
Button("Scroll to Bottom") {
withAnimation {
proxy.scrollTo(bottomID)
}
}
.id(topID)
ScrollView {
VStack(spacing: 0) {
ForEach(0..<100) { i in
color(fraction: Double(i) / 100)
.frame(height: 32)
}
}
}
Button("Top") {
withAnimation {
proxy.scrollTo(topID)
}
}
.id(bottomID)
}
}
}
func color(fraction: Double) -> Color {
Color(red: fraction, green: 1 - fraction, blue: 0.5)
}
}
I found that the buttons in your example didn't work, even when the scroll view was stationary. This is probably because, you are trying to scroll to ids that are outside the ScrollView
. This first problem can be resolved by applying an id to the inner VStack
, then changing the button callbacks to scroll to this id instead. The calls to .scrollTo
can use anchors of .top
and .bottom
, as applicable.
To resolve the issue that the buttons don't work until a scroll action has come to rest, try using a state variable to control whether scrolling is disabled or not. Then:
Here is the updated example to show it working:
struct TestScrollView: View {
@Namespace var stackID
@State private var isScrollingDisabled = false
var body: some View {
ScrollViewReader { proxy in
VStack(spacing: 10) {
Button("Scroll to Bottom") {
isScrollingDisabled = true
Task {
isScrollingDisabled = false
withAnimation {
proxy.scrollTo(stackID, anchor: .bottom)
}
}
}
ScrollView {
VStack(spacing: 0) {
ForEach(0..<100) { i in
color(fraction: Double(i) / 100)
.frame(height: 32)
}
}
.id(stackID)
}
.scrollDisabled(isScrollingDisabled)
Button("Top") {
isScrollingDisabled = true
Task {
isScrollingDisabled = false
withAnimation {
proxy.scrollTo(stackID, anchor: .top)
}
}
}
}
}
}
func color(fraction: Double) -> Color {
Color(red: fraction, green: 1 - fraction, blue: 0.5)
}
}
Another way to implement the programmatic scrolling is to use .scrollPosition
in combination with .scrollTargetLayout
. Here is how the example can be adapted to use this approach:
struct TestScrollView: View {
@State private var isScrollingDisabled = false
@State private var scrollPosition: Int?
var body: some View {
VStack(spacing: 10) {
Button("Scroll to Bottom") {
isScrollingDisabled = true
Task {
isScrollingDisabled = false
withAnimation {
scrollPosition = 99
}
}
}
ScrollView {
VStack(spacing: 0) {
ForEach(0..<100) { i in
color(fraction: Double(i) / 100)
.frame(height: 32)
.id(i)
}
}
.scrollTargetLayout()
}
.scrollDisabled(isScrollingDisabled)
.scrollPosition(id: $scrollPosition)
Button("Top") {
isScrollingDisabled = true
Task {
isScrollingDisabled = false
withAnimation {
scrollPosition = 0
}
}
}
}
}
func color(fraction: Double) -> Color {
Color(red: fraction, green: 1 - fraction, blue: 0.5)
}
}