Notice
Recent Posts
Recent Comments
Link
관리 메뉴

김종권의 iOS 앱 개발 알아가기

[iOS - swift] 1. 실시간 채팅 앱 구현 방법 (MessageKit, Firebase, Firestore) - 채팅 UI 본문

iOS 응용 (swift)

[iOS - swift] 1. 실시간 채팅 앱 구현 방법 (MessageKit, Firebase, Firestore) - 채팅 UI

jake-kim 2021. 11. 16. 03:35

1. 실시간 채팅 앱 구현 방법 (MessageKit, Firebase, Firestore) - 채팅 UI

2. 실시간 채팅 앱 구현 방법 (MessageKit, Firebase, Firestore) - Firebase

SPM으로 MessageKit 설치

https://github.com/MessageKit/MessageKit

화면 플로우

MessageKit 주요 모듈

  • MessagesViewController: UICollectionView를 상속하여 구현된 모듈이고 이 모듈을 상속하여 CahtViewController 생성
    • 내부적으로 messagesCollectionView 프로퍼티 사용 가능
import UIKit
import MessageKit
class ChatVC: MessagesViewController {
}
  • MessageKit SDK를 다운받으면 AccessoryView위에 붙어있는 InputBar도 사용할 수 있으므로, InputBarAccessoryView도 import
    • 내부적으로 messageInputBar 사용 가능
import InputBarAccessoryView

messagesCollectionView 델리게이트 3가지 

messagesCollectionView.messagesDataSource = self
messagesCollectionView.messagesLayoutDelegate = self
messagesCollectionView.messagesDisplayDelegate = self
  • MessagesDataSource

  • MessageLayoutDelegate

  • MessageDisplayDelegete

messageInputBar 델리게이트

  • InputBarAccessoryViewDelegate

구현 - 데이터 모델 정의

  • 채팅방에 들어가기 전에 해당 채팅방에 들어갈 때, 해당 채팅방 정보를 담을 Channel 모델 정의
// Channel.swift

struct Channel {
    var id: String?
    let name: String
    
    init(id: String? = nil, name: String) {
        self.id = id
        self.name = name
    }
}

extension Channel: Comparable {
    static func == (lhs: Channel, rhs: Channel) -> Bool {
        return lhs.id == rhs.id
    }
    
    static func < (lhs: Channel, rhs: Channel) -> Bool {
        return lhs.name < rhs.name
    }
}
  • 채팅 내용에 이미지가 들어갈 수 있으므로, 이미지 파일과 연관된 모델 정의
    • MediaItem은 MessageKit안에 존재하는 protocol이며 이미지를 표출하는데 필요한 필수 정보들을 정의
// ImageMediaItem.swift

import UIKit
import MessageKit

struct ImageMediaItem: MediaItem {
  var url: URL?
  var image: UIImage?
  var placeholderImage: UIImage
  var size: CGSize

  init(image: UIImage) {
    self.image = image
    self.size = CGSize(width: 240, height: 240)
    self.placeholderImage = UIImage()
  }
}
  • 대화 내용에 사용될 Message 모델 정의
    • MessageType은 MessageKit내부에 존재하는 protocol이며 message의 필수 정보들을 정의
// Message.swift

import Foundation
import MessageKit
import UIKit

struct Message: MessageType {
    
    let id: String?
    var messageId: String {
        return id ?? UUID().uuidString
    }
    let content: String
    let sentDate: Date
    let sender: SenderType
    var kind: MessageKind {
        if let image = image {
            let mediaItem = ImageMediaItem(image: image)
            return .photo(mediaItem)
        } else {
            return .text(content)
        }
    }
    
    var image: UIImage?
    var downloadURL: URL?
    
    init(content: String) {
        sender = Sender(senderId: "id(TODO...)", displayName: "displayName(TODO...)")
        self.content = content
        sentDate = Date()
        id = nil
    }
    
    init(image: UIImage) {
        sender = Sender(senderId: "id(TODO...)", displayName: "displayName(TODO...)")
        self.image = image
        sentDate = Date()
        content = ""
        id = nil
    }
    
}

extension Message: Comparable {
  static func == (lhs: Message, rhs: Message) -> Bool {
    return lhs.id == rhs.id
  }

  static func < (lhs: Message, rhs: Message) -> Bool {
    return lhs.sentDate < rhs.sentDate
  }
}

채팅 UI 구현 핵심 코드

  • import
import UIKit
import MessageKit
import InputBarAccessoryView
  • MessagesViewController 상속
class ChatVC: MessagesViewController {
}
  • 현재 자기 자신의 프로필 정보 및 collectionView에 표출 될 dataSource 선언
let channel: Channel
var sender = Sender(senderId: "any_unique_id", displayName: "jake")
var messages = [Message]()
  • delegate 선정
private func confirmDelegates() {
    messagesCollectionView.messagesDataSource = self
    messagesCollectionView.messagesLayoutDelegate = self
    messagesCollectionView.messagesDisplayDelegate = self
    
    messageInputBar.delegate = self
}
  • send 버튼이 눌려지면 메시지를 collectionView의 cell에 표출하는 코드
private func insertNewMessage(_ message: Message) {
    messages.append(message)
    messages.sort()
    
    messagesCollectionView.reloadData()
}
  • Delegate 구현 - MessageDataSource (메시지 데이터 정의)
extension ChatVC: MessagesDataSource {
    func currentSender() -> SenderType {
        return sender
    }
    
    func numberOfSections(in messagesCollectionView: MessagesCollectionView) -> Int {
        return messages.count
    }
    
    func messageForItem(at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> MessageType {
        return messages[indexPath.section]
    }
    
    func messageTopLabelAttributedText(for message: MessageType, at indexPath: IndexPath) -> NSAttributedString? {
        let name = message.sender.displayName
        return NSAttributedString(string: name, attributes: [.font: UIFont.preferredFont(forTextStyle: .caption1),
                                                             .foregroundColor: UIColor(white: 0.3, alpha: 1)])
    }
}
  • Delegate 구현 - MessageLayoutDelegate (셀 관련 높이 값)
extension ChatVC: MessagesLayoutDelegate {
    // 아래 여백
    func footerViewSize(for section: Int, in messagesCollectionView: MessagesCollectionView) -> CGSize {
        return CGSize(width: 0, height: 8)
    }
    
    // 말풍선 위 이름 나오는 곳의 height
    func messageTopLabelHeight(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CGFloat {
        return 20
    }
}
  • Delegate 구현 - MessagesDisplayDelegate (상대방이 보낸 메시지, 내가 보낸 메시지를 구분하여 색상과 모양 지정)
    • cf) 상대방이 보낸거면 좌측, 내가 보낸거면 우측에 말풍선을 배치하는 것은 내부적으로 구현되어 있음
extension ChatVC: MessagesDisplayDelegate {
    // 말풍선의 배경 색상
    func backgroundColor(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> UIColor {
        return isFromCurrentSender(message: message) ? .primary : .incomingMessageBackground
    }
    
    func textColor(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> UIColor {
        return isFromCurrentSender(message: message) ? .black : .white
    }
    
    // 말풍선의 꼬리 모양 방향
    func messageStyle(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> MessageStyle {
        let cornerDirection: MessageStyle.TailCorner = isFromCurrentSender(message: message) ? .bottomRight : .bottomLeft
        return .bubbleTail(cornerDirection, .curved)
    }
}
  • Delegate 구현 - InputBarAccessoryViewDelegate (검색창에서 send 버튼을 누를 경우 이벤트 처리)
extension ChatVC: InputBarAccessoryViewDelegate {
    func inputBar(_ inputBar: InputBarAccessoryView, didPressSendButtonWith text: String) {
        let message = Message(content: text)
        
        insertNewMessage(message)
        inputBar.inputTextView.text.removeAll()
    }
}

* 전체 소스 코드: https://github.com/JK0369/ExChatWithRealTime/tree/Implement-Chat-UI

 

* 참고

https://www.raywenderlich.com/22067733-firebase-tutorial-real-time-chat

https://github.com/MessageKit/MessageKit/blob/master/Documentation/QuickStart.md

Comments