Day 40 - 100 Days of Swift
Project 9 (part 2)
Day 40 is the second part of the ninth project. He gives you three challenges to expand on the GCD stuff we looked at yesterday. He challenges you to make the loading of the list of images in Project 1 run on a background thread. He challenges you to make the loading and parsing of a level in Project 8 run in the background. And he challenges you to make the filtering of petitions in Project 7 run in the background.
For the first challenge, I pulled out the image loading code into its own function, sent it to a background queue, and then back to the main queue for reloading the tableView:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
private func loadImages() {
DispatchQueue.global().async {
let fm = FileManager.default
let path = Bundle.main.resourcePath!
let items = try! fm.contentsOfDirectory(atPath: path)
for item in items {
if item.hasPrefix("nssl") {
self.pictures.append(item)
}
}
self.pictures.sort()
print("Finished loading pictures")
DispatchQueue.main.async {
self.tableView.reloadData()
}
}
}
For the second one I sent the whole of loadLevel
to the background, and then back to the main queue for the parts that update the UI:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
DispatchQueue.global().async {
var clueString = ""
var solutionString = ""
var letterBits: [String] = []
guard let levelFileURL = Bundle.main.url(forResource: "level\(self.level)",
withExtension: "txt") else { return }
guard let levelContents = try? String(contentsOf: levelFileURL) else { return }
var lines = levelContents.components(separatedBy: "\n")
lines.shuffle()
for (index, line) in lines.enumerated() {
let parts = line.components(separatedBy: ": ")
let answer = parts[0]
let clue = parts[1]
clueString += "\(index + 1). \(clue)\n"
let solutionWord = answer.replacingOccurrences(of: "|",
with: "")
solutionString += "\(solutionWord.count) letters\n"
self.solutions.append(solutionWord)
let bits = answer.components(separatedBy: "|")
letterBits += bits
}
DispatchQueue.main.async {
self.cluesLabel.text = clueString.trimmingCharacters(in: .whitespacesAndNewlines)
self.answersLabel.text = solutionString.trimmingCharacters(in: .whitespacesAndNewlines)
letterBits.shuffle()
if letterBits.count == self.letterButtons.count {
for i in 0..<self.letterButtons.count {
self.letterButtons[i].setTitle(letterBits[i],
for: .normal)
}
}
}
}
For filtering petitions, I dispatched the whole filterPetitions
method to the background, and then made sure the tableview was reloaded on the main thread in the didSet
observer on filteredPetitions
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
var filteredPetitions: [Petition] = [] {
didSet {
DispatchQueue.main.async { self.tableView.reloadData() }
}
}
private func filterPetitions(with string: String?) {
DispatchQueue.global().async {
// Make sure there is a search term,
// otherwise set the filtered petitions to all the petitions
guard let searchTerm = string?.lowercased(),
!searchTerm.isEmpty else {
self.filteredPetitions = self.petitions
return
}
// Get the petitions who's titles match
let titlesMatch = self.petitions.filter {
$0.title.lowercased().contains(searchTerm)
}
// Get the petitions who's bodies match
// and aren't in the first group
let bodiesMatch = self.petitions.filter {
$0.body.lowercased().contains(searchTerm) &&
titlesMatch.firstIndex(of: $0) == nil
}
// Add them together and put them in the filtered array
self.filteredPetitions = titlesMatch + bodiesMatch
}
}
All three apps look exactly the same as they did before, and none of these operations take a particularly long time to perform, so there is barely any noticeable difference at all. But the code it more robust in the sense that if some change happens in the future that causes them to take longer, they won’t be blocking the UI.
You can find my version of this project at the end of day 40 on Github here.