Hacking with Swift – Challenge 8

Review

Time for a new challenge but first, let’s review what we have learned in this huge lesson:

  • The placeholder text for a UILabel is shown in gray when the text field is empty.
  • Using UIFont.systemFont(ofSize:) ensures our app uses the default system font for iOS (which will keep us safe for future versions as well.
  • To run a method when a button is tapped we need to call addTarget() on the button.
  • Activating multiple constraints at once is faster than modifying isActive for each one.
  • A property observer is code that gets run when their property changes or is about to change.
  • We can remove whitespace from a string using the trimmingCharacters(in:) method.
  • The shuffle() method shuffles an array in place rather than returning a new array.
  • We can split strings into an array the components(separatedBy:) method.
  • We can use methods for the handler of a UIAlertAction.
  • We can use += to append one array to another.
  • The safe area layout guide is the space available to our view, excluding rounded corners and notches.
  • UILabel is responsible for showing static text.

Challenges

Challenge 1: draw a thin grey line around the buttons view to make it stand out from the rest of the UI.

This seems a good challenge for remembering what was taught in project 2 which, of course, I didn’t remember by heart and had to go back and check. My solution for this is around line 82:

buttonsView.layer.borderWidth = 1
buttonsView.layer.borderColor = UIColor.darkGray.cgColor

I had previously read, in the rush, “around the buttons’s view”, thinking that every button needed to have a border. That looked quite awful, though, and I reverted it immediately.

Here is the actual result:

Border around the buttons view
Border around the buttons view

Challenge 2: if the user enters an incorrect guess, show an alert telling them they are wrong.

For this challenge we will need to extend the submitTapped() method so that if firstIndex(of:) failed to find the guess you show the alert.

I created an else statement to the if let block of the submitTapped() method like this:

else {
            let errorAlertController = UIAlertController(title: "That's not correct!", message: "Please try again...", preferredStyle: .alert)
            errorAlertController.addAction(UIAlertAction(title: "OK", style: .default, handler: restart))
            present(errorAlertController, animated: true)
        }

At first there was no handler in my addAction segment but, trying it, I noticed that clicking on the OK button would have required the user to also click the clear button and this, to me, would be extra work to be asked our users who paid so much money for our … oh wait… never mind…

I therefore though to connect the clearTapped method but the addAction initialiser expects an optional handler of type ((UIAlertAction) -> Void)? = nil while our clearTapped method is of type (UIButton) -> Void).

As far as my knowledge goes it is not possible to convert one to the other so I created another method called restart that, yes, does exactly the same thing as clearTapped.

This worked, of course, but looked very inelegant to me so I decided to write my first closure ever inside the addAction method. I have no idea if it is correct but a) Xcode is not complaining b) it is working so, why fix what’s not broken?! Here is my code:

else {
            let errorAlertController = UIAlertController(title: "That's not correct!", message: "Please try again...", preferredStyle: .alert)
            
            errorAlertController.addAction(UIAlertAction(title: "OK", style: .default) {
                [weak currentAnswer] _ in
                currentAnswer?.text = ""
                
                for button in self.activatedButtons {
                    button.isHidden = false
                }
                
                self.activatedButtons.removeAll()
            })

            present(errorAlertController, animated: true)
        }

I guess the weak currentAnswer is needed so that we do not get extra problems afterwards in the game. The _ should be correct because we are not fiddling with the UIAlertAction in our closure.

I then pasted the code from the other method, set currentAnswer to be optional and added self. where Xcode was asking me to. I thought about adding [weak self] as well but the amount of errors and of ? and ! needed to fix them discouraged me from doing so.

I would be very curious to see where your solution led.

Challenge 3: try making the game also deduct points if the player makes an incorrect guess.

The issue here is not really about deducing points but how to move to the next level because at the moment we are using a %7 operator to move on. This would prevent even someone who just makes a single mistake to move on. We should either check for all the buttons being hidden or we can set up another property to count how many items were matched so that we can allow for proceeding when a certain amount of correct answers is given.

Insert about an hour or so here…

This challenge gave me a bit more of an headache but, again, it was because I was not clear enough in my intents. The machine was always doing what I was telling her to do, just… I was telling her the wrong thing.

My approach here has been the following: I created a variable to store the amount of hidden buttons. I also added a property observer because I used it during the debugging phase (I would have never gotten through this without that didSet thing!):

var hiddenButtons = 0 {
        didSet {
            print("Hidden buttons: \(hiddenButtons)")
        }
    }

I then added a call to hiddenButtons += 1 in the letterTapped method so that every time a letter is tapped and is therefore hidden, our variable gets a +1 increment. Accordingly, I put a -= 1 in every place where the contrary was happening. I performed a big refactor at the end so it would be useless now to show you where I put that change.

I then wrapped the score checker into an if statement saying that, if the hidden buttons property would have been equal to 20 we could have proceeded to the analysis of the results. Then, inside, I modified the original if statement adding two else if ones. One if the score was >= 5 and one if it was < 5 (quite unforgiving, right?). In the case one would have scored 5 or 6 points I added two alert actions, one to go to the next level and one to stay and repeat this level! Here is the code.

if hiddenButtons == 20 {
                if score % 7 == 0 {
                    let ac = UIAlertController(title: "Well done!", message: "Are you ready for the next level?", preferredStyle: .alert)
                    ac.addAction(UIAlertAction(title: "Let's go!", style: .default, handler: levelUp))
                    present(ac, animated: true)
                } else if score >= 5 {
                    let passAlertController = UIAlertController(title: "Not bad!", message: "You can proceed or repeat the level!", preferredStyle: .alert)
                    passAlertController.addAction(UIAlertAction(title: "Next level!", style: .default, handler: levelUp))
                    passAlertController.addAction(UIAlertAction(title: "Let's stay here!", style: .cancel, handler: restartLevel))
                    present(passAlertController, animated: true)
                } else if score < 5 {
                    let failureAlertController = UIAlertController(title: "Not good enough!", message: "Please try again", preferredStyle: .alert)
                    failureAlertController.addAction(UIAlertAction(title: "Restart", style: .default, handler: restartLevel))
                    present(failureAlertController, animated: true)
                }
            }

Now you may notice a new method: restartLevel. Here it is, with the companion refactoring method:

func restartLevel(action: UIAlertAction) {
        solutions.removeAll(keepingCapacity: true)
        loadLevel()
        showLetterButtons()
    }
    
    func showLetterButtons() {
        for button in letterButtons {
            button.isHidden = false
            hiddenButtons -= 1
        }
    }

Oh, I forgot: I put the score -= 1inside the outermost else statement of the submitTapped method, a score = 0 to reset the score after every level in the loadLevel method (which now gets called extensively) and a call to hiddenButtons -= 1 inside clearTapped and the aforementioned else closure.


I have the feeling that the other way might have been easier but my head is kind of burning now and I cannot work on it now.

Of course we could improve vastly on this game, such as making an alert that shows that there is, by now, nothing beyond the second level or making the interface more pleasing to the eyes but, for the purpose of this project, this seems already quite a good thing!

Generally speaking I loved the logic behind the game and how it made us think more and more!

As always, I appreciate your comments and feedbacks.

Let me know how you solved these challenges.

You can find the repository for the project here.


If you like what I’m doing here please consider liking this article and sharing it with some of your peers. If you are feeling like being really awesome, please consider making a small donation to support my studies and my writing (please appreciate that I am not using advertisement on my articles).

If you are interested in my music engraving and my publications don’t forget visit my Facebook page and the pages where I publish my scores (Gumroad, SheetMusicPlus, ScoreExchange and on Apple Books).

You can also support me by buying Paul Hudson’s books from this Affiliate Link.

Anyways, thank you so much for reading!

Till the next one!

Published by Michele Galvagno

Professional Musical Scores Designer and Engraver Graduated Classical Musician (cello) and Teacher Tech Enthusiast and Apprentice iOS / macOS Developer Grafico di Partiture Musicali Professionista Musicista classico diplomato (violoncello) ed insegnante Appassionato di tecnologia ed apprendista Sviluppatore iOS / macOS

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

Create your website with WordPress.com
Get started
%d bloggers like this: