# Origional labs :::warning ☝️ **NO submission is required for labs** ::: ## Tumblr - Part 1 ![Tumblr App example with required features|200](http://i.imgur.com/Xsbgslj.gif) ### 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 pairs--Pair Programming--to 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](https://www.youtube.com/watch?v=9yCn03s5mzI):** 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](http://guides.codepath.org/ios/Creating-Custom-View-Controllers#step-2-create-the-swift-view-controller) named **PhotosViewController**. 1. [Assign the class of the view controller in storyboard](http://guides.codepath.org/ios/Creating-Custom-View-Controllers#step-3-associating-the-view-controller) to `PhotosViewController`. ### Milestone 2: Hook up the Tumblr API 1. Send a test request to the [Tumblr API](https://www.tumblr.com/docs/en/api/v2#posts): 1. Using Google Chrome? Add the [Pretty JSON Viewer](https://chrome.google.com/webstore/detail/json-formatter/bcjindcccaagfpapjjmafapmmgkkhgoa?hl=en) plug-in to your browser makes it easier to visualize the data you get back from the API (in JSON format). 1. In a browser, access the Tumblr posts for the blog [Humans of New York](http://humansofnewyork.tumblr.com/) at https://api.tumblr.com/v2/blog/humansofnewyork.tumblr.com/posts/photo?api_key=Q6vHoaVm5L1u2ZAW1fqv3Jw48gFzYVg9P0vH0VHl3GVy6quoGV 1. 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. 1. 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. <img src="http://i.imgur.com/NS8AJGG.png" width= "800"/> 1. 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. 1. 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. 1. We will initialize it as an empty array so we don't have to worry about it ever being nil later on. ```swift // 1. 2. 3. var posts: [[String: Any]] = [] ``` 1. Create a request to the Tumblr [Photo Posts Endpoint](https://www.tumblr.com/docs/en/api/v2#photo-posts) 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 that--for the purposes of this course--won'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. 1. 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. ```swift // 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() ``` 1. 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. 1. Cast the value returned from each key to it's respective type. ```swift // 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](http://guides.codepath.org/ios/Using-UITableView#basic-table-view) - **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`. 1. 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](http://guides.codepath.org/ios/Using-UITableView#step-1-create-the-custom-cell). - **Note:** Since we are now using a custom cell, inside our `tableView(_:cellForRowAt:)` method we will change `let cell = UITableViewCell()` to... ```swift let cell = tableView.dequeueReusableCell(withIdentifier: "PhotoCell", for: indexPath) as! PhotoCell ``` 1. Setup the [Image View](https://guides.codepath.org/ios/Working-with-UIImageView) 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. 1. 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 ```swift let post = posts[indexPath.row] ``` 1. 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** 1. `post` is a dictionary containing information about the post. We can access the `photos` array of a `post` using a **key** and **subscript syntax**. 1. photos contains an array of dictionaries so we will cast as such. ```swift // 1. // 2. // 3. if let photos = post["photos"] as? [[String: Any]] { // photos is NOT nil, we can use it! // TODO: Get the photo url } ``` 1. 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 1. Get the original size dictionary from the photo 1. Get the url string from the original size dictionary 1. Create a URL using the urlString ```swift // 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) ``` 1. 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](http://guides.codepath.org/ios/CocoaPods#installing-cocoapods)** on your computer now. 1. Navigate to your project using the Terminal and create a podfile by running, `pod init`. 1. Add `pod 'AlamofireImage'` to your `podfile`, this will bring in the AlamofireImage library to your project. 1. 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. 1. 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. ```swift import AlamofireImage ``` 1. call the AlamofireImage method, `af_setImage(withURL:)` on your image view, passing in the url where it will retrieve the image. ```swift cell.photoImageView.af_setImage(withURL: url!) ``` 1. 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. ```swift 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](http://stackoverflow.com/questions/24924966/xcode-6-strange-bug-unknown-class-in-interface-builder-file/24924967#24924967). - Compile Error: "No such module AlamofireImage" - Try cleaning and building your project. <kbd>Command</kbd> + <kbd>Shift</kbd> + <kbd>K</kbd> and <kbd>Command</kbd> + <kbd>B</kbd> ## Tumblr - Part 2 <img src="http://i.imgur.com/iId98BO.gif" width=200 /></br> ### 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](http://guides.codepath.org/ios/Navigation-Controller-Quickstart) and [Navigation Controllers guide](http://guides.codepath.org/ios/Navigation-Controller) 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](http://i.imgur.com/YGMX8eM.png) - Set the custom class of the View Controller to `PhotoDetailsViewController` (similar to what we did for the PhotosViewController above). 2. Implement `PhotoDetailsViewController`: - The view should consist of a single `UIImageView`. - `PhotoDetailsViewController` should have a single public property for the photo Url. 3. Embed `PhotosViewController` inside of a Navigation Controller to allow it to push the details screen onto the nav stack. ![Imgur](http://i.imgur.com/4eF6pT7.png) 4. 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](http://guides.codepath.org/ios/Using-Modal-Transitions#passing-data) 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. <img src="http://i.imgur.com/KTsRrVn.gif" width="200"/></br> - In the real Tumblr app, each photo has a separate section. The reason for that is that [section header views](http://guides.codepath.org/ios/Table-View-Guide#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: ```swift 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](https://guides.codepath.org/ios/Table-View-Guide#responding-to-the-selection-event-at-the-cell-level) of our table view guide shows how to change the size of the font. See the [UILabel documentation](https://developer.apple.com/library/ios/documentation/UIKit/Reference/UILabel_Class/) for how to change other properties, such as the text color. ### 2. Zoomable Photo View <img src="http://i.imgur.com/uJkvJfx.gif" width="200"/></br> - Upon selecting the photo in the PhotoDetailsView, [modally present another view controller](http://guides.codepath.org/ios/Using-Modal-Transitions) 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](http://guides.codepath.org/ios/Using-Gesture-Recognizers) to the imageView (make sure to set `userInteractionEnabled` to true for the imageView). - Finally, you can [trigger the transition manually](http://guides.codepath.org/ios/Using-Modal-Transitions#triggering-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](http://courses.codepath.org/courses/ios_for_designers/pages/using_uiscrollview#heading-zooming-in-scroll-view). - Add a close button to dismiss the FullScreenPhotoViewController. ### 3. Infinite Scrolling - Add [infinite scrolling](https://guides.codepath.org/ios/Table-View-Guide#adding-infinite-scroll) 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. :::warning ☝️ **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](http://i.imgur.com/4fVKkQH.gif) ### 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. 1. You'll be using the same GitHub repository you've used for each iteration of your Flix App. 1. Add the [Week 3 Lab - README Template](/snippets/ios_university/readme_templates/lab_3_readme.md?raw=true) to the bottom of your previous README. ## Required User Stories ### Milestone 1 - View Debugging Tools 1. Simulator 1. Run on different device sizes 1. Test different orientations (portrait / landscape) using, "command" + left/right arrow 1. 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. 1. 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?<br> ![Debug View Hierarchy|250](http://i.imgur.com/tGfuq6t.png)<br> 1. Assistant Editor Preview Mode: Assess behavior of the table view frame on different device sizes 1. View the Assistant Editor in Preview Mode<br> ![Assistant Editor Preview Mode | 500](https://i.imgur.com/ND3ZHRE.gif)<br> 1. Set a background color on the root view of the view controller. 1. In the Assistant Editor, view the storyboard in Preview mode and add devices: iPhone 7 Plus, iPhone 7 and iPhone SE<br> ![Assistant Editor preview tool|400](http://i.imgur.com/bdDu8Ub.png)<br> 1. 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. :::warning **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](http://i.imgur.com/t0nr9VH.png) ![Top Pinned to view|200](http://i.imgur.com/goHpoZE.png) 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 1. Add size constraints for width and height to the poster image view 1. Configure Label Constraints 1. Set a background color on labels and observe behavior in Assistant Editor preview<br> ![Labels|500](http://i.imgur.com/Y0rtoHz.png)<br> 1. Title Label 1. Add a leading edge constraint from the title label to the poster view with a constant of 8 1. 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 1. Select the title label and poster image view and add an alignment constraint for "Top Edges" 1. Overview Label 1. Add a top edge constraint from the overview label to the title label with a constant of 8 1. Add a trailing edge constraint from the title label to the right side of the cell's content view with a constant of 8 1. 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 1. Select the overview label and title label and add an alignment constraint for "Leading Edges" 1. Run the app on a variety of device sizes and test different orientations<br> ![Portrait|250](http://i.imgur.com/FhpUoVb.png) ![Landscape|500](http://i.imgur.com/x7s7dru.png)<br> 1. **Challenge:** Adjust the label constraints so the labels hug their text content horizontally but are still prevented from going beyond the cell bounds.<br> ![Portrait|250](http://i.imgur.com/lCiEawf.png) ![Landscape|500](http://i.imgur.com/MH6YMjt.png)<br> ### 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. 1. Select backdrop image view and root view of view controller and add "Equal Height" constraints 1. 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. 1. Poster Image View midline = backdrop image view bottom 1. Add height and width constraints for the poster image view 1. Add a leading edge constraint to it's superview with a constant of 16 1. 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" 1. 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.<br> ![Detail Portrait|250](http://i.imgur.com/1qgVWjS.png) ![Detail Landscape|500](http://i.imgur.com/jeYnx6Y.png)<br> ## 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 1. Set rowHeight and estimatedRowHeight ```swift tableView.rowHeight = UITableViewAutomaticDimension tableView.estimatedRowHeight = 50 ``` ![Dynamic Sized Cells|200](http://i.imgur.com/kLNYyDc.png) ### 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. :::warning ☝️ **NO submission is required for labs** ::: ## MarioKart - Gestures & Animations <img src="https://i.imgur.com/2rbj1yM.gif" width=150>&nbsp;<img src="https://i.imgur.com/shRDp0C.gif" width=150>&nbsp;<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> - **Tier 2 - (Coming Soon)** 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 - (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. 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 ---- :::warning ☝️ **NO submission is required for labs** ::: ## Parse Chat In this lab you will build a chat client using [Parse](http://docs.parseplatform.org/ios/guide/) 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](http://i.imgur.com/xhiCRdml.png) ### Getting Started - The checkpoints below should be implemented using the Pair Programming method - [Parse Chat README template](/snippets/ios_university/readme_templates/lab_5_readme.md?raw=true) ### Milestone 1. Project Setup 1. Create a new Xcode project 1. Create a new project 1. [Add the Parse pod](http://guides.codepath.org/ios/CocoaPods#adding-a-pod) to your project: - `pod 'Parse'` 1. 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: ```swift 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`. 1. Add the following views to the login screen: - Username and password text fields - Create outlets - "Login" and "Sign up" buttons - Create Actions<br> ![Login view|250](http://i.imgur.com/HQ7esrQ.png)<br> 1. New user can tap "Sign Up" button to sign up - Guides: - [Parse User](http://guides.codepath.org/ios/Building-Data-driven-Apps-with-Parse#parse-user-pfuser) - [User Registration](http://guides.codepath.org/ios/Building-Data-driven-Apps-with-Parse#user-registration) 1. Existing user can tap "Login" button to login - Guides: - [User Login](http://guides.codepath.org/ios/Building-Data-driven-Apps-with-Parse#user-login) 1. 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: - [Alert Controllers](https://guides.codepath.org/ios/Using-UIAlertController) 1. User sees an alert with error description if there is a problem during sign up or login - Guides: - [Alert Controllers](https://guides.codepath.org/ios/Using-UIAlertController) ### 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". 1. Create a new file, "ChatViewController" as a subclass of UIViewController and associate it with the Chat View Controller in storyboard. 1. Embed the Chat View Controller in a Navigation Controller (Editor -> Embed In -> Navigation Controller) and set the navigation bar title to "Chat". 1. 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](http://i.imgur.com/mdriN0x.gif)<br> 1. After a successful sign up or login from the Login View Controller, modally present the Chat View Controller programmatically. ```swift self.performSegue(withIdentifier: "loginSegue", sender: nil) ``` 1. At the top of Chat View Controller, add a text field and a button to compose a new message<br> ![Compose Chat|250](http://i.imgur.com/RIyFAHQ.png)<br> 1. 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). ```swift let chatMessage = PFObject(className: "Message") ``` 1. Store the text of the text field in a key called `text`. (Provide a default empty string so message text is never `nil`) ```swift chatMessage["text"] = chatMessageField.text ?? "" ``` 1. Call `saveInBackground(block:)` and print when the message successfully saves or any errors. ```swift chatMessage.saveInBackground { (success, error) in if success { print("The message was saved!") } else if let error = error { print("Problem saving message: \(error.localizedDescription)") } } ``` 1. 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.<br> ![Table View Cell|200](http://i.imgur.com/G8zPlPS.png)<br> 1. Create a new file for the custom cell, "ChatCell" as a subclass of UITableViewCell and associate it with the Chat Cell in storyboard. 1. Set the "Reuse Identifier" to "ChatCell". 1. Create an outlet for the table view and set it's delegate property. 1. 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](http://guides.codepath.org/ios/Using-Timers). 1. [Query Parse](http://guides.codepath.org/ios/Building-Data-driven-Apps-with-Parse#fetching-data-from-parse-via-pfquery) for all messages using the `Message` class. 1. You can [sort the results](http://guides.codepath.org/ios/Building-Data-driven-Apps-with-Parse#query-constraints-filter-order-group-the-data) in descending order with the `createdAt` field. ```swift query.addDescendingOrder("createdAt") ``` 1. 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. 1. Add Auto Layout constraints to pin the label at a fixed distance from the edges of the cell on all sides.<br> ![Pin the label with auto layout constraints|500](http://i.imgur.com/s0l3qDH.gif)<br> 1. 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: ```swift // 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()` 1. Add a `username` label to the Chat cell to display the chat message author's username.<br> ![Username Label|250](http://i.imgur.com/7JyFSNa.png)<br> - 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. 1. When querying for messages, add an additional query parameter, `includeKey(_:)` on the query to instruct Parse to [fetch the related user](http://docs.parseplatform.org/ios/guide/#relational-queries). ```swift query.includeKey("user") ``` 1. In cellForRow(atIndexPath:), if a chat message has the *user* property set, set the username label to the user's username. Otherwise ```swift 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`. 1. 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()` ```swift if let currentUser = PFUser.current() { print("Welcome back \(currentUser.username!) 😀") // TODO: Load Chat view controller and set as root view controller ... } ``` 1. Programmatically load the Chat View Controller and set as root view controller. ```swift 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. 1. User can pull to refresh Chat feed 1. Add an "Adorable Avatar" for each user by requesting an avatar from the [Adorable Avatars API](https://github.com/adorableio/avatars-api). - Pass in the username in the url - Install the [AlamofireImage](https://github.com/Alamofire/AlamofireImage#cocoapods) Pod and use the `af_setImage(withURL:)` UIImageView instance method to fetch and set the image at the specified url. 1. Chat Bubble Style Design - Remove table view row outlines in viewDidLoad() ```swift 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. ```swift bubbleView.layer.cornerRadius = 16 bubbleView.clipsToBounds = true ``` ![Speech Bubble|200](http://i.imgur.com/AhzpOl9.png)<br> 1. 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](https://developer.apple.com/library/prerelease/ios/documentation/UIKit/Reference/UIStackView_Class_Reference) (iOS9+) for an easy way to hide views in your layout (for the case when there is no username). - Toggling a view's [hidden](https://developer.apple.com/library/ios/documentation/UIKit/Reference/UIView_Class/#//apple_ref/occ/instp/UIView/hidden) property will add or remove it from its contained UIStackView. :::warning ☝️ **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. <img src="http://i.imgur.com/dMQtYhZ.gif" width=250><br> ### Milestone 1: Setup 1. Download the [starter project](https://github.com/codepath/ios_photo_map/blob/master/StarterZips/MapsStarter(Swift5).zip). 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**. 1. Fill in the following constants in `LocationsViewController` to connect to the Foursquare API: - `CLIENT_ID` = QA1L0Z0ZNA2QVEEDHFPQWK0I5F1DE3GPLSNW4BZEBGJXUCFL - `CLIENT_SECRET` = W2AOE1TYC4MHK5SZYOUGX0J3LVRALMPB4CXT3ZH21ZCPUMCU 1. Add [Photo Map README template](/snippets/ios_university/readme_templates/lab_6_readme.md?raw=true) ### Milestone 2: Create the Map View <img src="http://i.imgur.com/tro9qJv.gif" width=200><br> Implement the PhotoMapViewController to display a map of San Francisco with a camera button overlay. 1. [Add the MapKit framework](http://guides.codepath.org/ios/Project-Frameworks#adding-frameworks-to-project) to the Build Phases. 1. Then `import MapKit` into `PhotoMapViewController`. 1. 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. 1. Create an outlet for the map view. 1. [Set initial visible region](http://guides.codepath.org/ios/Using-MapKit#centering-a-mkmapview-at-a-point-with-a-displayed-region) of the map view to San Francisco. 1. 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 ```swift var pickedImage: UIImage! ``` 1. Pressing the camera button should modally [present the camera](http://guides.codepath.org/ios/Camera-Quickstart). - 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](http://guides.codepath.org/ios/Camera-Quickstart#step-2-instantiate-a-uiimagepickercontroller). 1. When the user has chosen an image, in your [delegate method](http://guides.codepath.org/ios/Camera-Quickstart#step-3-implement-the-delegate-method) you'll want to: 1. Assign the picked image to the `pickedImage` property 1. Dismiss the modal camera view controller you previously presented. 1. [Launch the LocationsViewController](http://guides.codepath.org/ios/Using-Modal-Transitions#triggering-the-transition-manually) in the completion block of dismissing the camera modal using the segue identifier `tagSegue`. 1. Your code should look like this: ```swift 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. <img src="http://i.imgur.com/Ih8wIo9.gif" width=200><br> 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](http://guides.codepath.org/ios/Using-MapKit#drop-pins-at-locations). 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](https://developer.apple.com/library/prerelease/ios/documentation/MapKit/Reference/MKMapView_Class/index.html#//apple_ref/occ/instm/MKMapView/addAnnotation:) method on our `mapView` instance. ### Milestone 5: Add the photo you chose in the annotation view <img src="http://i.imgur.com/jsPJ3er.gif" width=200><br> 1. [Add a custom image to the annotation view](http://guides.codepath.org/ios/Using-MapKit#use-custom-images-for-map-annotations) 1. Add `MKMapViewDelegate` to your PhotoMapViewController's class declaration. The class declaration should look like: ```swift class PhotoMapViewController: UIViewController, UIImagePickerControllerDelegate, UINavigationControllerDelegate, LocationsViewControllerDelegate, MKMapViewDelegate ``` 1. Set the PhotoMapViewController as MapView's delegate. In `viewDidLoad`, add the following line: ```swift mapView.delegate = self ``` 1. Implement the [mapView:viewForAnnotation](https://developer.apple.com/library/prerelease/ios/documentation/MapKit/Reference/MKMapView_Class/index.html#//apple_ref/occ/instm/MKMapView/viewForAnnotation:) delegate method to provide an annotation view. The code below will add your `pickedImage` to the annotation view. ```swift 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 <img src="http://i.imgur.com/mbfp9PL.gif" width=200><br> - 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](https://developer.apple.com/library/prerelease/ios/documentation/MapKit/Reference/MKMapViewDelegate_Protocol/index.html#//apple_ref/occ/intfm/MKMapViewDelegate/mapView:annotationView:calloutAccessoryControlTapped:) that gets called when a user taps on the accessory view to [launch the FullImageViewController](https://developer.apple.com/library/ios/documentation/UIKit/Reference/UIViewController_Class/#//apple_ref/occ/instm/UIViewController/performSegueWithIdentifier:sender:) using the segue identifier `fullImageSegue`. ### Bonus 2: Replace the Pin with an Image <img src="http://i.imgur.com/WIwqNtn.gif" width=200><br> - The annotation view should use a custom image to replace the default red pin. - Set MKAnnotationView's image property to the appropriate image.