Day 49 - 100 Days of Swift

5 minute read

Project 12 (part 2)

Day 49 is the second part of the twelfth project. Today you redo the work you did yesterday using Codable instead of NSCoding, which is a lot simpler. Then he gives you three challenges to go back to some of the earlier projects and add persistence to them. He challenges you to keep track of a count of how many times each storm has been viewed in project 1. He challenges you to modify project 2 to save the user’s high score and give them a special alert if they beat it. And he challenges you to basically save the game state in project 5, so that if the user closes the app, it will come back with the game they were previously playing.

First you use Codable to add persistence to project 10:

1
class Person: NSObject, Codable {

Then the save() and load() functions look slightly different from yesterday, because they now use JSONDecoder and JSONEncoder:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
private func save() {
    let jsonEncoder = JSONEncoder()
    if let savedData = try? jsonEncoder.encode(people) {
        UserDefaults.standard.set(savedData,
                                  forKey: "people")
    } else {
        print("Failed to save people.")
    }
}

private func load() {
    let defaults = UserDefaults.standard

    if let savedPeople = defaults.object(forKey: "people") as? Data {
        let jsonDecoder = JSONDecoder()

        do {
            people = try jsonDecoder.decode([Person].self,
                                            from: savedPeople)
        } catch {
            print("Failed to load people:\n\(error)")
        }
    }
}

Those are the only differences, and the app looks exactly the same.

For the first challenge I added a viewCounts dictionary and save() and loadViewCounts() methods to ViewController:

1
2
3
4
5
6
7
8
9
10
11
viewCounts: [String: Int] = [:]

func save() {
    UserDefaults.standard.set(viewCounts, forKey: "viewCounts")
}

func loadViewCounts() {
    if let savedViews = UserDefaults.standard.dictionary(forKey: "viewCounts") as? [String: Int] {
        viewCounts = savedViews
    }
}

Then I added one to the view count each time an image is presented:

1
2
3
4
// In didSelectRowAt
let imageName = pictures[indexPath.row]
viewCounts[imageName, default: 0] += 1
save()

Added the view count in the detailTextLabel of the cell:

1
2
// In cellForRowAt
cell.detailTextLabel?.text = "Views: \(viewCounts[imageName, default: 0])"

And reloaded the table view each time the view is loaded:

1
2
3
4
5
override func viewWillAppear(_ animated: Bool) {
    super.viewWillAppear(animated)

    tableView.reloadData()
}

With that, it tracks the views and it looks like this:

Screenshot of Project 1 working with view counts

For the second challenge I just get the saved high score before resetting the game and check it agains the current score. If the current one is higher, I set the new high score and present an alert:

1
2
3
4
5
6
// In resetGame
let highScore = UserDefaults.standard.integer(forKey: "highScore")
if score > highScore {
    UserDefaults.standard.set(score, forKey: "highScore")
    presentHighScoreAlert(new: score, prev: highScore)
}

The alert looks like this:

1
2
3
4
5
6
7
8
9
10
11
private func presentHighScoreAlert(new: Int, prev: Int) {
    let alertController = UIAlertController(title: "New High Score!",
                                            message: "New high score: \(new)\nPrevious high score:\(prev)",
                                            preferredStyle: .alert)

    let action = UIAlertAction(title: "Cool!",
                               style: .default)
    alertController.addAction(action)

    present(alertController, animated: true)
}

And when it is done it looks like this:

Screenshot of Project 2 with new high score alert.

For the third challenge, I first added this save() function to the GameController:

1
2
3
4
5
private func save() {
    let userDefaults = UserDefaults.standard
    userDefaults.set(usedWords, forKey: "usedWords")
    userDefaults.set(startWord, forKey: "startWord")
}

Then I call that method when the user successfully adds a word and when we start a new game:

1
2
3
4
5
6
// at the end of checkAnswer
usedWords.insert(lowerAnswer, at: 0)
save()

// at the end of startGame
save()

Then in init() I pull out those values and store them:

1
2
3
// Pull saved info out
startWord = UserDefaults.standard.string(forKey: "startWord") ?? ""
usedWords = UserDefaults.standard.array(forKey: "usedWords") as? [String] ?? []

Finally, I just needed to make a small change the logic in ViewController. I split startGame into two functions, one that updates the views and one that starts a new game and then calls updateViews():

1
2
3
4
5
6
7
8
9
@objc func startGame() {
    gameController.startGame()
    updateViews()
}

func updateViews() {
    title = gameController.startWord
    tableView.reloadData()
}

Then I just call the one that makes sense in viewDidLoad:

1
2
3
4
5
if gameController.startWord != "" {
    updateViews()
} else {
    startGame()
}

With that, it looks the same but it remembers the word you were working on and the words you had guessed, even after restarting the app.

You can find my version of this project at the end of day 49 on Github here.