Here is my solution. A bit verbose though as I created all views in code. I added all the comments to understand what is going on in there. So hope it is clear :) All is based on things that were taught in previous days. More beautified version looks like that Hangman
var scoreLabel: UILabel!
var levelLabel: UILabel!
var promptLabel: UILabel!
var letterButtons = [UIButton]()
var spacer: UILabel!
var level = 0 {
didSet {
levelLabel.text = "Level: \(level + 1)"
}
}
var score = 0 {
didSet {
scoreLabel.text = "Score: \(score)"
}
}
var wrongAnswer = 0
var word = ""
var wordLetterArray = [String]()
var maskedWordString = ""
var maskedWordArray = [String]()
override func loadView() {
view = UIView()
view.backgroundColor = .white
// create score label
scoreLabel = UILabel()
scoreLabel.translatesAutoresizingMaskIntoConstraints = false
scoreLabel.textAlignment = .right
scoreLabel.text = "Score: 0"
scoreLabel.textColor = .black
scoreLabel.font = UIFont.systemFont(ofSize: 20)
view.addSubview(scoreLabel)
// create level label
levelLabel = UILabel()
levelLabel.translatesAutoresizingMaskIntoConstraints = false
levelLabel.textAlignment = .left
levelLabel.text = "Level: 1"
levelLabel.textColor = .black
levelLabel.font = UIFont.systemFont(ofSize: 20)
view.addSubview(levelLabel)
// create prompt label i.e. where our word will appear
promptLabel = UILabel()
promptLabel.translatesAutoresizingMaskIntoConstraints = false
promptLabel.textAlignment = .center
promptLabel.textColor = .black
promptLabel.font = UIFont.systemFont(ofSize: 80)
view.addSubview(promptLabel)
// create space as it stretches depending on the size of the screen see layout priority set to 1
spacer = UILabel()
spacer.translatesAutoresizingMaskIntoConstraints = false
spacer.setContentHuggingPriority(UILayoutPriority(1), for: .vertical)
view.addSubview(spacer)
// create a view that holds all buttons
let buttonsView = UIView()
buttonsView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(buttonsView)
// setting constrains to our views on the screen
NSLayoutConstraint.activate([
scoreLabel.topAnchor.constraint(equalTo: view.layoutMarginsGuide.topAnchor),
scoreLabel.trailingAnchor.constraint(equalTo: view.layoutMarginsGuide.trailingAnchor),
levelLabel.topAnchor.constraint(equalTo: view.layoutMarginsGuide.topAnchor),
levelLabel.trailingAnchor.constraint(equalTo: scoreLabel.leadingAnchor, constant: -20),
spacer.topAnchor.constraint(equalTo: scoreLabel.bottomAnchor),
spacer.centerXAnchor.constraint(equalTo: view.centerXAnchor),
promptLabel.topAnchor.constraint(equalTo: spacer.bottomAnchor, constant: 20),
promptLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor),
promptLabel.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: 0.8),
buttonsView.widthAnchor.constraint(equalToConstant: 735),
buttonsView.heightAnchor.constraint(equalToConstant: 232),
buttonsView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
buttonsView.topAnchor.constraint(equalTo: promptLabel.bottomAnchor, constant: 50),
buttonsView.bottomAnchor.constraint(equalTo: view.layoutMarginsGuide.bottomAnchor, constant: -50)
])
// create buttons sizes
let height = 64
let width = 64
// as english alphabet contains 26 letters we will place them on
// 3 rows and last row will be place in center relative to other buttons
for row in 0..<3 {
for column in 0..<9 {
// create the view of a button
let letterButton = UIButton(type: .system)
letterButton.titleLabel?.font = UIFont.systemFont(ofSize: 36)
letterButton.setTitle("A", for: .normal)
letterButton.layer.cornerRadius = 10
letterButton.layer.borderColor = UIColor.gray.cgColor
letterButton.layer.borderWidth = 1
// place buttons inside button view with 20 points between them
let frame = CGRect(x: (column * 20) + (column * width), y: (row * 20) + (row * height), width: width, height: height)
letterButton.frame = frame
// place last row in center -> 32 points what makes additional space from left
// and place the row in center
if row == 2 && column < 8 {
letterButton.frame = CGRect(x: 32 + (column * 20) + (column * width), y: (row * 20) + (row * height), width: width, height: height)
}
// remove the last button (i.e. 27th letter) from last row
if row == 2 && column == 8 {
continue
}
// add buttons to button view so that they become visible on our screen
buttonsView.addSubview(letterButton)
// add buttons to letter buttons array
letterButtons.append(letterButton)
// create action for buttons
letterButton.addTarget(self, action: #selector(letterTapped), for: .touchUpInside)
}
}
}
override func viewDidLoad() {
super.viewDidLoad()
loadLevel()
// create alphabet array
let alphabet = ["A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M",
"N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z"]
// set title for all the letters
for i in 0..<letterButtons.count {
letterButtons[i].setTitle(alphabet[i], for: .normal)
}
}
func loadLevel() {
// load words file (so depending on your file some info may be different)
if let wordsFileURL = Bundle.main.url(forResource: "words", withExtension: "txt") {
if let wordsContents = try? String(contentsOf: wordsFileURL) {
var listOfWords = wordsContents.components(separatedBy: "\n")
listOfWords.shuffle()
// removing line breaks
//(in my case words list contains additional "\" so i had to put it as "\\")
word = listOfWords[level].replacingOccurrences(of: "\\", with: "").trimmingCharacters(in: .whitespacesAndNewlines)
// loop through the word and adding letters as strings to word letter array
for letter in word {
wordLetterArray.append(String(letter))
}
print(wordLetterArray)
print(word)
// loop through word letter array created in previous step
// and add "*" in masked word string as well as append masked word array.
for _ in 0..<wordLetterArray.count {
maskedWordString += "*"
maskedWordArray.append("*")
}
// display word as "*****" in prompt label
promptLabel.text = maskedWordString
}
}
}
@objc func letterTapped(_ sender: UIButton) {
var usedLetters = [String]()
// retrieve the letter from title label
guard let letterChosen = sender.titleLabel?.text else { return }
// hide pressed button
sender.isHidden = true
// add letter to used letters array
usedLetters.append(letterChosen)
// if world letter array contains the letter you pressed
if wordLetterArray.contains(letterChosen) {
// loop through the word letter array and assign index to that letter
for (index, letter) in wordLetterArray.enumerated() {
// if the letter we chose == letter in word letter array
// assign the letter to our masked word array using its index
// i.e. if index is 2 and letter is "O" now masked word array looks like ["*", "*", "O"]
if letterChosen == letter {
maskedWordArray[index] = letter
// add point to score
score += 1
}
}
// convert masked word array back to string
maskedWordString = maskedWordArray.joined()
} else {
// if no letter is found deduct from score and add to wrong answer integer
score -= 1
wrongAnswer += 1
}
// assign masked word string to display the word i.e. now it is **O
promptLabel.text = maskedWordString
// check if the word is complete
checkIfCompleted()
}
func checkIfCompleted() {
// if wrong answers == 7 show the alert and restart the game
if wrongAnswer == 7 {
let ac = UIAlertController(title: "You lost!\n Correct word was \(word)", message: "Try one more time!", preferredStyle: .alert)
ac.addAction(UIAlertAction(title: "Restart", style: .default, handler: restartGame))
present(ac, animated: true, completion: nil)
}
// if word that was loaded == masked word string after all letters
// become unmasked then show the alert and go to next level
if word == maskedWordString {
let ac = UIAlertController(title: "Well done!", message: "Let's try another word!", preferredStyle: .alert)
ac.addAction(UIAlertAction(title: "Let's go", style: .default, handler: levelUp))
present(ac, animated: true, completion: nil)
}
}
// the rest is self-explanatory
func levelUp(action: UIAlertAction) {
level += 1
updateUI()
loadLevel()
}
func restartGame(action: UIAlertAction) {
level = 0
score = 0
updateUI()
loadLevel()
}
func updateUI() {
wrongAnswer = 0
wordLetterArray.removeAll()
maskedWordArray.removeAll()
maskedWordString = ""
for btn in letterButtons {
btn.isHidden = false
}
}