Reputation: 295
I'm developing an iOS chat app that uses Firebase Realtime Database for storing messages. I have a function that is called when the home chat screen is loaded. This function loads the recipient name, last message, timestamp and a profile pic. I've used DispatchGroup to sync all the calls. At first, I thought that it worked, but when I send a new message (update the DB in any way) the app crashes. I believe it is because the observe closure is being called again, and there is an imbalance between the enter/leave calls. I can't think of a way to make it work with DispatchGroup. Is there a way to fix this? Or is there a better option than DispatchGroup?
This is the main function with the firebase observer:
func getAllChatsForCurrentUser(completion: @escaping (_ chats: [Chat], _ error: Error?) -> Void) {
var chats = [Chat]()
let group = DispatchGroup()
let currentUserUID = Auth.auth().currentUser!.uid
let chatRef = Database.database().reference(withPath: "chats")
group.enter()
chatRef.observe(.value) { (snapshot) in
var childrenArray = [String]()
let children = snapshot.children
while let rest = children.nextObject() as? DataSnapshot {
childrenArray.append(rest.key) //1
}
for child in childrenArray {
if child.contains(currentUserUID) { //2
let otherUserUID = child.replacingOccurrences(of: currentUserUID, with: "")
group.enter()
self.getChatInfo(uid: otherUserUID, chatID: child) { (chat, err) in
chats.append(chat)
group.leave()
}
}
}
group.leave()
}
group.notify(queue: .main) {
completion(chats, nil)
}
}
1 - For the chat name I use a combination of 2 uid's. So here I have an array of all chats.
2 - If the chat name contains the current users uid - I'm working with it. The recipients uid in the other part of the string.
getChatInfo function below:
func getChatInfo(uid: String, chatID: String, completion: @escaping (_ chat: Chat, _ error: Error?) -> Void) {
let miniGroup = DispatchGroup()
var newChat = Chat()
newChat.otherUserUid = uid
miniGroup.enter()
self.getUserProfileFromUID(uid: uid) { (user, error) in
newChat.name = user.name
newChat.profilePic = user.photoURL
miniGroup.leave()
}
miniGroup.enter()
self.getLastMessageAndTimeForChat(chatID: chatID) { (message, time, error) in
newChat.lastMessage = message
newChat.lastMessageTime = time
miniGroup.leave()
}
miniGroup.notify(queue: .main) {
completion(newChat, nil)
}
}
I know that this is probably a bad way of structuring the data and calling the functions. At least I've been told so, without reasoning. Been stuck with this problem for nearly a week now, any info would be greatly appreciated.
UPDATE 1
Tried wrapping the leave()
calls in defer {}
, and tried playing around with NSOperations instead of DispatchGroup. Still no luck.
Upvotes: 2
Views: 285
Reputation: 295
So I figured it out by using a completion handler with a begin handler.
getChatsWithBeginAndComplete(beginHandler: {
self.group.enter()
self.group.notify(queue: .main) {
print("done")
self.tableView.reloadData()
}
}) {
self.group.leave()
}
And the function:
func getChatsWithBeginAndComplete(beginHandler: @escaping () -> (), completionHandler: @escaping () -> ()) {
allChatsHandle = allChatsRef.observe(.value) { (snapshot) in
let bigGroup = DispatchGroup()
beginHandler()
var childrenArray = [String]()
let children = snapshot.children
while let rest = children.nextObject() as? DataSnapshot {
childrenArray.append(rest.key)
}
for chatID in childrenArray {
if chatID.contains(currentUserUID) {
bigGroup.enter()
let funcGroup = DispatchGroup()
//Do more async stuff in the funcGroup
funcGroup.notify(queue: .main) {
self.chats.append(chat)
bigGroup.leave()
}
}
}
bigGroup.notify(queue: .main) {
completionHandler()
}
}
}
So here all the group.enter and group.leave calls are balanced, because they are called from the completion/begin handlers, or from inside the firebase observer. I don't think that it's the best way to handle this problem, but it's definitely one way. If somebody knows a better solution - please let me know.
Upvotes: 2