How To Make a Letter / Word Game with UIKit and Swift: Part 3/3

This third and final part of the series will be the most fun of them all! In this part, you’re going to be adding a lot of cool and fun features By Caroline Begbie.

Leave a rating/review
Save for later
Share
You are currently viewing page 3 of 4 of this article. Click here to view the first page.

Adding a Hint Feature

By now you’ve probably already noticed that if you aren’t trained in anagrams, the game can be quite challenging. Unless, of course, you’ve been cheating and looking up the answers in the Plist! :]

It would be a good idea to implement in-game help for your player so they don’t get stuck and lose interest. What you are going to develop next is a Hint button, which will move a tile onto a correct target, thus helping the player solve the puzzle.

Begin by adjusting the HUD. In HUDView.swift, add one more property to the class:

var hintButton: UIButton!

Then add the code to create the button on the HUD. At the end of init(frame:) add:

//load the button image
let hintButtonImage = UIImage(named: "btn")!
    
//the help button
self.hintButton = UIButton.buttonWithType(.Custom) as! UIButton
hintButton.setTitle("Hint!", forState:.Normal)
hintButton.titleLabel?.font = FontHUD
hintButton.setBackgroundImage(hintButtonImage, forState: .Normal)
hintButton.frame = CGRectMake(50, 30, hintButtonImage.size.width, hintButtonImage.size.height)
hintButton.alpha = 0.8
self.addSubview(hintButton)

This should configure the button nicely! You set the title to “Hint!”, set the custom game font you use for the HUD and also position the button on the left side of the screen. In the end, you add it to the hud view.

Build and run the project, and you should see the button appear onscreen:

That was quick! Now you need to connect the button to a method so that it will do something when player needs a hint.

Inside GameController.swift, update the hud property to add a didSet property observer. Replace var hud:HUDView! with:

var hud:HUDView! {
  didSet {
    //connect the Hint button
    hud.hintButton.addTarget(self, action: Selector("actionHint"), forControlEvents:.TouchUpInside)
    hud.hintButton.enabled = false
  }
}

Have a look at this observer. When a HUD instance is set to the property, the game controller can also hook up the HUD’s button to one of its own methods. It’s not the HUD’s job to give hints since it doesn’t know about the words and current state of the puzzle, so you want to do this from the game controller instead. The game controller just sets the target/selector pair of the hud.hintButton button to its own method, actionHint. Sleek! :]

Also, the Hint button starts off as disabled. You’ll turn this on and off at the right times as the game progresses.

You’ll also need to add actionHint() to the GameController class:

//the user pressed the hint button
@objc func actionHint() {
  println("Help!")
}

The @objc attribute is needed to make the function available to the Objective-C UIButton class. You’ll replace this code with more than just a log statement later.

Add the following inside checkForSuccess(), just before the line that reads self.stopStopwatch():

hud.hintButton.enabled = false

This disables the hint button at the end of the game, but before any effects begin to play.

Add the following at the end of dealRandomAnagram():

hud.hintButton.enabled = true

This enables the hint button when a new anagram is displayed.

And that should do it! Now when you tap the hint button, you should theoretically see “Help!” appear in the Xcode console. Build and run the project and give the button a try.

Button_why_no_click

Your Hint button does not work for a reason. Remember when you added this line earlier in HUDView’s init(frame:)?

self.userInteractionEnabled = false;

Because HUDView does not handle touches, it also does not forward them to its subviews! Now you have a problem:

  • If you disable touches on the HUD, you can’t connect HUD buttons to methods.
  • If you enable touches on the HUD, the player cannot interact with game elements that are under the HUD layer.

What can you do? Remember the layer hierarchy?

You are now going to use a special technique that comes in handy on rare occasions, but it’s the way to go in this case – and its the most elegant solution as well.

UIKit lets you dynamically decide for each view whether you want to handle a touch yourself, or pass it on to the view underneath. Since HUDView is at the top of the view hierarchy, you’ll add a method there that will handle all touches and then decide what to do with it.

In HUDView.swift, find the line in init(frame:) where you disable user interaction. Change that line to:

self.userInteractionEnabled = true

Now that you will be handling touches in HUDView, you need to decide which ones you want to handle. You’ll implement a custom override of hitTest(_:event:) for the HUDView class.

hitTest(_:withEvent:) gets called automatically from UIKit whenever a touch falls on the view, and UIKit expects as a result from this method the view that will handle the touch. If the method returns nil, then the touch is forwarded to a view under the current view, just as if userInteraction is disabled.

Add the following to HUDView.swift:

override func hitTest(point: CGPoint, withEvent event: UIEvent?) -> UIView? {
  //1 let touches through and only catch the ones on buttons
  let hitView = super.hitTest(point, withEvent: event)
    
  //2
  if hitView is UIButton {
    return hitView
  }
    
  //3
  return nil
}

The logic behind this method is quite simple:

  1. First you call the super’s implementation to get which view would normally handle the touch.
  2. Then you check whether this view is a button (i.e. the Hint button!), and if it is, then you forward the touch to that button.
  3. If it’s not a button, you return nil, effectively forwarding the touch to the underlaying game elements layer.

With this method in place, you have a working HUD layer. Build and run the project, and you will see that the Hint button reacts to touches AND you can drag the tiles around:

Hint button

Challenge: For puzzles with small tiles, it’s actually possible to drop a tile under the button, where it will be stuck forever. How would you solve this problem?

OK, going back to GameController.swift, you can also quickly implement the hinting function. Here’s the plan: you’ll take away some of the player’s score for using the Hint feature, then you’ll find the first non-matched tile and its matching target, and then you’ll just animate the tile to the target’s position.

Replace the println() statement in actionHint() with the following:

//1
hud.hintButton.enabled = false
    
//2
data.points -= level.pointsPerTile / 2
hud.gamePoints.setValue(data.points, duration: 1.5)

This is pretty simple:

  1. You temporarily disable the button so that no hint animations overlap.
  2. Then you subtract the penalty for using a hint from the current score and tell the score label to update its display.

Now add the following to the end of the same method:

//3 find the first unmatched target and matching tile
var foundTarget:TargetView? = nil
for target in targets {
  if !target.isMatched {
    foundTarget = target
    break
  }
}
    
//4 find the first tile matching the target
var foundTile:TileView? = nil
for tile in tiles {
  if !tile.isMatched && tile.letter == foundTarget?.letter {
    foundTile = tile
    break
  }
}

To continue:

  1. You loop through the targets and find the first non-matched one and store it in foundTarget.
  2. You do the same looping over the tiles, and get the first tile matching the letter on the target.

To wrap up, add this code to the end of the method:

//ensure there is a matching tile and target
if let target = foundTarget, tile = foundTile {
        
  //5 don't want the tile sliding under other tiles
  gameView.bringSubviewToFront(tile)
        
  //6 show the animation to the user
  UIView.animateWithDuration(1.5,
            delay:0.0,
          options:UIViewAnimationOptions.CurveEaseOut,
       animations:{
            tile.center = target.center
       }, completion: {
          (value:Bool) in
            
              //7 adjust view on spot
              self.placeTile(tile, targetView: target)
           
              //8 re-enable the button
              self.hud.hintButton.enabled = true
            
              //9 check for finished game
              self.checkForSuccess()
            
  })
}

The above code animates the tile to the empty target:

  1. First bring the tile to the front of its parent’s view hierarchy. This ensures that it doesn’t move under any other tiles, which would look weird.
  2. Set the tile’s center to the target’s center, and do so over 1.5 seconds.
  3. Call placeTile(tileView:, targetView:) to straighten the tile and hide the target.
  4. Enable the Hint button again so the player can use more hints.
  5. Finally, call checkForSuccess() in case that was the last tile. Players will probably not use a hint for the last tile, but you still have to check.

And that’s all! You had already implemented most of the functionality, like placing tiles on a target, checking for the end of the game, etc. So you just had to add a few lines and it all came together. :]

Build and run the project and give the Hint button a try!

Finished game play

Contributors

Over 300 content creators. Join our team.