I'm a newbie here and just got into coding recently, so this is my first post/question.
I'm currently writing a chat application in SwiftUI & Firebase.
When I enter a chatroom, I can't seem to be able to have the chat window to scroll automatically to show the latest message. (it's always pinned to the top). Based on my searches, I see different posts that says I should use "scrollTo" and "Anchor to bottom". However I'm not successful at getting it working.
My ViewModel for getting messages from firebase/firstore backend is following.
- Note: "chatrooms" collection in my firestore db has "messages" subcollection
import Foundation
import Firebase
struct Message: Codable, Identifiable {
var id: String?
var content: String
var name: String
var sender: String
var sendAt: String
}
class MessagesViewModel: ObservableObject {
@Published var messages = [Message]()
private let db = Firestore.firestore()
private let user = Auth.auth().currentUser
func sendMessage(messageContent: String, docId: String) {
if (user != nil) {
db.collection("chatrooms").document(docId).collection("messages")
.addDocument(data: ["sentAt": Date(), "displayName": user!.email, "content": messageContent, "sender": user!.uid])
}
}
func fetchData(docId: String) {
if (user != nil) {
db.collection("chatrooms").document(docId).collection("messages").order(by: "sentAt", descending: false).addSnapshotListener({(snapshot, error) in
guard let documents = snapshot?.documents else {
print("no documents")
return
}
self.messages = documents.map { docSnapshot -> Message in
let data = docSnapshot.data()
let docId = docSnapshot.documentID
let content = data["content"] as? String ?? ""
let displayName = data["displayName"] as? String ?? ""
let sender = data["sender"] as? String ?? ""
let sendAt = data["sendAt"] as? String ?? ""
return Message(id: docId, content: content, name: displayName, sender: sender, sendAt: sendAt)
}
})
}
}
}
And, this is my view for rendering the messages on the chat window
import SwiftUI
import Firebase
import FBSDKLoginKit
struct Messages: View {
let chatroom: Chatroom
@ObservedObject var viewModel = MessagesViewModel()
@ObservedObject var Session = SessionStore()
@State var messageField = ""
let userID = Auth.auth().currentUser?.uid
init(chatroom: Chatroom) {
self.chatroom = chatroom
viewModel.fetchData(docId: chatroom.id)
}
var body: some View {
VStack {
ScrollView {
ScrollViewReader { scrollView in
LazyVStack{
ForEach(viewModel.messages) { message in
HStack {
//places messages on left/right depending on who wrote it
if message.sender == userID{
Spacer().padding(.leading)
Text(message.content)
.padding()
.foregroundColor(.white)
.background(Color(red: 229 / 255, green: 27 / 255, blue: 78 / 255))
.clipShape(Capsule())
}
else{
Text(message.content)
.padding()
.foregroundColor(.white)
.background(Color.gray)
.border(Color.blue, width: 1)
.clipShape(Capsule())
Spacer().padding(.trailing)
}
}
}
}
}
}
HStack {
TextField("Enter message...", text: $messageField)
.textFieldStyle(RoundedBorderTextFieldStyle())
Button(action: {
viewModel.sendMessage(messageContent: messageField, docId: chatroom.id)
}, label: {
Text("Send")
})
}
}.navigationBarTitle(Text(chatroom.title), displayMode: .inline)
}
}
struct Messages_Previews: PreviewProvider {
static var previews: some View {
Messages(chatroom: Chatroom(id: "10101", title: "Hello!", joinCode: 10))
}
}
Couple of things I've tried so far
.onChange(of: viewModel.messages, perform: { value in scrollView.scrollTo(viewModel.messages.last!.id, anchor:.bottom)
.onAppear {scrollView.scrollTo(viewModel.messages[viewModel.messages.endIndex - 1])}
Two solutions I've tried above both builds fine, but unfortunately both did not work for me. (still chat window needs to be manually scrolled to the bottom)
Thanks so much for reading my post.
UPDATE
Fixed - based on feedback below. However I need .onAppear to have it scroll to bottom on load which I'm dealing with right now
ScrollView {
ScrollViewReader { scrollView in
LazyVStack{
ForEach(viewModel.messages) { message in
HStack {
//places messages on left/right depending on who wrote it
if message.sender == userID{
Spacer().padding(.leading)
Text(message.content)
.padding()
.foregroundColor(.white)
.background(Color(red: 229 / 255, green: 27 / 255, blue: 78 / 255))
.clipShape(Capsule())
}
else{
Text(message.content)
.padding()
.foregroundColor(.white)
.background(Color.gray)
.clipShape(Capsule())
Spacer().padding(.trailing)
}
}.id(message.id)
}
}.onChange(of: viewModel.lastMessageID) { id in
withAnimation {
scrollView.scrollTo(id)
}
}
}
}