I've been messing around with dispatch groups and am wondering how the placement of the notify callback of a dispatch group affects when the callback will be called. I'm reading data from my database and then fetching a picture for each item in the data that was read. When I have the notify outside of the initial database read block I notice it gets called immediately, however when the notify is inside the block it behaves the proper way. Here is my code:
override func viewDidAppear(_ animated: Bool) {
let ref = FIRDatabase.database().reference().child("users").child((FIRAuth.auth()?.currentUser?.uid)!).child("invites")
ref.observeSingleEvent(of: .value, with: { snapshot in
if let snapshots = snapshot.children.allObjects as? [FIRDataSnapshot] {
for child in snapshots {
self.dispatchGroup.enter()
let info = petInfo(petName: child.value! as! String, dbName: child.key)
print(info)
self.invitesData[info] = UIImage()
let picRef = FIRStorage.storage().reference().child("profile_images").child(info.dbName+".png")
picRef.data(withMaxSize: 1024 * 1024) { (data, error) -> Void in
if error != nil {
print(error?.localizedDescription ?? "Error getting picture")
}
// Create a UIImage, add it to the array
self.invitesData[info] = UIImage(data: data!)!
self.dispatchGroup.leave()
}
}
self.dispatchGroup.notify(queue: DispatchQueue.main, execute: {
print("YOOO")
self.invitesArray = Array(self.invitesData.keys)
print(self.invitesArray)
self.inviteTable.reloadData()
})
}
})
}
This code behaves properly when the notify is within the original database read block. However if I place this after the ref.observeSingleEvent block the notify gets called immediately.
Can somebody please explain this to me?
Yes. Asynchronous code :-)
Code execution runs all the way through to the end of the function, and then the completion handler will be called
Related
I am struggling to trigger the logic responsible for changing the view at the right time. Let me explain.
I have a view model that contains a function called createNewUserVM(). This function triggers another function named requestNewUser() which sits in a struct called Webservices.
func createNewUserVM() -> String {
Webservices().requestNewUser(with: User(firstName: firstName, lastName: lastName, email: email, password: password)) { serverResponse in
guard let serverResponse = serverResponse else {
return "failure"
}
return serverResponse.response
}
}
Now that's what's happening in the Webservices' struct:
struct Webservices {
func requestNewUser(with user: User, completion: #escaping (Response?) -> String) -> String {
//code that creates the desired request based on the server's URL
//...
URLSession.shared.dataTask(with: request) { data, response, error in
guard let data = data, error == nil else {
DispatchQueue.main.async {
serverResponse = completion(nil)
}
return
}
let decodedResponse = try? JSONDecoder().decode(Response.self, from: data)
DispatchQueue.main.async {
serverResponse = completion(decodedResponse)
}
}.resume()
return serverResponse //last line that gets executed before the if statement
}
}
So as you can see, the escaping closure (whose code is in the view model) returns serverResponse.response (which can be either "success" or "failure"), which is then stored in the variable named serverResponse. Then, requestNewUser() returns that value. Finally, the createNewUserVM() function returns the returned String, at which point this whole logic ends.
In order to move to the next view, the idea was to simply check the returned value like so:
serverResponse = self.signupViewModel.createNewUserVM()
if serverResponse == "success" {
//move to the next view
}
However, after having written a few print statements, I found out that the if statement gets triggered way too early, around the time the escaping closure returns the value, which happens before the view model returns it. I attempted to fix the problem by using some DispatchQueue logic but nothing worked. I also tried to implement a while loop like so:
while serverResponse.isEmpty {
//fetch the data
}
//at this point, serverResponse is not empty
//move to the next view
It was to account for the async nature of the code.
I also tried was to pass the EnvironmentObject that handles the logic behind what view's displayed directly to the view model, but still without success.
As matt has pointed out, you seem to have mixed up synchronous and asynchronous flows in your code. But I believe the main issue stems from the fact that you believe URLSession.shared.dataTask executes synchronously. It actually executes asynchronously. Because of this, iOS won't wait until your server response is received to execute the rest of your code.
To resolve this, you need to carefully read and convert the problematic sections into asynchronous code. Since the answer is not trivial in your case, I will try my best to help you convert your code to be properly asynchronous.
1. Lets start with the Webservices struct
When you call the dataTask method, what happens is iOS creates a URLSessionDataTask and returns it to you. You call resume() on it, and it starts executing on a different thread asynchronously.
Because it executes asynchronously, iOS doesn't wait for it to return to continue executing the rest of your code. As soon as the resume() method returns, the requestNewUser method also returns. By the time your App receives the JSON response the requestNewUser has returned long ago.
So what you need to do to pass your response back correctly, is to pass it through the "completion" function type in an asynchronous manner. We also don't need that function to return anything - it can process the response and carry on the rest of the work.
So this method signature:
func requestNewUser(with user: User, completion: #escaping (Response?) -> String) -> String {
becomes this:
func requestNewUser(with user: User, completion: #escaping (Response?) -> Void) {
And the changes to the requestNewUser looks like this:
func requestNewUser(with user: User, completion: #escaping (Response?) -> Void) {
//code that creates the desired request based on the server's URL
//...
URLSession.shared.dataTask(with: request) { data, response, error in
guard let data = data, error == nil else {
DispatchQueue.main.async {
completion(nil)
}
return
}
let decodedResponse = try? JSONDecoder().decode(Response.self, from: data)
DispatchQueue.main.async {
completion(decodedResponse)
}
}.resume()
}
2. View Model Changes
The requestNewUser method now doesn't return anything. So we need to accommodate that change in our the rest of the code. Let's convert our createNewUserVM method from synchronous to asynchronous. We should also ask the calling code for a function that would receive the result from our Webservice class.
So your createNewUserVM changes from this:
func createNewUserVM() -> String {
Webservices().requestNewUser(with: User(firstName: firstName, lastName: lastName, email: email, password: password)) { serverResponse in
guard let serverResponse = serverResponse else {
return "failure"
}
return serverResponse.response
}
}
to this:
func createNewUserVM(_ callback: #escaping (_ response: String?) -> Void) {
Webservices().requestNewUser(with: User(firstName: firstName, lastName: lastName, email: email, password: password)) { serverResponse in
guard let serverResponse = serverResponse else {
callback("failure")
return
}
callback(serverResponse.response)
}
}
3. Moving to the next view
Now that createNewUserVM is also asynchronous, we also need to change how we call it from our controller.
So that code changes from this:
serverResponse = self.signupViewModel.createNewUserVM()
if serverResponse == "success" {
//move to the next view
}
To this:
self.signupViewModel.createNewUserVM{ [weak self] (serverResponse) in
guard let `self` = self else { return }
if serverResponse == "success" {
// move to the next view
// self.present something...
}
}
Conclusion
I hope the answer gives you an idea of why your code didn't work, and how you can convert any existing code of that sort to execute properly in an asynchronous fashion.
This can be achieve using DispatchGroup and BlockOperation together like below:
func functionWillEscapeAfter(time: DispatchTime, completion: #escaping (Bool) -> Void) {
DispatchQueue.main.asyncAfter(deadline: time) {
completion(false) // change the value to reflect changes.
}
}
func createNewUserAfterGettingResponse() {
let group = DispatchGroup()
let firstOperation = BlockOperation()
firstOperation.addExecutionBlock {
group.enter()
print("Wait until async block returns")
functionWillEscapeAfter(time: .now() + 5) { isSuccess in
print("Returned value after specified seconds...")
if isSuccess {
group.leave()
// and firstoperation will be complete
} else {
firstOperation.cancel() // means first operation is cancelled and we can check later if cancelled don't execute next operation
group.leave()
}
}
group.wait() //Waits until async closure returns something
} // first operation ends
let secondOperation = BlockOperation()
secondOperation.addExecutionBlock {
// Now before executing check if previous operation was cancelled we don't need to execute this operation.
if !firstOperation.isCancelled { // First operation was successful.
// move to next view
moveToNextView()
} else { // First operation was successful.
// do something else.
print("Don't move to next block")
}
}
// now second operation depends upon the first operation so add dependency
secondOperation.addDependency(firstOperation)
//run operation in queue
let operationQueue = OperationQueue()
operationQueue.addOperations([firstOperation, secondOperation], waitUntilFinished: false)
}
func moveToNextView() {
// move view
print("Move to next block")
}
createNewUserAfterGettingResponse() // Call this in playground to execute all above code.
Note: Read comments for understanding. I have run this in swift playground and working fine. copy past code in playground and have fun!!!
I am a new developer. I'm using Swift 4.2 and Xcode 10.2.
I'm trying to show a spinner while a photo is being saved. I am simulating a slow connection on my iPhone in Developer Mode to test it. Using the code below the spinner does not show up. The view goes right to the end where the button shows and says the upload is complete (when it isn't). I tried putting it all into a DispatchQueue.global(qos: .userinitiated).async and then showing the button back on the main queue. I also tried putting the showSpinner on a DispatchQueue.main and then savePhoto on a .global(qos: .utility). But I cleary don't understand the GCD processes.
Here's my code:
func savePhoto(image:UIImage) {
// Add a spinner (from the Extensions)
self.showSpinner(onView: self.view)
PhotoService.savePhoto(image: image) { (pct) in
// Can show the loading bar here.
}
// Stop the spinner
self.removeSpinner()
// Show the button.
self.goToPhotosButtonLabel.alpha = 1
self.doneLabel.alpha = 1
}
What types of DispatchQueues should I use and where should I put them?
Here is the savePhoto code:
static func savePhoto(image:UIImage, progressUpdate: #escaping (Double) -> Void) {
// Get data representation of the image
let photoData = image.jpegData(compressionQuality:0.1)
guard photoData != nil else {
print("Couldn't turn the image into data")
return
}
// Get a storage reference
let userid = LocalStorageService.loadCurrentUser()?.userId
let filename = UUID().uuidString
let ref = Storage.storage().reference().child("images/\(String(describing: userid))/\(filename).jpg")
// Upload the photo
let uploadTask = ref.putData(photoData!, metadata: nil) { (metadata, error) in
if error != nil {
// An error during upload occurred
print("There was an error during upload")
}
else {
// Upload was successful, now create a database entry
self.createPhotoDatabaseEntry(ref: ref, filename: filename)
}
}
uploadTask.observe(.progress) { (snapshot) in
let percentage:Double = Double(snapshot.progress!.completedUnitCount /
snapshot.progress!.totalUnitCount) * 100.00
progressUpdate(percentage)
}
}
Since the code that saves the photo is asynchronous , so your current code removes the spinner directly after it's added before the upload is complete
func savePhoto(image:UIImage) {
// Add a spinner (from the Extensions)
self.showSpinner(onView: self.view)
PhotoService.savePhoto(image: image) { (pct) in
// remove spinner when progress is 100.0 = upload complete .
if pct == 100.0 {
// Stop the spinner
self.removeSpinner()
// Show the button.
self.goToPhotosButtonLabel.alpha = 1
self.doneLabel.alpha = 1
}
}
}
Here you don't have to use GCD as firebase upload runs in another background thread so it won't block the main thread
I have a (custom, linked-list based) queue that I want to deserialize when the app starts and serialize when the app stops, like so (AppDelegate.swift):
func applicationWillResignActive(_ application: UIApplication) {
RequestManager.shared.serializeAndPersistQueue()
}
func applicationDidBecomeActive(_ application: UIApplication) {
RequestManager.shared.deserializeStoredQueue()
}
The issue is during serialization when I exit the app. Here's the code that's running:
public func serializeAndPersistQueue() {
do {
let encoder = JSONEncoder()
let data = try encoder.encode(queue) // Bad access here
if FileManager.default.fileExists(atPath: url.path) {
try FileManager.default.removeItem(at: url)
}
FileManager.default.createFile(atPath: url.path, contents: data, attributes: nil)
}
catch {
print(error)
}
}
As you can see, fairly straightforward. It uses the JSONEncoder to convert my queue to a data object, then writes that data to the file at url.
However, during encoder.encode() I get EXC_BAD_ACCESS every time, without fail.
Additionally, I should note that peak and dequeue operations are conducted on the queue from a background thread. I'm not sure if that makes a difference due to my lack of understanding surrounding GCD. Here's what that method looks like:
private func processRequests() {
DispatchQueue.global(qos: .background).async { [unowned self] in
let group = DispatchGroup()
let semaphore = DispatchSemaphore(value: 0)
while !self.queue.isEmpty {
group.enter()
let request = self.queue.peek()!
self.sendRequest(request: request, completion: { [weak self] in
_ = self?.queue.dequeue()
semaphore.signal()
group.leave()
})
semaphore.wait()
}
group.notify(queue: .global(), execute: { [weak self] in
print("Ending the group")
})
}
}
Lastly, I'll note that:
My queue conforms to the Codable protocol just fine––well, there are no compiler errors, at least. If its implementation beyond that matters, let me know and I'll show it.
The crash occurs a few seconds after I exit the app, while the execution of the processRequests function stops immediately after
So I am trying to restructure my API calls to make use of dispatchGroups so I don't have to make collectionViews and other elements reload so quick. I know that with dispatch groups you typically have to create one, enter, and then leave a certain number of times. Upon completion of this, you typically notify the main queue to do some operation. However, in the code snippet below it notifies the main queue before I even enter the dispatch group once. Which is throwing things way off. If anyone could look at my code and tell me what's going wrong I would really appreciate that.
static func showFeaturedEvent(for currentLocation: CLLocation,completion: #escaping ([Event]) -> Void) {
//getting firebase root directory
let dispatchGroup1 = DispatchGroup()
var currentEvents:[Event]?
var geoFireRef: DatabaseReference?
var geoFire:GeoFire?
geoFireRef = Database.database().reference().child("featuredeventsbylocation")
geoFire = GeoFire(firebaseRef: geoFireRef!)
let circleQuery = geoFire?.query(at: currentLocation, withRadius: 17.0)
circleQuery?.observe(.keyEntered, with: { (key: String!, location: CLLocation!) in
print("Key '\(key)' entered the search area and is at location '\(location)'")
dispatchGroup1.enter()
print("entered dispatch group")
EventService.show(forEventKey: key, completion: { (event) in
if let newEvent = event {
currentEvents?.append(newEvent)
dispatchGroup1.leave()
print("left dispatch group")
}
})
})
dispatchGroup1.notify(queue: .main, execute: {
if let currentEventsFinal = currentEvents{
completion(currentEventsFinal)
}
})
}
I am running dispatch groups in other places. Im not sure if that would affect anything but I just felt like it was important to note in this question.
You need to enter the group before you start the asynchronous task and leave in the completion of the asynchronous task. You aren't executing the first enter until the first asynchronous task completes, so when execution hits your notify the dispatch group is empty and it fires straight away.
It is also important that you call leave the same number of times that you call enter or the group will never empty, so be wary of calling leave inside conditional statements.
static func showFeaturedEvent(for currentLocation: CLLocation,completion: #escaping ([Event]) -> Void) {
//getting firebase root directory
let dispatchGroup1 = DispatchGroup()
var currentEvents:[Event]?
var geoFireRef: DatabaseReference?
var geoFire:GeoFire?
geoFireRef = Database.database().reference().child("featuredeventsbylocation")
geoFire = GeoFire(firebaseRef: geoFireRef!)
let circleQuery = geoFire?.query(at: currentLocation, withRadius: 17.0)
disatchGroup1.enter()
circleQuery?.observe(.keyEntered, with: { (key: String!, location: CLLocation!) in
print("Key '\(key)' entered the search area and is at location '\(location)'")
dispatchGroup1.enter()
EventService.show(forEventKey: key, completion: { (event) in
if let newEvent = event {
currentEvents?.append(newEvent)
print("left dispatch group")
}
dispatchGroup1.leave()
})
dispatchGroup1.leave()
})
dispatchGroup1.notify(queue: .main, execute: {
if let currentEventsFinal = currentEvents{
completion(currentEventsFinal)
}
})
}
I need the "getUserInfo" to complete before I execute the next section of code (the push to storyboard). Currently the "getUserInfo" is still in process while the storyboard push executes. How can I make these execute in order? I'm need to keep these 2 functions separate, so putting the code in the completion handler of loginUser isn't a good solution. Many thanks to those who are smarter than me :)
func loginUser() {
PFUser.logInWithUsernameInBackground(txtEmailAddress.text, password:txtPassword.text) {
(user: PFUser?, error: NSError?) -> Void in
if user != nil {
// Successful login.
self.txtPassword.resignFirstResponder()
self.txtEmailAddress.resignFirstResponder()
getUserInfo()
// Push to Main.storyboard.
let storyboard = UIStoryboard(name: "Main", bundle: NSBundle.mainBundle())
let viewController: AnyObject = storyboard.instantiateInitialViewController()
self.presentViewController(viewController as! UIViewController, animated: true, completion: nil)
} else {
// The login failed. Display alert.
self.displayAlert("Error", message: "Login incorrect")
}
}
}
func getUserInfo() {
let currentUser = PFUser.currentUser()
let userQuery = PFQuery(className: "_User")
userQuery.whereKey("username", equalTo: PFUser.currentUser()!.username!)
userQuery.findObjectsInBackgroundWithBlock({ (results:[AnyObject]?, error:NSError?) -> Void in
if error == nil {
for result in results! {
userType = result["userType"] as! String
if userType == "admin" {
user = "AdminSetting"
} else {
user = "StandardSetting"
}
}
}
})
}
What you want to do is make the asynchronous function (the one with the completion handler) synchronous, so that it returns immediately. However that's usually a bad idea, because if the execution stops in the main thread, the user can't do anything and your app is stuck until the code continues again. This might take a couple seconds depending on the connection, which isn't very good, you should really update your UI asynchronous on asynchronous tasks. There usually aren't good reasons to do something like this, if you have them though, you can tell me.
You could also execute your storyboard code within getUserInfo() as a block/closure passed in as a parameter. That way you can ensure it is executed when the async call in getUserInfo completes
What Shadowman suggested is the correct/most elegant solution.
Here is an example:
func getUserInfo(completion: (results:[AnyObject]?, error:NSError?) -> Void) {
let currentUser = PFUser.currentUser()
let userQuery = PFQuery(className: "_User")
userQuery.whereKey("username", equalTo: PFUser.currentUser()!.username!)
userQuery.findObjectsInBackgroundWithBlock(completion)
}
This way, you get back the actual results from your call after it finished, handy, eh ?
And here is how you call it:
self.getUserInfo { (results, error) -> Void in
// Here the results are already fetched, so proceed with your
// logic (show next controller or whatever...)
if error == nil {
for result in results! {
userType = result["userType"] as! String
if userType == "admin" {
user = "AdminSetting"
} else {
user = "StandardSetting"
}
}
}
// depending on whether this will still run in a background thread, you might have to dispatch this code to the main thread.
// you can check whether this code block is called on the main thread
// by checking if NSThread.isMainThread() returns true
// if not, you will need to use this dispatch block!
dispatch_async(dispatch_get_main_queue(), { () -> Void in
// only call UI code in main thread!
// MOVE TO NEXT controller in here!
})
}