Day 27 - 100 Days of Swift

5 minute read

Project 5 (part 1)

Day 27 is the first part of the fifth project. This one is an anagram game. The user is given an eight letter word and asked to produce valid anagrams from that word. Today you set up the table view. You import the list of words from a file, split it up and store it in an array. You set the table view up to display the words the user has gotten right. And you present an alert controller that has a UITextField that asks the user to input their guess.

First, you make a new project and get it set up with a table view controller, embedded in a navigation controller, as the initial view controller. You set its class, and give the cell a reuse identifier. Then, you copy a file of 12,000 words called “start.txt” that he gives you into the project. To load it into an array in memory, you first add a couple of variables:

1
2
var allWords: [String] = []
var usedWords: [String] = []

Then, in viewDidLoad, you pull the words into a string and split it by the newline character, \n:

1
2
3
4
5
6
7
8
9
10
11
// Pull the list of words out of the file
if let startWordsURL = Bundle.main.url(forResource: "start",
                                       withExtension: "txt"),
    let startWords = try? String(contentsOf: startWordsURL) {
    allWords = startWords.components(separatedBy: "\n")
}

// Provide a default in case something goes wrong.
if allWords.isEmpty {
    allWords = ["silkworm"]
}

Next, you add a helper function for starting a new game:

1
2
3
4
5
func startGame() {
    title = allWords.randomElement()
    usedWords.removeAll(keepingCapacity: true)
    tableView.reloadData()
}

And call it at the end of viewDidLoad:

1
2
    startGame()
} // closing brace of viewDidLoad

At this point, the app will display a random word in the title when the view is loaded, but the table view isn’t actually hooked up to anything. So you adopt the two necessary methods next:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// MARK: UI Table View Data Source
override func tableView(_ tableView: UITableView,
                        numberOfRowsInSection section: Int) -> Int {
    return usedWords.count
}

override func tableView(_ tableView: UITableView,
                        cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell =
        tableView.dequeueReusableCell(withIdentifier: "WordCell",
                                      for: indexPath)

    cell.textLabel?.text = usedWords[indexPath.row]

    return cell
}

They won’t do anything yet, because we’re never actually changing usedWords to be anything other than an empty array, but I’m assuming that is something we’ll get to tomorrow.

Next, you add a UIBarButtonItem that will present an alert for the user to enter a new guess when it is tapped on:

1
2
3
4
navigationItem.rightBarButtonItem =
    UIBarButtonItem(barButtonSystemItem: .add,
                    target: self,
                    action: #selector(promptForAnswer))

This causes an error, because there isn’t a promptForAnswer method yet, but it is the next thing you add:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@objc func promptForAnswer() {
    // Build an alert controller
    let alertController =
        UIAlertController(title: "Enter answer",
                          message: nil,
                          preferredStyle: .alert)
    // Add a text field to it
    alertController.addTextField()

    // Make the submit action
    let submitAction = UIAlertAction(title: "Submit",
                                     style: .default)
    { [weak self, weak alertController] _ in
        // weak references to self and alertController
        // to avoid retain cycles
        guard let answer =
            alertController?.textFields?[0].text else { return }
        self?.submit(answer)
    }

    // Add the action and present it
    alertController.addAction(submitAction)
    present(alertController, animated: true)
}

This again causes an error, because there is no method called submit, but the last thing you do for the day is add an empty stub of it, so that it will compile:

1
2
3
func submit(_ answer: String) {
    print("Submitted '\(answer)'")
}

So far, it looks like this:

Screenshot of app in its state at the end of the first part.

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

Reflections

So far this one has seemed pretty simple. I think it will be a pretty cool little game by the time it is finished. He’s making a big deal about closures being hard again, which I’m sure is to help people not get put off by them, but I still don’t feel like they are that hard to grok. I did appreciate the reminder of the difference between weak and unowned, but I’m not really sure I can think of many situations where it would be better to use unowned, other than maybe to get something to fail faster? Or if you just don’t want to take the very small amount of time it takes to unwrap an optional?