iosswiftuiscrollviewdrag

Use programmatic scrolling while ScrollView is still scrolling after users gesture in SwiftUI app


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

Solution

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

    Animation


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