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

results matching ""

    No results matching ""