I am starting to to this today even if I have already coded quite a lot. Tomorrow I have the flight back home at 4am and then a whole day of passion at the Apple Store in Torino to hopefully quickly fix my iPhone’s camera and see if something is wrong with my Mac or not. We need to exclude every possible issue with the computer before going on and trying to recover the data from the disk.
Yesterday a great guy from the Ask Different forum suggested me something I could do to salvage my data and I will try that as soon as I have a second disk with 2TB free to accomodate that disk image. Until then, they suggested me not to touch the disk (that is, do not connect it) and familiarise with the documentation of the tool I will have to use. If you are interested I will post my workflow in a new article.
Time for the review
This project 9 was short but filled with great new things so let’s review what was learned:
- GCD runs code on a first in, first out (FIFO) basis.
- When calling
async()
we provide our work as a closure. performSelector()
can only run methods that are marked with@objc
.- Once work starts on a background thread, it will continue on that background thread until it finishes or you push work back to the main thread.
- The default GCD background queue has a lower priority than
.userInitiated
but higher than.utility
. - GCD will automatically adapt the number of threads it creates based on the user device.
- Background tasks prioritise battery efficiency.
- You can use
#selector
to point at methods from UIKit classes. - Blocking code stops any further code executing until some work finishes.
- GCD takes care of creating, scheduling, and destroying threads automatically.
- All user interface code should be run on the main thread.
- Multitasking refers to a computer running many pieces of code at the same time.
At this point these review questions are becoming more a matter of “Did you listen to me while I was speaking?”. Essentially you have to be careful not to choose the answer that looks like an absurdity. Somehow I would like more challenging review questions, something that can really be a plausible thing but it is still wrong.
Challenge Time!
Challenge 1: modify project 1 so that loading the list of NSSL images from our bundle happens in the background.
We need to be sure to call reloadData()
on the table view once loading has finished!
Good, apart from how idiot I can be in not just following the given instructions I solved this challenge in the following way. I moved the loading code outside of viedDidLoad
and put it inside a custom @objc
method called loadPictures
. I then called performSelector(inBackground: #selector(loadPictures), with: nil)
.
Then, in the following line of viewDidLoad
I added:
tableView.performSelector(onMainThread: #selector(UITableView.reloadData), with: nil, waitUntilDone: false)
I had some doubts on whether to put this inside viewDidLoad or inside the loadPictures
method, right at the end of it, but I think this is the best way because otherwise I would call something on the main thread while being in the background thread and I am not sure this could work.
Please find the finished code here.
Challenge 2: modify project 8 so that loading and parsing a level takes place in the background. Once we are done, make sure we update the UI on the main thread!
This looks like a step up in difficulty because while loading the level in the background is as simple as wrapping the loadLevel
method in an @objc
statement and calling the performSelector(inBackground:)
inside viewDidLoad
the parsing doesn’t look so easy to implement to me.
What I am not sure of at the moment is if the other two calls to loadLevel
, respectively in the levelUp
and in the restartLevel
methods, should also be interacted with using the perform selector thing. I will get back to it in a minute.
In any case, before proceeding, I tried to launch the app and I got all a bunch of warnings, such as my first meeting with the purple exclamation mark which says:
UI API called from background thread
UILabel.text must be used from main thread only
Now I need to fix the warning in line 246 of my code as I cannot call UI APIs from the background thread. I tried wrapping those two lines in the DispatchQueue
closure but something felt wrong as that purple exclamation mark simply moved down a few lines. It looks like I can of course call this method from the background thread in viewDidLoad
but I should wrap the other elements in separate methods so that I can call them safely. Let’s try this out.
I guess this is working now:
- I moved the parsing of the level to a whole new method called
parseLevel
and moved the three parsing properties to a global level (hope this is fine). - I moved the
cluesLabel.text
andanswersLabel.text
trimming update to a newupdateLabels
method and the title setting loop to its ownsetTitle
loop. - Modified the
loadLevel
method like this:
@objc func loadLevel(action: UIAlertAction! = nil) {
performSelector(inBackground: #selector(parseLevel), with: nil)
performSelector(onMainThread: #selector(updateLabels), with: nil, waitUntilDone: false)
score = 0
letterButtons.shuffle()
performSelector(onMainThread: #selector(setTitle), with: nil, waitUntilDone: false)
}
This should be enough but, for my curiosity, I will try to call loadLevel
on the background thread in viewDidLoad
to see what happens. It all looked well safe for a didSet
global property that resulted being called on a background thread. There are only two places where score
gets called from (and where it causes issue with a UI element): the loadLevel
method and the submitTapped
one. I assume this last one is connected to the loadView
method and will happen on the main thread (I assume so because if I would ever wrap this into a GCD closure I would have to change about 100 lines of code to have the self?.
attached and I kind of doubt it to be a good idea).
The culprit must therefore be either the loadLevel
method or the fact that didSet
property. We should then also exclude this last hypothesis because that didSet
is what allows us to update the label so…
I think I solved it in this way:
var score = 0 {
didSet {
DispatchQueue.main.async {
self.scoreLabel.text = "Score: \(self.score)"
}
}
}
If I set the [weak self] in
part as well I have to write it like this:
var score = 0 {
didSet {
DispatchQueue.main.async { [weak self] in
self?.scoreLabel.text = "Score: \(self?.score ?? 0)"
}
}
}
…which doesn’t look so nice to me… I don’t feel I have the knowledge to tell which one is correct but to me the first one looks better. Do I risk a strong reference cycle? Maybe but I do not have the means to check it… For safety reason I will leave the second option in the last version of the repository but well, I hope someone will comment this and tell me what they think of it.
Here is the updated repository.
Challenge 3: modify project 9 so that our filtering code takes place in the background.
Paul said we should modify project 9 with challenges from project 9 but I guess he means challenges from project 7 as… these are the challenges from project 9!
The first thing I tried to do was to wrap the filterAction
into a call to DispatchQueue.global(qos: .background).async
. Basically it worked but as soon as I pressed the Filter button I got this message in the console (no crash, just a warning):
Whitehouse Petitions [7336:756130] [Assert] Cannot be called with asCopy = NO on non-main thread.
Needless to say I have no idea what this meant so I made a quick web search which returned me this StackOverflow question. It seems that all the call to an alert action need to be run from the main thread. It is evident to me now that I was wrapping in the background something that involved text fields which… guess what… is a UI thing. So let’s go back…
I now tried to wrap the code in showPetitions(for filter)
method inside the same DispatchQueue
method but I had to fill in too many ?
and !
plus a coercion to Any
… this seems way too complex and looks bad. Let’s try something else.
I changed tactic and tried to modify the filter action, which I know is not involving any UI element like this:
let filterAction = UIAlertAction(title: "Filter", style: .default) {
[weak self, weak ac] _ in
guard let filterWord = ac?.textFields?[0].text else { return }
self?.performSelector(inBackground: #selector(self?.showPetitions(for: filterWord)), with: nil)
self?.tableView.performSelector(onMainThread: #selector(UITableView.reloadData), with: nil, waitUntilDone: false)
}
But I get an error saying:
Argument of ‘#selector’ does not refer to an ‘@objc’ method, property, or initializer
…which is strange because showPetitions
is now marked as an @objc
method. Anyway, this doesn’t compile even so let’s go back and change again.
…
…
ok…
breathe…
I was really close… just I needed to read the description of the method in the Documentation. That nil
we used to pass in the with
argument was reserved for all those cases when the method you pass in accepts arguments itself. So, the final code should be this one:
self?.performSelector(inBackground: #selector(self?.showPetitions(for:)), with: filterWord)
self?.performSelector(onMainThread: #selector(UITableView.reloadData), with: nil, waitUntilDone: false)
Mmm… now the app crashed when I pressed the OK button in the Filter alert. Let’s see what may have caused it. The error says:
unrecognised selector sent to instance 0x7fd6f7c0aae0
Now, while trying to fix it, my connection dropped and I got the Alert. Good, at least I know that works! You see? Sometimes it is good to have unstable connections, so you can test network issues!!
If I take tableView.reloadData
out of the performSelector
as it was before the app crashes with an index out of range error. Of course I had made the same error as before: I had forgotten the tableView before the performSelector. Now everything works!
Challenge completed!
You can find the code for this challenge here.
P.S.: I have gotten back home, my iPhone XS has been substituted—though yeah, for the 99€ premium so my iPhone now costed 1269+249+99€, nice that you make an insurance and you still have to pay an extra… I mean make it 349 or whatever, but the customer has to feel covered and the change has to be free of extra charges!— and my 2009 MacBook Pro coupled with a 10-year old mighty 2TB WD My Passport for Mac managed to read the damaged disk that was causing kernel panics. I am being able to save data! Also, the saviour has come, in the form of a LACIE d2 T3 6TB disk! Can’t wait to test it out!
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!