Day 37 - 100 Days of Swift
Project 8 (part 2)
Day 37 is the second part of the eighth project. Today you add all the logic to make the game playable. You add the targets to all the buttons you made yesterday, you add a function to “load a level” into memory. You implement the functions that all the buttons target. And you add a property observer to score
to update scoreLabel
whenever it changes.
The first thing you do is add a couple new variables and three empty functions that the buttons will target:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var activatedButtons: [UIButton] = []
var solutions: [String] = []
var score = 0
var level = 1
@objc func letterTapped(_ sender: UIButton) {
}
@objc func submitAnswer(_ sender: UIButton) {
}
@objc func clearAnswer(_ sender: UIButton) {
}
Then you add targets to the the buttons:
1
2
3
4
5
6
7
8
9
10
11
12
// In loadView, where each button is set up
submit.addTarget(self,
action: #selector(submitAnswer),
for: .touchUpInside)
clear.addTarget(self,
action: #selector(clearAnswer),
for: .touchUpInside)
letterButton.addTarget(self,
action: #selector(letterTapped),
for: .touchUpInside)
Then you add a function to load the level from a text file. The text file looks like this:
1
2
3
4
5
6
7
HA|UNT|ED: Ghosts in residence
LE|PRO|SY: A Biblical skin disease
TW|ITT|ER: Short but sweet online chirping
OLI|VER: Has a Dickensian twist
ELI|ZAB|ETH: Head of state, British style
SA|FA|RI: The zoological web
POR|TL|AND: Hipster heartland
Because of the very specific format of that text file, we can split on the string with .components(seperatedBy:)
on ”\n”
, or ”: “
or ”|"
to get to the individual elements that we care about. Here is what that looks like in the loadView
method:
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
42
43
44
45
46
var clueString = ""
var solutionString = ""
var letterBits: [String] = []
guard let levelFileURL = Bundle.main.url(forResource: "level\(level)",
withExtension: "txt") else { return }
guard let levelContents = try? String(contentsOf: levelFileURL) else { return }
// Split things up by line and shuffle their order
var lines = levelContents.components(separatedBy: "\n")
lines.shuffle()
for (index, line) in lines.enumerated() {
// Split the line into the answer and the clue
let parts = line.components(separatedBy: ": ")
let answer = parts[0]
let clue = parts[1]
// Add the clue to the clue string
clueString += "\(index + 1). \(clue)\n"
// Get the answer itself, without the pipes
let solutionWord = answer.replacingOccurrences(of: "|",
with: "")
// Add that to the solution string, and array
solutionString += "\(solutionWord.count) letters\n"
solutions.append(solutionWord)
// Get the word parts to display in the buttons
let bits = answer.components(separatedBy: "|")
letterBits += bits
}
// Trim off the extra newlines at the end
cluesLabel.text = clueString.trimmingCharacters(in: .whitespacesAndNewlines)
answersLabel.text = solutionString.trimmingCharacters(in: .whitespacesAndNewlines)
letterBits.shuffle()
// Put the word parts into the buttons
if letterBits.count == letterButtons.count {
for i in 0..<letterButtons.count {
letterButtons[i].setTitle(letterBits[i],
for: .normal)
}
}
Next, you implement the letterTapped
method:
1
2
3
4
guard let buttonTitle = sender.titleLabel?.text else { return }
currentAnswer.text = currentAnswer.text?.appending(buttonTitle)
activatedButtons.append(sender)
sender.isHidden = true
And the clearTapped
method:
1
2
3
4
5
6
7
currentAnswer.text = ""
for button in activatedButtons {
button.isHidden = false
}
activatedButtons.removeAll()
And finally, the submitAnswer
method. In this one, you figure out which position the solution is in (if it is a good solution) and replace the line at that index in the solution string. If the user has found all the solutions, you present an alert to go to the next level:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
guard let answerText = currentAnswer.text else { return }
guard let solutionPosition = solutions.firstIndex(of: answerText) else { return }
activatedButtons.removeAll()
var splitAnswers = answersLabel.text?.components(separatedBy: "\n")
splitAnswers?[solutionPosition] = answerText
answersLabel.text = splitAnswers?.joined(separator: "\n")
currentAnswer.text = ""
score += 1
if score % 7 == 0 {
let alertController = UIAlertController(title: "Well done!",
message: "Are you ready for the next level?",
preferredStyle: .alert)
let action = UIAlertAction(title: "Let's go!",
style: .default,
handler: levelUp)
alertController.addAction(action)
present(alertController, animated: true)
}
Then you just need to add levelUp(action:)
:
1
2
3
4
5
6
7
8
9
10
func levelUp(action: UIAlertAction) {
level += 1
solutions.removeAll(keepingCapacity: true)
loadLevel()
for button in letterButtons {
button.isHidden = false
}
}
With that, the game works. The only problem is that it doesn’t show the user’s score, so you add a didSet
observer to score
that updates scoreLabel
every time it changes:
1
2
3
4
5
var score = 0 {
didSet {
scoreLabel.text = "Score: \(score)"
}
}
Then you have a finished, playable game that looks like this:



You can find my version of this project at the end of day 37 on Github here.
Reflections
Today stood out to me because of the complexity of game logic that can be encapsulated in a relatively small amount of code. The core of this game is pretty much done and could be extended a lot by just writing new levels for it, without adding any code at all. That is pretty cool.