Performing an asynchronous task within a synchronous call - ios

I would like to register a user which is performed asynchronous. However, the calling function behaves synchronous since the program should only continue when a user is created successfully.
The current implementation is:
class SignUp: NSObject {
// ...
func signUpUser() throws -> Bool {
guard hasEmptyFields() else {
throw CustomErrorCodes.EmptyField
}
guard isValidEmail() else {
throw CustomErrorCodes.InvalidEmail
}
createUser( { (result) in
guard result else {
throw CustomErrorCodes.UserNameTaken
}
return true // Error: cannot throw....
})
}
func createUser( succeeded: (result: Bool) -> () ) -> Void {
let newUser = User()
newUser.username = username!
newUser.password = password!
// User is created asynchronously
createUserInBackground(newUser, onCompletion: {(succeed, error) -> Void in
if (error != nil) {
// Show error alert
} else {
succeeded(result: succeed)
}
})
}
}
and in a ViewController the signup is initiated as follows:
do {
try signup.signUpUser()
} catch let error as CustomErrorCodes {
// Process error
}
However, this does not work since createUser is not a throwing function. How could I ensure that signUpUser() only returns true when an new user is created successfully?

You say:
and in a ViewController the signup is initiated as follows:
do {
try signup.signUpUser()
} catch let error as CustomErrorCodes {
// Process error
}
But don't. That's not how asynchronous works. The whole idea is that you do not wait. If you're waiting, it's not asynchronous. That means you're blocking, and that's just what you mustn't do.
Instead, arrange to be called back at the end of your asynchronous process. That's when you'll hear that things have succeeded or not. Look at how a download task delegate is structured:
https://developer.apple.com/library/ios/documentation/Foundation/Reference/NSURLSessionDownloadTask_class/
The download task calls back into the delegate to let it know whether we completed successfully or not. That is the relationship you want to have with your asynchronous task. You want to be like that delegate.

You need to adjust your thinking. Instead of trying to write a synchronous method that we need to wait for an asynchronous event, write a method that takes a completion closure. The method will return immediately, but once the asynchronous process is complete it wild invoke the completion closure. When you call such a method you pass in code in the incompletion closure that gets called once the job is done.

Related

Serial DispatchQueue is not working as expected

To load some information in my app's view, I need it to finish networking because some methods depend on the result. I looked into serial DispatchQueue and .async methods, but it's not working as expected.
Here is what I tried so far. I defined 3 blocks:
Where I'd get hold of the user's email, if any
The email would be used as input for a method called getData, which reads the database based on user's email address
This block would populate the table view with the data from the database. I've laid it out like this, but I'm getting an error which tells me the second block still executes before we have access to user's email address, i.e. the first block is finished. Any help is appreciated, thanks in advance!
let serialQueue = DispatchQueue(label: "com.queue.serial")
let block1 = DispatchWorkItem {
GIDSignIn.sharedInstance.restorePreviousSignIn { user, error in
if error != nil || user == nil {
print("unable to identify user")
} else {
print(user!.profile?.email ?? "")
self.email = user!.profile?.email ?? ""
print("email is: \(self.email)")
}
}
}
let block2 = DispatchWorkItem{
self.getData(self.email)
}
let block3 = DispatchWorkItem {
DispatchQueue.main.async {
self.todoListTable.reloadData()
}
}
serialQueue.async(execute: block1)
block1.notify(queue: serialQueue, execute: block2)
block2.notify(queue: serialQueue, execute: block3)
Your problem is that you are dispatching asynchronous work inside your work items; GIDSignIn.sharedInstance.restorePreviousSignIn "returns" immediately but fires the completion handler later when the work is actually done. Since it has returned, the dispatch queue considers the work item complete and so moves on to the next item.
The traditional approach is to invoke the second operation from the completion handler of the first (and the third from the second).
Assuming you modified self.getData() to have a completion handler, it would look something like this:
GIDSignIn.sharedInstance.restorePreviousSignIn { user, error in
if error != nil || user == nil {
print("unable to identify user")
} else {
print(user!.profile?.email ?? "")
self.email = user!.profile?.email ?? ""
print("email is: \(self.email)")
self.getData(self.email) {
DispatchQueue.main.async {
self.todoListTable.reloadData()
}
}
}
}
You can see you quickly end up with a "pyramid of doom".
The modern way is to use async/await. This requires your functions to support this approach and imposes a minimum iOS level of iOS 13.
It would look something like
do {
let user = try await GIDSignIn.sharedInstance.restorePreviousSignIn()
if let email = user.profile?.email {
let fetchedData = try await getData(email)
self.data = fetchedData
self.tableView.reloadData()
}
} catch {
print("There was an error \(error)")
}
Ahh. Much simpler to read.

How to use the completion handler version of a async function in Swift?

I have a async function func doWork(id: String) async throws -> String. I want to call this function from a concurrent dispatch queue like this to test some things.
for i in 1...100 {
queue.async {
obj.doWork(id: "foo") { result, error in
...
}
}
}
I want to do this because queue.async { try await obj.doWork() } is not supported. I get an error:
Cannot pass function of type '#Sendable () async throws -> Void' to parameter expecting synchronous function type
But the compiler does not provide me with a completion handler version of doWork(id:). When I call it from Obj C, I am able to use the completion handler version: [obj doWorkWithId: #"foo" completionHandler:^(NSString * val, NSError * _Nullable error) { ... }]
How do I do something similar in Swift?
You can define a DispatchQueue. This DispatchQueue will wait for the previous task to complete before proceeding to the next. 
let queue = DispatchQueue(label: "queue")
func doWork(id: String) async -> String {
print("Do id \(id)")
return id
}
func doWorksConcurrently() {
for i in 0...100 {
queue.async {
Task.init {
await doWork(id: String(i))
}
}
}
}
doWorksConcurrently()
You are initiating an asynchronous task and immediately finishing the dispatch without waiting for doWork to finish. Thus the dispatch queue is redundant. One could do:
for i in 1...100 {
Task {
let results = try await obj.doWork(id: "foo")
...
}
}
Or, if you wanted to catch/display the errors:
for i in 1...100 {
Task {
do {
let results = try await obj.doWork(id: "foo")
...
} catch {
print(error)
throw error
}
}
}
Now, generally in Swift, we would want to remain within structured concurrency and use a task group. But if you are trying to mirror what you'll experience from Objective-C, the above should be sufficient.
Needless to say, if your Objective-C code is creating a queue solely for the purpose for calling the completion-handler rendition of doWork, then the queue is unnecessary there, too. But we cannot comment on that code without seeing it.

What Happens if Fulfill is called Twice on a Promise

This code is blocking the UI until the remote config fetch is done. I have put 2 seconds as timeout. Here both completionHandler and after will execute, whichever finishes first.
Will it be ok? Or do I need to take care that only one should execute?
func checkIfRemoteConfigFetched<T>(t:T) -> Promise<T>{
return Promise<T>{ seal in
if self.rConfig?.fetchComplete == true{
seal.fulfill(t)
}
after(seconds: 2).done{
seal.fulfill(t)
}
if let rConfig = self.rConfig{
rConfig.completionHandler = { success in
seal.fulfill(t)
}
}
}
}

Code being executed after completion handler is called

Why is this continuing after the completion handler is called?
See the comments in the code. See the code path to get to #1. At this point I'm expecting the code to call the completion handler complete(), and return from the function, preventing execution of #2. However, code at #2 still appears to be getting triggered. Any ideas why this os occurring?
func syncSessionLog(withCompletion complete: #escaping ((Bool, String?) -> Void)) {
... bunch of code
managedObjectContext.performAndWait {
let trackFetchRequest: NSFetchRequest<NSFetchRequestResult> = Track.fetchRequest()
let trackPredicate = NSPredicate(format: "id == \(session.track_id)")
trackFetchRequest.predicate = trackPredicate
trackFetchRequest.fetchLimit = 1;
do {
let foundTrack = try self.managedObjectContext.fetch(trackFetchRequest) as! [Track]
if foundTrack.count < 1 {
self.debug.log(tag: "SessionManager", content: "not found tID: \(session.track_id)")
//#1 When not found, complete is called, yet the code still manages to reach "do stuff" down the bottom.
complete(false, "Not found")
return
}
associatedTrack = foundTrack[0]
}
catch {
self.debug.log(tag: "SessionManager", content: "Failed to get Track object from Core Data: \(error.localizedDescription)")
fatalCoreDataError(error)
complete(false, "Failed to retrieve")
}
}
//#2 do stuff with associatedTrack
return will exit out of the current context, which is the closure associated with the performAndWait. After that closure returns, execution continues with the next statement after performAndWait which is whatever is at #2.
You can move the code from point #2 inside the closure
Its quite simple - the return statement is inside a block, so it returns from the block, not from the outside method. It would be more visible if the block had some return value.
As such, this return is not needed in your code. You will need to set a Bool flag to indicate the result of block execution and act accoridngly in #2.

Completion Handler - Parse + Swift

I'm trying to generate an array of PFObjects called 'areaList'. I've been researching this quite a bit and understand that I could benefit from using a completion handler to handle the asynchronous nature of the loaded results. My ask, specifically, is to get some guidance on what I'm doing wrong as well as potential tips on how to achieve the result "better".
Here is my query function with completion handler:
func loadAreasNew(completion: (result: Bool) -> ()) -> [Area] {
var areaList = self.areaList
let areaQuery = PFQuery(className: "Area")
areaQuery.findObjectsInBackgroundWithBlock {
(areas: [PFObject]?, error: NSError?) -> Void in
if error == nil {
for area in areas! {
let areaToAdd = area as! Area
areaList.append(areaToAdd)
// print(areaList) // this prints the list each time
// print(areaToAdd) // this prints the converted Area in the iteration
// print(area) // this prints the PFObject in the iteration
if areaList.count == areas!.count {
completion(result: true)
} else {
completion(result: false)
}
}
} else {
print("There was an error")
}
}
return areaList
}
Here is how I'm attempting to call it in viewDidLoad:
loadAreasNew { (result) -> () in
if (result == true) {
print(self.areaList)
} else {
print("Didn't Work")
}
}
I assigned this variable before viewDidLoad:
var areaList = [Area]()
In the console, I get the following:
Didn't Work
Didn't Work
Didn't Work
Didn't Work
[]
Representing the 5 items that I know are there in Parse...
This is an interesting question. First off, PFQuery basically has a built in completion handler, which is quiet nice! As you probably know, all of the code within the areaQuery.findObjectsInBackgroundWithBlock {...} triggers AFTER the server response. A completion most often serves the purpose of creating a block, with the ability of asynchronously returning data and errors.
Best practice would (IMO) to just call the code that you want to use with the results from your PFQuery right after your area appending loop (which I'm gonna take out because I'm picky like that), like so:
func loadAreasNew() {
var areaList = self.areaList
let areaQuery = PFQuery(className: "Area")
areaQuery.findObjectsInBackgroundWithBlock {
(areas: [PFObject]?, error: NSError?) -> Void in
if error == nil {
let areasFormatted = areas! As [Areas]
areasList += areasFormatted
//Something like this
self.codeINeedAreasFor(areasList)
}
} else {
print(error)
}
}
}
HOWEVER! If you really feel the need to use some completion handlers, check out this other answer for more info on how to use them. But keep in mind all tools have a time and a place...
There are a few issues here.
Your completion handler doesn't require you to define the name for your completion handler's parameters, so you could easily use completion: (Bool) -> ()
Further in your function, you're returning areaList. This should be put through the completion handler like this onComplete(areaList) and change your completion handler parameter to expect your area list.
Then, when you call your function, it could look more like this :
loadAreasNew { result in
if (result == true) {
print(self.areaList)
} else {
print("Didn't Work")
}
}
Here is my concern:
1) Don't pass in a local variable and make the function return it, it's meaningless and danger.
You may want to initiate an empty array and make your fetch, then "return" it.
2) The fetch request is processed in background, you will have no idea when it will have finished. If you return the array immediately it will always be an empty array.
Put the "return" in your completion too.
3) Parse already has a distance checking method, you don't have to do it manually. aPARSEQUERRY.where(key:,nearGeoPoint:,inKilometers:)
I will rewrite the function as:
func loadNewAreas(completion:([Area],err?)->()){
let areaQuery = PFQuery(className: "Area")
areaQuery.where("location",nearGeoPoint:MYCURRENTLOCATION,inKilometers:50)
areaQuery.findObjectInBackgroundWithBlock(){objects,err
if objects.count == 0
{
completion([],err)
}
let areas = Area.areasFromPFObjects(objects)
completion(areas,err)
}
}

Resources