Day 29 - 100 Days of Swift

5 minute read

Project 5 (part 3)

Day 29 is the third part of project 5. He gives you three main challenges and a bonus bug. He challenges you to disallow words that are three letters or less, and words that are exactly the same as the start word. He challenges you to pull out all the else statements into their own showErrorMessage() method. And he challenges you to add another bar button item that restarts the game. Finally, there is a small bug to fix after you’ve finished everything else.

This all went pretty quickly for me, because of all the extra time I put in yesterday. He suggests putting the checks for the word being the same, or being too short in isReal(), but since I already had an error enum set up, I went ahead and gave each their own custom error:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// In AnswerError
case tooShort
case sameAsOriginal

// In title() switch statement
case .tooShort:
    return "Word too short"
case .sameAsOriginal:
    return "Word isn't different"

// In message() switch statement
case .tooShort:
    return "Words need to be at least four letters long!"
case .sameAsOriginal:
    return "It doesn't count if it is the same word!"

Then, I added two new helper functions to make those checks:

1
2
3
4
5
6
7
private func isLongEnough(_ word: String) -> Bool {
    return word.count > 3
}

private func isNotTheSame(_ word: String) -> Bool {
    return word != startWord
}

And their corresponding checks in checkAnswer:

1
2
3
4
5
6
7
guard isLongEnough(lowerAnswer) else {
    return .tooShort
}

guard isNotTheSame(lowerAnswer) else {
    return .sameAsOriginal
}

I also added corresponding tests for these cases:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func testTooShortWord() {
    let prevCount = gameController.numberOfRows()

    let error = gameController.checkAnswer("gum")

    XCTAssertEqual(error, AnswerError.tooShort)
    XCTAssertEqual(gameController.numberOfRows(), prevCount)
}

func testSameWord() {
    let prevCount = gameController.numberOfRows()

    let error = gameController.checkAnswer("gumdrops")

    XCTAssertEqual(error, AnswerError.sameAsOriginal)
    XCTAssertEqual(gameController.numberOfRows(), prevCount)
}

With those in place, I didn’t even need to run the app to know that the game would catch those errors.

Next, to refactor into showErrorMessage, I changed checkAnswer to return and AnswerError? instead of a UIAlertController? (you may have noticed this in some of the preceding code). I realized this was probably a better way to handle it, since presenting alerts is more of a view thing than a model thing. The changes in ViewController look like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// In submit()
if let error = gameController.checkAnswer(answer) {
    showErrorMessage(for: error);
} else { // stays the same

private func showErrorMessage(for error: AnswerError) {
    let alertController =
        UIAlertController(title: error.title(),
                          message: error.message(),
                          preferredStyle: .alert)
    let okAction = UIAlertAction(title: "OK",
                                 style: .default)
    alertController.addAction(okAction)
    present(alertController, animated: true)
}

To add the new game button, I just needed to add @objc in front of startGame in ViewController, and add another bar button item in viewDidLoad:

1
2
3
4
5
navigationItem.leftBarButtonItem =
    UIBarButtonItem(title: "New Game",
                    style: .plain,
                    target: self,
                    action: #selector(startGame))

The bug he points out is that if you enter a valid word that is capitalized (or is anything other that lowercased) and then enter the same word lowercased, it counts both as separate words. As I started to try to track this down, the first thing I did was add test cases to check for it:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
func testSameWordCapitalAndLower() {
    let prevCount = gameController.numberOfRows()

    gameController.checkAnswer("Gumdrop")
    let error = gameController.checkAnswer("gumdrop")

    XCTAssertEqual(error, AnswerError.unoriginal)
    XCTAssertEqual(gameController.numberOfRows(), prevCount + 1)

}

func testWeirdCases() {
    let prevCount = gameController.numberOfRows()

    gameController.checkAnswer("GumDRop")
    gameController.checkAnswer("gUmdrOp")
    let error = gameController.checkAnswer("gumdrop")

    XCTAssertEqual(error, AnswerError.unoriginal)
    XCTAssertEqual(gameController.numberOfRows(), prevCount + 1)
}

These failed, as they should, because the bug hasn’t been fixed yet. It turns out that we were storing the word with its original casing in usedWords, but then just checking against the words in there with the lowercased version in isOriginal(), so if you entered “Lower” as the first word, and it was accepted, and then entered “lower” as the second word, the check didn’t find “lower” in usedWords, so it thought it was valid. I chose to fix this by just keeping the lowercased version in usedWords:

1
usedWords.insert(lowerAnswer, at: 0)

With that change, all the tests pass and the final app looks like this:

Screenshots of working app catching error where the word is the same.
Screenshots of working app catching error where word is too short.

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

Reflections

Today I really felt the difference that my refactoring and testing yesterday made. I was feeling pretty good after the work I put in, but I had no idea how trivial it would make all the challenges for today. Even something like tracking down a bug, and proving that it had been fixed only took a few moments, because the logic was easy to follow, and my GameController was set up to be as testable as possible. Seems like testing might be a useful thing to do.