1

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)
                        }
                    }
                }
            }

3 Answers3

1

Define id for the msg not for msg.id, then scroll to that msg -> scrollTo(msg)

Example:

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)
        }
    }.onChange(of: viewModel.messages.last) { msg in
        withAnimation {
            scrollView.scrollTo(msg)
        }
    }
}
Xreen
  • 21
  • 2
0

You need to give id for a view to scroll to to make scroll reader work, like

ScrollView {
    ScrollViewReader { scrollView in
        LazyVStack{
            ForEach(viewModel.messages) { message in
                HStack {
                   // ... other code here
                }.id(message.id)            // << here (or for Text inside)
            }
        }
    }
}

See also for working example next https://stackoverflow.com/a/60855853/12299030

Asperi
  • 228,894
  • 20
  • 464
  • 690
0

This should do the job:

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.messages.last) { msg in
                        withAnimation {
                            scrollView.scrollTo(msg.id)
                        }
                    }.onAppear {
                        withAnimation {
                           scrollView.scrollTo(viewModel.messages.last!.id)
                        }
                                
                                
                    }
                }
            }

I also highly recommend you add a @DocoumentID to your Model with FirebaseFirestoreSwift and make it codable. It will turn this:

struct Message: Codable, Identifiable {
    var id: String?
    var content: String
    var name: String
    var sender: String
    var sendAt: String
}
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)
                    
                }


To this:

struct Message: Codable, Identifiable {
    @DocumentID var id: String? = UUID().uuidString
    // or this depending on your preference -> @DocumentID var id: String?
    var content: String
    var name: String
    var sender: String
    var sendAt: String
}
guard let documents = snapshot?.documents else {
                    print("no documents")
                    return
                }
                  
                self.messages = documents.compactMap { queryDocumentSnapshot -> Message? in
                  return try? queryDocumentSnapshot.data(as: Message.self)
                }
              }

Source: https://peterfriese.dev/posts/swiftui-firebase-codable/

Thel
  • 396
  • 2
  • 8