Firebase

【Swift4】MessageKitというライブラリを使ったFirestoreチャットページ作成

import UIKit
import Firebase
import MessageKit
import SCLAlertView

class MessageKitViewController: MessagesViewController, Utils, WebViewUtils {

    let chatCnt: Int = 50
    var db: Firestore!
    var messageList: [MockMessage] = []
    var chatName: String?
    let appDelegate = UIApplication.shared.delegate as! AppDelegate
    private var activityIndicator: UIActivityIndicatorView = UIActivityIndicatorView()
    var blockUsers: [String]? = nil
    var blockKeywords: [String]? = nil
    var curDate: NSAttributedString? = nil
    
    lazy var timeFmt: DateFormatter = {
        let timeFmt = DateFormatter()
        timeFmt.dateStyle = .none
        timeFmt.timeStyle = .medium
        return timeFmt
    }()
    
    lazy var dateFmt: DateFormatter = {
        let dateFmt = DateFormatter()
        dateFmt.dateStyle = .medium
        dateFmt.timeStyle = .none
        return dateFmt
    }()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        self.title = Strings().MENU_CHAT
        appDelegate.updateChatBackFlg = false
        self.chkChatTerms()
        initChatView()
    }
    
    func chkChatTerms(){
        if !self.getChkChatTerms() {
            self.simplePopup(msg: Strings().CHK_TERMS_MSG) // 利用規約の同意を求める
            self.saveChkChatTerms()
        }
    }
    
    func initChatView(){
        self.startIndicator(aic: &activityIndicator, vc: self) // クルクルスタート
        // 通信状態を確認
        if CheckReachability(host_name: "google.com") {
            self.dispChatPage()
        }
    }
    
    func dispChatPage(){
        self.chatName = getChatName()
        
        // データベース読み取り
        self.setupFirebase()

        // アイコンの表示を消し、その分ラベルを移動させる
        if let layout = self.messagesCollectionView.collectionViewLayout as? MessagesCollectionViewFlowLayout {
            layout.setMessageIncomingAvatarSize(.zero)
            layout.setMessageOutgoingAvatarSize(.zero)
            layout.setMessageIncomingMessageTopLabelAlignment(LabelAlignment(textAlignment: .left, textInsets: UIEdgeInsets(left: 10)))
            layout.setMessageIncomingMessageBottomLabelAlignment(LabelAlignment(textAlignment: .left, textInsets: UIEdgeInsets(left: 10)))
            layout.setMessageOutgoingMessageTopLabelAlignment(LabelAlignment(textAlignment: .right, textInsets: UIEdgeInsets(right: 10)))
            layout.setMessageOutgoingMessageBottomLabelAlignment(LabelAlignment(textAlignment: .right, textInsets: UIEdgeInsets(right: 10)))
        }
        
        messagesCollectionView.messagesDataSource = self
        messagesCollectionView.messagesLayoutDelegate = self
        messagesCollectionView.messagesDisplayDelegate = self
        messagesCollectionView.messageCellDelegate = self
        
        messageInputBar.delegate = self
        messageInputBar.sendButton.tintColor = UIColor.lightGray
        
        // メッセージ入力欄の左に画像選択ボタンを追加
        // 画像選択とかしたいときに
        let items = [
            makeButton(named: "haguruma").onTextViewDidChange { button, textView in
                button.tintColor = UIColor.lightGray
                button.isEnabled = textView.text.isEmpty
            }
        ]
        items.forEach { $0.tintColor = .lightGray }
        messageInputBar.setStackViewItems(items, forStack: .left, animated: false)
        messageInputBar.setLeftStackViewWidthConstant(to: 45, animated: false)
        
        // メッセージ入力時に一番下までスクロール
        scrollsToBottomOnKeybordBeginsEditing = true // default false
        maintainPositionOnKeyboardFrameChanged = true // default false
    }
    
    // ボタンの作成
    func makeButton(named: String) -> InputBarButtonItem {
        return InputBarButtonItem()
            .configure {
                $0.spacing = .fixed(10)
                $0.image = UIImage(named: named)?.withRenderingMode(.alwaysTemplate)
                $0.setSize(CGSize(width: 30, height: 30), animated: true)
            }.onSelected {
                $0.tintColor = UIColor.gray
            }.onDeselected {
                $0.tintColor = UIColor.lightGray
            }.onTouchUpInside { _ in
                print("Item Tapped")
        }
    }
    
    func setupFirebase() {
        // Firestoreのインスタンス化
        self.db = Firestore.firestore()
        // 最新のデータが追加されるたびに最新データを取得する
        self.addMessageList()
    }
    
    func addMessageList() {
        // 最新50件のデータをデータベースから取得する
        self.db.collection("chat1").order(by: "timestamp", descending: true).limit(to: self.chatCnt + 25).addSnapshotListener { (snapShot, error) in
            guard let value = snapShot else {
                print("snapShot is nil")
                return
            }
            snapShot!.documentChanges.forEach { diff in
                if (diff.type == .added) {
                    print("New city: \(diff.document.data())")
                    let snapshotValue = diff.document.data()
                    let text = snapshotValue["text"] as! String
                    let name = snapshotValue["name"] as! String
                    let date = snapshotValue["timestamp"] as! Date
                    self.messageList.append(self.createMessage(text: text, sender: name, name: name, date: date))
                }
                if (diff.type == .modified) {
                    print("Modified city: \(diff.document.data())")
                }
                if (diff.type == .removed) {
                    print("Removed city: \(diff.document.data())")
                }
            }
            //逆順(昇順)ソート
            self.messageList.reverse()
            // リロード
            self.messagesCollectionView.reloadData()
            self.messagesCollectionView.scrollToBottom()
            self.stopIndicator(aic: self.activityIndicator) // クルクルストップ
        }
    }
    
    func chkBlockUser(name: String) ->Bool {
        if self.blockUsers == nil {
            self.blockUsers = self.getBlockUser()
        }
        if (self.blockUsers?.contains(name))! {
            return true
        }
        return false
    }
    
    func chkBlockKeyword(keyword: String) ->Bool {
        if self.blockKeywords == nil {
            self.blockKeywords = self.getBlockKeywords()
        }
        for k in self.blockKeywords! {
            if keyword.lowercased().contains(k.lowercased()) {
                return true
            }
        }
        return false
    }
    
    func createMessage(text: String, sender: String, name: String, date: Date) -> MockMessage {
        let attributedText = NSAttributedString(string: text, attributes: [.font: UIFont.systemFont(ofSize: 15),
                                                                           .foregroundColor: UIColor.white])
        return MockMessage(text: text, attributedText: attributedText, sender: otherSender(sender: sender, name: name), messageId: UUID().uuidString, date: date)
    }
    
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        self.navigationController?.setNavigationBarHidden(false, animated: false)
    }
    
    override func viewDidAppear(_ animated: Bool) {
        self.becomeFirstResponder() // inputバーを再表示
    }
    
    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
    }
}

extension MessageKitViewController: MessagesDataSource {
    
    func currentSender() -> Sender {
        return Sender(id: chatName!, displayName: chatName!)
    }
    
    func otherSender(sender: String, name: String) -> Sender {
        return Sender(id: sender, displayName: name)
    }
    
    func numberOfSections(in messagesCollectionView: MessagesCollectionView) -> Int {
        return messageList.count
    }
    
    func messageForItem(at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> MessageType {
        return messageList[indexPath.section]
    }
    
    // 日付区切り文字を表示
//    func cellTopLabelAttributedText(for message: MessageType, at indexPath: IndexPath) -> NSAttributedString? {
////        if indexPath.section % 3 == 0 {
////            return NSAttributedString(
////                string: MessageKitDateFormatter.shared.string(from: message.sentDate),
////                attributes: [NSAttributedStringKey.font: UIFont.boldSystemFont(ofSize: 10),
////                             NSAttributedStringKey.foregroundColor: UIColor.darkGray]
////            )
////        }
//        print(indexPath.section)
//        let dateString = lFormatter.string(from: message.sentDate)
//        let sentDateStr = NSAttributedString(string: dateString, attributes: [NSAttributedStringKey.font: UIFont.preferredFont(forTextStyle: .caption2)])
//        if indexPath.section == 0 || self.curDate != sentDateStr {
//            self.curDate = sentDateStr
//            return sentDateStr
//        } else {
//            return nil
//        }
//    }
    
    // メッセージの上に文字を表示(名前)
    func messageTopLabelAttributedText(for message: MessageType, at indexPath: IndexPath) -> NSAttributedString? {
        let name = message.sender.displayName
        return NSAttributedString(string: name, attributes: [NSAttributedStringKey.font: UIFont.preferredFont(forTextStyle: .caption1)])
    }
    
    // メッセージの下に文字を表示(日付)
    func messageBottomLabelAttributedText(for message: MessageType, at indexPath: IndexPath) -> NSAttributedString? {
        let today = dateFmt.string(from: Date())
        let date = dateFmt.string(from: message.sentDate)
        let time = timeFmt.string(from: message.sentDate)
        if today == date {
            return NSAttributedString(string: time, attributes: [NSAttributedStringKey.font: UIFont.preferredFont(forTextStyle: .caption2)])
        } else {
            return NSAttributedString(string: date + " " + time, attributes: [NSAttributedStringKey.font: UIFont.preferredFont(forTextStyle: .caption2)])
        }
    }
}

// メッセージのdelegate
extension MessageKitViewController: MessagesDisplayDelegate {
    
    // メッセージの色を変更(黒)
    func textColor(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> UIColor {
        return .darkText
    }
    
    // メッセージの背景色を変更している(デフォルトは自分:緑、相手:グレー)
    func backgroundColor(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> UIColor {
        return ColorStyles().chatMsgBackColor
    }
    
    // メッセージの枠にしっぽを付ける
    func messageStyle(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> MessageStyle {
        let corner: MessageStyle.TailCorner = isFromCurrentSender(message: message) ? .bottomRight : .bottomLeft
        return .bubbleTail(corner, .curved)
    }
    
    // アイコンをセット
//    func configureAvatarView(_ avatarView: AvatarView, for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) {
//        // message.sender.displayNameとかで送信者の名前を取得できるので
//        // そこからイニシャルを生成するとよい
//        let avatar = Avatar(initials: "人")
//        avatarView.set(avatar: avatar)
//    }
}


// 各ラベルの高さを設定(デフォルト0なので必須)
extension MessageKitViewController: MessagesLayoutDelegate {
    
    func cellTopLabelHeight(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CGFloat {
        if indexPath.section % 3 == 0 { return 10 }
        return 0
    }
    
    func messageTopLabelHeight(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CGFloat {
        return 16
    }
    
    func messageBottomLabelHeight(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CGFloat {
        return 16
    }
}

let topViewController = UIApplication.shared.windows.last!.rootViewController!
extension MessageKitViewController: MessageCellDelegate {
    // メッセージをタップした時の挙動
    func didTapMessage(in cell: MessageCollectionViewCell) {
        let indexPath = messagesCollectionView.indexPath(for: cell)
        let msgData = messageList[(indexPath?.section)!]
        
        let appearance = SCLAlertView.SCLAppearance(
            kCircleIconHeight: 35.0,
            kTitleFont: UIFont(name: "HelveticaNeue", size: 0)!,
            kTextFont: UIFont(name: "HelveticaNeue", size: 0)!,
            kButtonFont: UIFont(name: "HelveticaNeue-Bold", size: 13)!,
            showCloseButton: false
        )
        let alertView = SCLAlertView(appearance: appearance)
        alertView.addButton(Strings().CHAT_REPORT_BTN, backgroundColor: ColorStyles().chatMsgBackColor, textColor: UIColor.white) {
            // chat_reportテーブルへinsert
            self.executeReport(msgData: msgData)
        }
        alertView.addButton(Strings().CHAT_USER_BLOCK_BTN, backgroundColor: ColorStyles().chatMsgBackColor, textColor: UIColor.white) {
            // UserDefaultへユーザーを保存
            self.saveBlockUser(name: msgData.sender.displayName)
            // 通報完了メッセージ
            self.simplePopup(msg: Strings().ADD_BLOCK_USER_FINISH)
            // TODO リフレッシュ処理
        }
        alertView.addButton(Strings().CHAT_CANCEL_BTN, backgroundColor: ColorStyles().chatMsgBackColor, textColor: UIColor.white) {
        }
        alertView.showCustom("", subTitle: "", color: ColorStyles().chatMsgBackColor, icon: UIImage(named:"bikkuri_white")!)
    }
    
    func executeReport(msgData: MockMessage){
        // 通報(chat_reportテーブル更新)
        self.getAddChatReport(msgData: msgData)
        // 通報完了メッセージ
        self.simplePopup(msg: Strings().CHAT_REPORT_FINISH)
    }
    
    func getAddChatReport(msgData: MockMessage){
        let from: String = msgData.sender.id
        let name: String = msgData.sender.displayName
        let text: String = msgData.text
        let sentDate: String = self.formatDate(date: msgData.sentDate)
        
        // http通信
        let url = URL(string: makeSiteUrl() + Strings().addChatReportUrl)
        var request = URLRequest(url: url!)
        let postWord = "from=" + from + "&name=" + name + "&text=" + text + "&sentDate=" + sentDate
        request.httpMethod = "POST"
        request.httpBody = postWord.data(using: .utf8)
        
        let task = URLSession.shared.dataTask(with: request, completionHandler: {
            (data, response, error) in
            if error != nil {
                print(error)
                return
            }
        })
        task.resume()
    }
}

extension MessageKitViewController: MessageInputBarDelegate {
    // メッセージ送信ボタンをタップした時の挙動
    func messageInputBar(_ inputBar: MessageInputBar, didPressSendButtonWith text: String) {
        for component in inputBar.inputTextView.components {
            if let image = component as? UIImage {
                
                let imageMessage = MockMessage(image: image, sender: currentSender(), messageId: UUID().uuidString, date: Date())
                messageList.append(imageMessage)
                messagesCollectionView.insertSections([messageList.count - 1])
                
            } else if let text = component as? String {
                //Firestoreにデータを送信、保存する
                var ref: DocumentReference? = nil
                ref = db.collection("chat1").addDocument(data: [
                    "name": chatName as! String,
                    "text": text,
                    "timestamp": NSDate()
                ]) { err in
                    if let err = err {
                        print("Error adding document: \(err)")
                    } else {
                        print("Document added with ID: \(ref!.documentID)")
                    }
                }
            }
        }
        inputBar.inputTextView.text = String()
        messagesCollectionView.scrollToBottom()
    }
    
}

fileprivate extension UIEdgeInsets {
    init(top: CGFloat = 0, bottom: CGFloat = 0, left: CGFloat = 0, right: CGFloat = 0) {
        self.init(top: top, left: left, bottom: bottom, right: right)
    }
}

 

Firebaseの新DB「Firestore」を使ったチャットページです。

Firebase部分以外はほぼ参考ページのままですが、メッセージタップ時に

「ユーザーブロック or 通報する」アラートを表示するよう機能追加しています。

このソースだけじゃ動かないけど、ご参考までに。

 

参考にしたページ → MessageKitの使いかた(初歩)