SwiftUI: animating scroll on ScrollView programmatically?

5k Views Asked by At

I need to programmatically animate the scroll of a scrollview. The scrollview contains either an HStack or a VStack. Code I tested with is this:

        ScrollViewReader { proxy in
            ScrollView(.horizontal, showsIndicators: false) {
                HStack(spacing: spacing) {
                    
                    ForEach(cids, id: \.self) { cid in

                        ....
                                
                    }
                    
                }
                .onAppear {
                    withAnimation(Animation.easeInOut(duration: 4).delay(3)) {
                        proxy.scrollTo(testCid)
                    }
                    
                }
            }
            .frame(maxWidth: w, maxHeight: w / 2)
        }

The scrollview does land on the item with the testCid, however, it does not animate. As soon as the view comes on screen the scrollview is already on testCid...

How can I animate the scroll?

1

There are 1 best solutions below

1
On

The interactive scroll works if you start it from somewhere else (f.e. Button action) but not from the onAppear modifier. I'd guess this is intentional behavior to prevent the user seeing the scrolling when the view appears (or a bug in SwiftUI...). An ugly workaround is to defer the animation with an DispatchQueue.main.async:

import SwiftUI

struct ContentView: View {
    let words = ["planet", "kidnap", "harbor", "legislation", "soap", "management", "prejudice", "an", "trunk", "divide", "critic", "area", "affair"]

    @State var selectedWord: String?

    var body: some View {
        ScrollViewReader { proxy in
            VStack(alignment: .leading) {
                ScrollView(.horizontal, showsIndicators: false) {
                    HStack(spacing: 10) {
                        ForEach(words, id: \.self) { word in
                            Text(word)
                                .background(self.selectedWord == word ? Color.yellow : nil)
                                .id(word)
                        }
                    }
                }

                Button("Scroll to random word") {
                    withAnimation(Animation.easeInOut(duration: 1)) {
                        let word = words.randomElement()
                        self.selectedWord = word
                        proxy.scrollTo(word)
                    }
                }
            }
            .onAppear {
                DispatchQueue.main.async {   // <--- workaround
                    withAnimation(Animation.easeInOut(duration: 1).delay(1)) {
                        let word = self.words.last
                        self.selectedWord = word
                        proxy.scrollTo(word)
                    }
                }
            }
        }
        .padding(10)
    }
}