0

I am currently working on a SwiftUI chat interface where I display messages using a ScrollView and LazyVStack. I want the chat to always display the last message (i.e., scroll to the bottom) when a new message is received or when the view first appears. I know that ScrollViewReader is solution to this, but I'm not sure how to implement it correctly.

Here's my current code for the chat view:

struct ChatMessage: Identifiable {
    let id: Int
    let text: String
    let isFromCurrentUser: Bool
}

struct ChatView: View {
    var messages: [ChatMessage]

    var body: some View {
        ScrollView {
            ScrollViewReader { scrollView in
                LazyVStack(spacing: 20) {
                    ForEach(messages, id: \.id) { message in
                        HStack {
                            if message.isFromCurrentUser {
                                Spacer()
                                Text(message.text)
                                    .padding()
                                    .background(Color.gray.opacity(0.2))
                                    .foregroundColor(.black)
                            } else {
                                Text(message.text)
                                    .padding()
                                    .foregroundColor(.black)
                                Spacer()
                            }
                        }
                    }
                }
            }
        }
    }
}
6
  • Does this answer your question? Custom Reverse Scroll view in SwiftUI Commented Aug 18, 2023 at 14:47
  • 1
    IMHO, you don't want the list to scroll to the bottom when a message arrives. This is poor UX. Suppose, you want to read some previous message, and every time a new message comes in, the App scrolls it away. That's more than annoying. You want to programmatically scroll only, when the scroll view is scrolled down to the bottom. Once, the user has been scrolled to a previous message, no programmatic scroll should be performed. Commented Aug 18, 2023 at 15:05
  • 1
    How could I accomplish that? @CouchDeveloper Commented Aug 18, 2023 at 15:09
  • Given that above, the solution is to let the view perform a "get new messages" action once and only, when the bottom most pixel of the view will appear. Then, once your new messages have been loaded, you scroll to bottom programmatically. You preferably have a model to perform the load, and let the view only send events to the model, and receive the messages. Commented Aug 18, 2023 at 15:10
  • Alternatively, you may use a some special view placed at the bottom of your scrollable content, with height zero which sets a boolean @State variable, for example, isOnBottom when it appears respectively when it disappears. Then, when new massages arrive (or change), scroll only when isOnBottom is true. (of course, this is just an idea, needs to be confirmed with an actual implementation) Commented Aug 18, 2023 at 15:28

1 Answer 1

1

You'll have to conform your model to hashable protocol first and then use that in your ChatView with the .id modifier along with an .onChange modifier so that whenever a new message is added to the array of messages it will automatically scroll to the last message in the array.

Also it's better to use a List in a situation like this as chat lists can grow to be very long and you only want to render what the user has scrolled to in the chat list.

struct ChatMessage: Hashable {
    let id: Int
    let text: String
    let isFromCurrentUser: Bool
}

struct ChatView: View {
    
    @State var messages: [ChatMessage] = []
    
    var body: some View {
        ScrollViewReader { proxy in
            List {
                ForEach(messages, id: \.id) { message in
                    HStack {
                        if message.isFromCurrentUser {
                            Spacer()
                            Text(message.text)
                                .padding()
                                .background(Color.gray.opacity(0.2))
                                .foregroundColor(.black)
                        } else {
                            Text(message.text)
                                .padding()
                                .foregroundColor(.black)
                            Spacer()
                        }
                    }
                    .id(message)
                }
            }
            .onChange(of: messages) { _ in
                withAnimation {
                    proxy.scrollTo(messages.last)
                }
            }
        }
    }
}
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.