Waiting until a certain task completely finish to execute another task - swift

I'm trying to retrieve some encryption messages from Firebase Realtime Database, decrypt them, and display them in the CollectionView. The decrypting process is successful, but I have faced a problem about multithreading that: The order of the fetched-decrypted messages added to the Messages array is wrong, so they are not displayed in the CollectionView with the correct order, the order of displaying message in CollectionView varies during each run. I thought this problem happens because the time needed to finish decrypting process of each encrypted message is different, some encrypted messages need more time to decrypt, and some encrypted messages finish decrypting before others, so the order that they are added to the Messages array is no longer correct. The workflow that I expect that:
Making fetch request to the messages node on Firebase Database
With each fetched message:
3.1. Decrypt it
3.2. Append it to the Messages array
3.3. Reload the CollectionView to update UI
But I don't know how to use GCD to achieve that correctly, the showing messages is not in the correct order because the concurrency problem. But, I found a solution that if I try to place a sleep(1) command to my code, the code run correctly, but it's too slow because of sleeping command. I tried many ways, but it doesn't seem right, except for using sleep(1) command. Please help me to do this properly, thank you so much!. Here is my code:
func observeMessage(){
self.eThree = VirgilHelper.sharedVirgilHelper.eThreeToUse!
// Get current user's UID
guard let uid = FIRAuth.auth()?.currentUser?.uid , let toId = self.user?.id else {
return;
}
let userMessagesRef = FIRDatabase.database().reference().child("user-messages").child(uid).child(toId);
userMessagesRef.observe(.childAdded, with: { (snapshot) in
let messageId = snapshot.key;
let messagesRef = FIRDatabase.database().reference().child("messages").child(messageId);
// Observe the entire value of that node
messagesRef.observeSingleEvent(of: .value, with: { (snapshot) in
if let dictionary = snapshot.value as? [String:AnyObject] {
//sleep(1) // The working sleep command, but it's too slow
let message = Message(dictionary: dictionary)
if let fromUID = message.fromId, let toUID = message.toId, let cipherText = message.text {
self.eThree!.lookupPublicKeys(of: [fromUID], completion: { (lookupResult, error) in
if error != nil {
print("Error when looking up the Public Key of UID \(fromUID), \(String(describing: error))")
}
if let lookupResult = lookupResult {
message.text = try! self.eThree!.decrypt(text: cipherText, from: lookupResult[fromUID]!)
print("text: \(message.text)")
// The concurency prolem happens at here
self.messages.append(message);
// Go back to main thread to update UI
DispatchQueue.main.async {
// The concurency prolem happens at here, UI doesn't display with correct order of fetched-decrypted messages
self.collectionView?.reloadData()
let indexPath = IndexPath(item: self.messages.count-1, section: 0)
self.collectionView?.scrollToItem(at: indexPath, at: .bottom, animated: true);
}
}
})
}
}
}, withCancel: nil)
}, withCancel: nil)
}

In Swift, to wait for another task to be done before continue on with other task, you can use DispatchQueue.group().
let group = DispatchGroup()
group.enter()
DispatchQueue.main.async {
print("1")
group.leave()
}
group.enter()
DispatchQueue.main.async {
print("2")
group.leave()
}
group.enter()
DispatchQueue.main.async {
print("3")
group.leave()
}
group.notify(queue: .main) {
print("Done")
}
So the way you use it:
Initialize group
Enter the group by: group.enter(), before you start task
Put: group.leave(), after each task
Pass closure to group.notify. It will be executed when group task is empty.
NOTE:
A number of .enter() needs to match with .leave()

Related

Removing the Firebase Observer does not work

I load some data to show them on a tableView. I try to remove the observer for this. Right now the observer shows duplicate content. It shows the a post 10-15 times.
This is how I observe:
func loadData(){
let placeIdFromSearch = ViewController.placeidUebertragen
ref = Database.database().reference().child("placeID/\(placeIdFromSearch)/\(ViewComments.subComment)/subcomment/")
ref.observe(.childAdded) { (snapshot) in
print("something changed")
guard let dic = snapshot.value as? [String: Any] else { return }
let newPost = importSubPosts(dictionary: dic, key: snapshot.key)
guard let userUid = newPost.userID else { return }
self.fetchUser(uid: userUid, completed: {
self.table.insert(newPost, at: 0)
self.tableView.reloadData()
})
}
}
func fetchUser(uid: String, completed: #escaping () -> Void) {
ref = Database.database().reference().child("user").child(uid)
ref.observe(.value) { (snapshot) in
guard let dic = snapshot.value as? [String: Any] else { return }
let newUser = UserModel(dictionary: dic)
self.users.insert(newUser, at: 0)
completed()
}
}
This should remove the observer:
ref = Database.database().reference().child("placeID/\(placeIdFromSearch)/\(ViewComments.subComment)/subcomment/")
ref.removeAllObservers()
But nothing happens.
PS: If it helps if I deactivate this part the error goes away. But then I don't have user profile picture. Is It because I use this func already on a different tableView?
ref.observe(.value) { (snapshot) in
guard let dic = snapshot.value as? [String: Any] else { return }
let newUser = UserModel(dictionary: dic)
self.users.insert(newUser, at: 0)
The Firebase documentation provides a number of examples of how to read and write data
In this case it appears there are a couple of issues;
One is how to implement listeners:
.childAdded - This event is triggered once for each existing child and
then again every time a new child is added to the specified path
ChildAdded is great for initially loading a list of data e.g. several child nodes and watching for additions to that list thereafter. If you want to load the data and then stop listening, you can remove the listener from inside the .childAdded closure (there are other options as well)
If you're interested in loading just a single node, 'subcomment' see the load single node section below
If, however subcomment should be plural 'subcomments' because there are several, you can user this pattern to load them without leaving an observer
let usersRef = my_firebase.child("users")
usersRef.observeSingleEvent(by: .value, with: { snapshot in
//all users are returned to the app in the snapshot
// .value means 'give me everything in the specified node'
// so then we can iterate over the snapshot to get each users data
}
Load single node -
The second issue which ties to the first issue within the 'fetchUser' function - it appears you want to read in a single user from Firebase, one time.
While you can use .observe to do that, it actually reads in ALL data and listens for all events ongoing which doesn't appear like you want to do.
A better solution is to read the user one time with observeSingleEvent aka getData that reads it in and doesn't leave a listener - therefore eliminating the need to remove observers when the read is done.
There's a perfect example in the documentation that you could almost copy and paste the code - it reads a single user, and doesn't leave an observer. It could be used for reading the subcomment as well without leaving a listener attached. See the code at Read Data Once
ref.child("users/\(uid)/username").getData(completion: { error, snapshot in
guard error == nil else {
print(error!.localizedDescription)
return;
}
let userName = snapshot.value as? String ?? "Unknown";
});

Dispatch Group crash

I have created a method to fetch user messages from Firebase, however when leaving DispatchGroup app crashes leading to this error Thread 1: EXC_BAD_INSTRUCTION (code=EXC_I386_INVOP, subcode=0x0)
I'm not sure what I'm doing wrong. Please help and explain me.
Code:
public func fetchMessages(for userId: String, completion: #escaping (_ result: Result<([Message], [String: Message]), Error>) -> Void) {
let group = DispatchGroup()
var messages = [Message]()
var messagesDict = [String: Message]()
group.enter()
database.child("user-messages").child(userId).observe(.childAdded, with: { [weak self] snapshot in
let messageId = snapshot.key
let messagesRef = self?.database.child("messages").child(messageId)
messagesRef?.observeSingleEvent(of: .value, with: { snapshot in
if let dict = snapshot.value as? [String: AnyObject] {
let message = Message(dict: dict)
if let chatPartnerId = message.chatPartnerId() {
messagesDict[chatPartnerId] = message
messages = Array(messagesDict.values)
messages.sort { message1, message2 in
guard let timestamp1 = message1.timestamp?.intValue, let timestamp2 = message2.timestamp?.intValue else { return false }
return timestamp1 > timestamp2
}
group.leave() // Crashes
}
}
}, withCancel: nil)
}, withCancel: nil)
group.notify(queue: .main) {
print("Array: \(messages)\nDict: \(messagesDict)")
}
}
It is because you are using observe option. Which probably is notifying you several times. The crash happens due to the fact that you call ‘leave’ with calling ‘enter’ before. I mean you did call ‘enter’. And you do call ‘leave’. But because you observe the completion is probably called more than once. Which will trigger another ‘leave ‘ call while you called the ‘enter’ only once.
You can easily reproduce the crash with this code
import Foundation
let group = DispatchGroup()
group.enter()
group.notify(queue: .main) {
print("hello")
}
group.leave()
group.leave() // error: Execution was interrupted, reason: EXC_BREAKPOINT (code=1, subcode=0x18013d990).
I'm not sure if you need the observe functionality - i.e listen for changes on the object.
Generally, I'd recommend using Firestore - the new (not that new) DB by Firebase.
Or follow this guide for getting/setting data from Firebase Realtime Database https://firebase.google.com/docs/database/ios/read-and-write
If you need the "listening" i.e observe feature, I'm not sure how the usage of DispatchGroup helps with your implementation. You would generally use it when you, for example, release 2 (or more) API calls in parallel and want to gather all the information from them.
You would create a DispatfchGroup, call enter according to the number of calls you are releasiong, and call leave after you gathered the relevant information.
Something like this
struct AppInfo {
var items: [items] = []
var instructors: [Instructors] = []
var students: [Student] = []
}
func appBootstrapAPICalls(completion: ((AppInfo) -> Void)?) {
var appInfo = AppIfno()
let group = DispatchGroup()
group.enter()
group.enter()
group.enter()
self.fetchItems { items in
appInfo.items.append(contentsOf: items)
group.leave()
}
self.fetchStudents { students in
appInfo.students.append(contentsOf: students)
group.leave()
}
self.fetchInstructors { instructors in
appInfo.instructors.append(contentsOf: instructors)
group.leave()
}
group.notify(queue: .main) {
completion?(appInfo)
}
}

Why my DateTask code block does not work?

I create a request to the server, and in the end I expect to receive data, which I then transform into a model using a function, for this I created a session
func fetchNewsData(forCoutry country: String, category: String, complition: #escaping (NewsDataModel) -> ()) {
let urlString = "some url string"
guard let url = URL(string: urlString) else { return }
let session = URLSession(configuration: .default)
let task = session.dataTask(with: url) { data, response, error in
print ("ERROR: \(error)")
guard let data = data else { return }
guard let newsData = self.parseJSON(withData: data) else { return }
complition(newsData)
}
task.resume()
}
but the following code just doesn't work
print ("ERROR: \(error)")
guard let data = data else { return }
guard let newsData = self.parseJSON(withData: data) else { return }
complition(newsData)
I used breakpoints to find out until what point everything is going well, and I realized that this particular block of code is not working.
when I set a breakpoint between the let session and the let task, the code stopped there, but when I set my code to an print(error), this breakpoint did not work
I used the function fetchNewsData in viewDidLoad and I want to work to fill the array with elements that I expect to receive from the data that will come on this request, but my array does not receive any elements, and it remains empty, because of this my application does not work
why part of the code doesn't work, and how can I get the data I need from it?
The problem turned out to be a poor understanding of closures
I was not calling my method correctly to get the data. Having figured it out, I realized that the problem is precisely in a different approach when calling this method

swift wait until the urlsession finished

each time i call this api https://foodish-api.herokuapp.com/api/ i get an image. I don't want one image, i need 11 of them, so i made the loop to get 11 images.
But what i can't do is reloading the collection view once the loop is finish.
func loadImages() {
DispatchQueue.main.async {
for _ in 1...11{
let url = URL(string: "https://foodish-api.herokuapp.com/api/")!
let task = URLSession.shared.dataTask(with: url) {(data, response, error) in
guard let data = data else { return }
do {
let json = try JSONSerialization.jsonObject(with: data, options: []) as? [String : String]
print(json!["image"]!)
self.namesOfimages.append(json!["image"]!)
} catch {
print("JSON error: \(error.localizedDescription)")
}
}.resume()
}
}
self.collectionV.reloadData()
print("after resume")
}
Typically, when we want to know when a series of concurrent tasks (such as these network requests) are done, we would reach for a DispatchGroup. Call enter before the network request, call leave in the completion handler, and specify a notify block, e.g.
/// Load images
///
/// - Parameter completion: Completion handler to return array of URLs. Called on main queue
func loadImages(completion: #escaping ([URL]) -> Void) {
var imageURLs: [Int: URL] = [:] // note, storing results in local variable, avoiding need to synchronize with property
let group = DispatchGroup()
let count = 11
for index in 0..<count {
let url = URL(string: "https://foodish-api.herokuapp.com/api/")!
group.enter()
URLSession.shared.dataTask(with: url) { data, response, error in
defer { group.leave() }
guard let data = data else { return }
do {
let foodImage = try JSONDecoder().decode(FoodImage.self, from: data)
imageURLs[index] = foodImage.url
} catch {
print("JSON error: \(error.localizedDescription)")
}
}.resume()
}
group.notify(queue: .main) {
let sortedURLs = (0..<count).compactMap { imageURLs[$0] }
completion(sortedURLs)
}
}
Personally, rather than JSONSerialization, I use JSONDecoder with a Decodable type to parse the JSON response. (Also, I find the key name, image, to be a bit misleading, so I renamed it to url to avoid confusion, to make it clear it is a URL for the image, not the image itself.) Thus:
struct FoodImage: Decodable {
let url: URL
enum CodingKeys: String, CodingKey {
case url = "image"
}
}
Also note that the above is not updating properties or reloading the collection view. A routine that is performing network requests should not also be updating the model or the UI. I would leave this in the hands of the caller, e.g.,
var imageURLs: [URL]?
override func viewDidLoad() {
super.viewDidLoad()
// caller will update model and UI
loadImages { [weak self] imageURLs in
self?.imageURLs = imageURLs
self?.collectionView.reloadData()
}
}
Note:
The DispatchQueue.main.async is not necessary. These requests already run asynchronously.
Store the temporary results in a local variable. (And because URLSession uses a serial queue, we do not have to worry about further synchronization.)
The dispatch group notify block, though, uses the .main queue, so that the caller can conveniently update properties and UI directly.
Probably obvious, but I am parsing the URL directly, rather than parsing a string and converting that to a URL.
When fetching results concurrently, you have no assurances regarding the order in which they will complete. So, one will often capture the results in some order-independent structure (such as a dictionary) and then sort the results before passing it back.
In this particular case, the order doesn't strictly matter, but I included this sort-before-return pattern in my above example, as it is generally the desired behavior.
Anyway, that yields:
If you want to get one reload after finish loading of all 11 images you need to use DispatchGroup. Add a property that create a group:
private let group = DispatchGroup()
Then modify your loadImages() function:
func loadImages() {
for _ in 1...11 {
let url = URL(string: "https://foodish-api.herokuapp.com/api/")!
group.enter()
URLSession.shared.dataTask(with: url) { [weak self] data, response, error in
guard let self = self else { return }
self.group.leave()
guard let data = data else { return }
do {
let json = try JSONSerialization.jsonObject(with: data, options: []) as? [String : String]
print(json!["image"]!)
self.namesOfimages.append(json!["image"]!)
} catch {
print("JSON error: \(error.localizedDescription)")
}
}.resume()
}
group.notify(queue: .main) { [weak self] in
self?.collectionV.reloadData()
}
}
Some description:
On the method call group.enter() will be called 11 times
On each completion of image downloading group.leave() will be called
When group.leave() will be called the same count like group.enter() group make call of the block that you defined in group.notify()
More about DispatchGroup
Notice that you need handle create and store different DispatchGroup object if you need to download different groups of images in the same time.

Swift Dispatch Group in function

thank you in advance. I am working with a UITableView and need an array to be created before loading the cells. I am attempting to use DispatchGroup, I was successful in letting the first array be created but a second array which I also need validPhonesNotUsingApp, in the same function is not created.
I am leaving parts of overall file out.
Thank you.
override func viewDidLoad() {
super.viewDidLoad()
let group = DispatchGroup()
setUpElements()
group.enter()
checkContacts(group)
group.notify(queue: .main){
self.tableView.dataSource = self
self.tableView.delegate = self
self.searchBar.delegate = self
self.tableView.keyboardDismissMode = .onDrag
print(self.validPhonesNotUsingApp)
self.tableView.register(TableViewCellForContacts.nib(), forCellReuseIdentifier: TableViewCellForContacts.identifier)
}
}
func checkContacts(_ group: DispatchGroup){
let db = Firestore.firestore()
db.collection("UserProfile").document(UserDataConst.UserUID).getDocument { (DocumentSnapshot1, Error1) in
if Error1 != nil{
print("Error finding if contacts uploaded")
group.leave()
}
else{
let hasContacts: Bool = DocumentSnapshot1?.get("Contacts Uploaded") as? Bool ?? false
if hasContacts == true{
db.collection("UserProfile").document(UserDataConst.UserUID).collection("ContactFriends").getDocuments { (
Snapshot2, Error2) in
if Error2 != nil{
group.leave()
return
}
else{
for x in 0..<Snapshot2!.documents.count{
group.enter()
let validNumber = self.correctPhoneNumber(Snapshot2!.documents[x].documentID, group)
if validNumber != nil{
self.validPhoneNumbers.append(validNumber!)
let first = Snapshot2!.documents[x].get("First Name") as? String ?? "(ø)"
self.validPhoneFirstName.append(first)
let last = Snapshot2!.documents[x].get("Last Name") as? String ?? "(ø)"
self.validPhoneLastName.append(last)
}
else{
group.leave()
}
}
db.collection("AllPhoneNumbers").getDocuments { (Snapshot3, Error3) in
if Error3 != nil{
group.leave()
return
}
else{
print("OK lemme know what sup")
let docs = Snapshot3!.documents
group.enter()
for x1 in 0..<self.validPhoneNumbers.count{
group.enter()
var found = false
for x2 in 0..<docs.count{
group.enter()
if self.validPhoneNumbers[x1] == docs[x2].documentID{
let uid = docs[x2].get("UID") as! String
db.collection("UserProfile").document(UserDataConst.UserUID).collection("Friends").getDocuments { (QuerySnapshot4, Error4) in
if Error4 != nil{
group.leave()
return
}
else if QuerySnapshot4!.documents.count != 0{
var found2 = false
for x3 in 0..<QuerySnapshot4!.documents.count{
group.enter()
if QuerySnapshot4!.documents[x3].documentID == uid{
found2 = true
//group.leave()
break
}
else{
group.leave()
}
}
if found2 == false{
self.UIDsUsingApp.append(uid)
}
}
else{
self.UIDsUsingApp.append(uid)
}
}
//self.UIDsUsingApp.append(uid)
found = true
//group.leave()
break
}
}
if found == false{
self.validPhonesNotUsingApp.append(self.validPhoneNumbers[x1])
self.validFirstNotUsingApp.append(self.validPhoneFirstName[x1])
self.validLastNotUsingApp.append(self.validPhoneLastName[x1])
group.leave()
}
print("OK now we getting activ")
}
//print(self.UIDsUsingApp)
}
}
}
}
}
else{
group.leave()
return
}
}
}
}
I am working with a UITableView and need an array to be created before loading the cells. I am attempting to use DispatchGroup
Well, don't. You cannot do anything "before loading the cells". Do not intermingle table handling with dispatch. And don't use a dispatch group in that way.
Everything about the table view must be done immediately and on the main queue. You register directly in viewDidLoad on the main queue. You return a cell immediately in cellForRowAt:. You do not "wait" with a dispatch group or in any other manner.
If you have data to gather for the table in a time-consuming way, fine; do that on a background queue, and update your data model, and then reload the table (on the main queue). So:
Initially, if the data is not ready yet, your data source methods find there is no data and they display an empty table.
Later, once you gather your data and tell the table view to reload, your data source methods find there is data and they display it.
A few observations:
You do not want to entangle the “completion handler” logic of checkContacts with dispatch groups you might be using within the function. If you ever find yourself passing dispatch group objects around, that’s generally a sign that you are unnecessarily entangling methods.
So, if you need dispatch group within checkContacts, fine, use that, but don’t encumber the caller with that. Just use the completion handler closure pattern.
Make sure that you are not updating your model objects until the asynchronous process is done.
For example:
func checkContacts(completion: #escaping (Result<[Contact], Error>) -> Void) {
let group = DispatchGroup()
var contacts: [Contact] = [] // in this method, we will only update this local variable
...
group.notify(queue: .main) {
if let error = error {
completion(.failure(error))
} else {
completion(.success(contacts)) // and when we’re done, we’ll report the results
}
}
}
And you’d call it like
checkContacts { results in
switch results {
case .failure(let error):
...
case .success(let contacts):
self.contacts = contacts // only now will we update model properties
... // do whatever UI updates you want, e.g.
self.tableView.reloadData()
}