:::warning
☝️ **NO submission is required for labs**
:::
# MarioKart - Gestures & Animations
<img src="https://i.imgur.com/2rbj1yM.gif" width=150> <img src="https://i.imgur.com/shRDp0C.gif" width=150> <img src="https://i.imgur.com/j9lruic.gif" width=150>
### Overview
Tired of boring button-centric UI? Well...in iOS it's easy to implement interactive gestures and fun animations to give your UI some well deserved pop! In this lab you'll build an app that allows users to interact with characters from the iconic video game, Mario Kart, panning, scaling, rotating and then sending them zooming off the screen! 🏎
### User Story Tiers
The user stories for this lab are split up into 3 tiers. **Tier 1 stories** will introduce the core concepts of working with gestures and animations. **Your goal should be to get through Tier 1 stories during your in-class lab time.**
After completing **Tier 1 stories** you'll be able to...
1. Use gestures to trigger events.
1. Use gestures to move and transform views.
3. Use animations to transition views between various positions and transformations.
**Tier 2 & 3 stories** build on the core concepts from tier 1 and yield a more complete and nuanced app. Try out Tier 2-3 outside of class if you really find gestures & animations intriguing or go back to them later if you want to include some of these features in your group project app.
## 🛠 Let's Get Building - Tier 1 Stories (in-class)
### 1. User can move karts around the screen using a pan gesture.
In this user story, we'll leverage a pan gesture recognizer and it's location property to move around the position of our karts.<br>
<img src="https://i.imgur.com/OGRPIji.gif" width=200><br>
1. **Add the image assets** to your project
1. **Download** the MarioKart Image Assets: **@[[assets/marioKart_assets.zip]]**
1. **Drag** the `app_icon.png` to the `Assets.xcassets` -> `AppIcon` -> `iPhone App @2x`
<img src="https://i.imgur.com/cRoycKI.gif" width=600>
1. **Drag** the `kart_[x]@2x.png` images and `background@2x.png` image you downloaded to your `Assets.xcassets` folder.
:::info
The `@2x` in the image file name helps Xcode place the file in the correct resolution slot automatically.
:::
<img src=https://i.imgur.com/eVLERNH.gif width=600><br>
1. **Layout your views**
1. **Access the Media Library** and drag the **background image view** and all **kart image views** onto your view controller.
:::info
**Access the Media Library** by `long clicking` on the Object Library icon (see gif below) or use the quick key: `command` + `shift` + `m`
:::
1. **Re-size image views** and set the **content mode** as needed: *background* -> *Aspect Fill* and *karts* -> *Aspect Fit*.
:::info
You can **duplicate views** by holding `option` while you click and drag.
:::
<img src="https://i.imgur.com/bfGscoI.gif" width=400><br>
1. **Add pan gesture recognizers** for kart image views.
1. **Access the Object Library** and search for a **pan gesture recognizer**.
1. **Drag a pan gesture recognizer** to one of the kart image views by dragging it from the Object Library and placing it on a kart in the storyboard.
<img src="https://i.imgur.com/SIUIwgV.gif" width=500>
1. **Create and connect actions** for your pan gesture recognizers.
:::info
**Creating an action** will trigger a method to be called anytime your gesture recognizer recognizes a gesture.
:::
1. **Create an action** by `control` + `drag`ing **from** a gesture recognizer **to** your view controller swift file to create an action and associated function. You can name the function something like: `didPanKartView`
:::info
⚠️ Drag from the gesture recognizer listed in the *Document Outline*, **NOT** from the image view in the storyboard. (See example below)<br>
⚠️ Make sure you set the **type** to **UIPanGestureRecognizer** when creating the action. (See example below)<br>
:::
1. **Connect actions** from the remaining gesture recognizers by `control` + `drag`ing **from** from each one **to** the same function you created in the first action.
:::info
Connecting all of the gesture recognizers to a single function allows us to reuse our pan logic for each kart view. This is especially useful if we want to add more karts in the future.
:::
<img src="https://i.imgur.com/i9gzAkD.gif" width=600><br>
1. **Code the logic** to move the kart when it's panned.
1. Access the location property of the pan gesture recognizer.
```swift
let location = sender.location(in: view)
```
^^^
Where should I write this code?
^^^
Any code you want to run during a panning event should be in the **body of the function (action)** you created for the gesture recognizer.
^^^
^^^
What's `sender`?
^^^
When a gesture recognizer is triggered and calls it's associated function (action), it includes itself as the `sender`. When you reference the `sender` in the body of the function, you are referencing the specific gesture recognizer that was triggered. This is how you access properties of the gesture recognizer like it's `location` on the screen as well as the `view` it's attached to.
^^^
^^^
What's `location`?
^^^
`location` is a property of pan gesture recognizer's that tells us where the the user has panned in some area that we specify. In this case, it's the location in reference to the entire screen , aka the `view`. Positions of views are described using a data structure called `CGPoint`, which consists of an `x` and `y` coordinates.
^^^
^^^
What's `view`?
^^^
All view controllers come with a **root view called `view`** at the top of the view hierarchy. This is the main view that we are adding all of our other views into. In the above line, we are asking for the pan gestures current location within the root view.
^^^
1. Print the current location returned from the gesture recognizer.
```swift
print("Location: x: \(location.x), y: \(location.y)")
```
:::success
📲 **RUN YOUR APP** and pan on each kart. You should see the position of the gesture recognizer printed out in the console as you pan your finger.<br>
<br>
**Notice** how...
1. Panning on any of the karts calls our panning method.
1. The panning method is called continuously during a panning event.
1. The kart image view don't move yet...we'll fix that with one line of code in the next step!
:::
<img src="https://i.imgur.com/HFapb82.gif" width=500><br>
1. Access the view of the kart that was panned.
```swift
let kartView = sender.view!
```
:::info
Each gesture recognizer knows the `view` it's attached to. We can ask the gesture recognizer (`sender`) for it's view in order to access the specific view that was panned (i.e. which kart image view).
- 💡 We're forcefully unwrapping the `view` property of the `sender` using a `!` to avoid having to continuously account for it as an `optional` value. In our case, it's safe to do so since we can be assured that anyone calling this method will have a view attached and not be `nil`.
:::
1. Set the kart view's position to the current position of the gesture recognizer.
```swift
kartView.center = location
```
:::info
**What's `center`❓** - All views have a `center` property which describes the point of their position. Like the `position` property of the pan gesture recognizer, the `center` property is a `CGPoint` with values for `x` and `y` coordinates. The position of the center is in reference to the view that contains the view, this is called the super view.
:::
:::success
📲 **RUN YOUR APP** and see if you can move your kart!
- 💡 Panning is really *jerky* when I run the simulator on my computer so for smooth motion (as seen in the gif below) I prefer to run the app on an actual device.
:::
<img src="https://i.imgur.com/OGRPIji.gif" width=250><br>
### 2. User can adjust the size of a cart using a pinch gesture.
Most of the concepts you applied to get the karts panning will be used in a similar way to scale the karts using a pinch gesture. The main difference is that we will be referencing the pinch gesture's scale property to scale our karts up and down.<br>
<img src="https://i.imgur.com/H7sIkyA.gif" width=200><br>
1. Add pinch gesture recognizers to kart views.
1. **Access the Object Library**
1. **Search** for *pinch gesture recognizer*
1. **Drag** a pinch gesture recognizer to each kart view.
<img src="https://i.imgur.com/RYhidKE.gif" width=500><br>
1. Create an action for a pinch gesture recognizer and connect the remaining.
1. `control` + `drag` **from** a pinch gesture recognizer in the *Document Outline* **to** create an action in your view controller swift file.
1. Name it something like, `didPinchKartView`
1. Set the **type** to: **UIPinchGestureRecognizer**
1. Connect actions from the remaining pinch gesture recognizers by `ctrl` + `dragging` them each to the same action you created for the first pinch gesture recognizer.<br>
<img src="https://i.imgur.com/k49fZQQ.gif" width=500><br>
1. Access the scale property of the gesture recognizer that was pinched.
```swift
let scale = sender.scale
```
:::info
Similar to the pan gesture recognizer's `location` property, pinch gesture recognizers have a `scale` property that corresponds to the size of the user's pinch.
:::
1. Print the scale value to the console
```swift
print("scale: \(scale)")
```
:::info
**How do you pinch on the simulator?**<br>
• Hold down the `option` key and you'll see two gray circles appear. Those represent the user's fingers.<br>
• Move the cursor while continuing to hold the `option` key until the circles are close together.<br>
Now, Additionally hold down the `shift` key and move the two circles over the object you want to pinch.<br>
• Release the shift key, while continuing to hold the `option` key, `click` on the object you want to pan and (while continuing to hold the *click*) move the cursor to pinch *in* and *out*.
:::
:::success
📲 **RUN YOUR APP** and pinch on each kart. You should see the pinch value of the gesture recognizer printed out in the console as you pinch.
**Notice how...**<br>
• Pinching on any of the karts calls our pinching function.<br>
• The pinching function is called continuously during a panning event.<br>
• Wherever the pinching starts corresponds to a scale value of `1`
:::
<img src="https://i.imgur.com/EBt58d4.gif" width="500"><br>
1. Access the view of the kart that was panned.
```swift
let kartView = sender.view!
```
1. Adjust the scale of the kart view using the scale from the pinch gesture recognizer.
```swift
kartView.transform = CGAffineTransform(scaleX: scale, y: scale)
```
:::info
All views have a `transform` property which, among other things, allows you to modify the view's scale rotation and translation. The `transform` property is of type `CGAffineTransform`, which isn't really too important for us besides helping us navigate to handy constructors to make a new transform with whatever modifications we'd like, such as the scale. In the above line, we create the transform with the scale value we get from our pinch gesture recognizer. We'll plug the scale value in for both `x` and `y` to get a uniform scale in both width and height.
:::
:::success
**📲 RUN YOUR APP** and pinch to scale your karts up and down!
:::
<img src="https://i.imgur.com/H7sIkyA.gif" width=300><br>
### 3. User can rotate a cart using a rotation gesture.
Rotating the kart using a rotation gesture is going to be almost identical to scaling using a pinch. By this point you're really getting the hang of gestures so this should be a synch!<br>
<img src="https://i.imgur.com/YdyslP4.gif" width=200><br>
1. Add rotation gesture recognizers to kart views.
1. **Access the Object Library**
1. **Search** for *rotation gesture recognizer*
1. **Drag** a rotation gesture recognizer to each kart view.<br>
<img src="https://i.imgur.com/ptO87aE.gif" width=500><br>
1. Create an action for a rotation gesture recognizer and connect the remaining.
1. `control` + `drag` **from** a rotation gesture recognizer in the *Document Outline* **to** create an action in your view controller swift file.
1. Name it something like, `didRotateKartView`
1. Set the **type** to: **UIRotationGestureRecognizer**
1. Connect actions from the remaining rotation gesture recognizers by `ctrl` + `dragging` them each to the same action you created for the first.<br>
<img src="https://i.imgur.com/p5ZYAQe.gif" width=500><br>
1. Access the rotation property of the gesture recognizer that was rotated.
```swift
let rotation = sender.rotation
```
:::info
Similar to the pinch gesture recognizer's `scale` property, rotation gesture recognizers have a `rotation` property that corresponds to the amount of rotation in the user's gesture.
:::
1. Print the rotation value to the console
```swift
print("rotation: \(rotation)")
```
:::success
📲 **RUN YOUR APP** and rotate on each kart. You should see the rotate value of the gesture recognizer printed out in the console as you rotate.<br>
• As with all gestures, an actual device is the preferred way to test.<br>
• When using the simulator, the rotation gesture works similar to the pinch gesture, only instead of moving the *circles* (fingers) *in* and *out*, you're moving them in a circular motion.<br>
• Our current setup only allows for one gesture recognizer to work at a time. So, if you make a pinch motion before your rotation, the pinch gesture recognizer will claim the gesture event and the rotation gesture will not be triggered.<br>
• **The rotation values don't seem to be in degrees 🤔** They're not...they're in radian! Silly engineers...🤓
:::
<img src="https://i.imgur.com/tidUV88.gif" width=500><br>
1. Access the view of the kart that was panned.
```swift
let kartView = sender.view!
```
1. Adjust the rotation of the kart view using the rotation from the pinch gesture recognizer.
```swift
kartView.transform = CGAffineTransform(rotationAngle: rotation)
```
:::info
Similar to the approach we used to modify the scale, we'll create a new transform using one of `CGAffineTransform`s handy constructors which takes an angle (in radian). We'll then set the `transform` property of the view to our *rotated* transform to rotate the view.
:::
:::success
**📲 RUN YOUR APP** and rotate your karts around and around!
:::
<img src="https://i.imgur.com/YdyslP4.gif" width=300><br>
### 4. User can double tap a kart to make it *race* (animate) off the screen.
This story will incorporate view animations that will be triggered using a tap gesture recognizer. Incorporating the tap gesture will be almost identical to the last user stories.<br>
<img src="https://i.imgur.com/cVkp4fw.gif" width=200><br>
1. Add tap gesture recognizers to kart views.
1. **Access the Object Library**
1. **Search** for *tap gesture recognizer*
1. **Drag** a tap gesture recognizer to each kart view.<br>
<img src="https://i.imgur.com/CPr2Olv.gif" width=500><br>
1. Create an action for a tap gesture recognizer and connect the remaining.
1. `control` + `drag` **from** a tap gesture recognizer in the *Document Outline* **to** create an action in your view controller swift file.
1. Name it something like, `didTapKartView`
1. Set the **type** to: **UITapGestureRecognizer**
1. Connect actions from the remaining tap gesture recognizers by `ctrl` + `dragging` them each to the same action you created for the first pinch gesture recognizer.<br>
<img src="https://i.imgur.com/8AlrGxW.gif" width=500><br>
1. Set the number of taps required to `2`.
:::info
Tap gesture recognizers can be configured to respond to different numbers of taps. You can also configure the number of touches which is how many fingers the user is required to use during the gesture.
:::
<img src="https://i.imgur.com/t60ojEP.gif" width=500><br>
1. Test the tap gesture recognizer.
```swift
print("Double tap recognized")
```
:::success
📲 **RUN YOUR APP** and double tap on each kart. You should see the test statement print out in the console.
:::
<img src="https://i.imgur.com/UaAE7Cv.gif" width=500><br>
1. Access the view of the kart that was panned.
```swift
let kartView = sender.view!
```
1. Test moving the kart's position
```swift
kartView.center.x += 50
```
:::info
The karts move 50pts on the x-axis, however the *movement* is more like teleportation then smooth motion. We'll fix that next by using a view animation method.
:::
:::success
📲 **RUN YOUR APP** and see if the karts move!
:::
<img src="https://i.imgur.com/9ed08Wj.gif" width=500><br>
1. Animate the movement of the kart's position
:::info
View animation is a breeze using one of several animation methods available in the `UIView` class. We'll start with the simplest version which allows you to set the duration of the animation and set the end state of the view your animating.
- 💡 UIView animation methods are asynchronous so code in your app will continue to run even while the animation is in process.
:::
1. Set up the beginning state of your view before calling the animation method. (In our case this is just where the kart already is so we don't need to specify further)
2. Call the animation method, inputting the time duration (in seconds) you want the animation to take.
3. Use **tab** to access the various input values of the animation method.
4. Press **return** when the `() -> Void` closure is highlighted to expand it and reveal it's body.
5. Enter the end values for the view you're animating
6. In the body of the closure, specify the end state of your view animation. (In our case, this is the position we want the kart finish at)<br>
```swift
UIView.animate(withDuration: 0.8) {
// Closure body
kartView.center.x += 50
}
```
<img src="https://i.imgur.com/u8ku0hE.gif" width=600><br>
:::success
📲 **RUN YOUR APP** and test out your animation!
:::
<img src="https://i.imgur.com/VrS0Fiz.gif" width=500><br>
1. Tune your race animation!
:::info
Now that you can animate your karts, it's up to you to tune the values to get the perfect race effect
:::
1. To race the kart off the screen, you'll want to move your kart by a larger amount.
1. Change the speed of your kart by adjusting the value of the animation's duration.
```swift
UIView.animate(withDuration: 0.6) {
kartView.center.x += 400
}
```
:::success
📲 **RUN YOUR APP** and your off to the races! 🏎
:::
<img src="https://i.imgur.com/cVkp4fw.gif" width=300><br>
### 5. User can long press the background to reset the karts.
Our app has come a long way! We can move, scale, rotate and *race* our karts off the screen...the only problem is getting them back. In this final tier 1 user story, we'll add a long press gesture to trigger a kart reset.
1. Store the starting position of the karts.
:::info
In order to reset the karts to their original position, we'll need to know where to reset them to. We can store the position of the karts when our app first loads for reference.
:::
1. Create outlets for each kart view.
<img src="https://i.imgur.com/6woJOsa.gif" width=600><br>
1. Declare variables to store each kart's starting point.
:::info
We'll want to access these properties in several places in our app, so declare them in a broad scope, i.e. in the same place we created our outlets.
:::
```swift
var startingPointKartView0 = CGPoint()
var startingPointKartView1 = CGPoint()
var startingPointKartView2 = CGPoint()
```
3. Store each kart's starting point when the app loads.
:::info
The **viewDidLoad** method is a great place for any initial setup you need to do before your view controller is presented. As the name suggests, this method is called once all of the view controller's views have been loaded, so it's safe to reference properties from our kart views.
:::
```swift
startingPointKartView0 = kartView0.center
startingPointKartView1 = kartView1.center
startingPointKartView2 = kartView2.center
```
1. Add a long press gesture recognizer to the background image view.
1. **Access the Object Library**
1. **Search** for *long press gesture recognizer*
1. **Drag** a long press gesture recognizer to each kart view.<br>
<img src="https://i.imgur.com/IcKprFB.gif" width=400><br>
1. Create an action for the long press gesture recognizer.
1. `control` + `drag` **from** the long press gesture recognizer in the *Document Outline* **to** create an action in your view controller swift file.
1. Name it something like, `didLongPressBackground`
1. Set the **type** to: **UILongPressGestureRecognizer**<br>
<img src="https://i.imgur.com/AqtdSNI.gif" width=400><br>
1. Code the logic to reset the kart positions
```swift
kartView0.center = startingPointKartView0
kartView1.center = startingPointKartView1
kartView2.center = startingPointKartView2
```
:::success
**📲 RUN YOUR APP** and see if you can long press to reset your karts.
:::
<img src="https://i.imgur.com/vb6oKmo.gif" width=300><br>
1. Animate the resetting of karts.
:::info
The current resetting of the kart's is bit abrupt. Let's wrap the resetting logic in a view animation method to smooth it out.
:::
1. Cut and paste your previous kart view center updates inside your view animation method's closure..you'll notice some errors...
:::danger
**🛑 Reference to property `startingPointKartView0` in closure requires explicit `self`. to make capture semantics explicit...Insert `self`**.
:::
1. Go ahead and *click* the **Fix** button in the error popup window, *or* add `self.` before each object yourself.
:::info
The requirement of `self` in this case has to do with unique properties of closures in Swift and is not important for our purposes at this point.<br>
<br>
**All you need to know is**...<br>
Anytime you are working with animations and you get an error instructing you to add `self.`...**JUST DO IT! 👟**
:::
<img src="https://i.imgur.com/6OFmzjA.gif" width=600><br>
```swift
UIView.animate(withDuration: 0.8) {
self.kartView0.center = self.startingPointKartView0
self.kartView1.center = self.startingPointKartView1
self.kartView2.center = self.startingPointKartView2
}
```
:::success
**📲 RUN YOUR APP** to see the karts animate as they reset their positions.
:::
<img src="https://i.imgur.com/JqDIB6J.gif" width=300><br>
1. Reset karts to their unmodified states
:::info
The karts reset to their starting positions just fine, however their transforms are not being reset which is leading to odd behavior if a user has scaled or rotated a kart.
:::
<img src="https://i.imgur.com/ihkB3Lm.gif" width=250><br>
:::info
To *reset* a transform, we just need to create a new *unmodified* transform and assign it to the view we want to reset, which in this case is our kart views. You can create an unmodified transform using the transform `identity` property.
:::
```swift
self.kartView0.transform = CGAffineTransform.identity
self.kartView1.transform = CGAffineTransform.identity
self.kartView2.transform = CGAffineTransform.identity
```
:::success
**📲 RUN YOUR APP** and see if scaled or rotated karts animate back to their unmodified states.
:::
<img src="https://i.imgur.com/KNvz5uC.gif" width=250><br>
## Bonuses 🤯
- **Tier 2**
1. User can use pinch and rotation gestures simultaneously.
1. While panning, karts slightly scale up and back down to simulate being *picked up* and put back down.
1. When a user double taps a kart it
1. Animates backwards slightly before *racing* off to simulate *winding up*.
1. Pops a wheelie by rotating up and back down as it races off.
1. After finishing racing off the screen, the kart fades back in it's original position.
1. User can triple tap the background to make all karts on the track *zoom* (animate) off at different speeds.
- **Tier 3**
1. When a user triple taps to initiate a race sequence, a character with a stop light floats down, animates through the lights (gif sequence) ending on green to signal the race. The karts then go racing off.
1. In a race sequence, each kart races off at different speeds and the winner is presented in a *winner card* that drops in from the top of the screen.
1. In the *winner card*, the winner is shown in an animated gif sequence.
1. The user can tap or pan the *winner card* to dismiss the card and return to a reset version of the game.
1. After a race sequence, the karts *drive* into position from off the left side of the screen.
## Appendix
### Tier 1 Topics
1. **Assets**
1. Adding images to Assets folder
1. Adding images from Media Library to Storyboard
3. **Gesture Recognizers**
1. Objects
- Pan, Pinch, Rotation, Tap, Long Press
1. Actions
- Creating gesture actions in IB, working with the sender
1. Properties
- Location, rotation, scale
1. States
- Began
4. **View Animations**
1. Working with view animation methods.
- Asynchronous execution
1. Initial & destination states of animated views.
5. **View Properties**
1. Resizing views in IB
1. Adjusting view hierarchy in IB
1. Content Modes
- Aspect Fit
1. Transform
- Rotation, scale, identity
6. **Simulator**
1. Working with gestures
### Tier 2-3 Topics
1. **Gesture Recognizers**
1. Properties
- Translation
3. States
- changed, ended
4. Delegate
- Setting gesture delegate in IB, delegate methods
1. **View Animations**
1. Working with view animation methods.
- completion handlers, animation settings
1. Working with animated gifs
1. **View Properties**
1. View hierarchy
1. Subviews
1. **Swift**
1. Outlet collections
1. Iterating through collections
- Accessing item index while iterating
1. Generating random numbers
----
{"mode":"limited","isActive":true}