Reputation: 1360
I'm following Firebase's recommendation of flattening data, but I'm having trouble listing a series of items from my database.
Here's a sample of my database file:
"users" : {
"UID12349USER" : {
"firstName" : "Jon",
"lastName" : "Snow",
"email" : "[email protected]",
"albums" : {
"UID124ALBUM" : true,
"UID125ALBUM" : true
}
}
},
"albums" : {
"UID124ALBUM" : {
"name" : "My Artwork",
},
"UID125ALBUM" : {
"name" : "My Sketches",
}
}
I'm retrieving the list of albums for a given user:
let userAlbums = database.child(usersKey).child(user.uid).child(albumsKey)
userAlbums.observeSingleEventOfType(.Value, withBlock: { snapshot in
// fetch [UID124ALBUM: 1, UID125ALBUM: 1]
})
Now I wish I could retrieve all the user's albums in one single query. I could do a batch of queries, and populate an asynchronous array, but that doesn't seem like a good approach to me...
for key in albumKeys {
let album = database.child(self.albumsKey).child(key)
album.observeSingleEventOfType(.Value, withBlock: { snapshot in
// fetch album.. append to array
})
}
Using that approach makes it tricky to detect when the queries have finished, due to the asynchronous nature of the requests. Add to that the fact that some of the requests might fail, due to a bad connection.
Also, if I want to filter one of the albums with a given name (e.g. "My Artwork") or return nil if it doesn't exist, I also end up with a tricky end condition.
var found = false
for key in albumKeys {
let album = database.child(self.albumsKey).child(key)
album.observeSingleEventOfType(.Value, withBlock: { snapshot in
// if current.name == "My Artwork"
// completion(current)
})
}
// This block will be called before observeSingleEventOfType =.=
if !found {
completion(nil)
}
I have a good background on iOS and Swift, but I'm knew to Firebase and NoSQL databases. Can someone point me a good direction? Should I ditch Firebase and try something else? Am I missing some method that can query what I need? Is my json structure wrong and missing some extra keys?
Thanks
Upvotes: 13
Views: 5496
Reputation: 185
I would suggest using a DispatchGroup
and mutual exclusion to handle asynchronous functions within a for loop. Here is the code you provided with a DispatchGroup
to ensure that all of the asynchronous functions in the loop have completed before it checks the if statement:
let myGroup = DispatchGroup()
var found = false
// iterate through your array
for key in albumKeys {
let album = database.child(self.albumsKey).child(key)
// lock the group
myGroup.enter()
album.observeSingleEventOfType(.Value, withBlock: { snapshot in
if current.name == "My Artwork" {
found = true
}
// after the async work has been completed, unlock the group
myGroup.leave()
})
}
// This block will be called after the final myGroup.leave() of the looped async functions complete
myGroup.notify(queue: .main) {
if !found {
completion(nil)
}
}
Anything contained in the myGroup.notify(queue: .main) {
codeblock will not execute until myGroup.enter()
and myGroup.leave()
have been called the same amount of times. Be sure to call myGroup.leave()
within the Firebase observe block (after the async work) and make sure that it is called even if an error is produced from the observe.
Upvotes: 1
Reputation: 11
I'm new to both iOS and firebase, so take my solution with a grain of salt. Its a workaround, and it might not be airtight.
If I understood your question correctly, I'm facing a similar issue. You have "users" and "albums". I have "users" and "globalWalls". Inside "users" I have "wallsBuiltByUser" which holds keys of globalWalls.
I wanted to loop through wallsBuiltByUser and retrieve their corresponding node from globalWalls. At the end, I need to call my delegate notifying that the walls have been retrieved.
I believe this might be similar to what you are trying to do.
Here is my solution:
databaseRef.child("users/\(userID)/WallsBuiltByUser/").observeSingleEvent(of: FIRDataEventType.value, with: { (snapshot:FIRDataSnapshot) in
let numOfWalls = Int(snapshot.childrenCount)
var wallNum:Int = 0
for child in snapshot.children {
let wallId = (child as! FIRDataSnapshot).key
self.databaseRef.child("globalWalls").child(wallId).observeSingleEvent(of: .value, with: { (snapshot: FIRDataSnapshot) in
wallNum = wallNum + 1
let wallServerInfo = snapshot.value as? NSDictionary!
if (wallServerInfo != nil){
let wall = Wall(wallInfo: wallServerInfo)
self.returnedWalls.append(wall)
}
if(wallNum == numOfWalls){
print("this should be printed last")
self.delegate?.retrieved(walls: self.returnedWalls)
}
})//end of query of globalWalls
}//end of for loop
})//end of query for user's walls
Upvotes: 0
Reputation: 109
In your case, the only possible way around is to call another listener from inside of a listener. That way, you wouldn't need to separately handle the asynchronous nature of requests.
For example, in your case:-
let userAlbums = database.child(usersKey).child(user.uid).child(albumsKey)
userAlbums.observeSingleEventOfType(.Value, withBlock: { snapshot in
if(snapshot.exists()) {
// fetch [UID124ALBUM: 1, UID125ALBUM: 1] in the albumKeys
for key in albumKeys {
let album = database.child(self.albumsKey).child(key)
album.observeSingleEventOfType(.Value, withBlock: { snapshot in
// fetch album.. append to array
})
}
}
})
Now, to filter one of your albums, you may use completion like this inside a function:
var found = false
let userAlbums = database.child(usersKey).child(user.uid).child(albumsKey)
userAlbums.observeSingleEventOfType(.Value, withBlock: { snapshot in
if(snapshot.exists()) {
// fetch [UID124ALBUM: 1, UID125ALBUM: 1] in the albumKeys
for key in albumKeys {
let album = database.child(self.albumsKey).child(key)
album.observeSingleEventOfType(.Value, withBlock: { snapshot in
// if current.name == "My Artwork"
found = true
completion(current)
})
}
}
})
if !found {
completion(nil)
}
Upvotes: 0