Chapter 41: Touch ID

In 2013, Apple launched the iPhone 5S which included the Touch ID fingerprint sensor. Since then, many popular apps have adopted the use of Touch ID to secure content, perform fast re-authentication, and more.


What you will learn

  • LocalAuthentication
  • UIAlertController
  • UIAlertAction
  • Storyboard IDs
  • Error Handling
  • DispatchQueue

Key Terms

  • LAContext: a programmatic interface for evaluating authentication policies and access controls, managing credentials, and invalidating authentication contexts.
  • LAError: a list of errors that can occur within an LAContext.
  • Thread: a section of code that can be run independently of the main program.

Resources

Download here: https://github.com/devslopes/book-assets/wiki

Before fingerprint sensors came to smartphones, I thought of them in terms of what a deep-cover CIA operative would use to gain access to places in spy movies. But Apple brought that magic and security to everyone when they integrated the Touch ID sensor into the iPhone home button.

Touch ID is ultra-secure (though it does have it's weaknesses), as it stores all fingerprint data locally on a secured portion of the device CPU, inaccessible by iCloud or other servers. This makes it an amazing way to secure content of all kinds on Apple devices.

In this chapter, we will be building an app called Touchy that utilizes Touch ID. After authenticating our identity in the app via fingerprint, a new ViewController will load showing some secured content.

Setting Up Xcode

To begin, download and open up the Touchy starter project from the link above.

This starter project includes a pre-built UI, custom UIButton class called RoundedOutlineButton with @IBDesignable/@IBInspectable properties, and our intial ViewController called AuthenticationVC which you can see in Figure 5.10.1 below:

Figure 5.10.1 Screen Shot 2016-10-13 at 4.55.45 AM.png

Importing LocalAuthentication

In order to use Touch ID in our app, we need to import the framework LocalAuthentication. To do this, select the Touchy project settings (Figure 5.10.2) then scroll to the bottom of the screen until you see Linked Frameworks and Libraries (Figure 5.10.3)

Figure 5.10.2 Screen Shot 2016-10-13 at 6.03.03 AM.png

Figure 5.10.3 Screen Shot 2016-10-13 at 6.05.05 AM.png

Next, click the + button to add a framework. Search on the resulting pop-up for LocalAuthentication and double-click to add it to your project (Figure 5.10.4).

Figure 5.10.4

Screen Shot 2016-10-13 at 7.40.51 PM.png

We are now ready to begin coding our app. 👌

Adding an @IBAction to our Touch ID Button

Let's set the button in our UI to be able to respond to input. Open up Main.storyboard, click on AuthenticationVC, and open the Assistant Editor (Screen Shot 2016-10-13 at 7.56.03 PM.png).

Right-click on the rounded Touch ID button and drag to AuthenticationVC within the brackets of our AuthenticationVC class. A blue line will pop up showing where you dragged from. Release the mouse once you have entered the code side of the Assistant Editor (Figure 5.10.5).

Figure 5.10.5 Screen Shot 2016-10-13 at 8.00.54 PM.png

Click on the drop down menu that says Outlet and select Action to create an@IBAction. In the Name field, type onTouchIDButtonPressed. We always want to be descriptive about what our @IBAction does. Click Connect to add an @IBAction (Figure 5.10.6).

Figure 5.10.6 Screen Shot 2016-10-13 at 7.55.50 PM.png

Building Out AuthenticationVC

Now switch over to AuthenticationVC and, we need to import LocalAuthentication into our AuthenticationVC. Beneath import UIKit, add import AuthenticationVC:

import UIKit
import LocalAuthentication

class AuthenticationVC: UIViewController {

    @IBAction func onTouchIDButtonPressed(_ sender: AnyObject) {

    }

}

Now we have access to the framework which will allow us to use Touch ID.

The first thing we need to do to integrate Touch ID is to define an LAContext. "LA" stands for LocalAuthentication, in case you were wondering.

We also need to create a variable to handle and hold the value for any errors we may encounter.

Inside of the brackets of our @IBAction, add the following:

...

@IBAction func onTouchIDButtonPressed(_ sender: AnyObject) {
    let authenticationContext = LAContext()
    var error: NSError?
}

LAContext is going to allow us to check if our device has a Touch ID sensor. We're going to use one of LAContext's built-in delegate methods called canEvaluatePolicy(policy:error:) to check this.

...

@IBAction func onTouchIDButtonPressed(_ sender: AnyObject) {
    let authenticationContext = LAContext()
    var error: NSError?

    if authenticationContext.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error) {

    }

}

.deviceOwnerAuthenticationWithBiometrics is a LocalAuthentication policy that, according the the Xcode documentation, "indicates that the device owner authenticated using Touch ID" – just what we need!

We use the ampersand (&), to indicate that we are passing in error as an input value so that we can modify it directly. I will explain why later on in this chapter.

Now that we have checked to see if we have a Touch ID sensor, we need to actually authenticate with it. To do this add the following code inside the brackets of .canEvaluatePolicy(policy:error:):

...

@IBAction func onTouchIDButtonPressed(_ sender: AnyObject) {
    let authenticationContext = LAContext()
    var error: NSError?

    if authenticationContext.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error) {
        authenticationContext.evaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, localizedReason: "We need your fingerprint to authenticate.", reply: { (success, error) in

        })
    }
}

So basically what we have now done is allowed our app to perform authentication since it now knows that we have a Touch ID sensor. We used .deviceOwnerAuthenticationWithBiometrics as our policy.

We declared a String value for localizedReason, which is the messaging that is shown beneath the stock iOS Touch ID popup (Figure 5.10.7). We then determined our reply which is a closure statement. Within our reply to the Touch ID sensor, we have two things to think about – what to do if we are successful and what to do if not.

There are two values we can use to manage this – success and error right at the end of our closure statement.

We're going to add some conditional code to operate if we are successful or not:

...

@IBAction func onTouchIDButtonPressed(_ sender: AnyObject) {
    let authenticationContext = LAContext()
    var error: NSError?

    if authenticationContext.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error) {
        authenticationContext.evaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, localizedReason: "We need your fingerprint to authenticate.", reply: { (success, error) in
            if success {
                // Navigate to locked content
            } else {
                if let error = error as? NSError {
                    // Display a specific error
                }
            }
        })
    }
}

Great! So now, if we have success in verifying Touch ID, then we will navigate to the locked content we have created. If not, we will display a specific error so the user knows what is wrong.

If you're wondering why error is followed by as? NSError that's because the error value passed in via our closure is of type Error not NSError. We are casting the value this way because NSError has property called code which returns an integer based on an error code. Later on we will use these error codes to display specific error information.

Let's now write the function to navigate to our Success ViewController once we have successfully authenticated.

Below the last bracket of the @IBAction, add the following function:

...

@IBAction func onTouchIDButtonPressed(_ sender: AnyObject) {
    ///Code redacted for spacing purposes.
}

func navigateToSuccessVC() {

}

Now let's instantiate a ViewController and give it a Storyboard ID, then push it onto our Navigation Controller so that we can navigate to the Success ViewController with the green success image:

...

func navigateToSuccessVC() {
    if let successVC = storyboard?.instantiateViewController(withIdentifier: "SuccessVC") {
        self.navigationController?.pushViewController(successVC, animated: true)
    }
}

We created a conditional by writing if, then we added a constant called successVC and instantiated a ViewController on it. We gave it an identifier – in this case a Storyboard ID – of "SuccessVC". Then, inside the conditional we told our Navigation Controller to push successVC onto our navigation stack as the next ViewController inside it.

Now wherever we call navigateToSuccessVC() it will push our Success ViewController onto the Navigation Controller.

Let's call it in the success condition block inside of our @IBAction:

  @IBAction func onTouchIDButtonPressed(_ sender: AnyObject) {
      let authenticationContext = LAContext()
      var error: NSError?

      if authenticationContext.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error) {
          authenticationContext.evaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, localizedReason: "We need your fingerprint to authenticate.", reply: { (success, error) in

              if success {
                  self.navigateToSuccessVC()
              } else {
                if let error = error as? NSError {
                      // Display a specific error
                  }
              }

          })
      }
  }

You may be wondering why we needed to type self before we called our navigation function. This is because we are inside of a closure. We are using navigateToSuccessVC() as an unowned reference. We're doing this since we know that our reference to navigateToSuccessVC() can't ever be nil (or empty) once we have initialized it.

Our function has code that must run and has no option but to work. If you don't understand this yet, that's okay. For the curious, you could do some research on strong, weak, and unowned references in Swift. In many closures, you will need to use self keyword before calling functions, variables, constants, etc.

Errors: Creating An Alert Pop-Up

Now that we've handled what should happen if we are successful, we should handle what should happen in case of an error.

To begin, let's create a function that will create a stock iOS alert pop-up. Beneath navigateToSuccessVC() add the following:

...

func showAlertWithTitle(title: String, message: String) {
    let alertVC = UIAlertController(title: title, message: message, preferredStyle: .alert)
    let okAction = UIAlertAction(title: "OK", style: .default, handler: nil)
}

We created a function called showAlertWithTitle(title:message:) and have declared two constants alertVC and okAction.

The constant alertVC is of type UIAlertController and we have passed in the title, message, and set a style (alert). Our okAction is of type UIAlertAction and we passed it a title, style (default), and made it's handler nil because we don't need to do anything after our error pop-up displays.

Now let's add our action to alertVC and ask our ViewController to present it:

func showAlertWithTitle(title: String, message: String) {
    let alertVC = UIAlertController(title: title, message: message, preferredStyle: .alert)
    let okAction = UIAlertAction(title: "OK", style: .default, handler: nil)

    alertVC.addAction(okAction)

    self.present(alertVC, animated: true, completion: nil)
}

Okay, awesome. 🙌 We now have added okAction to alertVC and asked to present the alert pop-up whenever our function is called. So let's call it. We will create a function to display an error message for a device that doesn't have a Touch ID sensor.

Beneath showAlertWithTitle(title:message:) write the following:

...

func showAlertForNoBiometrics() {
    showAlertWithTitle(title: "Error", message: "This device does not have a Touch ID sensor.")
}

We're written a function inside a function. Aside from any Inception-related jokes, we now can call showAlertForNoBiometrics() in the else clause of canEvaluatePolicy(policy:error:) above. Remember that we've been writing code inside of the if clause so far, meaning if we have a Touch ID sensor. We want the following code to be written in case the device doesn't have what it needs. Add an else clause at the bottom of canEvaluatePolicy(policy:error:) like so:

if authenticationContext.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error) {
    authenticationContext.evaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, localizedReason: "We need your fingerprint to authenticate.", reply: { (success, error) in

            if success {
                self.navigateToSuccessVC()
            } else {
                if let error = error as? NSError {
                    // Display a specific error
                }
            }
        })
    } else {
    showAlertForNoBiometrics()
    }
}

Awesome, so we now can display an error if our app is run on a device without a Touch ID sensor.

Build & Run

We should now check to see if what we've done works. Make sure that you set the active scheme to an iPhone/iPad of choice. Click the triangular Build & Run button from the top left corner of Xcode and wait for the app to build (Figure 5.10.7).

Figure 5.10.7 Screen Shot 2016-10-15 at 5.12.30 AM.png

Once it launches in Simulator, click on the Touch ID button on the screen. We should see an error message pop up saying we don't have a Touch ID sensor (Figure 5.10.8)!

Figure 5.10.8

Simulator Screen Shot Oct 15, 2016, 5.16.14 AM.png

This is because we have not yet enabled the virtual Touch ID sensor in Simulator. To do this, go to the macOS menu bar and click Hardware > Touch ID > Toggle Enrolled State.

Now, try clicking on the Touch ID button again. You should see a nice stock iOS Touch ID pop-up with our custom localizedReason for why we need Touch ID (Figure 5.10.9).

Figure 5.10.9

Simulator Screen Shot Oct 15, 2016, 5.18.34 AM.png

We can now pass in a successful or unsuccessful fingerprint scan by selecting Hardware > Touch ID > Matching Touch or Non-matching Touch from the macOS menu bar.

Select Matching Touch and you'll probably notice that nothing happens. Actually, something is happening. Our app works and after about 10 seconds you should see that the app successfully transitions to our locked content, but it takes forever. This is a terrible user experience! Why is this happening?

Certain functions in Swift (and all other programming languages) can be performed on what are called "threads". Think of them as different channels where code is run.

There is a main thread where most code is run, but there are also background threads where other code is run. Threading allows for a much more efficient running of our apps because the work the CPU needs to do is split up into several sections (threads).

Touch ID happens to be run on a background thread. So is the pushing of ViewControllers onto a NavigationController. Since they are both happening simultaneously, the background thread they're running on is full and therefore causes a serious memory issue and delay of our app. Let's save it! 👍

Inside navigateToSuccessVC, we pushed a ViewController onto our NavigationController. We need to add the following code to bring this animation up to the main thread using DispatchQueue:

func navigateToSuccessVC() {
    if let successVC = storyboard?.instantiateViewController(withIdentifier: "SuccessVC") {
        DispatchQueue.main.async {
            self.navigationController?.pushViewController(successVC, animated: true)
        }
    }
}

DispatchQueue is like a thread manager and it can move code around and place it on different threads. In our case, we moved some code up onto the main thread of our app, then called async, which stands for "asynchronously". This just means that the code runs from top to bottom as you'd expect.

Now, build and run the app once more. If we tap the Touch ID button and then send in a successful fingerprint by selecting Hardware > Touch ID > Matching Touch it should push SuccessVC and animate the transition instantly! Huzzah! 🙌

Specific Error Handling

Next, let's create a function to handle the variety of errors that could occur for issues with Touch ID. Perhaps our user tapped cancel. Maybe they closed the app after the Touch ID pop-up appeared. Regardless, we should have a way of handling these errors. We will write one function to handle all errors with a message.

Beneath showAlertForNoBiometrics() write:

func showAlertViewAfterEvaluatingPolicyWithMessage(message: String) {
    showAlertWithTitle(title: "Error", message: message)
}

Wow, this function has a huge name. But it is specific and leaves other developers with no question as to what it does. This function is basically going to create a pop-up alert with a title of "Error" and a message that will change. Whenever we call showAlertViewAfterEvaluatingPolicyWithMessage(message:) we will pass in a message value of type String. This way we can display a number of different errors with a single function.

Now we need to create the function beneath this one to handle what error has occurred and passing the message into the message parameter of showAlertViewAfterEvaluatingPolicyWithMessage(message:).

func errorMessageForLAErrorCode(errorCode: Int) -> String {

}

Before we dive into adding anything to the above function, I want you to see all possible errors, so you know what type of errors to add to it. I borrowed this from inside of the LAError class:

        /// Authentication was not successful, because user failed to provide valid credentials.
        case authenticationFailed

        /// Authentication was canceled by user (e.g. tapped Cancel button).
        case userCancel

        /// Authentication was canceled, because the user tapped the fallback button (Enter Password).
        case userFallback

        /// Authentication was canceled by system (e.g. another application went to foreground).
        case systemCancel

        /// Authentication could not start, because passcode is not set on the device.
        case passcodeNotSet

        /// Authentication could not start, because Touch ID is not available on the device.
        case touchIDNotAvailable

        /// Authentication could not start, because Touch ID has no enrolled fingers.
        case touchIDNotEnrolled

        /// Authentication was not successful, because there were too many failed Touch ID attempts and Touch ID is now locked.
        case touchIDLockout

        /// Authentication was canceled by application (e.g. invalidate was called while authentication was in progress).
        case appCancel

        /// LAContext passed to this call has been previously invalidated.
        case invalidContext

After reading the above errors, it is easy to see that there is a lot that can go wrong with Touch ID. To make sure we are covering all of these potential errors, we are going to write a switch that can sift through them based on the error code associated with them and pass in the message we want.

Return back to the function we just created called errorMessageForLAErrorCode(errorCode:). We need to create a variable to store the message provided by each error. Then we need to write a switch statement to handle all the potential cases. Finally, we need to return our message as a String as required by our function. Add the following:

func errorMessageForLAErrorCode(errorCode: Int) -> String {
    var message = ""

    switch errorCode {
    case LAError.appCancel.rawValue:
        message = "Authentication was cancelled by application"

    case LAError.authenticationFailed.rawValue:
        message = "The user failed to provide valid credentials"

    case LAError.invalidContext.rawValue:
        message = "The context is invalid"

    case LAError.passcodeNotSet.rawValue:
        message = "Passcode is not set on the device"

    case LAError.systemCancel.rawValue:
        message = "Authentication was cancelled by the system"

    case LAError.touchIDLockout.rawValue:
        message = "Too many failed attempts."

    case LAError.touchIDNotAvailable.rawValue:
        message = "TouchID is not available on the device"

    case LAError.userCancel.rawValue:
        message = "The user did cancel"

    case LAError.userFallback.rawValue:
        message = "The user chose to use the fallback"

    default:
        message = "Did not find error code on LAError object"
    }
    return message
}

Don't freak out. 😳 I know that was a lot of code to add, but let's unpack it now.

The variable message will hold the error message to pass into our alert pop-up later on. The switch is looking for the errorCode passed in by errorMessageForLAErrorCode(errorCode:). We have declared cases for every case noted in LAError, but we have used a neat little hack to create a custom error code for each error – it's rawValue property. Each error has a rawValue property which is of type Int.

Let's say that we passed in an errorCode of 12. If LAError.userCancel.rawValue is equal to 12, then our message variable will be set to "The user did cancel" and it will be returned.

We also have identified a default case which will return the message "Did not find error code on LAError object", which is unlikely but it's great to cover all the bases.

One of the last things we need to do is add some error handling. In the evaluatePolicy(policy:localizedReason:reply:) function above we wrote a conditional to determine what to do if we had success or not. Inside the else block, we said that if there is an error that we should do something. Add the following:

if success {
    self.navigateToSuccessVC()
} else {
    if let error = error as? NSError {
        let message = self.errorMessageForLAErrorCode(errorCode: error.code)
        self.showAlertViewAfterEvaluatingPolicyWithMessage(message: message)
    }
}

If we encounter an error with Touch ID, we will be given an Int value from LAError to use as an error code. We pass that error code into errorMessageForLAErrorCode(errorCode:) as error.code. The return value of that function will set our message variable to a relevant error code for whatever went wrong. Then, we call showAlertViewAfterEvaluatingPolicyWithMessage(message:) and pass in our message variable to display it.

Build & Run

Now that we have come this far, it's a great time to build and run our app. Let's check to see if different errors are handled properly.

Once Touchy opens in Simulator, try to perform various actions that a user might purposefully or accidentally do which would result in an error. First, click on the red Touch ID button in our app. Tap the Cancel button on the Touch ID pop-up and an error pops up with the proper message (Figure 5.10.10). Click on the Touch ID button again and try pressing Shift + Command + H to simulate pressing the home button. We should see an error message that we tried to cancel authentication (Figure 5.10.11).

Figure 5.10.10

Simulator Screen Shot Oct 15, 2016, 5.44.24 AM.png

Figure 5.10.11

Simulator Screen Shot Oct 15, 2016, 5.44.30 AM.png

And of course, finally click the Touch ID button and send in a successful fingerprint selecting Hardware > Touch ID > Matching Touch (Figure 5.10.12).

Figure 5.10.12

Simulator Screen Shot Oct 15, 2016, 5.47.20 AM.png

Wrapping up

We've done a lot of work to integrate Touch ID. This can be used in so many amazing ways in apps – securing content, easy re-authentication, etc. We learned about LAContext and how we must first check to see if we have a Touch ID sensor, then move on from there.

We handled errors in specific, helpful ways to provide the best experience for the user. A confused user is a lost user so letting them know why something isn't working if there is an error is very important. You even learned about threading and how to move some code from a background thread up onto the main thread.

Pat yourself on the back because you just built a really cool app. Nicely done! 👊

Exercise

Extend this app by making it possible for a user to enter their passcode instead of Touch ID. Do some research online on how to do this. If you can do this, it makes for the best user experience because some people are still rockin' the iPhone 5 with no Touch ID sensor. We want to make sure that everyone can use our apps easily.

Chapter 42: Sprite Kit - How to Build a Tiki Bird Game

SpriteKit is a full-featured platform that can be used to build amazing games. We will use it to make an addicting game in Swift.


What you will learn

  • How to move an object
  • How to animate the movement of a Sprite
  • Detecting user taps
  • Creating collisions

Key Terms

  • Sprite Kit
  • SKGameScene
  • SKSpriteNode
  • zPosition
  • SKActions
  • AKTextureAtlas
  • categoryBitMask
  • contactTestBitMask

Resources

Download here: https://github.com/devslopes/book-assets/wiki

We're going to build an awesome game that is similar to the Flappy Bird game, with a few changes. Evan has made us some pretty sweet assets to use on this project. Hopefully you have those downloaded already. I will take us through a step by step process and by the end of this tutorial you will have a pretty cool game to show your friends. We’ll also have some suggestions for you to make it even better.

Your Sprite Kit iOS App (Figure 5.11.0) will have a ground, background, obstacles and a Bird that will flap its wings.

Figure 5.11.0

5.11.0.png

Creating an Xcode Project

Open Xcode and, from the File menu, select New and then New Project.... A new workspace window will appear, and a sheet will slide from its toolbar with several application templates to choose from. On the lefthand side, select Application from the iOS section. From the choices that appear, select Game and press the Next button (Figure 5.11.01). (Apple changes these templates and their names often.

Figure 5.11.1

5.11.1.png

Under Product name enter TikiBird. Language should be Swift. Game Technology is **SpriteKitand Devices isiPhone. Then pressNext`.

Figure 5.11.2

5.11.2.png

Then select where you want to save your project (you can create a Git repository if you’d like) and press Create.

Under the General tab of the TikiBird target change the device orientation to Portrait and Upside down. Uncheck Landscape Left and Landscape Right.

Figure 5.11.3

5.11.3.png

In the Project Navigator click on the Assets.xcassets folder.
First, delete the Spaceship image that’s already there. Highlight it and just press the delete key.
Next, open the folder where you have the assets saved for this game and drag the Ground.png(1x,2x,3x), Mountains.png(1x,2x,3x), and Sky.png(1x,2x,3x) images into the space where the Spaceship image was.

Figure 5.11.4

5.11.4.png

Building the Scenery

We’re almost ready to start coding! We will add our Sky and Mountains to the background, place the ground at the bottom and eventually get the ground to continuously move to the left.
Open GameScene.swift. Before we can add our own code we need to delete all the canned stuff apple put there.
Delete everything you see there except for the didMove function and update function. It should look like this when you are done deleting all the nonsense. Also, open GameScene.sks, click on “Hello World!” and press the delete key.

import SpriteKit
import GameplayKit

class GameScene: SKScene {

    override func didMove(to view: SKView) {

    }

    override func update(_ currentTime: TimeInterval) {
        // Called before each frame is rendered
    }
}

While still inside the GameScene.swift file add the following code below the class declaration and above the didMove method.

    let totalGroundPieces = 3
    var groundPieces = [SKSpriteNode]()

Alright, so what we’re doing above is defining how many ground images we’ll need to use and creating an empty array that will hold these ground pieces. This is how we give the effect of the ground moving.

We animate the image from right to left and once it’s out of frame we move it from the left to the right side of the frame. This gives the appearance of never ending ground.

Now we get to add objects to our scene. First we’ll add the sky, then the mountains, then the ground.
Let’s create a function that will load the scenery for us, call it func setupScenery(), place it right underneath the update method. (Make sure you don’t place it inside the update method’s opening and closing brackets, though).

    func setupScenery(){
        //Add background sprites
        let bg = SKSpriteNode(imageNamed: "Sky")
        bg.size = CGSize(width: self.frame.width, height: self.frame.height)
        bg.position = CGPoint(x: 0, y: 0)
        bg.zPosition = 1
        self.addChild(bg)

        let mountains = SKSpriteNode(imageNamed: "Mountains")
        mountains.size = CGSize(width: self.frame.width, height: self.frame.height/4)
        mountains.position = CGPoint(x: 0, y: -self.frame.height / 2 + 200)
        mountains.zPosition = 2
        self.addChild(mountains)

You are probably thinking.... what the heck is all this? It’s not that bad, I promise. Since the origin (0, 0) of a SpriteKit scene’s frame and the origin of SKSpriteNode is in the center of the object, we’re creating a SKSpriteNode object for each image, setting the position of the image, then adding them to the scene.

We’ll set the sky view to equal the frame of the device and then position the center of the image with the center of the frame (x: 0, y: 0).
We set the mountains a little higher on the Y axis and, since this sprite is not moving, it’s okay to hard code it at 200.

We couldn’t just set the Y to zero because the mountains would be in the middle of the screen, so we have to move them down half of the screen height and add 200 to get them where we would like them to stay.

When setting the mountains size, I had to just play around with the height of the mountains until it looked good to me. The zPosition is important to add. This determines what layer the sprite is on, the higher the number the closer the image is to you. So setting the sky to 1, places it in the back and setting the mountains to 2 makes sure they load in front of the sky.

Time to add the ground sprites! We will generate 3 ground sprites and then position them one after the other. We grab the position of the previous sprite to do this. Add this code right under self.addChild(mountains) from above.

        //Add ground sprites
        for x in 0..<totalGroundPieces {
            let sprite = SKSpriteNode(imageNamed: "Ground")
            sprite.physicsBody = SKPhysicsBody(rectangleOf: sprite.size)
            sprite.physicsBody?.isDynamic = false
            sprite.physicsBody?.categoryBitMask = category_ground
            sprite.zPosition = 5
            groundPieces.append(sprite)
            let wSpacing:CGFloat = -sprite.size.width / 2
            let hSpacing = -self.frame.height / 2 + sprite.size.height / 2
            if x == 0 {
                sprite.position = CGPoint(x: wSpacing, y: hSpacing)
            } else {
                sprite.position = CGPoint(x: -(wSpacing * 2) + groundPieces[x-1].position.x, y: groundPieces[x-1].position.y)
            }
            self.addChild(sprite)
        }

One last step and then we can press run and see what we’ve done! We need to call setupScenery() from the didMove method.

    override func didMove(to view: SKView) {
        setupScenery()
    }

Once you do that, go ahead and press the Run button at the top and let your simulator run. You should see the sky, mountains and ground in place.

Figure 5.11.5

5.11.5.png

Now it’s time to get that ground moving!! We’re going to use the scene editor to help us estimate some X coordinate values so we know when we can take the ground and move it to the other side after it’s out of the view.

In your Project Navigator click on GameScene.sks. If you still have "Hello World!" there, click on it to select it and just press the delete key.
Click on Show Attributes Inspector on the right side and change the size to 640 x 1136.

Figure 5.11.6

5.11.6.png

Now drag a Color Sprite from the bottom right Object Library onto the screen. Change its texture to Ground.png. Since we are generating 3 pieces of ground in the code we just need to find a good X position to make our relocation threshold.

You can quickly duplicate the ground piece 3 ties by pressing ⌘ + d then dragging the pieces one after the other.
-514 on the X position seems like a good number to use (notice how you can see the position on the right hand size in the Attributes Inspector).

Figure 5.11.7

5.11.7.png

Note: If you were to run the app right now you would see these ground pieces in your scene on your device. We don’t want that because we have already added them in code, so just delete them all from the GameScene.sks. (We only need the X coordinate)

Moving the Ground Sprites

First, we will get our project organized a little. We will define a initSetup() method to take care of any setup we need to do when the view is loaded.
Then we will create a startGame() method. This will load the scene and start the game. We will learn how to add SKActions to the ground images for movement. Then simply continuously monitor whether we need to move the ground pieces back to the starting position.

We need to store ground speed, the actions, and the ground reset x coordinate (that was what the -514 was for from the scene editor).
Put this code just below the groundPieces array at the top.

    let groundSpeed: CGFloat = 3.5
    let groundResetXCoord: CGFloat = -514
    var moveGroundAction: SKAction!
    var moveGroundForeverAction: SKAction!

Create a initSetup() method, define the actions, then call initSetup() from the didMove method:

    override func didMove(to view: SKView) {
        initSetup()
        setupScenery()
        startGame()
    }

    func initSetup() {
        moveGroundAction = SKAction.moveBy(x: -groundSpeed, y: 0, duration: 0.02)
        moveGroundForeverAction = SKAction.repeatForever(SKAction.sequence([moveGroundAction]))
    }

We hard coded the duration of the moveBy on the X coordinate by the groundSpeed in 0.02 seconds.
We then want this action repeat forever. Notice the negative sign before groundSpeed, this is so it moves to the left.

Now start the actions we created in the initSetup() method in the startGame() method:

    func startGame(){
        for sprite in groundPieces {
            sprite.run(moveGroundForeverAction)
        }
    }

If you build and run your app, your ground is now moving!
Oh no, wait a second! The ground runs out and leaves the screen! Let’s fix this.

Remember that the groundResetXCoord variable we created and never used? Well, now it’s time to use it.
We will create a groundMovement() method that loops through the ground pieces continually and checks whether they have passed the reset X coordinate.
If they’ve hit that point we will take that image out and place it at the back of the line.

    func groundMovement() {
        for x in 0..<groundPieces.count {
            if groundPieces[x].position.x <= groundResetXCoord {
                if x != 0 {
                    groundPieces[x].position = CGPoint(x: groundPieces[x-1].position.x + groundPieces[x].size.width, y: groundPieces[x].position.y)
                } else {
                    groundPieces[x].position = CGPoint(x: groundPieces[groundPieces.count-1].position.x + groundPieces[x].size.width, y: groundPieces[x].position.y)
                }
            }
        }
    }

Okay, so lets think about what this section of code is doing. We have 3 ground images laying across the bottom in a continuous line. The far left would be index 0 of the array, so as they move to the left we take index 0 and place it at index 2 once it hits our reset coordinate we found earlier.
Lastly, call the groundMovement() method from your update method:

    override func update(_ currentTime: TimeInterval) {
        // Called before each frame is rendered
        groundMovement()
    }

Build and run the game. You will now have endless ground moving! Pretty cool hey? You can also control the ground speed by changing the groundSpeed constant we declared at the top.

Bird Animation

Click on Assets.xcassets and click the + arrow at the bottom of your assets window, click on New Sprite Atlas, then rename the folder from Sprites to Bird. Next, drag Bird-0 (1x,2x,3x), Bird-1 (1x,2x,3x), and Bird-2 (1x,2x,3x) into that Bird Atlas.

Figure 5.11.8

5.11.8.png

Now we need to create a bird node. This is the actual object that will be used in the game.
Then we need a AKTextureAtlas for the bird animation frames, and an array to store the frames of the texture.
Add the following code above the didMove(to view: SKView) method:

    var bird: SKSpriteNode!
    var birdAtlas = SKTextureAtlas(named: "Bird")
    var birdFrames = [SKTexture]()

    override func didMove(to view: SKView) {
        initSetup()
        setupScenery()
        setupBird()
        startGame()
    }

Notice we need a method called setupBird(). We will add that now:

    func setupBird() {
        let totalImgs = birdAtlas.textureNames.count
        for x in 0..<totalImgs{
            let textureName = "Bird-\(x)"
            let texture = birdAtlas.textureNamed(textureName)
            birdFrames.append(texture)
        }

        bird = SKSpriteNode(texture: birdFrames[0])
        bird.zPosition = 4
        addChild(bird)
        bird.position = CGPoint(x: self.frame.midX, y: self.frame.midY)
        bird.run(SKAction.repeatForever(SKAction.animate(with: birdFrames, timePerFrame: 0.2, resize: false, restore: true)))
    }

Alright, so what is this? We assigned the variable birdAtlas = SKTextureAtlas(named: “Bird”). Since this was assigned in the global scope of the class it will be initialized right when the class is loaded.
Bird is the name of our atlas folder. Remember, when we added the New Sprite Atlas and renamed it to Bird? The name in this variable must be exactly the same as the Atlas folder we created.

Now down to our setupBird() method. We first grab the total number of images in the atlas.
We then create SKTextures for each of the bird animation frames and add them to the birdFrames array so we can use them.

We then create the actual bird object and give it a default texture bird = SKSpriteNode(texture: birdFrames[0]) and add it to the scene.
Then we set its position and run a repeat forever action that runs the animateWithTextures action that will play our animation. Don’t forget to set the birds zPosition to make sure it’s in front of the background!

Let’s check out our new animation, build and run the app. You will have a bird flapping its wings in the middle of the screen.

Figure 5.11.9

5.11.9.png

Bird Physics (Jumping... or flying)

One way to implement physics to the bird would be just to use SpriteKits, but we will make our own for this project.

Here’s what we need to do:

  1. Detect Touch
  2. Give bird initial Y velocity
  3. Decrease Y velocity incrementally as bird moves up (this is to simulate the pull of gravity)
  4. When max jump duration is reached, begin gaining negative Y velocity.
  5. Velocity picks up as bird continues to fall.

Above the didMove(to view: SKView) method add the following code:

    //simulated jump physics
    var isJumping = false
    var touchDetected = false
    var jumpStartTime: CGFloat = 0.0
    var jumpCurrentTime: CGFloat = 0.0
    var jumpEndTime: CGFloat = 0.0
    let jumpDuration: CGFloat = 0.35
    let jumpVelocity: CGFloat = 500.0
    var currentVelocity: CGFloat = 0.0
    var jumpInertiaTime: CGFloat!
    var fallInertiatime:CGFloat!

    //Delta time
    var lastUpdateTimeInterval: CFTimeInterval = -1.0
    var deltaTime:CGFloat = 0.0
  1. isJumping is a boolean that indicates if the bird is jumping.
  2. touchDetected is a boolean that indicates that a touch just occurred so we can manage some things in the update method.
  3. jumpStartTime is the time that the initial touch took place.
  4. jumpCurrentTime is how long the bird has been jumping.
  5. jumpEndTime is the time that the jump ended (when it met it’s max duration).
  6. jumpDuration is a constant that says how long the bird should jump (adjustable)
  7. jumpVelocity is a constant that says how fast the bird should jump (adjustable)
  8. currentVelocity is the current velocity of the bird
  9. jumpInertiaTime is a time frame in which “gravity” should not affect the jump.
  10. fallInertiaTime is a tie frame in which the bird should float without falling at the height of the jump.
  11. lastUpdateTimeInterval stores the last time update needed to capture the delta time.
  12. deltaTime stores the delta time (which is the time difference between current frame and previous frame)

Delta Time Explained

What does delta time mean and why do I need to worry about it? Delta time is the time between the current frame and the previous frame. We need this time to be the same on all devices our game will run on. Some devices will run at 30 FPS (frames per second) and others could be 60 FPS.

This could be a huge problem if we wanted to make our game multiplayer and was running on two devices. On one device the bird could actually jump faster.
To fix this issue we create the variable deltaTime.
We multiply calculations by deltaTime to get the same performance across multiple devices with different FPS.

Change your initSetup method to look like this:

    func initSetup() {
        jumpInertiaTime = jumpDuration * 0.7
        fallInertiatime = jumpDuration * 0.3

        moveGroundAction = SKAction.moveBy(x: -groundSpeed, y: 0, duration: 0.02)
        moveGroundForeverAction = SKAction.repeatForever(SKAction.sequence([moveGroundAction]))

        self.physicsWorld.gravity = CGVector(dx: 0.0, dy: 0.0)
    }

You can adjust your jump and fall inertia as you see fit. Basically, on the jump inertia, gravity won’t start taking effect on the bird until the last 30% of the jump. Similarly with the fall inertia, we won’t increase downward velocity until we have been falling for 70% of the fall (this is the 0.3 value).

Make sure to set the physicsWorld gravity to 0. Since our bird will use physics to detect collisions he will be subject to SpriteKits world gravity. We don’t need a value here because we have created our own physics engine for gravity.

Next, change your touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) method to look like:

    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        touchDetected = true
        isJumping = true
    }

The Update Method

Alright, this is where all the important heavy lifting happens in our game. The first thing we will do is add our delta time calculation under our groundMovement() call. (Note, this is being added inside the func update(_ currentTime: TimeInterval) method):

        //Calculate delta time
        deltaTime = CGFloat(currentTime - lastUpdateTimeInterval)
        lastUpdateTimeInterval = currentTime

        //Prevents problems with an anomaly that occurs when delta
        //time is too long- apple does a similar thing in their code
        if lastUpdateTimeInterval > 1 {
            deltaTime = 1.0/60.0
            lastUpdateTimeInterval = currentTime
        }

Remember, delta time = current time minus last recorded time. That’s it! The second part of the code deals with a delta time anomaly that can happen when delta time is too long to be effective. So the added code takes care of that problem.

The next step is to do some setup whenever a touch is detected. Place the following code under the delta time code we just added.

        //this is called one time per touch, sets jump start time
        //and sets current velocity to max jump velocity
        if touchDetected {
            touchDetected = false
            jumpStartTime = CGFloat(currentTime)
            currentVelocity = jumpVelocity
        }

We set the jump start time as the current time and then set the current velocity to the max jump velocity so the jump has full force in the beginning.

Now onto the jump, we are still inside the update(_ currentTime: TimeInterval) method:

        //If we are jumping
        if isJumping {
            //How long we have been jumping
            let currentDuration = CGFloat(currentTime) - jumpStartTime
            //time to end jump
            if currentDuration >= jumpDuration {
                isJumping = false
                jumpEndTime = CGFloat(currentTime)
            } else {
                //Rotate the bird to a certain euler angle over a certain period of time
                if bird.zRotation < 0.5 {
                    bird.zRotation += 2.0 * CGFloat(deltaTime)
                }

                //Move the bird up
                bird.position = CGPoint(x: bird.position.x, y: bird.position.y + (currentVelocity * CGFloat(deltaTime)))

                //We dont decrease velocity until after the initial jump inertia has taken place
                if currentDuration > jumpInertiaTime {
                    currentVelocity -= (currentVelocity * CGFloat(deltaTime)) * 2
                }
            }
        }

Remember, isJumping is set in the touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) method.
So if the bird is jumping then the current jump duration = currentTime - jumpStartTime. The jump ends when the bird reaches maximum jump duration, so we do a check on that next: if currentDuration >= jumpDuration.

If the jump continues we want to set the rotation of the bird (similar to how Flappy Bird rotates when he jumps). The values in this part are arbitrary numbers and can be adjusted to desired settings. We are just allowing the bird to rotate to a max rotation angle (0.5) over a certain period of time.

We then move the bird by adding the currentVelocity * deltaTime to the Y position.

Finally, we want to have some gravity kick in! But not right away. We want to wait until the jump inertia time passes and then we start decreasing velocity.

Now if we aren’t jumping? We need the bird to fall… so add this else statement to that.

    else { //If we aren't jumping then we are falling
            //Rotate the bird to a certain euler angle over a certian period of time
            if bird.zRotation > -0.5 {
                bird.zRotation -= 2.0 * CGFloat(deltaTime)
            }
            // move the bird down
            bird.position = CGPoint(x: bird.position.x, y: bird.position.y - (currentVelocity * CGFloat(deltaTime)))

            //only start increasing velocity after floating for a little bit
            if CGFloat(currentTime) - jumpEndTime > fallInertiatime{
                currentVelocity += currentVelocity * CGFloat(deltaTime)
            }
        }

So we do the same thing with the bird’s rotation as before but instead of upwards we want to rotate it downwards.

We also need that Y position to go down instead of up.

And we also need to increase the bird’s speed as the inertia time passes. This acts just like gravity, the longer you fall the more velocity you gain... until you hit terminal velocity of course ;)

Time to build and run this game! Now your bird will fly and fall as you tap on the screen.

Time to add Obstacles

In the Project Navigator click on the Assets.xcassets folder. Open the folder where you have the assets saved for this game and drag the Tiki_Down.png(1x,2x,3x) and Tiki_Upright.png(1x,2x,3x) into the space.
Now your project should look like this:

Figure 5.11.10

5.11.10.png

Like we did before with the ground, we need to open up the Scene Editor by clicking on the GameScene.sks.

Let’s add the Upright Tiki and the Down Tiki to the scene like below. We need to move them around and figure out the Min and Max Y and the space between the tiki’s.
The Min and Max of the bottom tiki is important so we can add it at a random height and then always placing the top tiki the same amount of space from the bottom tiki each time.

Figure 5.11.11

5.11.11.png

Playing around with the numbers and running the simulator, the values below are what I decided seemed right. You can make tweaks as you see fit. Don’t forget to delete the Tikis from the Game Scene.

  1. Height between obstacles: 907
  2. Bottom Tiki max Y: 308
  3. Bottom Tiki min Y: -120
  4. Tiki start pos: 830
  5. Tiki destroy pos: -187

Tiki Code

    //Obstacles
    var tikis = [SKNode]()
    let heightBetweenObstacles: CGFloat = 900
    let timeBetweenObstacles = 3.0
    let bottomTikiMaxYPos = 234
    let bottomTikiMinYPos = 380
    let tikiXStartPos: CGFloat = 830
    let tikiXDestroyPos: CGFloat = -187
    var moveObstacleAction: SKAction!
    var moveObstacleForeverAction: SKAction!
    var tikiTimer: Timer!

What these variables will do.

  1. tikis - an array that holds the active tiki objects
  2. heightBetweenObstacles - the height between tikis we found in scene editor
  3. timeBetweenObstacles- the time span between the generation of tikis
  4. bottomTikiMaxYPos- the max Y coordinate of the bottom tiki
  5. bottomTikiMinYPos- the min Y coordinate of the bottom tiki
  6. tikiXStartPos- the X coordinate where the tikis will be created
  7. tikiDestroyPos- the X coordinate where the tikis will be destroyed
  8. moveObstacleAction- the action that moves the obstacles by a certain amount
  9. moveObstacleForeverAction- the action that moves the action forever
  10. tikiTimer- the timing mechanism we use to create tikis at certain time intervals

Take note that we refactored (Changing all the previous code) the moveGroundAction and moveGroundForeverAction into moveObstacleAction and moveObstacleForeverAction.
In this case, we want the ground and tiki poles to move at the same speed. This allows us to use the same actions.

    //Collision categories
    let category_bird: UInt32 = 1 << 0
    let category_ground: UInt32 = 1 << 1
    let category_tiki: UInt32 = 1 << 2
    let category_score: UInt32 = 1 << 3

Next, let’s add some category constants that we will use for collision detections: These are bit mask categories that we set on the physics bodies of the obstacles and the bird. They allow us to know when the bird has collided with an object.

Now make a createTikiSet(timer: NSTimer) method. What we will do is create a top and bottom tiki, assign the tiki graphic and add physics to it, then add those tikis to a generic SKNode that will be created and added to the game. Add this code to the new method you just created:

    func createTikiSet(_ timer: Timer) {
        let tikiSet = SKNode()
        //Set up Tikis and Score Collider, bottom tiki
        let bottomTiki = SKSpriteNode(imageNamed: "Tiki_Upright")
        tikiSet.addChild(bottomTiki)
        let rand = arc4random_uniform(UInt32(bottomTikiMaxYPos)) + UInt32(bottomTikiMinYPos)
        let yPos = -CGFloat(rand)
        bottomTiki.position = CGPoint(x: 0, y: CGFloat(yPos))
        bottomTiki.physicsBody = SKPhysicsBody(rectangleOf: bottomTiki.size)
        bottomTiki.physicsBody?.isDynamic = false
        bottomTiki.physicsBody?.categoryBitMask = category_tiki
        bottomTiki.physicsBody?.contactTestBitMask = category_bird
    }

First we create a tikiSet node that will hold our tikis (and eventually our score collider node to detect when the bird has made it through tikis).

After that, we create the bottom tiki node and set its graphic. We give it a random Y position that keeps within our min and max bounds and we add a physics body with a size that equals the size of the graphic.

We don’t want the tikis subject to gravity or other forces so we set the physicsBody.dynamic to false.
Lastly, we set the categoryBitMask to category_tiki and the contactTestBitMask to category_bird.
By setting the contactTestBitMask we are saying we want to get a notification whenever an intersection happens between the bird and the tiki object. We will work on the collision soon. Make sure to add the tikis as a child of the tikiSet node.

Now we will add very similar code to add the top tikis. Place this right under the code we just added.

        //Top Tiki
        let topTiki = SKSpriteNode(imageNamed: "Tiki_Down")
        topTiki.position = CGPoint(x: 0, y: bottomTiki.position.y + heightBetweenObstacles)
        tikiSet.addChild(topTiki)
        topTiki.physicsBody = SKPhysicsBody(rectangleOf: topTiki.size)
        topTiki.physicsBody?.isDynamic = false
        topTiki.physicsBody?.categoryBitMask = category_tiki
        topTiki.physicsBody?.contactTestBitMask = category_bird

Now we need to add the tikiSet to the tikis array and set the zPosition to 4 (behind the ground) Run the movement action on the tikiSet, add the tikiSet to the scene, then set it’s starting position. Place this under the code we just placed.

        tikis.append(tikiSet)
        tikiSet.zPosition = 4
        tikiSet.run(moveObstacleForeverAction)
        addChild(tikiSet)
        tikiSet.position = CGPoint(x: tikiXStartPos, y: tikiSet.position.y)

Now we can modify the code in setupScenery() method to add the physics body for collisions.

       //Add ground sprites
        for x in 0..<totalGroundPieces {
            let sprite = SKSpriteNode(imageNamed: "Ground")
            sprite.physicsBody = SKPhysicsBody(rectangleOf: sprite.size)
            sprite.physicsBody?.isDynamic = false
            sprite.physicsBody?.categoryBitMask = category_ground
            sprite.zPosition = 5
            groundPieces.append(sprite)
            let wSpacing:CGFloat = -sprite.size.width / 2
            let hSpacing = -self.frame.height / 2 + sprite.size.height / 2
            if x == 0 {
                sprite.position = CGPoint(x: wSpacing, y: hSpacing)
            } else {
                sprite.position = CGPoint(x: -(wSpacing * 2) + groundPieces[x-1].position.x, y: groundPieces[x-1].position.y)
            }
            self.addChild(sprite)
        }

We need to do the same thing for our bird. In setupBird() method let’s make the same changes.

    func setupBird() {
        let totalImgs = birdAtlas.textureNames.count
        for x in 0..<totalImgs{
            let textureName = "Bird-\(x)"
            let texture = birdAtlas.textureNamed(textureName)
            birdFrames.append(texture)
        }

        bird = SKSpriteNode(texture: birdFrames[0])
        bird.zPosition = 4
        addChild(bird)
        bird.position = CGPoint(x: self.frame.midX, y: self.frame.midY)
        bird.run(SKAction.repeatForever(SKAction.animate(with: birdFrames, timePerFrame: 0.2, resize: false, restore: true)))
        bird.physicsBody = SKPhysicsBody(circleOfRadius: bird.size.height / 2.0)
        bird.physicsBody?.isDynamic = true
        bird.zPosition = 4
        bird.physicsBody?.categoryBitMask = category_bird
        bird.physicsBody?.collisionBitMask = category_ground | category_tiki
        bird.physicsBody?.contactTestBitMask = category_ground | category_tiki
    }

The difference to pay attention to, is that we’re setting the physics body to be circular and to match the size of the bird.
We are also adding a collisionBitMask and setting which objects can collide with our bird. This is the only physics body we’ll set this on because the objects should only collide with the bird.

In the startGame() method let’s get our timer set up so we can see some tikis move!

    func startGame(){
        for sprite in groundPieces {
            sprite.run(moveObstacleForeverAction)
        }
        tikiTimer = Timer(timeInterval: timeBetweenObstacles, target: self, selector: #selector(GameScene.createTikiSet(_:)), userInfo: nil, repeats: true)
        RunLoop.main.add(tikiTimer, forMode: RunLoopMode.defaultRunLoopMode)
        tikiTimer.fire()
    }

If we want the timer to repeat, we must retain an instance of it. This is why we have the tikiTimer variable.
Build and run! You have yourself a pretty sweet start to your Tiki Bird game!

Wrapping up

We covered a lot in this Tiki Bird Tutorial, all from moving an object across the screen and animating movement of a sprite. We integrated user taps to move the bird, added collision bit masks to it as well, tiki posts, and ground to prevent the bird from passing through. You are leaving this tutorial with enough knowledge to make a fun and simple game.

Exercise

Now, I didn’t add everything to this tutorial, it’s time for you to make it your own. I think we built a pretty solid foundation for you to make something extra cool.

One thing you should consider adding is the didBegin(_ contact: SKPhysicsContact) method, this will be called every time your bird collides with an object. You can have the game end or maybe give an option to restart it.

    func didBegin(_ contact: SKPhysicsContact) {
        //Add game over here
    }

Now to use didBegin(_ contact: SKPhysicsContact) you will need to add SKPhysicsContactDelegate extension next to the SKScene of your GameScene classe.

class GameScene: SKScene, SKPhysicsContactDelegate {

Also, in the initSetup() method you need to set the contactDelegate to self.

swift
        physicsWorld.contactDelegate = self

I also included some sounds in the assets. Game noises would be a pretty awesome addition. For instance, if you'd like to add a flap noise every time you tap the screen you would need a tapSound variable of type AVAudioPlayer. Place this at the top with all your other variables.

    var tapSound: AVAudioPlayer!

Oops! We also need to remember to import AVFoundation at the very top with import import SpriteKit and import GameplayKit.

Then in your initSetup() method we need to set up the AVAudioPlayer to play the sound we want. The sound assets should be dragged into your project either in their own folder or just on the same level as your Storyboard.

        let tapSoundURL = Bundle.main.url(forResource: "tap", withExtension: "wav")!
        do {
            tapSound = try AVAudioPlayer(contentsOf: tapSoundURL)
            print("DW: tap loaded")
        } catch {
            print("DW: Music not played")
        }
        tapSound.numberOfLoops = 0
        tapSound.prepareToPlay()

All we need to call this sound to play is place it inside our update(_ currentTime: TimeInterval) method inside our if touchDetected statement calling tapSound.play().

        if touchDetected {
            touchDetected = false
            jumpStartTime = CGFloat(currentTime)
            currentVelocity = jumpVelocity
            tapSound.play()
        }

Keeping track of how many tikis you pass. That would be a great way to keep score. Make this game your own with customizing it and taking it to the next level!