2 분 소요

ChatListController의 최신 메세지 목록을 위해서 ChatController에서 해줘야하는 작업들, 그리고 retain cycle을 다뤄보자.

  1. ChatListController - Recent messages list
  2. Save RecentMessage to Firestore
  3. Remove listener
    • isMovingFromParent
  4. weak

1. ChatListController - Recent messages list

ChatController : 채팅 컨트롤러

ChatController 에서 사용했던 것과 동일하게, qeury에 listener를 추가할 수 있다.

addSnapshotListener 함수를 이용해 listener를 추가한다. documentChangetype은 꼭 addedmodified를 모두 체크해줘야한다.

guard let currentUserId = Auth.auth().currentUser?.uid else { return }

let query = Firestore.firestore().collection("matches_messages").document(currentUserId).collection("recent_messages")
query.addSnapshotListener { (querySnapshot, err) in
    
    querySnapshot?.documentChanges.forEach({ (change) in
        if change.type == .added || change.type == .modified {
            let dictionary = change.document.data()
            let recentMessage = RecentMessage(dictionary: dictionary)
            self.recentMessagesDictionary[recentMessage.uid] = recentMessage
        }
    })
    
}

RecentMessage

struct RecentMessage {
    let uid, name, profileImageUrl, text: String
    let timestamp: Timestamp
}

Firebase DB dummy data를(RecentMessage) 직접 넣어보면 목록이 잘 나오는 것을 확인할 수 있다.

collection("matches_messages").document(currentUserId).collection("recent_messages")

여기서 recent message list에서 하나를 클릭하면 ChatController로 이동할 수 있도록 해야한다. 기존에서는 Match struct를 넘겨주면 이를 기반으로 ChatController 정보를 불러왔다.

Match가 필요하므로 RecentMessage로 Match를 만들어준다.

// selectItem
let recentMessage = Items[Index.item]
let dictionary = [
  // match 정보에 맞는 data
]
let match = Match(dictionary: dictionary)
let chatController = ChatController(match: match)
navigationController?.pushViewController(chatController, animated: true)

2. Save RecentMessage to Firestore

ChatListController에 정상적으로 나오는게 확인 되었으니 채팅을 할 때 DB에 저장해주면 된다.

주의할 점은 일반 채팅과 동일하게 나의 상대방 DB에 모두 동시에 업데이트 해줘야한다. data에서 살짝 실수해서 개고생했다!

나의 RecentMessage에는 상대방의 정보, 상대방의 RecentMessage에는 나의 정보를 넣어서 업데이트 해준다. setData() 함수를 사용하여서 업데이트 시켜준다.

guard let currentUserId = Auth.auth().currentUser?.uid else { return }

let data = ["text": customInputView.textView.text ?? "", "name": match.name, "profileImageUrl": match.profileImageUrl, "timestamp": Timestamp(date: Date()), "uid": match.uid] as [String : Any]

Firestore.firestore().collection("matches_messages").document(currentUserId).collection("recent_messages").document(match.uid).setData(data) { (err) in
    if let err = err {
        print("Failed to save recent message FROM", err)
        return
    }
}

guard let currentUser = self.currentUser else { return }
let toData = ["text": customInputView.textView.text ?? "", "name": currentUser.name ?? "", "profileImageUrl": currentUser.imageUrl1 ?? "", "timestamp": Timestamp(date: Date()), "uid": currentUserId] as [String : Any]
Firestore.firestore().collection("matches_messages").document(match.uid).collection("recent_messages").document(currentUserId).setData(toData) { (err) in
    if let err = err {
        print("Failed to save recent message TO", err)
        return
    }

3. Remove listener

ChatListController와 ChatController 모두 firebase의 listener를 사용하고 있는데, listener를 선언하면 계속해서 listener가 만들어진다. 따라서 Controller에 들어갈 때마다 listener가 생성되고, 제거되지는 않기 때문에 memory 문제를 야기시킨다. 그래서 우리는 listener를 직접 처리해줘야한다.

deinit

메모리 해제가 잘 됐는지 확인할 수 있는 방법은 deinit을 override하면 확인할 수 있다. 참조카운트가 0이라면 메모리가 해제될 수 있지만, 0이 아니면 해제되지 않는다. deinit은 클래스가 인스턴스가 할당 해지되기 전에 즉각 호출되며 클래스 타입에서만 사용가능하다.

deinit {
    print("reclaiming memory from Controller")
}

listener 처리

listener를 사용한 곳에서 listener를 전역변수에 저장한다.

listener = query.addSnapshotListener { (querySnapshot, err) in
    if let err = err {
        print("Failed to fetch messages", err)
        return
    }

viewWillDisappear

Controller가 사라질 때 listener를 remove해준다.

    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)
        
        if isMovingFromParent {
            listener?.remove()
        }
    }

isMovingFromParent

A Boolean value indicating whether the view controller is being removed from a parent view controller.

하지만 위에서 viewWIllDisappear은 여기에서 다른 controller에 의해서 가려지거나 새로운 controller를 push해버려도 호출된다.

이를 방지하기 위해서 isMovingFromParent를 사용한다. isMovingFromParent 는 부모 view controller에서 제거 여부를 반환한다.

그래서 isMovingFromParent를 통해 현재 이 controller가 pop되는지를 체크할 수 있다.

4. weak

weak를 사용하면 View 소유 관계에서 객체가 사라지면 약한참조는 nil로 바뀐다.

현재 ChatListController에서는 NewMatchesHorizontalController를 headerViewController로 사용하고 있다. 이전에 headerViewController에서 selectItem을 처리해주기 위해서 내부에 ChatListController를 변수를 선언해 연결했다.

header.matchesHorizontalController.chatListController = self

이를 weak로 선언해준다.

weak var chatListController: ChatListController?

이것까지 처리해주면 deinit이 정상적으로 실행되는 것을 확인할 수 있다.

References

댓글남기기