Các bước xây dựng màn hình chat trong ứng dụng chat SkylabChat
Hôm nay mình sẽ hướng dẫn các bạn làm màn hình Chat trong ứng dụng SkylabChat của team
Đây là màn hình chúng ta sẽ xây dựng.
Bước 1 : Xây dựng User Interface trên Xcode 8
Đầu tiên mình sẽ phân tích sơ lược về những phần sẽ có trong màn hình này
Màn hình sẽ bao gồm 2 view chính là:
+ TableView chứa những bong bóng tin nhắn
Title Navigation bar chứa tên người chat, và status của người chat
5 Cell chính
Cell 1 (Người gửi - "Chatter" ) :
Avatar - ImageView
Tin nhắn văn bản - 1 View chứa 1 label
Cell 2 (Người nhận - "Mine" )
Logo "Seen" thể hiện việc đọc hay chưa đọc tin nhắn - ImageView
Tin nhắn văn bản - 1 View chứa 1 label
Cell 3 (Người gửi - "Chatter" ) :
Avatar - ImageView
Tin nhắn hình - ImageView
Cell 4 (Người nhận - "Mine" )
Logo "Seen" thể hiện việc đọc hay chưa đọc tin nhắn - ImageView
Tin nhắn hình - ImageView
Cell 5 :
- Thời gian của tin nhắn - Label
+ View sẽ chứa Button Send , khung TextField , và 1 Button Attachment
Kéo thả những item sẽ sử dụng vào các cell
Đặt constrain cho các item bên trong cell
Bước 2 : Tạo Struct Message
Ở đây mình sẽ đi tạo file .swift mới để chứa struct Message này
Thì struct này sẽ chứa những thông tin trong một tin nhắn
struct Message {
var content:String
var imgHeight = 0
var imgWidth = 0
var imgName:String
var time:TimeInterval = 0
var type:String = "sender" // or receiver
var user_id:String
init( content: String, imgHeight: Int, imgWidth: Int, imgName: String , time: TimeInterval, type: String, user_id: String){
self.content = content
self.imgHeight = imgHeight
self.imgWidth = imgWidth
self.imgName = imgName
self.time = time
self.type = type
self.user_id = user_id
}
init?(dict : Dictionary<String, Any>){
guard let content = dict["content"] as? String else { return nil }
self.content = content
guard let imgHeight = dict["imgHeight"] as? Int else { return nil }
self.imgHeight = imgHeight
guard let imgWidth = dict["imgWidth"] as? Int else { return nil }
self.imgWidth = imgWidth
guard let imgName = dict["imgName"] as? String else { return nil }
self.imgName = imgName
guard let time = dict["time"] as? TimeInterval else { return nil }
self.time = time
guard let type = dict["type"] as? String else { return nil }
self.type = type
guard let user_id = dict["user_id"] as? String else { return nil }
self.user_id = user_id
}
}
Bước 3 : Tạo File Quản Trị Cho Các Cell
Chúng ta sẽ đi tạo 5 file .swift để quản lý cho 5 cell riêng biệt đã tạo ở trên
Các bạn lưu ý là nhớ đặt "identifier" cho các cell nhé.
Ở đây mình đặt tên giống như tên file quản trị của nó : receiverImageCell, receiverTextCell, senderImageCell, senderTextCell
Bên trong mình sẽ ánh xạ những thành phần contentChat (Label or ImageView), avatarImage ... bên trong mỗi cell vào file quản trị tương ứng.
Tạo func fillData(msg : Message) để truyền vào message và fill message đó vào contentChat
class ReceiverImageCell: UITableViewCell {
@IBOutlet weak var avatarImage: UIImageView!
@IBOutlet weak var imageChat: UIImageView!
@IBOutlet weak var seenImage: UIImageView!
@IBOutlet weak var imgHeight: NSLayoutConstraint!
@IBOutlet weak var imgWidth: NSLayoutConstraint!
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
// Configure the view for the selected state
}
func fillData(msg: Message)
{
if let checkURL = URL(string: msg.content)
{
imageChat.contentMode = .scaleAspectFit
// 262
self.imgWidth.constant = 252
self.imgHeight.constant = CGFloat(252 * msg.imgHeight / msg.imgWidth)
self.layoutIfNeeded()
self.imageChat.layer.masksToBounds = true
self.imageChat.layer.cornerRadius = 14
self.imageChat.backgroundColor = UIColor.lightGray
self.imageChat.image = nil
imageChat.kf.setImage(with: checkURL)
}
}
}
Cell nhận hình ảnh
class ReceiverTextCell: UITableViewCell {
@IBOutlet weak var secondContentView: UIView!
@IBOutlet weak var seenImage: UIImageView!
@IBOutlet weak var contentChat: UILabel!
// @IBOutlet weak var avatarImage: UIImageView!
override func awakeFromNib() {
super.awakeFromNib()
secondContentView.layer.cornerRadius = 14
secondContentView.layer.masksToBounds = true
// Initialization code
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
// Configure the view for the selected state
}
func fillData(msg : Message)
{
contentChat.text = msg.content
}
}
Cell nhận tin nhắn text
Làm tương tự cho 2 file của 2 cell còn lại.
Bước 4 : Tạo file quản trị cho màn hình chat
Ở đây mình đặt tên file quản trị này là ScreenChatViewController
Bên trong mình sẽ ánh xạ tableView , textField , buttonSend , buttonAttach.
kế đến mình sẽ khởi tạo các hàm quen thuộc khi sử dụng tableView : numbersOfSection , numberOfRowInSection ...
Ở ScreenChatViewController mình tạo 1 mảng chứa các message
class ScreenChatViewController: UIViewController,UITableViewDelegate,UITableViewDataSource {
@IBOutlet weak var tableView: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
var messageList : [ Message ] = [ ]
self.tableView.delegate = self
self.tableView.dataSource = self
self.tableView.estimatedRowHeight = 100
self.tableView.rowHeight = UITableViewAutomaticDimension
}
/* Implement TableView */
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return messageList.count
}
func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
return 14
}
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let headerView = UIView()
headerView.backgroundColor = UIColor.clear
return headerView
}
Kế đến mình sẽ tạo 4 func
createSenderTextCell
createReceiverTextCell
createSenderImageCell
createReceiverImageCell
thì các hàm này sẽ làm nhiệm vụ gọi đến những cell tương ứng.
func createSenderTextCell(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> SenderTextCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "senderTextCell", for: indexPath) as! SenderTextCell
return cell
}
func createReceiverTextCell(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> ReceiverTextCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "receiverTextCell", for: indexPath) as! ReceiverTextCell
return cell
}
func createSenderImageCell(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> SenderImageCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "senderImageCell", for: indexPath) as! SenderImageCell
return cell
}
func createReceiverImageCell(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> ReceiverImageCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "receiverImageCell", for: indexPath) as! ReceiverImageCell
return cell
Hàm cellForRowAtIndexPath sẽ hiện thị nội dung tương ứng với từng cell đã khởi tạo.
Đồng thời ở đây mình sẽ phân biệt là tin nhắn của mình và người khác dựa vào currentUserId , vì trong struct Message đã tạo lúc đầu có chứa thông tin user_id nên ta sẽ dựa vào đó mà so sánh với user hiện tại đang log in
Khi đã phân biệt được tin nhắn nào là tin nhắn của mình hay người khác , ta sẽ chọn đúng cell để hiển thị nó.
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
//let refAvatar = usersReference.child(chattterID).value(forKey: "avatar")
// let current_id = FIRAuth.auth()?.currentUser?.uid
let msg = messageList[indexPath.row]
if msg.user_id == self.currentUserId{
if msg.type == "text" {
let cell = createReceiverTextCell(tableView, cellForRowAt: indexPath)
cell.fillData(msg: msg)
return cell
} else {
let cell = createReceiverImageCell(tableView, cellForRowAt: indexPath)
cell.fillData(msg: msg)
return cell
}
} else {
if msg.type == "text" {
let cell = createSenderTextCell(tableView, cellForRowAt: indexPath)
cell.fillData(msg: msg)
return cell
} else {
let cell = createSenderImageCell(tableView, cellForRowAt: indexPath)
cell.fillData(msg: msg)
return cell
}
}
}
Bước 5: Gửi message dạng text
5.1 Một số lưu ý
Tính năng chat giữa 2 người được hiểu gồm các bước sau :
1. Gửi message lên database của Firebase
2. Lắng nghe toàn bộ message và những message mới được thêm vào từ Chatter
3. Hiển thị lên màn hình mảng những message mà đã lắng nghe được từ Firebase
Firebase ở đây như là một người trung gian với nhiệm vụ nhận tin của Sender và gửi cho Receiver
Trước hết chúng ta sẽ import những module của Firebase (FirebaseStorage, FirebaseDatabase, FirebaseAuth)
Và còn một lưu ý hết sức quan trọng ở đây là, ta phải có những thông tin sau :
+ CurrentUserId(Id của user đang log in)
+ ChatterId (Id của người kia )
những thông tin này mình sẽ lấy từ ở màn hình Chat Log, trước khi chuyển sang màn hình Chat của chúng ta
5.2 Xây dựng function gửi message
/* Send Message */
func sendMessage (msg : Message)
{
let data: Dictionary<String,Any> = [
"content" : msg.content,
"imgHeight" :msg.imgHeight,
"imgName" : msg.imgName,
"imgWidth" : msg.imgWidth,
"time" : FIRServerValue.timestamp(),
"user_id" : msg.user_id ,
"type": msg.type,
]
messagesRefernce.childByAutoId().setValue(data)
let newConversationRef = conversationsRefernce.child(roomID)
newConversationRef.child("lastMessage").setValue(msg.content)
newConversationRef.child("lastTimeUpdated").setValue(FIRServerValue.timestamp())
}
Bên trong hàm này mình sẽ tạo 1 Dictionary Data , để đưa vào những thông tin của 1 message
Kế đó mình sẽ gọi đường dẫn đến FirebaseDatabase và setValue cái data mình vừa tạo
5.3 Implement func send message vào nút SendButton
@IBAction func btnSendMessage(_ sender: Any) {
if(textFieldInput.text != "")
{
let text = textFieldInput.text
let content = text
let imgHeight = 0
let imgWidth = 0
let type = "text"
let user_id = currentUserId
let imgName = ""
let msg : Message = Message(content: content!, imgHeight: imgHeight, imgWidth: imgWidth, imgName:imgName , time: 0, type: type, user_id: user_id)
sendMessage(msg: msg)
textFieldInput.text = ""
}
}
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
if(textFieldInput.text != "")
{
let text = textFieldInput.text
let content = text
let imgHeight = 0
let imgWidth = 0
let type = "text"
let user_id = currentUserId
let imgName = ""
let msg : Message = Message(content: content!, imgHeight: imgHeight, imgWidth: imgWidth, imgName:imgName , time: 0, type: type, user_id: user_id)
sendMessage(msg: msg)
textFieldInput.text = ""
}
return true
}
ở trong hàm btnSendMessage mình sẽ khởi tạo những thành phần như :
content , imgHeight , imgWidth, type để gán vào các biến bên trong hàm sendMessage.
hàm textFieldShouldReturn sẽ có chức năng là khi người dùng nhấn phím return trên bàn phím thì tin nhắn cũng sẽ tự gửi thay vì phải bấm nút Send
Đây là kết quả thực hiện trên Firebase
Bước 6 : Real-Time Chat
Ở bước này mình sẽ lắng nghe những tin nhắn được gửi lên firebase và hiển thị nó lên trên bong bóng chat
/* Listen from Firebase */
//
func ObserveMessage()
{
messagesRefernce.queryLimited(toLast: 50).observeSingleEvent(of: .value, with: { (snapshot) in
guard let msgSnapshotArr = snapshot.children.allObjects as? [FIRDataSnapshot] else { return }
for msgSnapshot in msgSnapshotArr {
if let mes = Message(dict: msgSnapshot.value as! Dictionary<String,Any>) {
self.messageList.append(mes)
}
}
self.tableView.reloadData()
if self.messageList.count > 0 {
let newIndexPath = IndexPath(row: self.messageList.count-1 , section: 0)
self.tableView.scrollToRow(at: newIndexPath, at: .bottom, animated: true)
}
self.AlwaysObserveMessage()
}) { (err) in
}
}
Hàm ObserveMessage sẽ có chức năng lắng nghe toàn bộ những tin chat và giới hạn khi query là 50 tin nhắn cuối cùng. và hàm ObserveMessage này chỉ thực hiện một lần khi lần đầu load vào màn hình chat này.
//Listen message was added in Firebase
func AlwaysObserveMessage()
{
messagesRefernce.queryLimited(toLast: 1).observe(FIRDataEventType.childAdded){ (snapshot : FIRDataSnapshot) in
//Skip the first run-time
if self.messageList.count > 0 && self.isFirst {
self.isFirst = false
return
}
guard let data = snapshot.value as? [String:Any] else { return }
if let msg = Message(dict: data) {
self.messageList.append(msg)
self.tableView.reloadData()
let newIndexPath = IndexPath(row: self.messageList.count-1 , section: 0)
self.tableView.scrollToRow(at: newIndexPath, at: .bottom, animated: true)
}
self.tableView.reloadData()
}
}
Hàm AlwaysObserveMessage này nhiệm vụ sẽ lắng nghe tất cả những tin chat vừa được thêm vào. và sau khi lắng nghe sẽ append những tin nhắn vừa nhận vào mảng các tin nhắn (messageList) để hiện lên trên các khung chat
Sau khi có được 2 hàm lắng nghe trên, ta sẽ gọi lại 2 hàm này ở func ViewDidLoad()
Đến đây các bạn đã có thể gửi và nhận tin nhắn text qua lại với nhau rồi đấy
Làm theo hướng dẫn và xem và kết quả nhé...
Chúc các bạn học tốt ^^
Nguyễn Khánh Hưng