Origional labs

☝️ NO submission is required for labs

Tumblr - Part 1

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

Overview

In this lab, you'll build the first part of an app that will allow users to view Tumblr posts. You'll work in collaborative pairsPair Programmingto apply the skills you've learned so far building your Flix App Assignment. Just like the Flix App, you'll create a network request to fetch data from the web, however instead of movies, you'll be getting blog post data from the Tumblr API.

Pair Programming - Working in Collaborative Pairs

The checkpoints below should be implemented as pairs. In pair programming, there are two roles: navigator and driver.

  • How to Collaborate in Labs - Pair Programming Video: Check out this short video before getting started with the lab.
  • Navigator: Makes the decision on what step to do next. Their job is to describe the step using high level language ("Let's print out something when the user is scrolling"). They also have a browser open in case they need to do any research.
  • Driver: is typing and their role is to translate the high level task into code ("Set the scroll view delegate, implement the didScroll method").
  • After you finish each checkpoint, switch the Navigator and Driver roles. The person on the right will be the first Navigator.

User Stories - What will our app do?

  1. User can view and scroll through a list of Tumblr photo posts.

Let's Get Building!

Milestone 1: Setup

  1. Setup a custom view controller:
    1. Delete the automatically generated ViewController.swift file and create a custom view controller file named PhotosViewController.
    2. Assign the class of the view controller in storyboard to PhotosViewController.

Milestone 2: Hook up the Tumblr API

  1. Send a test request to the Tumblr API:

    1. Using Google Chrome? Add the Pretty JSON Viewer plug-in to your browser makes it easier to visualize the data you get back from the API (in JSON format).
    2. In a browser, access the Tumblr posts for the blog Humans of New York at https://api.tumblr.com/v2/blog/humansofnewyork.tumblr.com/posts/photo?api_key=Q6vHoaVm5L1u2ZAW1fqv3Jw48gFzYVg9P0vH0VHl3GVy6quoGV
    3. Notice that the response dictionary contains two dictionaries, blog and posts. posts is a giant array of dictionaries where each dictionary represents a post and all of it's associated information using key value pairs.
    4. There are often more arrays and dictionaries nested inside the original post dictionaries, as is the case with the image url: photos -> original_size -> url. This will require us to dig into these nested elements and cast to appropriate types as we go.
      Image Not Showing Possible Reasons
      • The image file may be corrupted
      • The server hosting the image is unavailable
      • The image path is incorrect
      • The image format is not supported
      Learn More →
  2. Create a property in your PhotosViewController class to store posts

    1. This property will store the data returned from the network request. It's convention to create properties near the top of the view controller class where we create our outlets.

    2. Looking at the sample request in the browser, we see the photos key returns an array of dictionaries so that's what type we will make our property.

    3. We will initialize it as an empty array so we don't have to worry about it ever being nil later on.

      ​​​​​​​// 1.       2.             3.
      ​​​​​​​var posts: [[String: Any]] = []
      
  3. Create a request to the Tumblr Photo Posts Endpoint for the, Humans of New York blog by adding the following network request snippet to the PhotoViewController's viewDidLoad() method.

    • Note: For the purposes of this lab, we won't go into a lot of detail about the network request code because it's mainly a lot of repetitive configuration thatfor the purposes of this coursewon't ever change. Whats important for you to know is:
      1. The url specifies which API and to go to and what set of data to retrieve.
      2. If the network request is successful, it will retrieve the information we want from the API endpoint, parse it, and store it in a variable for us to access.
    • Printing dataDictionary to the console should look very similar to your test request in your browser.
      ​​​​​​​// Network request snippet
      ​​​​​​​let url = URL(string: "https://api.tumblr.com/v2/blog/humansofnewyork.tumblr.com/posts/photo?api_key=Q6vHoaVm5L1u2ZAW1fqv3Jw48gFzYVg9P0vH0VHl3GVy6quoGV")!
      ​​​​​​​let session = URLSession(configuration: .default, delegate: nil, delegateQueue: OperationQueue.main)
      ​​​​​​​session.configuration.requestCachePolicy = .reloadIgnoringLocalCacheData
      ​​​​​​​let task = session.dataTask(with: url) { (data, response, error) in
      ​​​​​​​   if let error = error {
      ​​​​​​​      print(error.localizedDescription)
      ​​​​​​​   } else if let data = data,
      ​​​​​​​      let dataDictionary = try! JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] {
      ​​​​​​​      print(dataDictionary)
      
      ​​​​​​​      // TODO: Get the posts and store in posts property
      
      ​​​​​​​      // TODO: Reload the table view
      ​​​​​​​  }
      ​​​​​​}
      ​​​​​​task.resume()
      
  4. Get the posts and store in your posts property

    • 💡 Network requests are asynchronous tasks, which allows the app to continue to execute code while our network request runs in the background. This is key to having responsive app design, otherwise the app would appear frozen while waiting for lengthy processes like network requests to finish. This also means that our table view will initially build before we get any data back from the network. We will need to make sure we reload the table view data any time we get more data back from the network that we want to display.
    1. Use the respective keys and bracket notation to dig into the nested dictionaries.

    2. Cast the value returned from each key to it's respective type.

      ​​​​​​​// Get the dictionary from the response key
      ​​​​​​​let responseDictionary = dataDictionary["response"] as! [String: Any]
      ​​​​​​​// Store the returned array of dictionaries in our posts property
      ​​​​​​​self.posts = responseDictionary["posts"] as! [[String: Any]]
      

Milestone 3: Build the Photo Feed

  1. Add and Configure a Table View in PhotosViewController:

    • You can reference steps 1-4 of the Basic Table View Guide
    • Note: The numberOfRowsInSection method simply tells the table view how many rows, or cells in this case, to create. How many cells do we want? Well, as many as we have posts. We can get that number by calling the count method on our posts array. So, instead of returning a hard coded number like 5 we will want to return posts.count. This is where you can get into trouble if posts contains nil which is why we initialized posts as an empty array because although empty, it is not nil.
  2. Add and Configure a Custom Table View Cell:

    • We will want to create a custom table view cell, PhotoCell, so we can get it looking just right. You can reference steps 1-2 of the Create the Custom Cell Guide.
    • Note: Since we are now using a custom cell, inside our tableView(_:cellForRowAt:) method we will change let cell = UITableViewCell() to
    ​​​​let cell = tableView.dequeueReusableCell(withIdentifier: "PhotoCell", for: indexPath) as! PhotoCell
    
  3. Setup the Image View in your Custom Cell:

    • Each cell will need a single UIImageView. Make sure to create an outlet from the image view to your PhotoCell class and not your PhotosViewController class; after all, we created a the custom cell class to control the properties of our reusable cell. DO NOT name this outlet imageView to avoid colliding with the default imageView property of the UITableViewCell base class.
  4. Get the post that corresponds to a particular cell's row:

    • NOTE: The tableView(_:cellForRowAt:) method is called each time a cell is made or referenced. Each cell will have a unique indexPath.row, starting at 0 for the first cell, 1 for the second and so on. This makes the indexPath.row very useful to pull out objects from an array at particular index points and then use the information from a particular object to populate the views of our cell.
    1. In the tableView(_:cellForRowAt:) method, pull out a single post from our posts array
    ​​​​let post = posts[indexPath.row]
    
  5. Get the photos dictionary from the post:

    1. It's possible that we may get a nil value for an element in the photos array, i.e. maybe no photos exist for a given post. We can check to make sure it is not nil before unwrapping. We can check using a shorthand swift syntax called if let
    2. post is a dictionary containing information about the post. We can access the photos array of a post using a key and subscript syntax.
    3. photos contains an array of dictionaries so we will cast as such.
    ​​​​// 1.            // 2.          // 3.
    ​​​​if let photos = post["photos"] as? [[String: Any]] {
    ​​​​     // photos is NOT nil, we can use it!
    ​​​​     // TODO: Get the photo url
    ​​​​}
    
  6. Get the images url:

    • 💡 This is the url location of the image. We'll use our AlamofireImge helper method to fetch that image once we get the url.
    1. Get the first photo in the photos array
    2. Get the original size dictionary from the photo
    3. Get the url string from the original size dictionary
    4. Create a URL using the urlString
    ​​​​// 1.
    ​​​​let photo = photos[0]
    ​​​​// 2.
    ​​​​let originalSize = photo["original_size"] as! [String: Any]
    ​​​​// 3.
    ​​​​let urlString = originalSize["url"] as! String
    ​​​​// 4.
    ​​​​let url = URL(string: urlString)
    
  7. Set the image view

    1. We'll be bringing in a 3rd party library to help us display our movie poster image. To do that, we'll use a library manager called CocoaPods. If you haven't already, install CocoaPods on your computer now.

    2. Navigate to your project using the Terminal and create a podfile by running, pod init.

    3. Add pod 'AlamofireImage' to your podfile, this will bring in the AlamofireImage library to your project.

    4. In the Terminal run, pod install. When it's finished installing your pods, you'll need to close your xcodeproj and open the newly created xcworkspace file.

    5. import the AlamofireImage framework to your file. Do this at the top of the file under import UIKit. This will allow the file access to the AlamofireImage framework.

      ​​​​​​​import AlamofireImage
      
    6. call the AlamofireImage method, af_setImage(withURL:) on your image view, passing in the url where it will retrieve the image.

      ​​​​​​​cell.photoImageView.af_setImage(withURL: url!)
      
  8. Update the table view to display any new information

    • Our table view will likely be created before we get our photos data back from the network request. Anytime we have fresh or updated data for our table view to display, we need to call:

    • Do this inside the network request completion handler, right after we load the data we got back into our posts property.

      ​​​​​​​self.tableView.reloadData()
      

Gotchas

  • If your app crashes with the exception: Unknown class PhotosViewController in Interface Builder file, try following the steps in this stackoverflow answer.

  • Compile Error: "No such module AlamofireImage"

    • Try cleaning and building your project. Command + Shift + K and Command + B

Tumblr - Part 2

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

Overview

Extend your Tumblr app by building a Detail View. To do that, you'll be passing data between view controllers and implementing push (show) navigation.

Pair Programming

  • The checkpoints below should be implemented using the Pair Programming method

Project Setup

  1. Use your Unit 1 Tumblr project as a staring point.

Milestone 1: Build the Details Screen

This milestone will have you work with Navigation Controllers and transitions between screens while passing data. Review our quickstart for Navigation Controllers and Navigation Controllers guide for more info.

  1. Create a new View Controller for the details screen (PhotoDetailsViewController):
    - Create a new Cocoa Touch Class (without a XIB, subclass of UIViewController). (File -> New -> File...)
    - Using the Object Library, add a UIViewController to the Main.storyboard by dragging it from the Object Library and placing it on the storyboard:

    Imgur|250

  • Set the custom class of the View Controller to PhotoDetailsViewController (similar to what we did for the PhotosViewController above).
  1. Implement PhotoDetailsViewController:
    - The view should consist of a single UIImageView.
    - PhotoDetailsViewController should have a single public property for the photo Url.

  2. Embed PhotosViewController inside of a Navigation Controller to allow it to push the details screen onto the nav stack.

    Imgur

  3. Wire up the tap on the photo to launch into the detail view:
    - In the storyboard, ctrl-drag from the cell to the PhotoDetailsViewController and choose "Show".
    - In PhotosViewController, implement the prepareForSegue method to pass the photo to the details screen.

    • Get a reference to the PhotoDetailsViewController
      let vc = segue.destination as! PhotoDetailsViewController
    • Get the cell that triggered the segue
      let cell = sender as! UITableViewCell
    • Get the indexPath of the selected photo
      let indexPath = tableView.indexPath(for: cell)!
    • Set the photo property of the PhotoDetailsViewController
​​​​ - Remove the gray selection effect:
​​​​   - Implement `tableView:didSelectRowAtIndexPath` inside of `PhotosViewController`
​​​​   - Get rid of the gray selection effect by deselecting the cell with animation
​​​​     `tableView.deselectRow(at: indexPath, animated: true)`

Bonus User Stories

The following bonus user stories are optional and meant to serve as an extra challenge if you'd like to take your app further.

1. Add Avatar and Publish Dates.

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

  • In the real Tumblr app, each photo has a separate section. The reason for that is that section header views in iOS have a "sticky" behavior when scrolling.
  • Implement numberOfSectionsInTableView to return the number of posts. numberOfRowsInSection should now return 1.
    • Note: Make sure to update your cellForRowAtIndexPath method to now use the section number (instead of the row number) to index into your array of posts.
  • Add a section header view for each post that has the blog avatar and date. Implement the methods, tableView:viewForHeaderInSection: and tableView:heightForHeaderInSection:.
  • Manually construct the view, like this:
let headerView = UIView(frame: CGRect(x: 0, y: 0, width: 320, height: 50))
headerView.backgroundColor = UIColor(white: 1, alpha: 0.9)

let profileView = UIImageView(frame: CGRect(x: 10, y: 10, width: 30, height: 30))
profileView.clipsToBounds = true
profileView.layer.cornerRadius = 15;
profileView.layer.borderColor = UIColor(white: 0.7, alpha: 0.8).CGColor
profileView.layer.borderWidth = 1;

// Set the avatar
profileView.af_setImage(withURL: URL(string: "https://api.tumblr.com/v2/blog/humansofnewyork.tumblr.com/avatar")!)
headerView.addSubview(profileView)

// Add a UILabel for the date here
// Use the section number to get the right URL
// let label = ...

return headerView

Hint This section of our table view guide shows how to change the size of the font. See the UILabel documentation for how to change other properties, such as the text color.

2. Zoomable Photo View

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

  • Upon selecting the photo in the PhotoDetailsView, modally present another view controller called FullScreenPhotoViewController, similar to the photo view in the Facebook app.
    • You won't be able to directly create a segue from the UIImageView in PhotoDetailsViewController to FullScreenPhotoViewController. Instead you'll need to create a segue from PhotoDetailsViewController to FullScreenPhotoViewController and give the segue a unique name.
    • Then you'll want to add a tap gesture recognizer to the imageView (make sure to set userInteractionEnabled to true for the imageView).
    • Finally, you can trigger the transition manually from the action method of your tap gesture recognizer.
  • Place a UIImageView in a UIScrollView and implement the UIScrollViewDelegate to support zooming the photo.
  • Add a close button to dismiss the FullScreenPhotoViewController.

3. Infinite Scrolling

  • Add infinite scrolling to the main photo feed.
  • Be sure to avoid loading more if there is already a request in-flight or you've reached the end of the feed.

☝️ NO submission is required for labs

Flix - Auto Layout

In this exercise, you will add and configure Auto Layout constraints to your Flix Movie app in order to achieve a dynamic layout that looks good on multiple device sizes and orientations.

At the end of the exercise your app will look like this:

Flix Auto Layout|500

Getting Started

  • The checkpoints below should be implemented using the Pair Programming method

Project Setup:

  1. Use your Week 2 Flix project as a staring point.
  2. You'll be using the same GitHub repository you've used for each iteration of your Flix App.
  3. Add the Week 3 Lab - README Template to the bottom of your previous README.

Required User Stories

Milestone 1 - View Debugging Tools

  1. Simulator
    1. Run on different device sizes
    2. Test different orientations (portrait / landscape) using, "command" + left/right arrow
  2. Debug View Hierarchy: Assess behavior of the table view frame on different device sizes
    1. Set a background color on the table view, run on iPhone 7 Plus simulator
      • NOTE: Setting background colors on views while debugging and configuring layouts can be a helpful technique.
    2. Examine the views with the Debug View Hierarchy tool.
      • What do you notice about the table view frame vs. the main view of the view controller?

        Debug View Hierarchy|250
  3. Assistant Editor Preview Mode: Assess behavior of the table view frame on different device sizes
    1. View the Assistant Editor in Preview Mode

      Assistant Editor Preview Mode | 500
    2. Set a background color on the root view of the view controller.
    3. In the Assistant Editor, view the storyboard in Preview mode and add devices: iPhone 7 Plus, iPhone 7 and iPhone SE

      Assistant Editor preview tool|400
  4. What differences and advantages do you find between the debug view Hierarchy tool and the Assistant Editor preview tool? What tasks might each one be best at?

Milestone 2 - Pin Table View Frame

  1. Add constraints to the table view for Left/Right/Top/Bottom constraints.

    • NOTE: "Left" and "right" constraints are also referred to as "leading" and "trailing" respectively.

    Common issue: Pinning to the top of the Top Layout Guide (pictured left) vs. Top of view (pictured right)

    Top Pinned to Top Layout Guide|200 Top Pinned to view|200

  2. Run the app on iPhone 7 Plus, 7 and SE simulators and use your view debug tools to observe the results after adding constraints.

Milestone 3 - Movie Cell Constraints (static height cell)

  1. Configure Poster Image View Constraints
    1. Add edge constraints to pin the poster image view to the top, left and bottom of the cell's content view, all with constants of 8
    2. Add size constraints for width and height to the poster image view
  2. Configure Label Constraints
    1. Set a background color on labels and observe behavior in Assistant Editor preview

      Labels|500
    2. Title Label
      1. Add a leading edge constraint from the title label to the poster view with a constant of 8
      2. Add a trailing edge constraint from the title label to the right side of the cell's content view with a constant of 8
        • NOTE: Un-check "constrain to margins" when adding edge constraints
      3. Select the title label and poster image view and add an alignment constraint for "Top Edges"
    3. Overview Label
      1. Add a top edge constraint from the overview label to the title label with a constant of 8
      2. Add a trailing edge constraint from the title label to the right side of the cell's content view with a constant of 8
      3. Add a bottom edge constraint from the overview label to the bottom edge of the cell's content view with a constant of 8.
        • Edit the bottom edge constraint to make it a >= inequality
      4. Select the overview label and title label and add an alignment constraint for "Leading Edges"
    4. Run the app on a variety of device sizes and test different orientations

      Portrait|250 Landscape|500
    5. Challenge: Adjust the label constraints so the labels hug their text content horizontally but are still prevented from going beyond the cell bounds.

      Portrait|250 Landscape|500

Milestone 5 - Detail View Constraints

  1. Backdrop image view: height = 1/3 of parent
    1. Pin the backdrop image view's leading, top and right edges to it's parent view.
    2. Select backdrop image view and root view of view controller and add "Equal Height" constraints
    3. Edit the "Equal Height" constraint and change it's multiplier to 1:3
      • HINT: Use the "incrementing" arrows on the constraints parameters (such as the multiplier) to see how it affects the views layout.
  2. Poster Image View midline = backdrop image view bottom
    1. Add height and width constraints for the poster image view
    2. Add a leading edge constraint to it's superview with a constant of 16
    3. Select the poser image view and the backdrop image view and add a constraint for aligned "Vertical Centers"
      1. Edit the "Vertical Centers" constraint and change the backdrop item from "Center Y" to "Bottom"
  3. Labels
    1. Use the same technique used in the Movie Cell to configure the labels in the Detail Screen.
      • HINT: Make the overview bottom edge constraint a >= in equality to avoid unnecessary vertical stretching.

        Detail Portrait|250 Detail Landscape|500

Optional and Stretch User Stories

Milestone 4 - Dynamic Height Cells

You will need bottom inequality constraints for any views that may be the bottom most view. For instance, in movie with a short overview will have the poster view as the bottom most view and the cell should be expanded to fit that view with any minimum spacing determined by the constant of the inequality constraint, >=. For a long overview, the label will extend down past the bottom of the poster view and should expand the bottom of the cell to accommodate the full size of the label with any minimum spacing set by it's inequality bottom constraint.

  1. Configure inequality constraints for the poster image view and overview label

  2. Set rowHeight and estimatedRowHeight

    ​​​​​​​tableView.rowHeight = UITableViewAutomaticDimension
    ​​​​​​​tableView.estimatedRowHeight = 50
    

    Dynamic Sized Cells|200

Milestone 6 - Collection View Cell

The approach to Auto Layout in the collection view is very similar what we did in the table view cell, the main difference is that collection view cells do not have an automatic resizing property. Any resizing of collection view cells will need to be handled in code using the appropriate collection view datasource, delegate and flowlayout methods. For our use case, we only have a single image view in the cell, so we can just pin the image view on all sides using edge constraints.

  1. Pin the poster image view edges to the respective sides of the cell's content view.

☝️ NO submission is required for labs

MarioKart - Gestures & Animations

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
 
Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
 
Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

Overview

Tired of boring button-centric UI? Wellin 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.
  2. 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.

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

  1. Add the image assets to your project

    1. Download the MarioKart Image Assets: @[[assets/marioKart_assets.zip]]
    2. Drag the app_icon.png to the Assets.xcassets -> AppIcon -> iPhone App @2x
      Image Not Showing Possible Reasons
      • The image file may be corrupted
      • The server hosting the image is unavailable
      • The image path is incorrect
      • The image format is not supported
      Learn More →
    3. Drag the kart_[x]@2x.png images and background@2x.png image you downloaded to your Assets.xcassets folder.

      The @2x in the image file name helps Xcode place the file in the correct resolution slot automatically.

      Image Not Showing Possible Reasons
      • The image file may be corrupted
      • The server hosting the image is unavailable
      • The image path is incorrect
      • The image format is not supported
      Learn More →

  2. Layout your views

    1. Access the Media Library and drag the background image view and all kart image views onto your view controller.

      Access the Media Library by long clicking on the Object Library icon (see gif below) or use the quick key: command + shift + m

    2. Re-size image views and set the content mode as needed: background -> Aspect Fill and karts -> Aspect Fit.

      You can duplicate views by holding option while you click and drag.

      Image Not Showing Possible Reasons
      • The image file may be corrupted
      • The server hosting the image is unavailable
      • The image path is incorrect
      • The image format is not supported
      Learn More →

  3. Add pan gesture recognizers for kart image views.

    1. Access the Object Library and search for a pan gesture recognizer.
    2. 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.
      Image Not Showing Possible Reasons
      • The image file may be corrupted
      • The server hosting the image is unavailable
      • The image path is incorrect
      • The image format is not supported
      Learn More →
  4. Create and connect actions for your pan gesture recognizers.

    Creating an action will trigger a method to be called anytime your gesture recognizer recognizes a gesture.

    1. Create an action by control + draging 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

      ⚠️ Drag from the gesture recognizer listed in the Document Outline, NOT from the image view in the storyboard. (See example below)

      ⚠️ Make sure you set the type to UIPanGestureRecognizer when creating the action. (See example below)

    2. Connect actions from the remaining gesture recognizers by control + draging from from each one to the same function you created in the first action.

      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.

      Image Not Showing Possible Reasons
      • The image file may be corrupted
      • The server hosting the image is unavailable
      • The image path is incorrect
      • The image format is not supported
      Learn More →

  5. Code the logic to move the kart when it's panned.

    1. Access the location property of the pan gesture recognizer.

      ​​​​​​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.
      ^^^

    2. Print the current location returned from the gesture recognizer.

      ​​​​​​print("Location: x: \(location.x), y: \(location.y)")
      

      📲 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.



      Notice how

      1. Panning on any of the karts calls our panning method.
      2. The panning method is called continuously during a panning event.
      3. The kart image view don't move yetwe'll fix that with one line of code in the next step!

      Image Not Showing Possible Reasons
      • The image file may be corrupted
      • The server hosting the image is unavailable
      • The image path is incorrect
      • The image format is not supported
      Learn More →

    3. Access the view of the kart that was panned.

      ​​​​​​let kartView = sender.view!
      

      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.
    4. Set the kart view's position to the current position of the gesture recognizer.

      ​​​​​​kartView.center = location
      

      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.

      📲 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.

      Image Not Showing Possible Reasons
      • The image file may be corrupted
      • The server hosting the image is unavailable
      • The image path is incorrect
      • The image format is not supported
      Learn More →

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.

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

  1. Add pinch gesture recognizers to kart views.

    1. Access the Object Library
    2. Search for pinch gesture recognizer
    3. Drag a pinch gesture recognizer to each kart view.
      Image Not Showing Possible Reasons
      • The image file may be corrupted
      • The server hosting the image is unavailable
      • The image path is incorrect
      • The image format is not supported
      Learn More →

  2. 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.
    2. Name it something like, didPinchKartView
    3. Set the type to: UIPinchGestureRecognizer
    4. 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.

      Image Not Showing Possible Reasons
      • The image file may be corrupted
      • The server hosting the image is unavailable
      • The image path is incorrect
      • The image format is not supported
      Learn More →

  3. Access the scale property of the gesture recognizer that was pinched.

    ​​​let scale = sender.scale
    

    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.

  4. Print the scale value to the console

    ​​​print("scale: \(scale)")
    

    How do you pinch on the simulator?

    • Hold down the option key and you'll see two gray circles appear. Those represent the user's fingers.

    • Move the cursor while continuing to hold the option key until the circles are close together.

    Now, Additionally hold down the shift key and move the two circles over the object you want to pinch.

    • 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.

    📲 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

    • Pinching on any of the karts calls our pinching function.

    • The pinching function is called continuously during a panning event.

    • Wherever the pinching starts corresponds to a scale value of 1

    Image Not Showing Possible Reasons
    • The image file may be corrupted
    • The server hosting the image is unavailable
    • The image path is incorrect
    • The image format is not supported
    Learn More →

  5. Access the view of the kart that was panned.

    ​​​let kartView = sender.view!
    
  6. Adjust the scale of the kart view using the scale from the pinch gesture recognizer.

    ​​​kartView.transform = CGAffineTransform(scaleX: scale, y: scale)
    

    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.

    📲 RUN YOUR APP and pinch to scale your karts up and down!

    Image Not Showing Possible Reasons
    • The image file may be corrupted
    • The server hosting the image is unavailable
    • The image path is incorrect
    • The image format is not supported
    Learn More →

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!

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

  1. Add rotation gesture recognizers to kart views.

    1. Access the Object Library
    2. Search for rotation gesture recognizer
    3. Drag a rotation gesture recognizer to each kart view.

      Image Not Showing Possible Reasons
      • The image file may be corrupted
      • The server hosting the image is unavailable
      • The image path is incorrect
      • The image format is not supported
      Learn More →

  2. 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.
    2. Name it something like, didRotateKartView
    3. Set the type to: UIRotationGestureRecognizer
    4. Connect actions from the remaining rotation gesture recognizers by ctrl + dragging them each to the same action you created for the first.

      Image Not Showing Possible Reasons
      • The image file may be corrupted
      • The server hosting the image is unavailable
      • The image path is incorrect
      • The image format is not supported
      Learn More →

  3. Access the rotation property of the gesture recognizer that was rotated.

    ​​​let rotation = sender.rotation
    

    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.

  4. Print the rotation value to the console

    ​​​print("rotation: \(rotation)")
    

    📲 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.

    • As with all gestures, an actual device is the preferred way to test.

    • 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.

    • 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.

    The rotation values don't seem to be in degrees 🤔 They're notthey're in radian! Silly engineers🤓

    Image Not Showing Possible Reasons
    • The image file may be corrupted
    • The server hosting the image is unavailable
    • The image path is incorrect
    • The image format is not supported
    Learn More →

  5. Access the view of the kart that was panned.

    ​​​let kartView = sender.view!
    
  6. Adjust the rotation of the kart view using the rotation from the pinch gesture recognizer.

    ​​​kartView.transform = CGAffineTransform(rotationAngle: rotation)
    

    Similar to the approach we used to modify the scale, we'll create a new transform using one of CGAffineTransforms 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.

    📲 RUN YOUR APP and rotate your karts around and around!

    Image Not Showing Possible Reasons
    • The image file may be corrupted
    • The server hosting the image is unavailable
    • The image path is incorrect
    • The image format is not supported
    Learn More →

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.

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

  1. Add tap gesture recognizers to kart views.

    1. Access the Object Library
    2. Search for tap gesture recognizer
    3. Drag a tap gesture recognizer to each kart view.

      Image Not Showing Possible Reasons
      • The image file may be corrupted
      • The server hosting the image is unavailable
      • The image path is incorrect
      • The image format is not supported
      Learn More →

  2. 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.
    2. Name it something like, didTapKartView
    3. Set the type to: UITapGestureRecognizer
    4. 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.

      Image Not Showing Possible Reasons
      • The image file may be corrupted
      • The server hosting the image is unavailable
      • The image path is incorrect
      • The image format is not supported
      Learn More →

  3. Set the number of taps required to 2.

    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.

    Image Not Showing Possible Reasons
    • The image file may be corrupted
    • The server hosting the image is unavailable
    • The image path is incorrect
    • The image format is not supported
    Learn More →

  4. Test the tap gesture recognizer.

    ​​​print("Double tap recognized")
    

    📲 RUN YOUR APP and double tap on each kart. You should see the test statement print out in the console.

    Image Not Showing Possible Reasons
    • The image file may be corrupted
    • The server hosting the image is unavailable
    • The image path is incorrect
    • The image format is not supported
    Learn More →

  5. Access the view of the kart that was panned.

    ​​​let kartView = sender.view!
    
  6. Test moving the kart's position

    ​​​kartView.center.x += 50
    

    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.

    📲 RUN YOUR APP and see if the karts move!

    Image Not Showing Possible Reasons
    • The image file may be corrupted
    • The server hosting the image is unavailable
    • The image path is incorrect
    • The image format is not supported
    Learn More →

  7. Animate the movement of the kart's position

    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)
    ​​​UIView.animate(withDuration: 0.8) {
    ​​​   // Closure body
    ​​​   kartView.center.x += 50
    ​​​}
    

    Image Not Showing Possible Reasons
    • The image file may be corrupted
    • The server hosting the image is unavailable
    • The image path is incorrect
    • The image format is not supported
    Learn More →

    📲 RUN YOUR APP and test out your animation!

    Image Not Showing Possible Reasons
    • The image file may be corrupted
    • The server hosting the image is unavailable
    • The image path is incorrect
    • The image format is not supported
    Learn More →

  8. Tune your race animation!

    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.
    2. Change the speed of your kart by adjusting the value of the animation's duration.
    ​​​UIView.animate(withDuration: 0.6) {
    ​​​    kartView.center.x += 400
    ​​​}
    

    📲 RUN YOUR APP and your off to the races! 🏎

    Image Not Showing Possible Reasons
    • The image file may be corrupted
    • The server hosting the image is unavailable
    • The image path is incorrect
    • The image format is not supported
    Learn More →

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 screenthe 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.

    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.
      Image Not Showing Possible Reasons
      • The image file may be corrupted
      • The server hosting the image is unavailable
      • The image path is incorrect
      • The image format is not supported
      Learn More →

    2. Declare variables to store each kart's starting point.

      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.

      ​​​​​​var startingPointKartView0 = CGPoint()
      ​​​​​​var startingPointKartView1 = CGPoint()
      ​​​​​​var startingPointKartView2 = CGPoint()
      
    3. Store each kart's starting point when the app loads.

      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.

      ​​​​​​startingPointKartView0 = kartView0.center
      ​​​​​​startingPointKartView1 = kartView1.center
      ​​​​​​startingPointKartView2 = kartView2.center
      
  2. Add a long press gesture recognizer to the background image view.

    1. Access the Object Library
    2. Search for long press gesture recognizer
    3. Drag a long press gesture recognizer to each kart view.

      Image Not Showing Possible Reasons
      • The image file may be corrupted
      • The server hosting the image is unavailable
      • The image path is incorrect
      • The image format is not supported
      Learn More →

  3. 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.
    2. Name it something like, didLongPressBackground
    3. Set the type to: UILongPressGestureRecognizer

      Image Not Showing Possible Reasons
      • The image file may be corrupted
      • The server hosting the image is unavailable
      • The image path is incorrect
      • The image format is not supported
      Learn More →

  4. Code the logic to reset the kart positions

    ​​​kartView0.center = startingPointKartView0
    ​​​kartView1.center = startingPointKartView1
    ​​​kartView2.center = startingPointKartView2
    

    📲 RUN YOUR APP and see if you can long press to reset your karts.

    Image Not Showing Possible Reasons
    • The image file may be corrupted
    • The server hosting the image is unavailable
    • The image path is incorrect
    • The image format is not supported
    Learn More →

  5. Animate the resetting of karts.

    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

      🛑 Reference to property startingPointKartView0 in closure requires explicit self. to make capture semantics explicitInsert self.

    2. Go ahead and click the Fix button in the error popup window, or add self. before each object yourself.

      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.



      All you need to know is

      Anytime you are working with animations and you get an error instructing you to add self.JUST DO IT! 👟

      Image Not Showing Possible Reasons
      • The image file may be corrupted
      • The server hosting the image is unavailable
      • The image path is incorrect
      • The image format is not supported
      Learn More →

      ​​​​​​UIView.animate(withDuration: 0.8) {
      ​​​​​​      self.kartView0.center = self.startingPointKartView0
      ​​​​​​      self.kartView1.center = self.startingPointKartView1
      ​​​​​​      self.kartView2.center = self.startingPointKartView2
      ​​​​​​}
      

    📲 RUN YOUR APP to see the karts animate as they reset their positions.

    Image Not Showing Possible Reasons
    • The image file may be corrupted
    • The server hosting the image is unavailable
    • The image path is incorrect
    • The image format is not supported
    Learn More →

  6. Reset karts to their unmodified states

    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.

    Image Not Showing Possible Reasons
    • The image file may be corrupted
    • The server hosting the image is unavailable
    • The image path is incorrect
    • The image format is not supported
    Learn More →

    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.

    ​​​self.kartView0.transform = CGAffineTransform.identity
    ​​​self.kartView1.transform = CGAffineTransform.identity
    ​​​self.kartView2.transform = CGAffineTransform.identity
    

    📲 RUN YOUR APP and see if scaled or rotated karts animate back to their unmodified states.

    Image Not Showing Possible Reasons
    • The image file may be corrupted
    • The server hosting the image is unavailable
    • The image path is incorrect
    • The image format is not supported
    Learn More →

  • Tier 2 - (Coming Soon)
    1. User can use pinch and rotation gestures simultaneously.
    2. While panning, karts slightly scale up and back down to simulate being picked up and put back down.
    3. When a user double taps a kart it
      1. Animates backwards slightly before racing off to simulate winding up.
      2. Pops a wheelie by rotating up and back down as it races off.
      3. After finishing racing off the screen, the kart fades back in it's original position.
    4. User can triple tap the background to make all karts on the track zoom (animate) off at different speeds.
  • Tier 3 - (Coming Soon)
    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.
    2. 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.
    3. In the winner card, the winner is shown in an animated gif sequence.
    4. The user can tap or pan the winner card to dismiss the card and return to a reset version of the game.
    5. 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
    2. Adding images from Media Library to Storyboard
  2. Gesture Recognizers
    1. Objects
      • Pan, Pinch, Rotation, Tap, Long Press
    2. Actions
      • Creating gesture actions in IB, working with the sender
    3. Properties
      • Location, rotation, scale
    4. States
      • Began
  3. View Animations
    1. Working with view animation methods.
      • Asynchronous execution
    2. Initial & destination states of animated views.
  4. View Properties
    1. Resizing views in IB
    2. Adjusting view hierarchy in IB
    3. Content Modes
      • Aspect Fit
    4. Transform
      • Rotation, scale, identity
  5. Simulator
    1. Working with gestures

Tier 2-3 Topics

  1. Gesture Recognizers
    1. Properties
      • Translation
    2. States
      • changed, ended
    3. Delegate
      • Setting gesture delegate in IB, delegate methods
  2. View Animations
    1. Working with view animation methods.
      • completion handlers, animation settings
    2. Working with animated gifs
  3. View Properties
    1. View hierarchy
    2. Subviews
  4. Swift
    1. Outlet collections
    2. Iterating through collections
      • Accessing item index while iterating
    3. Generating random numbers

☝️ NO submission is required for labs

Parse Chat

In this lab you will build a chat client using Parse to explore the features of its ORM and backend service. We'll explore how to authenticate a user, design schema, and save and retrieve data from a Parse server.

At the end of the exercise your app will look like this:

Chat|250

Getting Started

Milestone 1. Project Setup

  1. Create a new Xcode project
  2. Create a new project
  3. Add the Parse pod to your project:
    • pod 'Parse'
  4. In this lab, we'll be sharing a Parse Server that has already been created for us. To use this account, the applicationId and server are provided in the code snipped below:
    • In any Swift file that you're using Parse, add import Parse to the top of the file.

    • In the AppDelegate, register Parse in the application:didFinishLaunchingWithOptions: method:

      ​​​​​​​Parse.initialize(with: ParseClientConfiguration(block: { (configuration: ParseMutableClientConfiguration) in
      ​​​​​​​    configuration.applicationId = "CodePath-Parse"
      ​​​​​​​    configuration.server = "http://45.79.67.127:1337/parse"
      ​​​​​​​}))
      

Milestone 2. Create a Login Screen

The Login Screen should allow a new user to signup for the chat app or an existing user to login.

  1. Create a new View Controller (or rename the default one) called LoginViewController.

  2. Add the following views to the login screen:

    • Username and password text fields
      • Create outlets
    • "Login" and "Sign up" buttons
      • Create Actions

    Login view|250

  3. New user can tap "Sign Up" button to sign up

  4. Existing user can tap "Login" button to login

  5. User sees an alert if either username or password field is empty when trying to sign up

    • 💡 Use the isEmpty property of a text field's text to check if it's empty. Use the || operator to require one condition or the other.
    • Guides:
  6. User sees an alert with error description if there is a problem during sign up or login

Milestone 3: Send a Chat Message

The Chat Screen will allow the user to compose and send a message.

  1. Create a new View Controller, "ChatViewController".

  2. Create a new file, "ChatViewController" as a subclass of UIViewController and associate it with the Chat View Controller in storyboard.

  3. Embed the Chat View Controller in a Navigation Controller (Editor -> Embed In -> Navigation Controller) and set the navigation bar title to "Chat".

  4. Create a Modal segue from the Login View Controller to the navigation controller. Once created, select the Segue and in the Attributes inspector (DJ Slider) give the segue an identifier, "loginSegue".
    Create a Modal Segue|500

  5. After a successful sign up or login from the Login View Controller, modally present the Chat View Controller programmatically.

    ​​​​self.performSegue(withIdentifier: "loginSegue", sender: nil)
    
  6. At the top of Chat View Controller, add a text field and a button to compose a new message

    Compose Chat|250

  7. When the user taps the "Send" button, create a new Message of type PFObject and save it to Parse

    1. Use the class name: Message (this is case sensitive).

      ​​​​​​​​let chatMessage = PFObject(className: "Message")
      
    2. Store the text of the text field in a key called text. (Provide a default empty string so message text is never nil)

      ​​​​​​​​chatMessage["text"] = chatMessageField.text ?? ""
      
    3. Call saveInBackground(block:) and print when the message successfully saves or any errors.

      ​​​​​​​​chatMessage.saveInBackground { (success, error) in
      ​​​​​​​​   if success {
      ​​​​​​​​      print("The message was saved!")
      ​​​​​​​​   } else if let error = error {
      ​​​​​​​​      print("Problem saving message: \(error.localizedDescription)")
      ​​​​​​​​   }
      ​​​​​​​​}
      
    4. On successful message save, clear the text from the text chat field.

Milestone 4: View a List of Chat Messages

  1. Setup the a TableView to display the Chat Messages
    1. Add a tableView to the Chat View Controller and a custom cell that will contain each message.
      • For now, the cell will only contain a UILabel (multi-line) for the message.

        Table View Cell|200
    2. Create a new file for the custom cell, "ChatCell" as a subclass of UITableViewCell and associate it with the Chat Cell in storyboard.
    3. Set the "Reuse Identifier" to "ChatCell".
    4. Create an outlet for the table view and set it's delegate property.
    5. Declare the Chat View Controller to be a UITableViewDataSource and conform to the data source protocol by implementing the required methods.
  2. Pull down all the messages from Parse:
    1. Create a refresh function that is run every second.

    2. Query Parse for all messages using the Message class.

    3. You can sort the results in descending order with the createdAt field.

      ​​​​​​​​query.addDescendingOrder("createdAt")
      
    4. Once you have a successful response, save the resulting array, [PFObject] in a property (instance variable) of the Chat View Controller and reload the table view data.

Milestone 5: Automatically Adjust Cell Size to Fit Text

There are several ways to handle chat message text that is to long to fit on a single line. For instance, if we wanted to only support a single line in our label we might set the Line Break property to Truncate Tail or we could set the Autoshrink property and provide a minimum font size. Alternatively, (in the following approach) we can leverage Auto Layout to expand our cell as needed to fit a label that requires multiple lines to fit the text.

  1. Set the Lines property of the label to 0 in the Attributes Inspector. A setting of 0 will allow the label to have as many lines as needed to fit it's text within the space that the label is given.
  2. Add Auto Layout constraints to pin the label at a fixed distance from the edges of the cell on all sides.

    Pin the label with auto layout constraints|500
  3. Configure the table view to auto-resize it's rows height based on Auto Layout constraints within each cell.
    • In viewDidLaod(), configure the following table view properties:

      ​​​​​​​​// Auto size row height based on cell autolayout constraints
      ​​​​​​​​tableView.rowHeight = UITableViewAutomaticDimension
      ​​​​​​​​// Provide an estimated row height. Used for calculating scroll indicator
      ​​​​​​​​tableView.estimatedRowHeight = 50
      

Milestone 6: Associating Users with Messages

  1. When creating a new message, add a key called user and set it to PFUser.current()

  2. Add a username label to the Chat cell to display the chat message author's username.

    Username Label|250

    • Note: You will need to adjust the autolayout constraints and cell layout to accommodate the username label. Try removing the chatTextLabel's top constraint to the cell content view and then creating a new top constraint to the username label. You can then pin the username label's leading, top and trailing constraints to the cell's content view.
  3. When querying for messages, add an additional query parameter, includeKey(_:) on the query to instruct Parse to fetch the related user.

    ​​​​query.includeKey("user")
    
  4. In cellForRow(atIndexPath:), if a chat message has the user property set, set the username label to the user's username. Otherwise

    ​​​​if let user = chatMessage["user"] as? PFUser {
    ​​​​   // User found! update username label with username
    ​​​​   cell.usernameLabel.text = user.username
    ​​​​} else {
    ​​​​   // No user found, set default username
    ​​​​   cell.usernameLabel.text = "🤖"
    ​​​​}
    

Milestone 7: Persist Logged in User

  1. On app launch, if current user is found in cache, user is taken directly to Chat Screen
    1. Select the Chat View Controller in the storyboard canvas and in the Identity Inspector (Driver's License), set the Storyboard ID to ChatViewController.

    2. In the AppDelegate, check if there is a current logged in user.

      • Parse automatically caches the current user on sign up or login. The current user can be accessed using, PFUser.current()

        ​​​​​​​​if let currentUser = PFUser.current() {
        ​​​​​​​​   print("Welcome back \(currentUser.username!) 😀")
        
        ​​​​​​​​   // TODO: Load Chat view controller and set as root view controller
        ​​​​​​​​   ...
        ​​​​​​​​}
        
    3. Programmatically load the Chat View Controller and set as root view controller.

      ​​​​​​​​let storyboard = UIStoryboard(name: "Main", bundle: nil)
      ​​​​​​​​let chatViewController = storyboard.instantiateViewController(withIdentifier: "ChatViewController")
      ​​​​​​​​window?.rootViewController = chatViewController
      

Optional Stories

  1. User sees an activity indicator while waiting for authentication.

  2. User can pull to refresh Chat feed

  3. Add an "Adorable Avatar" for each user by requesting an avatar from the Adorable Avatars API.

    • Pass in the username in the url
    • Install the AlamofireImage Pod and use the af_setImage(withURL:) UIImageView instance method to fetch and set the image at the specified url.
  4. Chat Bubble Style Design

    • Remove table view row outlines in viewDidLoad()

      ​​​​​​​tableView.separatorStyle = .none
      
    • Add a view to serve as your speech bubble. Move the label as a subview of the bubble view and re-do autolayout constraints

      • Set desired color (In code or IB)

      • Configure rounded edges in code.

        ​​​​​​​​​​bubbleView.layer.cornerRadius = 16
        ​​​​​​​​​​bubbleView.clipsToBounds = true
        

    Speech Bubble|200

  5. Expand or contract the cell layout as needed to show the chat message author (user) if it exists

    • You might want to check out UIStackView (iOS9+) for an easy way to hide views in your layout (for the case when there is no username).
    • Toggling a view's hidden property will add or remove it from its contained UIStackView.

☝️ NO submission is required for labs

Photo Map

Today we'll be building a photo map. It will allow the user to take a photo, tag it with a location, and then see a map with all the previously tagged photos.

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

Milestone 1: Setup

  1. Download the starter project. The starter project contains the following view controllers:
    • PhotoMapViewController => This is where you'll add the map in Milestone #2.
    • LocationsViewController => This is already implemented and allows you to search Foursquare for a location that you want to drop a photo.
    • FullImageViewController => This is where you'll add a fullscreen image in Bonus #2.
  2. Fill in the following constants in LocationsViewController to connect to the Foursquare API:
    • CLIENT_ID = QA1L0Z0ZNA2QVEEDHFPQWK0I5F1DE3GPLSNW4BZEBGJXUCFL
    • CLIENT_SECRET = W2AOE1TYC4MHK5SZYOUGX0J3LVRALMPB4CXT3ZH21ZCPUMCU
  3. Add Photo Map README template

Milestone 2: Create the Map View

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

Implement the PhotoMapViewController to display a map of San Francisco with a camera button overlay.

  1. Add the MapKit framework to the Build Phases.
  2. Then import MapKit into PhotoMapViewController.
  3. Add the MKMapView and the UIButton for displaying the camera (asset included in the starter project). In the Storyboard, find the Photo Map View Controller, and drag the map view from the Objects Library and resize it so it fills the screen. Then, drag a button over the map view, and toggle the image property to the "camera" image asset.
  4. Create an outlet for the map view.
  5. Set initial visible region of the map view to San Francisco.
  6. Run the app in the simulator and confirm that you see the map and button.

Milestone 3: Take a Photo

  1. Create a property in your PhotoMapViewController to store your picked image

    ​​​​var pickedImage: UIImage!
    
  2. Pressing the camera button should modally present the camera.

    • Create an action for the camera button and follow the guide to launch the camera (or image picker for the simulator).
    • NOTE: The simulator does not support using the actual camera. Check that the source type is indeed available by using UIImagePickerController.isSourceTypeAvailable(.camera) which will return true if the device supports the camera. See the note here for an example.
  3. When the user has chosen an image, in your delegate method you'll want to:

    1. Assign the picked image to the pickedImage property
    2. Dismiss the modal camera view controller you previously presented.
    3. Launch the LocationsViewController in the completion block of dismissing the camera modal using the segue identifier tagSegue.
    4. Your code should look like this:
    func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
        let originalImage = info[UIImagePickerController.InfoKey.originalImage] as! UIImage
        let editedImage = info[UIImagePickerController.InfoKey.editedImage] as! UIImage

        // Do something with the images (based on your use case)
        pickedImage = editedImage

        // Dismiss UIImagePickerController to go back to your original view controller
        dismiss(animated: true) {
            self.performSegue(withIdentifier: "tagSegue", sender: nil)
        }
    }

Milestone 4: Drop a Pin on the map

After the user selects an image, if you completed Milestone 3, they'll be taken to a view controller where they can pick a location to tag the image with. In this milestone, we'll drop a pin at that location.

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

  1. Add a pin to the map (We won't be actually using our image yet)
    • In the PhotoMapViewController, inside the locationsPickedLocation(controller:latitude:longitude:) method, Add a pin to the map. Note: this function is already in the starter project.
      • You can set the title to the longitude using annotation.title = String(describing: latitude)
      • Notice how we call the addAnnotation method on our mapView instance.

Milestone 5: Add the photo you chose in the annotation view

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

  1. Add a custom image to the annotation view
    1. Add MKMapViewDelegate to your PhotoMapViewController's class declaration. The class declaration should look like:
class PhotoMapViewController: UIViewController, UIImagePickerControllerDelegate, UINavigationControllerDelegate,
LocationsViewControllerDelegate, MKMapViewDelegate
  1. Set the PhotoMapViewController as MapView's delegate. In viewDidLoad, add the following line:
mapView.delegate = self
  1. Implement the mapView:viewForAnnotation delegate method to provide an annotation view. The code below will add your pickedImage to the annotation view.
    func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
        let reuseID = "myAnnotationView"

        var annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: reuseID)
        if (annotationView == nil) {
            annotationView = MKPinAnnotationView(annotation: annotation, reuseIdentifier: reuseID)
            annotationView!.canShowCallout = true
            annotationView!.leftCalloutAccessoryView = UIImageView(frame: CGRect(x:0, y:0, width: 50, height:50))
        }

        let imageView = annotationView?.leftCalloutAccessoryView as! UIImageView
        // Add the image you stored from the image picker
        imageView.image = pickedImage

        return annotationView
    }
  1. Run the app in the simulator. After adding an image at a location, if you tap on the pin, you should see a popup with the photo.

Bonus 1: See Fullscreen Picture

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

  • Tapping on an annotation's callout should push a view controller showing the full-size image.
  • Add a button to the rightCalloutAccessoryView of type UIButtonType.DetailDisclosure
  • Implement the delegate method that gets called when a user taps on the accessory view to launch the FullImageViewController using the segue identifier fullImageSegue.

Bonus 2: Replace the Pin with an Image

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

  • The annotation view should use a custom image to replace the default red pin.
  • Set MKAnnotationView's image property to the appropriate image.