0

I'm facing an issue with SwiftUI where the animation doesn't seem to work as expected in a ScrollView. I've created a minimal, reproducible example with a list of messages, and I want to scroll to the bottom of the list with a smooth animation when a new message is added. However, the animation doesn't happen consistently or smoothly. What might be causing this problem, and how can I fix it?

I have a SwiftUI view that contains a ScrollView with a list of messages displayed in a chat-like interface. When a new message is added to the list, I want the view to automatically scroll to the bottom to show the latest message. To achieve this, I'm using the onChange modifier along with the proxy.scrollTo method, wrapped in a withAnimation block. However, the animation doesn't consistently work as expected.

import SwiftUI

 struct ContentView: View {
@State private var chats = ["Message 1", "Message 2", "Message 3"]

var body: some View {
    VStack {
        ScrollViewReader { proxy in
            ScrollView {
                ForEach(chats, id: \.self) { message in
                    Text(message)
                        .padding()
                        .id(message)
                        .onChange(of: chats.count) { newCount in
                            if newCount > 0 {
                                withAnimation(.easeInOut(duration: 1.0)) {
                                    proxy.scrollTo(chats.indices.last, anchor: .bottom)
                                }
                            }
                        }
                }
            }
        }
    }
}
  }

1 Answer 1

0

I couldn’t reproduce your problem on my iPad running iPadOS 16.6.1 (the latest released version as of right now).

That said, I see one real bug in your code and one questionable thing that might also be giving you problems.

The real bug is that you use .id(message) to tag each Text, where message is a String. But then you say proxy.scrollTo(chats.indices.last, ...), where chats.indices.last is an Int. So your scroll target is never going to be found by the proxy.

The questionable thing is that you’re attaching an onChange modifier to every Text. If there are 100 messages, do you need all 100 of them to run the onChange closure? Answer: no. Try attaching the onChange modifier to a single view within the ScrollView. I’d wrap all the Texts in a VStack and attach the onChange to the VStack, like this:

import PlaygroundSupport
import SwiftUI

struct ContentView: View {
    @State private var chats = (1 ... 15).map { "Message \($0)" }
    
    var body: some View {
        VStack {
            Button("Add new message") {
                chats.append("Message \(chats.count + 1)")
            }
            ScrollViewReader { proxy in
                ScrollView {
                    VStack {
                        ForEach(chats, id: \.self) { message in
                            Text(message)
                                .padding()
                                .id(message)
                        }
                    }
                    .onChange(of: chats.count) { newCount in
                        if newCount > 0 {
                            withAnimation(.easeInOut(duration: 1.0)) {
                                proxy.scrollTo(chats.last!, anchor: .bottom)
                            }
                        }
                    }
                }
            }
        }
    }
}

PlaygroundPage.current.setLiveView(ContentView())
Sign up to request clarification or add additional context in comments.

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.