Reputation: 4516
I am writing some code to let the user tap a button to log in, and after the login is successful, immediately make another call to pull in data from the system if it's available. I'm a little confused about where I need/don't need to wrap my code in different thread blocks. Can I just put everything in DispatchQueue.main.async given that I'm doing UI work on the main thread? Here is my code flow - I was wondering if someone could review and let me know if I've got the right structure of wrapping code in async/main thread blocks.
let messageFrame = AlertCreator.progressBarDisplayer(view: self.view, message: "Loading", true)
DispatchQueue.main.async(execute: {
//Present loading spinner while call is made
self.view.addSubview(messageFrame)
//Make an AUTH call using URLSession.shared.dataTask (with callback)
UserSecurityService.AuthenticateUser(username: self.txtUsername.text!, password: self.txtPassword.text!)
{ (authenticationResponse) -> () in
if (authenticationResponse.Status == ResponseCode.LOGIN_SUCCESS)
{
//If this user logged in successfully, and now need to import data, then do so, otherwise just proceed to the app.
if (authenticationResponse.Value!.HasDataForImport) {
//Make ANOTHER async call using URLSession.shared.dataTask (with callback)
UserDataService.GetUserSettings()
{ response in
//Remove the spinner
messageFrame.removeFromSuperview()
if (response.Status == ResponseCode.OK)
{
//Success, go to dashboard
self.presentHomeViewController()
}
else {
//Show alert failure, with clicking 'ok' firing a callback to take the user to the dashboard
}
}
}
else {
//Data does not exist, so just stop the spinner and take the user to the dashboard
self.presentHomeViewController()
}
else if (authenticationResponse.Status == ResponseCode.INVALID_USERNAME_PASSWORD) {
//User entered the wrong username/password
messageFrame.removeFromSuperview()
<alert is created/presented here>
}
}) //main dispatch async execute
If you noticed, I am making an async call to authenticate, that does something on the UI (shows a spinner to prevent any other activity, checks if the login was successful or not, and removes the spinner. It also potentially presents an alert, and then takes the user to a view controller.
My questions specifically are:
Thank you for any assistance / suggestions you could provide!
Upvotes: 0
Views: 1668
Reputation: 20379
I have tried adding DispatchQueue.main
and DispatchQueue.global
stub to your question just to show how to and when to switch between queue. This may not be a complete copy, paste solution as I dont have complete code of yours so I could not compile it. This is intended only to show how to use DispatchQueue
let messageFrame = AlertCreator.progressBarDisplayer(view: self.view, message: "Loading", true)
//Present loading spinner while call is made
self.view.addSubview(messageFrame)
DispatchQueue.global(qos: .default).async {
//Make an AUTH call using URLSession.shared.dataTask (with callback)
UserSecurityService.AuthenticateUser(username: self.txtUsername.text!, password: self.txtPassword.text!)
{ (authenticationResponse) -> () in
if (authenticationResponse.Status == ResponseCode.LOGIN_SUCCESS)
{
//If this user logged in successfully, and now need to import data, then do so, otherwise just proceed to the app.
if (authenticationResponse.Value!.HasDataForImport) {
//Make ANOTHER async call using URLSession.shared.dataTask (with callback)
UserDataService.GetUserSettings()
{ response in
//Remove the spinner
messageFrame.removeFromSuperview()
if (response.Status == ResponseCode.OK)
{
DispatchQueue.main.async {
//Success, go to dashboard
self.presentHomeViewController()
}
}
else {
DispatchQueue.main.async {
//Show alert failure, with clicking 'ok' firing a callback to take the user to the dashboard
}
}
}
}
else {
DispatchQueue.main.async {
//Data does not exist, so just stop the spinner and take the user to the dashboard
self.presentHomeViewController()
}
}
else if (authenticationResponse.Status == ResponseCode.INVALID_USERNAME_PASSWORD) {
DispatchQueue.main.async {
//User entered the wrong username/password
messageFrame.removeFromSuperview()
<alert is created/presented here>
}
}
}
// final = final?.replacingOccurrences(of: "Rs.", with: "")
}
}
Advice (Sort of)
Should I not wrap this entire block in DispatchQueue.main.async, but rather only the two web calls? I've done all kinds of combinations, most of which worked (and some that crashed), so I'm not entirely sure if I just need DispatchQueue.main.async, or need something nested inside of it such as a DispatchQueue.global(qos: DispatchQoS.QoSClass.userInteractive).async as well?
You should never wrap web service calls in DispatchQueue.main.async
you should wrap it in DispatchQueue.global(qos: .default).async
I am assuming Quality Of Service (QOS) to be default which is normally the case unless you have specific requirement for background
,hight
or low
.
Thumb rule, update all your UI components on main thread. Because your main thread is associated with serialized MainQueue it is important to switch to mainQueue context.
DispatchQueue.main.async
will only grab the main thread asynchronously where DispatchQueue.main.sync
will try to grab main thread synchronously there by pausing the current execution of the main thread. But both will get you an access to main thread only So DispatchQueue.main.async
is not a ideal way to make non UI related calls like web services.
Issue with making a long, non ui related task on main thread is that, there can only be one main thread in the entire life cycle of Application and because it gets busy with non UI related task and because Main Queue is Serialized
all other ui tasks will starve in main queue waiting for main thread and hence will result in non responsive UI or at worst (best for user :P) results in iOS killing your app.
Should I wrap each web call, or only the outer one
That depends on how is your web call designed. If you use Alamofire/AFNewtorking
kind of frameworks, they return the result on main thread even if you call the web service on non-ui thread. In such cases its necessary to switch to non-ui thread again. So in those scenarios you will have to wrap each web service calls in DispatchQueue.global.async
else outer one should do just fine :)
In the answer I posted above assumes your web service call completion block returns the response using non-ui thread hence I have wrapped both web service calls in single DispatchQueue.global.async
modify it if thats not the case.
Should I be wrapping the UI-related things in their own blocks, for example when I present alerts, view controllers, or even stop the spinner
Again depends. If the control reaches the UI component update statements on any thread other that main thread you should use DispatchQueue.main.async
If you are sure your control reaches such statements always on main thread you dont need to wrap them. If you are unsure, then you can either blindly go and add a block around such statement or make it little more intelligent decision by adding a if condition to check if thread is main thread or not :)
One final advice : Context switching is not cheap so use it wisely.
Hope it helps
Upvotes: 4