Chat app (4)
user 검색
Firebase에 있는 유져를 검색한다.
1
2
3
4
5
6
7
static func fetchUsers(completion: @escaping([User]) -> Void) {
collection_User.getDocuments { snapshot, error in
guard let snapshot = snapshot else { return }
let users = snapshot.documents.map( {User(dictionary: $0.data())})
completion(users)
}
}
document에 접속하여 유저 데이터를 가져오는 함수를 작성.
NewChatVC에 user변수와 fetchUser함수 생성
1
2
3
4
5
6
7
8
9
10
11
12
13
14
private var users: [User] = [] {
didSet {
self.tableView.reloadData()
}
}
private func fetchUsers() {
showLoader(true)
UserServices.fetchUsers { users in
self.showLoader(false)
self.users = users
print(users)
}
}
실행하면 현재 등록된 유져에 대한 추가한 내용이 print 된다.
UserViewModel 생성
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
struct UserViewModel {
let user: User
var fullname: String { return user.fullname }
var username: String { return user.username }
var profileImageView: URL? {
return URL(string: user.profileImageURL)
}
init(user: User) {
self.user = user
}
}
UserCell에 추가
1
2
3
4
5
6
7
8
9
10
11
12
var viewModel: UserViewModel? {
didSet {
configure()
}
}
private func configure() {
guard let viewModel = viewModel else { return }
self.fullname.text = viewModel.fullname
self.username.text = viewModel.username
self.profileImageView.sd_setImage(with: viewModel.profileImageView)
}
이후 Newchat VC에 셀 적용
1
2
3
4
5
6
7
8
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: reuseIdentifier, for: indexPath) as! UserTableViewCell
let user = users[indexPath.row] // added
cell.viewModel = UserViewModel(user: user) // added
return cell
}
확인 완료.
User에서 자기자신은 제외하고 출력
1
2
3
4
5
6
7
8
9
10
11
12
13
private func fetchUsers() {
showLoader(true)
UserServices.fetchUsers { users in
self.showLoader(false)
self.users = users
guard let uid = Auth.auth().currentUser?.uid else { return }
guard let index = self.users.firstIndex(where: {$0.uid == uid}) else { return }
self.users.remove(at: index)
print(users)
}
}
이렇게 현재 유져의 uid에 해당하는 index를 제거.
User에게 채팅을 보내는 프로토콜 구현
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// NewChatVC
protocol NewChatViewControllerDelegate: AnyObject {
func controller(_ vc: NewChatViewController, wnatChatWithUser otherUser: User)
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let user = users[indexPath.row]
delegate?.controller(self, wnatChatWithUser: user)
}
// ConversationVC
@objc func handleNewChat() {
let controller = NewChatViewController()
controller.delegate = self // added
let nav = UINavigationController(rootViewController: controller)
present(nav,animated: true)
}
extension ConversationViewController: NewChatViewControllerDelegate {
func controller(_ vc: NewChatViewController, wnatChatWithUser otherUser: User) {
vc.dismiss(animated: true)
print(otherUser.fullname)
}
}
ChatVC에 나이외의 유저를 추가
1
2
3
4
5
6
private var otherUser: User
init(otherUser: User) {
self.otherUser = otherUser
super.init(collectionViewLayout: UICollectionViewFlowLayout())
}
다시 ConversationViewController로 돌아가서
didselectRowAt에 있던 내용을
1
2
3
4
5
6
7
8
9
10
private func openChat(user: User) { // new
let controller = ChatViewController(otherUser: user)
navigationController?.pushViewController(controller, animated: true)
}
func controller(_ vc: NewChatViewController, wantChatWithUser otherUser: User) {
vc.dismiss(animated: true)
print(otherUser.fullname)
openChat(user: otherUser) // added
}
이렇게 하면
화면전환을 하면서 채팅창이 보여진다.
Socket을 사용하여 메세지 기능 구현
기본 틀을 만들어 준다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
struct MessageServices {
static func fetchMessages() {
}
static func fetchRecentMessages() {
}
static func uploadMessage(message: String, currentUser: User, otherUser: User) {
let dataFrom: [String: Any] = [
"text": message,
"fromID": currentUser.uid,
"toID": otherUser.uid,
"timeStamp": Timestamp(date: Date()),
"username": otherUser.username,
"fullname": otherUser.fullname,
"profileImageURL": otherUser.profileImageURL
]
let dataTo: [String: Any] = [
"text": message,
"fromID": currentUser.uid,
"toID": otherUser.uid,
"timeStamp": Timestamp(date: Date()),
"username": currentUser.username,
"fullname": currentUser.fullname,
"profileImageURL": currentUser.profileImageURL
]
}
}
여기서 주목해야할건 uploadMessage에 대한 내용.
dictionary 형태로 하고, 이내용을 통헤 firebase에 저장이 되고, 또 로드를 하게된다.
Constansts에 관련 내용도 추가
let collection_Message = Firestore.firestore().collection("messages")
메세지를 올리는 함수의 내용은 다음과 같다
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
static func uploadMessage(message: String, currentUser: User, otherUser: User, completion: ((Error?) -> Void)?) {
let dataFrom: [String: Any] = [
"text": message,
"fromID": currentUser.uid,
"toID": otherUser.uid,
"timeStamp": Timestamp(date: Date()),
"username": otherUser.username,
"fullname": otherUser.fullname,
"profileImageURL": otherUser.profileImageURL
]
let dataTo: [String: Any] = [
"text": message,
"fromID": currentUser.uid,
"toID": otherUser.uid,
"timeStamp": Timestamp(date: Date()),
"username": currentUser.username,
"fullname": currentUser.fullname,
"profileImageURL": currentUser.profileImageURL
]
collection_Message.document(currentUser.uid).collection(otherUser.uid).addDocument(data: dataFrom) { _ in
collection_Message.document(otherUser.uid).collection(currentUser.uid).addDocument(data: dataTo, completion: completion)
collection_Message.document(currentUser.uid).collection("recent-message").document(otherUser.uid).setData(dataFrom)
collection_Message.document(otherUser.uid).collection("recent-message").document(currentUser.uid).setData(dataTo)
}
}
dataFrom
- 현재 사용자가 보낸 메세지의 데이터를 나타내며, 상대방의 사용자 정보를 포함
dataTo
- 상대방이 받은 메세지의 데이터를 나타내며, 현재 사용자의 정보를 포함
collection_Message 컬렉션에 두 개의 Documents를 저장.
- 현재 사용자의 컬렉션에 메세지 추가
- 상대방의 컬렉션에 같은 메세지 추가
- 최근 메세지를 업데이트
- setData를 사용함으로써 기존 데이터를 덮어 씌우게 된다.
- 각 사용자의 recent-message라는 서브 컬렉션에 최근 메세지 정보를 저장.
ChatVC에서 해당 함수 호출
1
2
3
4
5
6
7
8
9
private var currentUser: User
private var otherUser: User
// MARK: - Lifecycle
init(currentUser: User, otherUser: User) {
self.currentUser = currentUser
self.otherUser = otherUser
super.init(collectionViewLayout: UICollectionViewFlowLayout())
}
init에 현재 유저가 추가되면서 관련 되어있던 메서드들을 변경해준다.
1
2
3
4
private func openChat(currentUser: User, otherUser: User) {
let controller = ChatViewController(currentUser: currentUser, otherUser: user)
navigationController?.pushViewController(controller, animated: true)
}
실행하여 작동확인
hello라는 메세지를 보내본다.
제대로 작동한다면 collection에 추가가 되어있을것이다.
하나밖에 추가가 안되어 다시 코드를 보다가 문제를 찾았다.
1
2
3
4
private func openChat(currentUser: User, otherUser: User) {
let controller = ChatViewController(currentUser: currentUser, otherUser: otherUser) // modified
navigationController?.pushViewController(controller, animated: true)
}
otherUser가 user로 되어있어서 생긴 문제.
해결완료.
확인 완료.
Message 모델만들기
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
struct Message {
let text: String
let fromId: String
let toID: String
let timeStamp: Timestamp
let username: String
let fullname: String
let profileImageURL: String
var isFromCurrentUser: Bool
init(dictionary: [String: Any]) {
self.text = dictionary["text"] as? String ?? ""
self.fromId = dictionary["fromId"] as? String ?? ""
self.toID = dictionary["toID"] as? String ?? ""
self.username = dictionary["username"] as? String ?? ""
self.fullname = dictionary["fullname"] as? String ?? ""
self.profileImageURL = dictionary["profileImageURL"] as? String ?? ""
self.timeStamp = dictionary["timeStamp"] as? Timestamp ?? Timestamp(date: Date())
self.isFromCurrentUser = fromId == Auth.auth().currentUser?.uid
}
}
여기서 특이점이라면
init할때 self.isFromCurrentUser = fromId == Auth.auth().currentUser?.uid
이렇게 해서했다는것.
현재 유져가 보낸건지 아닌지에대한 True or false를 위와 같이 구분한다.
구분방법은
- Firebase Authentication을 통해 현재 사용자 확인
- Auth.auth().currentUser는 현재 로그인된 사용자를 리턴
- 사용자가 로그인되어 있지 않다면 currentUser는 nil이 된다.
- currentUser?.uid를 통해 현재 사용자의 고유 ID를 가져온다.
- 메시지의 보낸 사람 ID와 현재 사용자 ID 비교
- fromId와 currentUser?.uid를 비교하여 동일하면, 현재 사용자가 메시지를 보낸 것이므로 self.isFromCurrentUser를 true로 설정
- 그렇지 않으면 self.isFromCurrentUser를 false로 설정
이렇게 된다.
Messsage ViewModel 만들기
우선 만들기 전에 메세지를 가져오는 함수를 구현한다
1
2
3
4
5
6
7
8
9
10
11
12
static func fetchMessages(otherUser: User, completion: @escaping([Message]) -> Void) {
guard let uid = Auth.auth().currentUser?.uid else { return }
var messages = [Message]()
let query = collection_Message.document(uid).collection(otherUser.uid).order(by: "timeStamp", descending: true)
query.addSnapshotListener { snapshot, _ in
guard let documentChanges = snapshot?.documentChanges.filter({$0.type == .added}) else { return }
messages.append(contentsOf: documentChanges.map({Message(dictionary: $0.document.data())}))
completion(messages)
}
}
query는 timeStamp를 기준으로 내림순으로 정렬를 하게 한다.
최신 메세지가 먼저 오게 하기 위함
query.addSnapshotListener
를 통해 사용하여 쿼리에 대한 실시간 수신 대기를 설정
- snapshot이 변경될 때마다(즉, 새로운 메시지가 추가될 때마다) 호출
- documentChanges 배열에서 .added 타입의 변경만 필터링.
- 새로 추가된 문서(메시지)만을 가져오기 위함.
- 필터링된 문서 데이터를 Message 객체로 변환하여 messages 배열에 추가
- 업데이트된 messages 배열을 escaping Closure를 통해 전달.
그리고 ChatVC에서 호출하는 메서드를 구현
1
2
3
4
5
6
private func fetchMessages() {
MessageServices.fetchMessages(otherUser: otherUser) { messages in
self.messages = messages
print(messages)
}
}
1
[ChatApp.Message(text: "Hello", fromID: "ItlrMBBVskOUuenmDxNwocCowzS2", toID: "S91QM1IxdZYXNvxuBpgIFdYOHyf2", timeStamp: <FIRTimestamp: seconds=1717678529 nanoseconds=784600000>, username: "dd", fullname: "D D", profileImageURL:
이런식으로 출력이 되는걸 확인!
이제 진짜 ViewModel 생성
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
struct MessageViewModel {
let message: Message
var messageText: String { return message.text }
var messageBackgroundColor: UIColor { return message.isFromCurrentUser ? #colorLiteral(red: 0.4196078431, green: 0.831372549, blue: 0.431372549, alpha: 1) : #colorLiteral(red: 0.9058823529, green: 0.9098039216, blue: 0.9137254902, alpha: 1) }
var messageColor: UIColor { return message.isFromCurrentUser ? .white : .black }
var rightAnchorActive: Bool { return message.isFromCurrentUser }
var leftAnchorActive: Bool { return !message.isFromCurrentUser }
var shouldHideProfileImage: Bool { return message.isFromCurrentUser }
var profileImageURL: URL? { return URL(string: message.profileImageURL)}
var timeStampString: String? {
let date = message.timeStamp.dateValue()
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "hh:mm a"
return dateFormatter.string(from: date)
}
init(message: Message) {
self.message = message
}
}
이걸 하면서 느끼지만 여기 강의에서의 ViewModel은 확실히 성격이 다르다.
ViewModel과 데이터 바인딩
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// chatcell
func configure() {
guard let viewModel = viewModel else { return }
bubbleContainer.backgroundColor = viewModel.messageBackgroundColor
textView.text = viewModel.messageText
textView.tintColor = viewModel.messageColor
bubbleRightAnchor.isActive = viewModel.rightAnchorActive
dateRightAnchor.isActive = viewModel.rightAnchorActive
bubbleLeftAnchor.isActive = viewModel.leftAnchorActive
dateLeftAnchor.isActive = viewModel.leftAnchorActive
profileImageView.sd_setImage(with: viewModel.profileImageURL)
profileImageView.isHidden = viewModel.shouldHideProfileImage
guard let timeStampString = viewModel.timeStampString else { return }
datelabel.text = timeStampString
}
ChatVc에 적용
1
2
3
4
5
6
private func fetchMessages() {
MessageServices.fetchMessages(otherUser: otherUser) { messages in
self.messages = messages
self.collectionView.reloadData() // added
}
}
보낸쪽의 메세지가 시간이 조금 안으로 들어갔으나 전달이 되는걸 확인
dateRightAnchor = datelabel.rightAnchor.constraint(equalTo: bubbleContainer.leftAnchor, constant: -12)
-12인데 12로 되어서 안으로 말리는것도 수정완료.
보낸사람의 textColor도 흰색이어야하는데 검은색인것도 수정완료
textView.textColor = viewModel.messageColor
TextColor인데 TintColor로 되어있었다.
최근 보낸 메시지 Fetch
1
2
3
4
5
6
7
8
9
10
11
12
static func fetchRecentMessages(completion: @escaping([Message]) -> Void) {
guard let uid = Auth.auth().currentUser?.uid else { return }
let query = collection_Message.document(uid).collection("recent-message").order(by: "timeStamp")
query.addSnapshotListener { snapshot, _ in
guard let documentChanges = snapshot?.documentChanges else { return }
let messages = documentChanges.map({Message(dictionary: $0.document.data())})
completion(messages)
}
}
그리고 ConversationVC에 다음과 같이 호출하는 함수를 구현
1
2
3
4
5
private func fetchConversations() {
MessageServices.fetchRecentMessages { conversations in
print(conversations)
}
}
우선 프린트로 확인
1
[ChatApp.Message(text: "Hello", fromID: "ItlrMBBVskOUuenmDxNwocCowzS2", toID: "ItlrMBBVskOUuenmDxNwocCowzS2", timeStamp: <FIRTimestamp: seconds=1717677153 nanoseconds=38753000>, username: "ttt", fullname: "Test", profileImageURL:
출력이 되는걸 확인.
다음과 같이 변수를 만들어 준다.
1
2
3
4
5
6
7
private var conversations: [Message] = []{
didSet {
tableView.reloadData()
}
}
private var conversationDictionary = [String: Message]()
대화내용을 저장할 배열 conversation 이건 값이 들어올때마다 reload를 하기위해 didSet을 사용한다.
그리고 conversationDictionary를 사용하기전
Message 로가서
var chatPartnerID: String { return isFromCurrentUser ? toID : fromID }
를 추가.
대화 내용을 불러오는 메서드를 수정
1
2
3
4
5
6
7
8
9
private func fetchConversations() {
MessageServices.fetchRecentMessages { conversations in
conversations.forEach { conversation in
self.conversationDictionary[conversation.chatPartnerID] = conversation
}
self.conversations = Array(self.conversationDictionary.values)
}
}
위에 언급하지 않았던 conversationDictionary는 내가 다른사람과 대화한 대상과, 그에 해당하는 메세지를 담는 딕셔너리형배열이다.
MessageViewModel 에서
1
2
var fullname: String { return message.fullname }
var username: String { return message.username }
이걸 추가해준다.
추가하는 이유는 보낸사람의 이름을 확인하기 위함.
그 확인은 ChatCell에서
1
2
3
4
5
6
7
8
9
10
11
12
13
14
var viewModel: MessageViewModel? {
didSet{
configure()
}
}
private func configure() {
guard let viewModel = viewModel else { return }
self.profileImageView.sd_setImage(with: viewModel.profileImageURL)
self.fullname.text = viewModel.fullname
self.recentMessage.text = viewModel.messageText
self.dateLabel.text = viewModel.timeStampString
}
이렇게 추가해주었다.
클릭했을때 대화창 열기
ConversationVC에서 didSelectRowAt
메서드를 수정해주면된다.
1
2
3
4
5
6
7
8
9
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let conversation = conversations[indexPath.row]
showLoader(true)
UserServices.fetchUser(uid: conversation.chatPartnerID) { [self] otherUser in
showLoader(false)
openChat(currentUser: user, otherUser: otherUser)
}
}
이제 유져를 클릭하면 그 유져와의 대화가 열리게 된다.