Day 40 - 100 Days of Swift

4 minute read

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.