SwiftUI - 滚动到聊天应用程序的最新消息

Posted

技术标签:

【中文标题】SwiftUI - 滚动到聊天应用程序的最新消息【英文标题】:SwiftUI - scrolling to latest message for chat applictaion 【发布时间】:2021-01-17 20:46:09 【问题描述】:

我是这里的新手,最近才开始编码,所以这是我的第一篇文章/问题。

我目前正在用 SwiftUI 和 Firebase 编写一个聊天应用程序。

当我进入聊天室时,我似乎无法让聊天窗口自动滚动以显示最新消息。 (它总是固定在顶部)。根据我的搜索,我看到不同的帖子说我应该使用“scrollTo”和“Anchor to bottom”。但是我没有成功地让它工作。

我的用于从 firebase/firstore 后端获取消息的 ViewModel 如下。

注意:我的 Firestore 数据库中的“chatrooms”集合包含“messages”子集合
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)
                    
                
            )
        
    


而且,这是我在聊天窗口上呈现消息的视图

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


到目前为止我尝试过的几件事

.onChange(of: viewModel.messages, perform:  value in scrollView.scrollTo(viewModel.messages.last!.id, anchor:.bottom) 
.onAppear scrollView.scrollTo(viewModel.messages[viewModel.messages.endIndex - 1])

我在上面尝试过的两种解决方案都可以正常工作,但不幸的是,这两种解决方案都不适合我。 (还是聊天窗口需要手动滚动到底部)

非常感谢您阅读我的帖子。

更新

已修复 - 基于以下反馈。但是我需要 .onAppear 让它在我现在正在处理的加载时滚动到底部

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)
                        
                    
                
            

【问题讨论】:

【参考方案1】:

为 msg 定义 id 而不是 msg.id,然后滚动到该 msg -> scrollTo(msg)

例子:

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)
        
    

【讨论】:

【参考方案2】:

您需要提供id 以使视图滚动到以使滚动阅读器工作,例如

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

另请参阅下一个工作示例https://***.com/a/60855853/12299030

【讨论】:

我尝试定义每条消息的 id,但仍然没有运气(在上面的帖子中更新)。你能再看看吗?【参考方案3】:

这应该可以完成工作:

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)
                        
                                
                                
                    
                
            

我还强烈建议您使用 FirebaseFirestoreSwift 向您的模型添加 @DocumentID 并使其可编码。它会变成这样:

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)
                    
                


到这里:

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)
                
              

来源:https://peterfriese.dev/posts/swiftui-firebase-codable/

【讨论】:

以上是关于SwiftUI - 滚动到聊天应用程序的最新消息的主要内容,如果未能解决你的问题,请参考以下文章

让DIV的滚动条自动滚动到最底部 - 3种方法

SwiftUI 将部分列表固定到底部

使用 MessageKit 构建的聊天视图永远不会滚动到带有图片的最后一条消息

出现键盘时滚动到 SwiftUI Scrollview 的最底部

SwiftUI - 调整 ScrollView 滚动指示器插图

分享将滚动条保持在最底部的方法