iosswiftuiswiftui-scrollview

SwiftUI ScrollView scrollTargetLayout not working


I have the following scrubber implementation in SwiftUI. The + button is supposed to move the ScrollView up by 1 tick (or scrollPosition incremented by 1) but the issue is no scrolling happens until I click 8-9 times. Is this a bug in iOS or a programming error?


struct BrokenVerticalScrubberDemo: View {
    @State private var scrollPosition: Int? = 0
    @State private var count: Int = 20

    var body: some View {
        VStack {
            Text("Scroll Position: \(scrollPosition ?? 0)")
                .font(.headline)

            ScrollView(.vertical, showsIndicators: true) {
                VStack(spacing: 8) {
                    ForEach(0..<count, id: \.self) { index in
                        Text("Tick \(index)")
                            .frame(height: 30)
                            .id(index)
                    }
                }
                .scrollTargetLayout()
                .padding(.vertical, 50)
            }
            .scrollTargetBehavior(.viewAligned)
            .scrollPosition(id: $scrollPosition)
            .frame(height: 300)
            .border(Color.gray)

            Button("+1") {
                withAnimation {
                    scrollPosition = min((scrollPosition ?? 0) + 1, count - 1)
                }
            }
            .padding(.top)
        }
        .padding()
    }
}

#Preview {
    BrokenVerticalScrubberDemo()
}

In contrast, if I use ScrollViewReader as a workaround, it starts scrolling after 2 '+' button taps.

import SwiftUI

struct SomeWhatWorkingVerticalScrubberDemo: View {
    @State private var scrollPosition: Int = 0
    @State private var count: Int = 20

    var body: some View {
        VStack {
            Text("Scroll Position: \(scrollPosition)")
                .font(.headline)

            ScrollView(.vertical, showsIndicators: true) {
                ScrollViewReader { scrollViewProxy in
                    VStack(spacing: 8) {
                        ForEach(0..<count, id: \.self) { index in
                            Text("Tick \(index)")
                                .frame(height: 30)
                                .id(index)
                        }
                    }
                    .padding(.vertical, 50)
                    .onChange(of: scrollPosition) { newPosition in
                        withAnimation {
                            scrollViewProxy.scrollTo(newPosition, anchor: .center)
                        }
                    }
                }
            }
            .frame(height: 300)
            .border(Color.gray)

            Button("+1") {
                scrollPosition = min(scrollPosition + 1, count - 1)
            }
            .padding(.top)
        }
        .padding()
    }
}

#Preview {
    SomeWhatWorkingVerticalScrubberDemo()
}


Solution

  • Please try to use top anchor

    .scrollPosition(id: $scrollPosition, anchor: .top)
    

    on the scroll position.

    The documentation about the default anchor behaviour is a bit cryptic to me. But in your example it seems to be the bottom anchor.

     /// If no anchor has been provided, SwiftUI will scroll the minimal amount
     /// when using the scroll position to programmatically scroll to a view.