Rick
Rick

Reputation: 3857

SwiftUI ScrollView animated scrolling is sometimes slow

I've got a simple chat view that scrolls up to reveal new messages that appear at the bottom of the chat. What's weird is that sometimes, the duration of the scroll animation is much longer than normal.

Animated GIF showing sometimes-slow scrolling behavior.

I think it happens when a message comes in while the animation is still underway.

My code looks like this:

struct
OverlayChatView: View
{
    @ObservedObject public  var     stream          :   ChatStream
    
    var body: some View {
        ScrollViewReader { scrollView in
            ScrollView {
                LazyVStack(alignment: .leading, spacing: 0.0) {
                    ForEach(self.stream.messages) { inMsg in
                        ChatMessageCell(message: inMsg)
                    }
                    .onChange(of: self.stream.messages, perform: { inMessages in
                        if inMessages.count < 1 { return }
                    
                        withAnimation(.linear(duration: 0.25)) {
                            scrollView.scrollTo(inMessages.last!.id, anchor: .bottom)
                        }
                    })
                    .padding(8)
                }
            }
        }
    }
}

self.stream is passed in from an @ObservedObject ChatStream with a simple Timer inserting a new message every 5.0 seconds. If the interval is 2.0 seconds, then it just continually scrolls upward slowly.

One other thing I notice is that .onChange() gets called three times for each insert. Perhaps my ChatStream is doing something dumb. I mention it because just invoking the animation three times in quick succession doesn't cause the slowdown. It seems more related to where the scroll is currently vs where it has to go.

Any idea how I can avoid this slowdown?

Upvotes: 6

Views: 2195

Answers (1)

griff dog
griff dog

Reputation: 31

I ran into this bug when testing my app for iOS 14 (the issue seems to have been fixed in iOS 15).

The solution I found is to just wrap the withAnimation call within a main thread async call, like so:

DispatchQueue.main.async {
    withAnimation(.linear(duration: 0.25)) {
        scrollView.scrollTo(inMessages.last!.id, anchor: .bottom)
    }
}

Upvotes: 3

Related Questions