Swift中Firebase时间戳无法显示且崩溃问题求助
问题分析与解决方案
直接崩溃原因
你在loadMsg()里的这行代码:
self.message.timestamp = time //HERE IT CRASHES!!!!!
崩溃是因为self.message是一个隐式可选类型(var message: Message!),但你从未初始化它,此时它的值是nil,当你尝试访问它的timestamp属性时,隐式可选会被强制解包,触发Unexpectedly found nil错误。
而且这段逻辑本身就不对——你应该把转换后的时间赋值给每个从Firebase读取到的Message实例,而不是给self.message这个未初始化的属性。
分步修复方案
1. 清理Message类的冗余与错误
你的Message类存在两个关键问题:
- 多余的
var message: Message!属性,完全没必要,只会造成混淆,直接删除它; - 第一个初始化方法里,你尝试把Firebase的timestamp(Double类型)直接转成String,这会导致
timestamp属性始终为nil,因为Firebase里存的是数字时间戳,不是字符串。
修改后的Message类:
class Message: NSObject { var fromId: String? var textMessages: String? // 先存原始的Double类型时间戳,后续在需要显示时再转换为字符串 var timestamp: Double? var toId: String? var _messageKey: String! init(dictionary: [String: Any]) { self.fromId = dictionary["fromId"] as? String self.textMessages = dictionary["textMessages"] as? String self.toId = dictionary["toId"] as? String // 正确读取Double类型的timestamp self.timestamp = dictionary["timestamp"] as? Double } init(messageKey: String, postData: Dictionary<String, AnyObject>) { _messageKey = messageKey if let message = postData["textMessages"] as? String { textMessages = message } if let sender = postData["fromId"] as? String { fromId = sender } // 补充读取timestamp if let time = postData["timestamp"] as? Double { timestamp = time } } // 新增一个方法,把timestamp转换为显示用的字符串 func formattedTimestamp() -> String? { guard let timeStamp = timestamp else { return nil } let date = Date(timeIntervalSince1970: timeStamp) let formatter = DateFormatter() formatter.dateStyle = .short formatter.timeStyle = .short return formatter.string(from: date) } }
2. 修正loadMsg()的逻辑错误
你嵌套了observe(.value)和observeSingleEvent(.childAdded),这会导致重复监听和逻辑混乱。正确的做法是直接监听childAdded或者一次性读取所有数据,然后为每个Message实例处理时间戳:
修改后的loadMsg():
func loadMsg() { guard let toId = user?.id, let fromId = Auth.auth().currentUser?.uid else { // 提前处理nil情况,避免后续崩溃 return } let chatRoomId = fromId < toId ? "\(fromId)_\(toId)" : "\(toId)_\(fromId)" let ref = Database.database().reference().child("privateMessages").child(chatRoomId) // 用observeSingleEvent(.value)一次性读取所有消息 ref.observeSingleEvent(of: .value) { snapshot in self.messages.removeAll() guard let snapshotChildren = snapshot.children.allObjects as? [DataSnapshot] else { DispatchQueue.main.async { self.tableView.reloadData() } return } for data in snapshotChildren { if let msgDict = data.value as? [String: Any] { let newMsg = Message(dictionary: msgDict) self.messages.append(newMsg) } } // 回到主线程刷新表格 DispatchQueue.main.async { self.tableView.reloadData() } } }
3. 修正TableViewCell的显示逻辑
在cell里调用Message的formattedTimestamp()方法来获取显示用的时间字符串,同时避免不必要的赋值:
// 假设你的cell类里的配置方法是这样的 func configure(with message: Message, currentUser: String) { self.message = message if message.fromId == currentUser { sentView.isHidden = false sentMsgLabel.text = message.textMessages receivedMsgLabel.text = "" receivedView.isHidden = true // 只给发送时间赋值 timeSent.text = message.formattedTimestamp() timeReceived.text = nil } else { sentView.isHidden = true sentMsgLabel.text = "" receivedMsgLabel.text = message.textMessages receivedView.isHidden = false // 只给接收时间赋值 timeReceived.text = message.formattedTimestamp() timeSent.text = nil } }
额外注意事项
- 永远不要随意使用隐式可选类型(
!),除非你能保证它在使用前一定被初始化; - 从Firebase读取数据时,要严格匹配数据类型(比如timestamp是Double,不是String);
- 所有UI操作必须放在主线程执行,你的
tableView.reloadData()已经放在主线程了,这部分是对的; - 可以添加更多的nil校验,避免因为Firebase数据缺失导致的崩溃。
内容的提问来源于stack exchange,提问作者William Loke
相关产品推荐
相关产品推荐

