I am trying to call "observeSingleEvent" under swift loop but it get's called after local loop get's done
func fetchAllComments(){
let myGroup = DispatchGroup()
DispatchQueue.main.async {
myGroup.enter()
for index in self.spotVideos {
var id = String(index.videoId)
print(id)
var count = 0
self.videoRef = rootRef.child(id)
self.videoRef!.observeSingleEvent(of: .value) { snapshot in
print(snapshot.childrenCount)
myGroup.leave()
}
myGroup.wait()
}
myGroup.notify(queue: .main) {
print("Finished all requests.")
}
}
}
You should use the myGroup.enter() after the loop. Number of enter depends on number of leave. If your loop runs 3 times, there should be 3 enter and 3 leave, In your case you have 1 enter so when it get one leave, it notifies.
for index in self.spotVideos {
myGroup.enter()
...
}
Let's try a simpler approach.
Here's the function to call to get the ball rolling. Note there's a completion closure to handle whatever needs to be done once the children are counted.
func startFetch() {
self.fetchAllComments {
print("done!")
}
}
then the function to iterate over the spotVideos and get their id's. Print the child count of each node and return when done.
func fetchAllComments(completion: #escaping () -> Void) {
let lastIndex = self.spotVideos.count - 1
for (index, vidId) in self.spotVideos.enumerated() {
let ref = rootRef.child(vidId)
ref.observeSingleEvent(of: .value, with: { snapshot in
print("vid id: \(vidId) has \(snapshot.childrenCount) children")
if index == lastIndex {
completion()
}
})
}
}
Related
I created a method where I fetch all the published Books in Firebase. Each Book object stores a userId string value. I would like to fetch all books excluding currentUser books. I fetch 5 books every time I call the method starting from lastBookId, however if a user publishes more than 5 books, and are the first five, they can not be shown and as a result can not continue fetching them. I was thinking about increasing the limit value and calling the query observation again.
My code:
public func fetchBooksStarting(with lastBookId: String? = nil, completion: #escaping (Result<[Book], Error>) -> Void) {
var limit: UInt = 5
var books = [Book]()
let group = DispatchGroup()
var query = database.child("books").queryOrdered(byChild: "type")
if lastBookId != nil {
query = query.queryStarting(afterValue: BookType.Fiction.rawValue, childKey: lastBookId)
} else {
query = query.queryEqual(toValue: BookType.Fiction.rawValue)
}
query.queryLimited(toFirst: limit).observeSingleEvent(of: .value, with: { snap in
guard let snapshot = snap.children.allObjects as? [DataSnapshot] else {
completion(.failure(DatabaseErrors.failedToFetch))
return
}
books.removeAll()
for data in snapshot {
group.enter()
if let dict = data.value as? [String: AnyObject] {
let book = Book(dict: dict, bookId: data.key)
if book.userId == currentUserUid {
limit += 1
// recall query observe
} else {
books.append(book)
}
}
group.leave()
}
group.notify(queue: .main) {
completion(.success(books))
}
}, withCancel: nil)
}
I have 2 methods I call. I need to produce a model that contains the result of both and call another method.
I wanted to avoid placing 1 method inside another as this could expand out to 3 or 4 additional calls.
Essentially once I have the results for setUserFollowedState and loadFollowersForTopic I want to send both values to another function.
Coming from a JS land I would use async/await but this does not exist in Swift.
func setUserFollowedState() {
following.load(for: userID, then: { [weak self, topic] result in
guard self != nil else { return }
let isFollowed = (try? result.get().contains(topic)) ?? false
// do something with isFollowed?
})
}
func loadFollowersForTopic() {
followers.load(topic, then: { [weak self, topic] result in
guard self != nil else { return }
let count = (try? result.get().first(where: { $0.tag == topic })?.followers) ?? 0
// do something with count?
})
}
You can store both async call results as optional properties. When your callbacks happen, set these properties then check that both properties have been set. If they've been set, you know both async calls have returned.
private var isFollowed: Bool?
private var count: Int?
func setUserFollowedState() {
following.load(for: userID, then: { [weak self, topic] result in
guard let self = self else { return }
let isFollowed = (try? result.get().contains(topic)) ?? false
self.isFollowed = isFollowed
performPostAsyncCallFunctionality()
})
}
func loadFollowersForTopic() {
followers.load(topic, then: { [weak self, topic] result in
guard let self = self else { return }
let count = (try? result.get().first(where: { $0.tag == topic })?.followers) ?? 0
self.count = count
performPostAsyncCallFunctionality()
})
}
private func performPostAsyncCallFunctionality() {
// Check that both values have been set.
guard let isFollowed = isFollowed, let count = count else { return }
// Both calls have returned, do what you need to do.
}
The good thing about this approach is that you can easily add more async calls using the pattern. However, if you need to make that many async network calls at once, I would recommend you think about rewriting your server-side logic so you only need one network call for this functionality.
Another approach (which I believe is a bit cleaner) would be to use a DispatchGroup to combine the result of the mentioned methods.
You would modify your original methods to take a completion handler and then combine the two results where you actually need the data.
See example below.
func setUserFollowedState(completion: #escaping ((Bool) -> Void)) {
following.load(for: userID, then: { [weak self, topic] result in
guard self != nil else { return }
let isFollowed = (try? result.get().contains(topic)) ?? false
// Call completion with isFollowed flag
completion(isFollowed)
})
}
func loadFollowersForTopic(completion: #escaping ((Int) -> Void)) {
followers.load(topic, then: { [weak self, topic] result in
guard self != nil else { return }
let count = (try? result.get().first(where: { $0.tag == topic })?.followers) ?? 0
// Call completion with follower count
completion(count)
})
}
func loadFollowedAndCount() {
let group = DispatchGroup()
var isFollowed: Bool?
// Enter group before triggering data fetch
group.enter()
setUserFollowedState { followed in
// Store the fetched followed flag
isFollowed = followed
// Leave group only after storing the data
group.leave()
}
var followCount: Int?
// Enter group before triggering data fetch
group.enter()
loadFollowersForTopic { count in
// Store the fetched follow count
followCount = count
// Leave group only after storing the data
group.leave()
}
// Wait for both methods to finish - enter/leave state comes back to 0
group.notify(queue: .main) {
// This is just a matter of preference - using optionals so we can avoid using default values
if let isFollowed = isFollowed, let followCount = followCount {
// Combined results of both methods
print("Is followed: \(isFollowed) by: \(followCount).")
}
}
}
Edit: always make sure that a group.enter() is followed by a group.leave().
Now, I'm so confused about firebase with observe using childAdded data event type. The reason why I use childAdded to observe my firebase because I want to make my list page dynamic whether firebase has new data insert.
And my question is how to know observe is stop calling when reach the queryLimit? Because I have a indicator and I want to turn it off when reach the queryLimit.
My firebase structure below:
root {
talkID1(id by auto create) {
attribute1 ...
attribute2 ...
attribute3 ...
time
}
talkID2(id by auto create){
...
time
}
... // many talk ID which auto create by firebase
}
As what I know, if using childAdd to observe, data will one child by child to passing data in call back. So If I have N datas in firebase and I think it will calls N<=5 times, right?
My completion handler below:
func receiveIfExist(completion: #escaping (_ data: (My data type) -> Void) {
let requestWithQuery = Database.database.reference().child("root").queryOrdered(byChild: "time")
requestWithQuery.queryLimited(toLast: 5).observe(.childAdded, with: { (snapshot) in
guard let value = snapshot.value as? [String: Any] else { return }
self.getSingleTalkInfo(key: snapshot.key, value: value, completion: { (data) in
completion(data)
})
})
}
I'm calling receiveIfExist this function in viewDidLoad().
override func viewDidLoad() {
super.viewDidLoad()
self.myIndicator.startAnimating() // start running indicator
self.receiveIfExist { (data) in
self.handleTalk(with: data) // do something with request data
self.myIndicator.stopAnimating() // WEIRD!!!! Please look comments below
/*
I think it can not be added here because this completion will call N<=5 times just what I said before.
I think it should detect what my queryLimit data is and check the request data is this queryLimit data or not.
If yes then stop indicator animating, if not then keep waiting the queryLimit reach.
*/
}
}
How can I detect the observe is reach queryLimit?
If I can detect then I can turn off my indicator when it reach.
Thank you!
queryLimited(toLast: 5)
means (in much simpler words) please get the last 5 values (order is decided by the previous part of your query)
1. Now, since you are sorting the data by times , the values with the last 5 times will be retrieved, therefore your observer will be triggered 5 times
2. Note that if you have less than 5 records say 2 records, then it will be triggered only twice because maximum limit is 5, not minimum limit
3. Another point is that say if a new child is added and when you sort the items again according to the time and the new child is one of the last 5 items then this observer will be triggered again.
so to get the query limit you can make some changes in your code like this:
func receiveIfExist(completion: #escaping (data: YourType, limitCount: Int) -> Void) {
let requestWithQuery = Database.database.reference().child("root").queryOrdered(byChild: "time")
requestWithQuery.queryLimited(toLast: 5).observe(.childAdded, with: { (snapshot) in
guard let value = snapshot.value as? [String: Any] else { return }
self.getSingleTalkInfo(key: snapshot.key, value: value, completion: { (data) in
self.index = self.index + 1
completion(data, self.index)
})
})
}
Then using the above function as follows:
var index = 0
override func viewDidLoad() {
super.viewDidLoad()
self.myIndicator.startAnimating() // start running indicator
self.receiveIfExist { (data, limitCount) in
self.handleTalk(with: data) // do something with request data
if limitCount == 5 {
self.myIndicator.stopAnimating()
}
}
}
UPDATED:
Since very good point raised by Kevin, that above solution will fail if we have say only two records and index will never be equal to 5 and myIndicator will not stop animating,
One solution that comes to my mind is this:
First we get the children count using observeSingleEvent:
func getChildrenCount(completion: #escaping (_ childrenCount: Int) -> Void){
Database.database.reference().child("root").observeSingleEvent(of:.value with: { (snapshot) in
completion(snapshot.children.count)
}
}
then we apply the query to get last 5 items:
func receiveIfExist(completion: #escaping (data: YourType, limitCount: Int) -> Void) {
let requestWithQuery = Database.database.reference().child("root").queryOrdered(byChild: "time")
requestWithQuery.queryLimited(toLast: queryLimit).observe(.childAdded, with: { (snapshot) in
guard let value = snapshot.value as? [String: Any] else { return }
self.getSingleTalkInfo(key: snapshot.key, value: value, completion: { (data) in
self.index = self.index + 1
completion(data, self.index)
})
})
}
then use this count in your code as follows:
var index = 0
var childrenCount = 0
var queryLimit = 5
override func viewDidLoad() {
super.viewDidLoad()
self.myIndicator.startAnimating() // start running indicator
self.getChildrenCount {(snapChildrenCount) in
self.childrenCount = snapChildrenCount
self.receiveIfExist { (data, limitCount) in
self.handleTalk(with: data) // do something with request data
if (self.childrenCount < self.queryLimit && limitCount == self.childrenCount) || limitCount == self.queryLimit {
DispatchQueue.main.async {
self.myIndicator.stopAnimating()
}
}
}
}
}
func receiveIfExist(limitCount: UInt, completion: #escaping (data: MyDataType) -> Void) {
let requestWithQuery = Database.database.reference().child("root").queryOrdered(byChild: "time")
requestWithQuery.queryLimited(toLast: limitCount).observe(.childAdded, with: { (snapshot) in
guard let value = snapshot.value as? [String: Any] else { return }
self.getSingleTalkInfo(key: snapshot.key, value: value, completion: { (data) in
completion(data)
})
})
}
I also do this function for only observe single child's value
let requestTalks = Database.database.reference().child("root")
func getSingleTalk(by key: String = "", at limitType: TalkLimitType, completion: #escaping (_ eachData: MyDataType) -> Void) {
var requestSingleTalk: DatabaseQuery {
switch limitType {
case .first :
return self.requestTalks.queryOrdered(byChild: "time").queryLimited(toFirst: 1)
case .last :
return self.requestTalks.queryOrdered(byChild: "time").queryLimited(toLast: 1)
case .specificKey :
return self.requestTalks.child(key)
}
}
requestSingleTalk.observeSingleEvent(of: .value, with: { (snapshot) in
if limitType == .specificKey {
guard let value = snapshot.value as? [String: Any] else { return }
self.getSingleTalkInfo(key: snapshot.key, value: value, completion: { (data) in
completion(data)
})
} else {
guard let snapshotValue = snapshot.value as? NSDictionary,
let eachTalkKey = snapshotValue.allKeys[0] as? String,
let eachTalkValue = snapshotValue.value(forKey: eachTalkKey) as? [String: Any] else { return }
self.getSingleTalkInfo(key: eachTalkKey, value: eachTalkValue, completion: { (data) in
completion(data)
})
}
})
}
As a result, I can do something like this in my viewDidLoad()
override func viewDidLoad() {
super.viewDidLoad()
self.myIndicator.startAnimating()
self.receiveIfExist(limitCount: 5) { (eachData) in
self.handleTalk(with: eachData)
self.getSingleTalk(at: .last, completion: { (lastData) in
if eachData.keyID == lastData.keyID{
DispatchQueue.main.async {
self.myIndicator.stopAnimating()
}
}
})
}
}
I'm trying to create a completion handler for some data in my firebase database. I'm trying to use a while loop but nothing is happening. Code is below:
func ifUserIsMember(counter: Int, completionHandler: #escaping ((_ exist : Bool) -> Void)) {
let ref = FIRDatabase.database().reference()
ref.child("Test").child("\(counter)").observeSingleEvent(of: .value, with: { (snapshot) in
if snapshot.exists(){
let value = snapshot.value as? NSDictionary
test1 = value?["cal1"] as! String
test2 = value?["cal2"] as! String
test3 = value?["cal3"] as! String
completionHandler(true)
}else{
print("user is not a member of a team")
completionHandler(false)
}
})
}
//called this in viewDidLoad
var counter = 0
var ref: FIRDatabaseReference!
ref = FIRDatabase.database().reference()
while counter < 6 {
ifUserIsMember(counter: counter + 1) { (exist) -> () in
if exist == true {
print("Found something")
counter += 1
}
else {
print("NO DATA")
}
}
}
I'm trying to use a while loop to get all the data but it's not working. It won't come out of the loop and start over
Your code with some adds. Should work
func ifUserIsMember(counter: Int, completionHandler: #escaping (_ exist : Bool) -> Void)) {
let ref = FIRDatabase.database().reference()
ref.child("Test").child("\(counter)").observeSingleEvent(of: .value, with: { (snapshot) in
if snapshot.exists(){
let value = snapshot.value as? NSDictionary
test1 = value?["cal1"] as! String
test2 = value?["cal2"] as! String
test3 = value?["cal3"] as! String
completionHandler(true)
} else {
print("user is not a member of a team")
completionHandler(false)
}
})
}
//called this in viewDidLoad
var counter = 0
var ref: FIRDatabaseReference!
ref = FIRDatabase.database().reference()
while counter < 6 {
ifUserIsMember(counter: counter + 1,
completionHandler: { existing in
if existing {
print("Found something")
} else {
print("NO DATA")
}
})
counter += 1 // or you will have infinite loop
}
If you want to load an array of objects, for tableView for example, you should make another one function with completion handler. Something like
// for example it will return [String]
func getAllObjects(completion: (_ hasFinished: [String]) -> Void) {
var arrayToReturn = [String]()
var countOfChecked = 0
while counter < 6 {
ifUserIsMember(counter: counter + 1,
completionHandler: { existing in
var countOfChecked += 1
if existing {
print("Found something")
arrayToReturn(//append some data)
if countOfChecked == 6 { // its your number (6)
completion(arrayToReturn)
}
} else {
print("NO DATA")
}
})
counter += 1 // or you will have infinite loop
}
}
Something like this. You should get the idea.
Hope it helps
I'm not very familiar with Firebase and I'm not sure what you're trying to accomplish exactly, but updating counter in the completion handler isn't going to work.
If you put a log statement at the top of your ifUserIsMemberFunc, like: print("counter: \(counter)"), you might be surprised by the output.
If I'm reading your code correctly, you've basically got an infinite loop that creates Firebase queries to check for the existence of the child at 1.
If you want to run those queries in parallel, you'll want to put the counter += 1 outside of the completion block. However, then you'll need to wait for all of the queries to complete. (DispatchGroup is a good option.)
If you want to do something else, there are other options. Hope this helps!
Im working with NSURLSession. I have an array with restaurants and i'm requesting the dishes for every restaurant in the array to the api. The dataTask works,i'm just having a real hard time trying to call a method only when the all dataTasks are finished.
self.findAllDishesOfRestaurants(self.restaurantsNearMe) { (result) -> Void in
if result.count != 0 {
self.updateDataSourceAndReloadTableView(result, term: "protein")
} else {
print("not ready yet")
}
}
the self.updateDataSourceAndREloadTableView never gets called, regardless of my completion block. Here is my findAllDishesOfRestaurants function
func findAllDishesOfRestaurants(restaurants:NSArray, completion:(result: NSArray) -> Void) {
let allDishesArray:NSMutableArray = NSMutableArray()
for restaurant in restaurants as! [Resturant] {
let currentRestaurant:Resturant? = restaurant
if currentRestaurant == nil {
print("restaurant is nil")
} else {
self.getDishesByRestaurantName(restaurant, completion: { (result) -> Void in
if let dishesArray:NSArray = result {
restaurant.dishes = dishesArray
print(restaurant.dishes?.count)
allDishesArray.addObjectsFromArray(dishesArray as [AnyObject])
self.allDishes.addObjectsFromArray(dishesArray as [AnyObject])
print(self.allDishes.count)
}
else {
print("not dishes found")
}
// completion(result:allDishesArray)
})
completion(result:allDishesArray)
}
}
}
And here is my the function where i perform the dataTasks.
func getDishesByRestaurantName(restaurant:Resturant, completion:(result:NSArray) ->Void) {
var restaurantNameFormatted = String()
if let name = restaurant.name {
for charachter in name.characters {
var newString = String()
var sameCharacter:Character!
if charachter == " " {
newString = "%20"
restaurantNameFormatted = restaurantNameFormatted + newString
} else {
sameCharacter = charachter
restaurantNameFormatted.append(sameCharacter)
}
// print(restaurantNameFormatted)
}
}
var urlString:String!
//not to myself, when using string with format, we need to igone all the % marks arent ours to replace with a string, otherwise they will be expecting to be replaced by a value
urlString = String(format:"https://api.nutritionix.com/v1_1/search/%#?results=0%%3A20&cal_min=0&cal_max=50000&fields=*&appId=XXXXXXXXXappKey=XXXXXXXXXXXXXXXXXXXXXXXXXXXX",restaurantNameFormatted)
let URL = NSURL(string:urlString)
let restaurantDishesArray = NSMutableArray()
let session = NSURLSession.sharedSession()
let dataTask = session.dataTaskWithURL(URL!) { (data:NSData?, response:NSURLResponse?, error:NSError?) -> Void in
do {
let anyObjectFromResponse:AnyObject = try NSJSONSerialization.JSONObjectWithData(data!, options: NSJSONReadingOptions.AllowFragments)
if let asNSDictionary = anyObjectFromResponse as? NSDictionary {
let hitsArray = asNSDictionary.valueForKey("hits") as? [AnyObject]
for newDictionary in hitsArray! as! [NSDictionary]{
let fieldsDictionary = newDictionary.valueForKey("fields") as? NSDictionary
let newDish = Dish.init(dictionary:fieldsDictionary!, restaurant: restaurant)
restaurantDishesArray.addObject(newDish)
}
}
completion(result:restaurantDishesArray)
} catch let error as NSError {
print("failed to connec to api")
print(error.localizedDescription)
}
}
dataTask.resume()
}
Like i said before, I need to wait until the fun findAllDishesOfRestaurants is done. I tried writing my completion blocks but I'm not sure I'm doing it right. Any help is greatly appreciated. Thank
The problem is that you are calling the completion method in findAllDishesOfRestaurants before al tasks are complete. In fact, you are calling it once for each restaurant in the list, which is probably not what you want.
My recommendation would be for you to look into NSOperationQueue for two reasons:
It will let you limit the number of concurrent requests to the server, so your server does not get flooded with requests.
It will let you easily control when all operations are complete.
However, if you are looking for a quick fix, what you need is to use GCD groups dispatch_group_create, dispatch_group_enter, dispatch_group_leave, and dispatch_group_notify as follows.
func findAllDishesOfRestaurants(restaurants:NSArray, completion:(result: NSArray) -> Void) {
let group = dispatch_group_create() // Create GCD group
let allDishesArray:NSMutableArray = NSMutableArray()
for restaurant in restaurants as! [Resturant] {
let currentRestaurant:Resturant? = restaurant
if currentRestaurant == nil {
print("restaurant is nil")
} else {
dispatch_group_enter(group) // Enter group for this restaurant
self.getDishesByRestaurantName(restaurant, completion: { (result) -> Void in
if let dishesArray:NSArray = result {
restaurant.dishes = dishesArray
print(restaurant.dishes?.count)
allDishesArray.addObjectsFromArray(dishesArray as [AnyObject])
// self.allDishes.addObjectsFromArray(dishesArray as [AnyObject]) <-- do not do this
// print(self.allDishes.count)
}
else {
print("not dishes found")
}
// completion(result:allDishesArray) <-- No need for this, remove
dispatch_group_leave(group) // Leave group, marking this restaurant as complete
})
// completion(result:allDishesArray) <-- Do not call here either
}
}
// Wait for all groups to complete
dispatch_group_notify(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0)) {
completion(result:allDishesArray)
}
}