It seems that with this project we are going to start doing serious things! Not that before was easy or anything like that but now we are going to begin unlocking the true power of our devices. We are going back to project 7 (the Whitehouse petition collector) to solve its main issue, that is the fact that the app locks down while it is downloading the data. This is solved using one of the most important frameworks in the Apple ecosystem: the Grand Central Dispatch (or GCD). As usual I will keep evolving my app instead of copying it as I can always refer back to the Hacking with Swift repository if I want to go back.
Let’s get started!
Why is locking the UI bad?
This first part contains an explanatory video and a clarifying article which I suggest you read before moving on, as it contains fundamental informations for continuing the project. Here is what I can take out from all this:
- Downloading data from the Internet is known as a blocking code—that is, it freezes your app until all the data has been downloaded.
- Each CPU can act independently from the other and create different channels of execution that run parallel to each other known as threads.
- By default our code executes on only one CPU! That’s right, we have to tell the machine to use the other threads otherwise it will be as lazy as most us humans are!
- All the user interface creation must happen on the main thread (even if I didn’t understand if this means the first CPU, the first thread of the first CPU or what else…).
- Most important: we do not get to choose which thread executes what code and when, we just create a list of things to do and divide it into piles of to-dos. The system will then figure out how to best do the rest.
The moral of the story is that when we are working with data from a remote source we should do that using a background thread. Slow code on background thread, code that can benefit from parallelisation on multiple threads.
GCD for dummies: the async()
method.
The plan is to call the async()
method twice: once to push our code to the background thread and once to pull it back to the main thread.
The Grand Central Dispatch works with a system of queues—you can think of it as of the queue at the post office where you have one queue for shipping items, one for paying services and so on…— and therefore follows the real-life method of first come first served which, in computer language, translates to FIFO (First In, First Out). As said in point 5. of the previous chapter we do not create threads, our tasks are automatically assigned to one of the existing ones.
GCD creates a number of queues for us but we are the one who say how important our tasks are. The system will faithfully reproduce our starting order but who cuts the rope first at the end of the run is not guaranteed nor foreseeable.
We have four choices for “how important” our code is and they are a case
in the QoS
enumeration, or “quality of service”. In reality there are five of them but the first one is the main queue and we are not allowed to touch it. The others are, in order of importance: user interactive, user initiated, utility and background. Let’s start from the first one.
The User Interactive priority will ask the system to do its best (that is, dedicate all possible CPU time) to complete the given task. Tasks that require such a urgency could be ones that are important to keep the UI interactive (hence the name, I guess). An example of this could be the tap on a button.
Next in priority is User initiated which should be user for tasks requested by the user. These can be put in waiting but not for too long because having the user wait is bad UX (user experience).
We then have the Utility queue, ideal for long tasks that the user is aware of but without which he can survive.
Still for long running tasks but for which the user is not actively taking part of (or that he cannot take part in) the Background queue should be used.
All this seems at first sight complete gibberish and not too important for our programmers’ life but there is another point to be discussed: energy consumption and battery life. There was a point in time where battery life was everything in life and even if that is behind us we still need to know how much to prioritise our tasks in order to achieve maximum efficiency. For example, user interactive and user initiated tasks need be executed as fast as possible regardless of their effect on battery life. I can remember some badly programmed games such as the Magic: The Gathering app for iPad which would drain 20% of battery in one game, or some of the first AR apps which would turn your iPhone into an oven ready for Thanksgiving’s turkey!
Dropping to utility priority power efficiency will resume its importance but a balance between energy consumption and performance will at least be tried to be achieved. For background tasks, instead, energy efficiency will be the only parameter.
Also, GCD allocates more CPU time for higher priority tasks compared to lower priority ones, even at the expense of delaying those last ones.
Before jumping into code we should mention the Default queue which is prioritised between user initiated and utility.
But where’s the real code?
After all this boxing of theory we can jump straight to the code. I will paste it here and then explain because how it was done in the video confused me so much I started to get errors all over because of how I had misplaced the parentheses. Of course Paul’s been doing these things for years and his hands go immediately to the right key but I have to take my time to think and do things one at a time.
DispatchQueue.global(qos: .userInitiated).async { [weak self] in
// try to convert that string into an URL
if let url = URL(string: urlString) {
// fetch that from the API
if let data = try? Data(contentsOf: url) {
self?.parse(json: data)
return
}
}
}
showError()
This is still at the end of our viewDidLoad
method but the first line is different.
DispatchQueue
is so defined in the description:
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.
class DispatchQueue : DispatchObject
So, this is a subclass of Dispatch Object
which is the base class for many dispatch types. In the Documentation for Dispatch Object
we find this:
By default, dispatch objects are declared as Objective-C types when building with an Objective-C compiler. This lets them adopt ARC, enables memory leak checks by the static analyzer, and allows them to be added to Cocoa collections.
What is ARC I could not find but I will accept it for the time being.
The call to .global
could be left just with the ()
at the end and it would call the .default
QoS but here we write the .userInitiated
one, which is the second in priority.
The async()
method then takes one parameter which is a closure to be executed asynchronously. In reality the async
method is slightly more complex, here is its Documentation page:
Summary
Submits a work item to a dispatch queue and optionally associates it with a dispatch group. The dispatch group may be used to wait for the completion of the work items it references.
Declaration
func async(group: DispatchGroup? = default, qos: DispatchQoS = default, flags: DispatchWorkItemFlags = default, execute work: @escaping @convention(block) () -> Void)
Parameters
group
the dispatch group to associate with the submitted work item. If this is nil, the work item is not associated with a group.
qos
the QoS at which the work item should be executed. Defaults to DispatchQoS.unspecified.
flags
flags that control the execution environment of the work item.
work
The work item to be invoked on the queue.
Thus, we can see that our closure is in reality the last parameter and that the other three have a default value which allows us to ignore them.
As we are using closures we need to be sure that we use [weak self] in
at the beginning to avoid any accidental strong reference cycle and then to add self?.
before the call to parse
(otherwise Xcode will complain!).
Back to the main thread: DispatchQueue.main
As Paul makes us aware of, our code now is better because it no longer blocks the main thread code execution, but it’s worse because we are asking all of the rest of our code to be executed on that same background thread as well.
The main issue now is that showError()
will be called regardless of whether the loading succeeds or not. Our previous return
call will just exit the closure which, well, will happen anyway, while we wanted to get out of the method.
This brings two consequences:
showError
when called, will be executed on a background thread and it is a UI work, which should just never happen.- assuming the download of the
JSON
succeeds, theparse
method will still be on the background thread, and it does UI work so… well, just read above!
Solution
It is not clear to me why we are talking about background thread now when we are calling the JSON parsing on the user initiated thread. Does that count as some sort of background? This is not explained in the video so, I will assume it is not important—for now.
To get back to the main thread we need to call async()
again using the DispatchQueue.main
parameter, though. The most efficient way is to wrap the whole alert controller code inside an async
call and to do the same for when the table-view is about to be reloaded.
So, we modify the parse
method like this:
DispatchQueue.main.async { [weak self] in
self?.tableView.reloadData()
}
…and wrap the whole alert controller code in a similar way in the showError
method to avoid this being called in the background thread.
We still have one problem, though: showError
is still being called regardless of the parsing result so we need to move it up one level, inside the call to DispatchQueue.global(qos:)
, not forgetting to add self?.
before.
This last capturing of [weak self]
was optional because we were already sure that no strong reference cycle would happen but, in programming, you are never too safe, right?
Recap
Our code is now much better than before:
- All the slow work is being performed off the main thread
- All the UI work is pushed back to the main thread
Easy GCD using performSelector(inBackground:)
Do you recall when I was telling you I hated when viewDidLoad
was filled with too much code? Well, I guess Saint Paul listened to my prayers.
We are here discovering another way of covering GCD (don’t know why I always keep thinking of it as of Global Cooldown!!) which is easier than all this closuring. It is performSelector()
with its (inBackground:)
and (onMainThread:)
variants. They simply require us to pass in the name of the method to run. The nice thing here is that we do not have to care about how all this is done: everything is taken care of by GCD for us. Long story short: if we have an entire method that needs to be moved to a thread or to another, this way is the easiest.
First we need to cut all the fetching code out of viewDidLoad
and paste it into a new @objc func
method called fetchJSON()
and then call it inside viewDidLoad
with the performSelector(inBackground:with:)
method. At the end of the fetchJSON
method we need to move the showError
call to a performSelector(onMainThread:with:)
call for the same reasons explained above.
The parse()
method can remain the same but we can change the reload data call to make the table-view itself call the performSelector(onMainThread:with:)
itself. The only difference will be that the #selector
method will be UITableView.reloadData
.
Last but not least we need to clean the showError
method and add an @objc
call in front.
Please remember yourself to clean all the closure trash we have left around.
Now that the code is easier to read we can add an else
block to our JSON decoding so that it calls the showError
method with the performSelector(onMainThread:with:)
method.
I just wonder why we keep calling the showError
both in this else block and the the parseJSON
method. Isn’t this a duplicate?
A little bonus
For those who, like me, added these changes to the version which includes all the addition and challenges please remember not to move out of viewDidLoad
the following code:
let filterButton = UIBarButtonItem(title: "Filter", style: .plain, target: self, action: #selector(filterPetitions))
let resetButton = UIBarButtonItem(title: "Reset", style: .plain, target: self, action: #selector(resetList))
navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Credits", style: .plain, target: self, action: #selector(showCredits))
navigationItem.leftBarButtonItems = [filterButton, resetButton]
If you do forget this and you end up with this code inside the fetchJSON
method you will end up with a whole lot of interesting messages in the console telling you that you are trying to perform Auto Layout work in a background thread and that this can lead to undesired results or weird crashes!
In Italian we say “Uomo avvisato, mezzo salvato!”, that is: “Warned man, half-saved!”. I’m sure there is an equivalent in English!
Anyway, we are done with this project. You can find the repository here.
If you like what I’m doing here please consider liking this article and sharing it with some of your peers. If you are feeling like being really awesome, please consider making a small donation to support my studies and my writing (please appreciate that I am not using advertisement on my articles).
If you are interested in my music engraving and my publications don’t forget visit my Facebook page and the pages where I publish my scores (Gumroad, SheetMusicPlus, ScoreExchange and on Apple Books).
You can also support me by buying Paul Hudson’s books from this Affiliate Link.
Anyways, thank you so much for reading!
Till the next one!