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().
Related
I have a chat but i don't want to show all the messages at once because it's laggy. When i click on the chat i want to show the last 20 messages and everytime i scroll i want to fetch 20 older messages, that's why i'm using a query limitation. When i start loading the view, the last 20 messages are showing fine but everytime i scroll, nothing happens, it shows and print the same last 20 messages instead of displaying the 20 older one. I don't know how to insert the new messages correctly inside my collectionView, here's what i've tried so far:
[UPDATED]RoomMessageViewController
var lastDocumentSnapshot: DocumentSnapshot!
var fetchingMore = false
private var messages = [RoomMessage]()
private var chatMessages = [[RoomMessage]]()
override func viewDidLoad() {
super.viewDidLoad()
loadMessages()
}
// MARK: - Helpers
fileprivate func attemptToAssembleGroupedMessages(completion: (Bool) -> ()){
chatMessages.removeAll()
let groupedMessages = Dictionary(grouping: messages) { (element) -> Date in
return element.timestampDate.reduceToMonthDayYear() }
// provide a sorting for the keys
let sortedKeys = groupedMessages.keys.sorted()
sortedKeys.forEach { (key) in
let values = groupedMessages[key]
chatMessages.append(values ?? [])
self.collectionView.reloadData()
}
completion(true)
}
// MARK: - API
func loadMessages() {
var query: Query!
guard let room = room else{return}
guard let roomID = room.recentMessage.roomID else{return}
showLoader(true)
fetchingMore = true
if messages.isEmpty {
query = COLLECTION_ROOMS.document(roomID).collection("messages").order(by: "timestamp", descending: false).limit(toLast: 20)
print("First 10 msg loaded")
} else {
query = COLLECTION_ROOMS.document(roomID).collection("messages").order(by: "timestamp", descending: false).end(beforeDocument: lastDocumentSnapshot).limit(toLast: 20)
print("Next 10 msg loaded")
}
query.addSnapshotListener { querySnapshot, error in
guard let snapshot = querySnapshot else {
print("Error fetching snapshots: \(error!)")
return
}
guard let lastSnap = snapshot.documents.first else {return}
self.lastDocumentSnapshot = lastSnap
snapshot.documentChanges.forEach({ (change) in
if change.type == .added {
let dictionary = change.document.data()
let timestamp = dictionary["timestamp"] as? Timestamp
var message = RoomMessage(dictionary: dictionary)
self.messages.append(message)
self.messages.sort(by: { $0.timeStamp.compare($1.timeStamp) == .orderedAscending })
self.collectionView.reloadData()
}
self.attemptToAssembleGroupedMessages { (assembled) in
if assembled {
}
}
self.lastDocumentSnapshot = snapshot.documents.first
})
}
}
}
extension RoomMessageViewController {
override func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
let contentOffset = scrollView.contentOffset.y
if contentOffset <= -40 {
loadMessages()
}
}
Here is my asset fetching function.
You can see I am using limit logic, at the line limit(to: AlbumRepository.assetPageSize).
Then you will need an "index" to fetch documents right before/after your "index".
Ex: your message will have a prop named created_date.
We consider that asset is your message, albumDocId is your chatRoomId, asset_created is your message's timestamp.
So we will:
Query all documents that have created_date older than "index" (index here is a Date in second or TimeStamp). Line: whereField("asset_created", isLessThanOrEqualTo: lastCreatedForQuery)
Sort the result from newest to oldest by using created_date. Line: order(by: "asset_created", descending: true) to sort by asset_created ASC. (ASC when you display newest at the bottom, DESC when you display newest at the top)
Now we limit the number of returned documents to x items. Line: limit(to: AlbumRepository.assetPageSize), my value is 20 items.
After you get the first batch with index is now, just save the created_date of the last document in the returned list (from query's response). Then put in whereField("asset_created", isLessThanOrEqualTo: {index}). Last item on the list will be the oldest message.
// MARK: - Asset repo functions
static let assetPageSize = 20
func fetchAsset(albumDocId id: String, lastCreated: TimeInterval?, done: #escaping ([FSAssetModel]) -> Void) {
let lastCreatedForQuery = lastCreated ?? Date().timeIntervalSince1970
FirebaseFirestoreManager.db
.collection(tableName)
.document(id)
.collection("asset")
.whereField("asset_created", isLessThan: lastCreatedForQuery)
.order(by: "asset_created", descending: false)
.limit(to: AlbumRepository.assetPageSize)
.getDocuments() { (querySnapshot, err) in
if let err = err {
print("Error getting documents: \(err)")
done([])
} else {
var finalResult: [FSAssetModel] = []
for document in querySnapshot!.documents {
if let asset = FSAssetModel(JSON: document.data()) {
asset.doc_id = document.documentID
finalResult.append(asset)
}
}
done(finalResult)
}
}
}
Next, in your message screen, you have an array messages, to use it as dataSource.
And, private var _lastCreatedOfMessage: Date?
// When you open your message screen, you will call this function to fetch last x message,
// and the first time you call this function, you have no _lastCreatedOfMessage yet, it will be null.
// That means we will last x message from NOW
// You can check this line in the fetch method:
// let lastCreatedForQuery = lastCreated ?? Date().timeIntervalSince1970
// albumDocId: "ABCD1234" -> it will be your room id
// You will call this method anytime the user scrolls to the top/bottom to load history messages, after the `firstLoadMessages()` when you open screen.
fetchAsset(albumDocId: "ABCD1234", lastCreated: self._lastCreatedOfMessage) { [weak self] messages
guard let _self = self else, !messages.isEmpty { return }
// 1) Save your lastCreated for next fetch/query
// We order by created_date ASC so the first item will be oldest
// It will be used for next list [history message] fetching
_self.lastCreatedOfMessage = items.first!.created_date
// 2) Now we insert this message list to begin of your message list, that is dataSource for displaying
_self.yourMessageList.insert(contentsOf: messages, at: 0)
// 3) Reload your list, then scroll to the top or first indexPath
// Because you are dragging the listView down to see the older message
// That is why you use order by created_date [ASC]
_self.yourListView.reloadData() // ex: UICollectionView
_self.yourListView.scrollToItem(at: IndexPath.init(item: 0, section: 0), at: .top, animated: true)
// or more simple trick
// _self.yourListView.setContentOffset(.zero, animated: true)
}
Finally, to update your observer logic, you just need to observe the last new/changed message. Whenever you have a new incoming message, append it to your message list.
THE MOST REASONABLE LOGIC IS:
Open screen, fetch the list of last x messages,
When you get the list of last x messages, initialize your observer now, we will observe for any new message that has timestamp that is newer than the newest message from the above list.
static func observeNewMessage(roomId: String, lastTimeStamp: TimeInterval, completion: #escaping(RoomMessage?, Error?) -> Void) -> Query {
let now = Date().timeIntervalSince1970
let query = COLLECTION_ROOMS.document(roomId)
.collection("messages")
.whereField("timestamp", isGreaterThan: lastTimeStamp) // only take message that newer than lastTimeStamp
.order(by: "timestamp", descending: false)
.limit(toLast: 1)
// You will need to retain this query instance to keep observing for new message until you close message screen.
query.addSnapshotListener { querySnapshot, error in
guard let snapshot = querySnapshot else {
print("Error fetching snapshots: \(error!)")
return
}
snapshot.documentChanges.forEach { change in
let dictionary = change.document.data()
var message = RoomMessage(dictionary: dictionary)
if (change.type == .added) {
completion(message, nil)
print("Added msg: \(message.text)")
}
if (change.type == .modified) {
print("Modified msg: \(message.text)")
}
if (change.type == .removed) {
print("Removed msg: \(message.text)")
}
}
}
return query
}
How to use:
private var newMessageQueryListener: Query? // this variable is used to retain query [observeNewMessage]
// You only call this function once when open screen
func firstLoadMessages() {
showLoader(true)
guard let room = room else, let roomId = room.recentMessage.roomID { return }
fetchAsset(albumDocId: "ABCD1234", lastCreated: self._lastCreatedOfMessage) { [weak self] messages
guard let _self = self else, !messages.isEmpty { return }
// 1) Save your lastCreated for next fetch/query
// We order by created_date ASC so the first item will be oldest
// It will be used for next list [history message] fetching
_self.lastCreatedOfMessage = items.first!.timestamp
// 2) Now we insert this message list to begin of your message list, that is dataSource for displaying
_self.yourMessageList.insert(contentsOf: messages, at: 0)
// 3) Reload your list, then scroll to the top or LAST indexPath
// Because user just open screen not loading older messages
// ALWAYS call reload data before you do some animation,...
_self.yourListView.reloadData() // ex: UICollectionView
// Scroll to last index with position: bottom for newest message
let lastIndex = _self.messages.count - 1
_self.yourListView.scrollToItem(at: IndexPath.init(item: lastIndex, section: 0), at: .botom, animated: true)
// 4) Setup your observer for new message here
let lastTimeStamp = items.last!.timestamp
_self.makeNewMessageObserver(roomId: roomId, lastTimeStamp: lastTimeStamp)
}
}
private func makeNewMessageObserver(roomId: String, lastTimeStamp: TimeInterval) {
self.newMessageQueryListener = RoomService.observeNewMessage(roomId: roomId, lastTimeStamp: lastTimeStamp) { [weak self] newMess, error in
// DO NOT CALL self or REFER self in the block/closure IF you dont know its life-cycle.
// [weak self] will make an optional refering to self to prevent memory-leak.
guard let _self = self else { return } // here we check if self(MessageScreen) is null or not, if not null we continue
_self.messages.append(newMess) // ASC so the last item in list will be newest message
// Reload then scroll to the bottom for newest message
DispatchQueue.main.async {
_self.collectionView.reloadData()
let lastIndex = _self.messages.count - 1
_self.collectionView.scrollToItem(at: IndexPath(item: lastIndex, section: 0), at: .bottom, animated: true)
}
}
}
The rule is: Fetch a list, but observe one.
func loadMessages() {
var query: Query!
guard let room = room else{return}
guard let roomID = room.recentMessage.roomID else{return}
showLoader(true)
fetchingMore = true
if messages.isEmpty {
query = COLLECTION_ROOMS.document(roomID).collection("messages").order(by: "timestamp", descending: false).limit(toLast: 20)
print("First 10 msg loaded")
} else {
query = COLLECTION_ROOMS.document(roomID).collection("messages").order(by: "timestamp", descending: false).end(beforeDocument: lastDocumentSnapshot).limit(toLast: 20)
print("Next 10 msg loaded")
}
query.addSnapshotListener { querySnapshot, error in
guard let snapshot = querySnapshot else {
print("Error fetching snapshots: \(error!)")
return
}
guard let lastSnap = snapshot.documents.first else {return}
self.lastDocumentSnapshot = lastSnap
snapshot.documentChanges.forEach({ (change) in
if change.type == .added {
let dictionary = change.document.data()
let timestamp = dictionary["timestamp"] as? Timestamp
var message = RoomMessage(dictionary: dictionary)
self.messages.append(message)
self.messages.sort(by: { $0.timeStamp.compare($1.timeStamp) == .orderedAscending })
self.collectionView.reloadData()
}
self.attemptToAssembleGroupedMessages { (assembled) in
if assembled {
}
}
self.lastDocumentSnapshot = snapshot.documents.first
})
}
}
}
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()
}
})
}
}
I have two observers within my app, one that is ordered, one that isn't. The unordered observer seems to interfere with the ordered observer's results.
My database looks like this:
"events" : {
"Oo75nbcDsUK7vPWGDbnL" : {
"queue" : {
"K7THdbKzd2aSfaD9a0xmsszkReq1" : {
"queuePosition" : 1
},
"R5UwSxlH3vhH6SjTNMGfMoiaGae2" : {
"queuePosition" : 2
}
}
}
}
I have a class that handles the observer creation for the real-time database with the following static function:
static func listenToRtdbDocument<T: JSONDecodable>(_ refString: String, eventType: DataEventType = .value, orderedByChild: String? = nil, limit: Int? = nil, fromCollection collection: Firebase.RtdbCollections? = nil, completion: #escaping (_ decodedDoc: T?, _ error: Error?) -> ()) -> DatabaseHandle {
var query: DatabaseQuery
if let orderedByChild = orderedByChild {
query = rtdb.child(refString).queryOrdered(byChild: orderedByChild)
} else {
query = rtdb.child(refString)
}
if let limit = limit {
query.queryLimited(toFirst: UInt(limit))
}
return query.observe(eventType, with: { snapshot in
guard var docData = snapshot.value as? [String : Any] else {
completion(nil, nil)
return
}
docData["id"] = snapshot.key
let decodedDoc = T(json: docData)
completion(decodedDoc, nil)
}) { error in
completion(nil, error)
}
}
This creates the observer and then returns a DatabaseHandle reference. I use this function in two different places in my app. The first is inside a collection view cell model. This calls the function like so:
queuerRefString = "events/Oo75nbcDsUK7vPWGDbnL/queue/R5UwSxlH3vhH6SjTNMGfMoiaGae2"
func listenToQueuer(updateHandler: #escaping (QueuerJSONModel?) -> ()) {
guard queuerListener == nil,
let refString = queuerRefString else { return }
queuerListener = FirebaseClient.listenToRtdbDocument(refString) { (queuer: QueuerJSONModel?, error) in
guard error == nil else {
return
}
updateHandler(queuer)
}
}
The second is from a view controller model. This view controller gets presented over the collection view cell:
queueRefString = "events/Oo75nbcDsUK7vPWGDbnL/queue"
func listenToQueue() {
guard queueChildAddedListener == nil
let refString = queueRefString else { return }
queueChildAddedListener = FirebaseClient.listenToRtdbDocument(refString, eventType: .childAdded, orderedByChild: "queuePosition", limit: 25) { [weak self] (queuer: QueuerJSONModel?, error) in
guard let strongSelf = self,
let queuer = queuer,
error == nil else {
print("an error occurred")
return
}
strongSelf.queuers?.append(queuer)
}
}
For the ordered array observer, this always returns the current user first, and then the rest of the ordered queue. E.g. If the current user is at position 5, the queuers array will look like this:
5, 1, 2, 3, 4, 6, 7, 8, 9, 10
How can I stop them from interfering with each other??
UPDATE
How to reproduce:
Put this code in one view controller's viewDidLoad method:
let test1 = Database.database().reference()
.child("events/Oo75nbcDsUK7vPWGDbnL/queue/R5UwSxlH3vhH6SjTNMGfMoiaGae2")
.observe(.value, with: { snapshot in
guard var docData = snapshot.value as? [String : Any] else {
return
}
docData["id"] = snapshot.key
let queuer = QueuerJSONModel(json: docData)!
print("ok we got", queuer.queuePosition) // Prints out 2
})
Then put this code in another view controller's viewDidLoad method:
let test2 = Database.database().reference()
.child("events/Oo75nbcDsUK7vPWGDbnL/queue")
.queryOrdered(byChild: "queuePosition")
.queryLimited(toFirst: 25)
.observe(.childAdded, with: { snapshot in
guard var docData = snapshot.value as? [String : Any] else {
return
}
docData["id"] = snapshot.key
let queuer = QueuerJSONModel(json: docData)!
print("ok we got", queuer.queuePosition) // Prints out 2, then 1
})
First view the view controller that has test1 in, then view the one with test2 in. I use a tab bar controller to switch between the two.
Oddly, if these two pieces of code are put within the same viewDidLoad method of a view controller, then the ordered listener works as expected.
The behavior you're observing is because of to the way 'value' events are different from 'childAdded' events.
In your first observer (using .value), you're simply requesting all the data from events/Oo75nbcDsUK7vPWGDbnL/queue/R5UwSxlH3vhH6SjTNMGfMoiaGae2 in a single snapshot. If any data under the location changes while that observer is still added, your observer will get invoked again with a snapshot of everything at that location.
In your second observer (using .childAdded), you're requesting that your observer be called once for each child at events/Oo75nbcDsUK7vPWGDbnL/queue. If you have two children there, your observer will be called once for each child. As new children are added at that location, your observer will get called again, once for each new one.
The observers are not interfering with each other. They're just doing different things.
This is Firebase support's response to my question:
Apparently, this is an intended behavior. The idea of childAdded is, here is what you have retrieved so far, and what hasn't been added yet. So the childAdded gives R5UwSxlH3vhH6SjTNMGfMoiaGae2 as your preloaded data, and comes back with K7THdbKzd2aSfaD9a0xmsszkReq1 as your new data.
This way, even if you have requested it to be ordered, it won't necessarily come back ordered, if you have preloaded data on your device.
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()
}
}
})
}
}
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)
}
}