I'm following a tutorial about working with REST/web requests. In the tutorial, we're working towards a Pokedex app where we fetch Pokemon details from an API using Alamofire, and then displaying that data in our UIs.
Here's the related code:
typealias DownloadComplete = (Bool) -> ()
// Model class
func downloadPokemonDetails(completed: #escaping DownloadComplete)
{
Alamofire.request(_pokemonURL).responseJSON { (response) in
var success = true
if let jsonData = response.result.value as? Dictionary<String, Any>
{
// parse the json here
...
}
else
{
success = false
}
completed(success)
}
}
// Controller class
override func viewDidLoad() {
super.viewDidLoad()
pokemon.downloadPokemonDetails(completed: { (success) in
if success
{
self.updateUI()
}
else
{
print("FAILED: TO PARSE JSON DATA")
}
})
}
func updateUI()
{
attackLbl.text = pokemon.attack
defenseLbl.text = pokemon.defense
heightLbl.text = pokemon.height
weightLbl.text = pokemon.weight
}
Now my question is: shouldn't we use DispatchQueue.main. and update the UI there like so?
pokemon.downloadPokemonDetails(completed: { (success) in
if success
{
DispatchQueue.main.async {
self.updateUI()
}
}
The tutorial left it out and I'm not sure if DispatchQueue is needed here to update the UI. I know that updating UI in a background thread is bad practice so if anyone can shed some light on whether using DispatchQueue here to get the main thread is necessary or not, I would very much appreciate it.
If one does not want to read the whole comments section, I am posting here it as answer.
Firstly, read Alamofire docs, which clearly states: "Response handlers by default are executed on the main dispatch queue."
It means, you can call any UI related code in response block. If you still feel uncomfortable to rely on 3rd party lib doc, you can check by executing this swift3 snippet:
if Thread.isMainThread {
print("Main Thread")
}
xcode 9
Starting from xcode 9 there is a built-in Main Thread Checker which detects invalid use of AppKit, UIKit, and other APIs from a background thread. Main Thread Checker is automatically enabled when you run your app with the Xcode debugger.
If any part of your project contains invalid UI calls from background thread, you will see the following:
** Demonstrated in Xcode Version 9.1 (9B55)
Related
I heard that I should always do user interface work on the main thread while reading data from Firebase.
I tried to do self.tableView.reloadData on the background thread inside a Firebase observe(.value) function and the app did not crash or freeze.
databaseReference.observe(.value) { [weak self] snapshot in
guard let self = self else { return }
self.tableView.reloadData()
}
I tried to do the same work inside getData function instead of observe(.value) function and the app crashed.
databaseReference.getData { [weak self] error, snapshot in
guard let self = self else { return }
self.tableView.reloadData()
}
So why the app crashed inside getData function and it did not crash inside observe(.value) function?
firebaser here
While the Firebase SDK performs its network and other I/O on a background thread, it actually calls your callback on the main thread.
But it seems that getData does not do that here, which is a bug. So thanks for catching and reporting that. 🙏
I filed bug #8245 on the Github repo, so check there for progress.
UI updates (tableView.reloadData, for instance) must be done on the main thread only not dependant if Firebase or another third-party framework is used.
If you are not sure if a method of a third-party framework (as Firebase is) will execute its closure on the main thread you can check if you are on the main thread and dispatch execution onto it if needed.
For example, you can use this simple extension for Thread class to run any code that needs to be run on the main thread.
extension Thread {
class func runOnMainThread(_ closure: #escaping () -> Void) {
if Thread.isMainThread {
closure()
} else {
DispatchQueue.main.async(execute: closure)
}
}
}
Then you can use it inside Firebase completion handlers as following:
Thread.runOnMainThread {
self.tableView.reloadData()
}
I am trying to create an iOS app to get data from API that I want to show the user in a Label.
So far I have this:
func getJoke(completion: #escaping (ChuckNorrisResponse) -> ()) {
URLSession.shared.dataTask(with: URL(string: url)!) { data, response, error in
guard let data = data, error == nil else {
print("Something fucked up")
return
}
var result: ChuckNorrisResponse?
do {
result = try JSONDecoder().decode(ChuckNorrisResponse.self, from: data)
} catch {
print("Fucked up to convert \(error.localizedDescription)")
}
guard let joke = result else {
return
}
completion(joke)
}.resume()
}
And in the ViewController
func setNewJoke() {
jokesProvier.getJoke { joke in
self.JokeLabel.text = joke.value
}
But it doesn't like that I try to edit the text in the Label inside the closure.
It shows error - UILabel.Text must be used from main thread only.
I cannot find anywhere how should I do this properly so it works.
Thanks in advance
Basically, as Aaron stated - you have to pass the closure to the main thread with DispatchQueue.main.async. The reason is that URLSession.shared.dataTask completionHandler runs on the thread different from main and self.JokeLabel.text = joke.value is an UI update - you're changing the text on the label, and UIKit requires you to update the screen on the main thread! That's why you have to pass label update to the main thread. Trying to update it on the thread different from main will result in undefined behaviour - it may work, it may freeze, it may crash.So, whenever you're doing something on the background thread and at some point want to update the UI always pass this job to the main thread Hope this helps
I'd recommend calling the completion handler on the main thread. So instead of
completion(joke)
do
DispatchQueue.main.async { completion(joke) }
I need to sync web database in my coredata, for which I perform service api calls. I am using Alamofire with Swift 3. There are 23 api calls, giving nearly 24k rows in different coredata entities.
My problem: These api calls blocks UI for a minute, which is a long time for a user to wait.
I tried using DispatchQueue and performing the task in background thread, though nothing worked. This is how I tried :
let dataQueue = DispatchQueue.init(label: "com.app.dataSyncQueue")
dataQueue.async {
DataSyncController().performStateSyncAPICall()
DataSyncController().performRegionSyncAPICall()
DataSyncController().performStateRegionSyncAPICall()
DataSyncController().performBuildingRegionSyncAPICall()
PriceSyncController().performBasicPriceSyncAPICall()
PriceSyncController().performHeightCostSyncAPICall()
// Apis which will be used in later screens are called in background
self.performSelector(inBackground: #selector(self.performBackgroundTask), with: nil)
}
An API call from DataSyncController:
func performStateSyncAPICall() -> Void {
DataSyncRequestManager.fetchStatesDataWithCompletionBlock {
success, response, error in
self.apiManager.didStatesApiComplete = true
}
}
DataSyncRequestManager Code:
static func fetchStatesDataWithCompletionBlock(block:#escaping requestCompletionBlock) {
if appDelegate.isNetworkAvailable {
Util.setAPIStatus(key: kStateApiStatus, with: kInProgress)
DataSyncingInterface().performStateSyncingWith(request:DataSyncRequest().createStateSyncingRequest() , withCompletionBlock: block)
} else {
//TODO: show network failure error
}
}
DataSyncingInterface Code:
func performStateSyncingWith(request:Request, withCompletionBlock block:#escaping requestCompletionBlock)
{
self.interfaceBlock = block
let apiurl = NetworkHttpClient.getBaseUrl() + request.urlPath!
Alamofire.request(apiurl, parameters: request.getParams(), encoding: URLEncoding.default).responseJSON { response in
guard response.result.isSuccess else {
block(false, "error", nil )
return
}
guard let responseValue = response.result.value else {
block (false, "error", nil)
return
}
block(true, responseValue, nil)
}
}
I know many similar questions have been already posted on Stackoverflow and mostly it is suggested to use GCD or Operation Queue, though trying DispatchQueues didn't work for me.
Am I doing something wrong?
How can I not block UI and perform the api calls simultaneously?
You can do this to run on a background thread:
DispatchQueue.global(qos: .background).async {
// Do any processing you want.
DispatchQueue.main.async {
// Go back to the main thread to update the UI.
}
}
DispatchQueue manages the execution of work items. Each work item submitted to a queue is processed on a pool of threads managed by the system.
I usually use NSOperationQueue with Alamofire, but the concepts are similar. When you set up an async queue, you allow work to be performed independently of the main (UI) thread, so that your app doesn't freeze (refuse user input). The work will still take however long it takes, but your program doesn't block while waiting to finish.
You really have only put one item into the queue.
You are adding to the queue only once, so all those "perform" calls wait for the previous one to finish. If it is safe to run them concurrently, you need to add each of them to the queue separately. There's more than one way to do this, but the bottom line is each time you call .async {} you are adding one item to the queue.
dataQueue.async {
DataSyncController().performStateSyncAPICall()
}
dataQueue.async {
DataSyncController(). performRegionSyncAPICall l()
}
I have a simple function loading data from Firebase.
func loadFromFireBase() -> Array<Song>? {
var songArray:Array<Song> = []
ref.observe(.value, with: { snapshot in
//Load songArray
})
if songArray.isEmpty {
return nil
}
return songArray
}
Currently this function returns nil always, even though there is data to load. It does this because it doesn't ever get to the perform the completion block where it loads the array before the function returns. I'm looking for a way to make the function only return once the completion block has been called but I can't put return in the completion block.
(Variations on this question come up constantly on SO. I can never find a good, comprehensive answer, so below is an attempt to provide such an answer)
You can't do that. Firebase is asynchronous. Its functions take a completion handler and return immediately. You need to rewrite your loadFromFirebase function to take a completion handler.
I have a sample project on Github called Async_demo (link) that is a working (Swift 3) app illustrating this technique.
The key part of that is the function downloadFileAtURL, which takes a completion handler and does an async download:
typealias DataClosure = (Data?, Error?) -> Void
/**
This class is a trivial example of a class that handles async processing. It offers a single function, `downloadFileAtURL()`
*/
class DownloadManager: NSObject {
static var downloadManager = DownloadManager()
private lazy var session: URLSession = {
return URLSession.shared
}()
/**
This function demonstrates handling an async task.
- Parameter url The url to download
- Parameter completion: A completion handler to execute once the download is finished
*/
func downloadFileAtURL(_ url: URL, completion: #escaping DataClosure) {
//We create a URLRequest that does not allow caching so you can see the download take place
let request = URLRequest(url: url,
cachePolicy: .reloadIgnoringLocalAndRemoteCacheData,
timeoutInterval: 30.0)
let dataTask = URLSession.shared.dataTask(with: request) {
//------------------------------------------
//This is the completion handler, which runs LATER,
//after downloadFileAtURL has returned.
data, response, error in
//Perform the completion handler on the main thread
DispatchQueue.main.async() {
//Call the copmletion handler that was passed to us
completion(data, error)
}
//------------------------------------------
}
dataTask.resume()
//When we get here the data task will NOT have completed yet!
}
}
The code above uses Apple's URLSession class to download data from a remote server asynchronously. When you create a dataTask, you pass in a completion handler that gets invoked when the data task has completed (or failed.) Beware, though: Your completion handler gets invoked on a background thread.
That's good, because if you need to do time-consuming processing like parsing large JSON or XML structures, you can do it in the completion handler without causing your app's UI to freeze. However, as a result you can't do UI calls in the data task completion handler without sending those UI calls to the main thread. The code above invokes the entire completion handler on the main thread, using a call to DispatchQueue.main.async() {}.
Back to the OP's code:
I find that a function with a closure as a parameter is hard to read, so I usually define the closure as a typealias.
Reworking the code from #Raghav7890's answer to use a typealias:
typealias SongArrayClosure = (Array<Song>?) -> Void
func loadFromFireBase(completionHandler: #escaping SongArrayClosure) {
ref.observe(.value, with: { snapshot in
var songArray:Array<Song> = []
//Put code here to load songArray from the FireBase returned data
if songArray.isEmpty {
completionHandler(nil)
}else {
completionHandler(songArray)
}
})
}
I haven't used Firebase in a long time (and then only modified somebody else's Firebase project), so I don't remember if it invokes it's completion handlers on the main thread or on a background thread. If it invokes completion handlers on a background thread then you may want to wrap the call to your completion handler in a GCD call to the main thread.
Edit:
Based on the answers to this SO question, it sounds like Firebase does it's networking calls on a background thread but invokes it's listeners on the main thread.
In that case you can ignore the code below for Firebase, but for those reading this thread for help with other sorts of async code, here's how you would rewrite the code to invoke the completion handler on the main thread:
typealias SongArrayClosure = (Array<Song>?) -> Void
func loadFromFireBase(completionHandler:#escaping SongArrayClosure) {
ref.observe(.value, with: { snapshot in
var songArray:Array<Song> = []
//Put code here to load songArray from the FireBase returned data
//Pass songArray to the completion handler on the main thread.
DispatchQueue.main.async() {
if songArray.isEmpty {
completionHandler(nil)
}else {
completionHandler(songArray)
}
}
})
}
Making Duncan answer more precise. You can make the function like this
func loadFromFireBase(completionHandler:#escaping (_ songArray: [Song]?)->()) {
ref.observe(.value) { snapshot in
var songArray: [Song] = []
//Load songArray
if songArray.isEmpty {
completionHandler(nil)
}else {
completionHandler(songArray)
}
}
}
You can return the songArray in a completion handler block.
In Swift (that's Swift) there are a number of ways to handle asynchronous,
Say you have a loop like this - it's calling a parse cloud code call which goes to background anyway.
public func runImages()
{
print("test begins...")
for i in 1...3
{
print("Tick tock tick tock ..\(i)")
PFCloud.callFunctionInBackground("blah", withParameters:["bla":i,"bla":"bla] )
{
(response: AnyObject?, error: NSError?) -> Void in
print(".. done! Now go again...")
if let rr = response as? String { print(rr) }
if let err = error { print(err.domain) }
}
}
}
How to make that wait for the end of each PFCloud call?
Really is just an ordinary flag best, or? (Note that (a) I can't get a flag to work in Swift and (b) as Paul points out you're blocking the UI!!)
What is the "Swift way" in the context you see here? I feel it would be very inelegant to use a recursive pattern here, for example.
If you want the three calls to execute serially then you can use a serial dispatch queue and a dispatch_barrier_async to do something when all three are finished -
let dispatchQueue=dispatch_queue_create("queue", DISPATCH_QUEUE_SERIAL)
for i in 1...3 {
dispatch_async(dispatchQueue, { () -> Void in
print("i=\(i)")
let result = PFCloud.callFunction("blah", withParameters:["bla":i,"bla":"bla] )
})
}
dispatch_barrier_async(dispatchQueue, { () -> Void in
print("really done")
})
print(" done")
In order for this to work with your Parse function you need to use the synchronous cloud code call, not an asynchronous. And if you update UI in the dispatch_barrier_async closure you would need to dispatch that on the main queue.