# Yelpy - Part 1 <img src="https://i.imgur.com/JL1snRo.gif" width=200 /></br> ### Overview In this lab, you'll build the first part of an app that will allow users to view Yelp restaurants. 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 restaurant data from the Yelp 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 Yelp restaurants. ## Let's Get Building <details> <summary style="font-size:1.25em; font-weight: 600"> Milestone 1: Setup Project + Storyboard </summary> #### 0. Download the Yelpy Starter Lab: @[[assets/yelpy_starter_1.zip]], and then double-click the `Yelpy.xcworkspace` file to open the project in Xcode. #### 1. Add TableView to our `Main.storyboard` - From the `+` objects library button, search and add the tableView to your storyboard: - ==NOTE:== You can reference **steps 1-4** of the [Basic Table View Guide](http://guides.codepath.org/ios/Using-UITableView#basic-table-view) ![Add TableView](https://i.imgur.com/iMtrlzo.gif) - Connect your tableView outlet to your `RestaurantsViewController` file - Open the your `RestaurantsViewController` file by holding the <kbd>alt/option</kbd> key and click on the file - Hold the <kbd>control</kbd> key, then drag and drop the tableView to your View controller: ![Connect tableView outlet](https://i.imgur.com/OXU5uJO.gif) #### 2. Add a Cell to our tableView - Add a `tableViewCell` from `+` objects library - [**Reference:** Creating a Custom Cell Guide](http://guides.codepath.org/ios/Using-UITableView#step-1-create-the-custom-cell) - Inside the cell, add a `label` and an `Image View` as well: ![Add label and Image view](https://i.imgur.com/PMCT5NB.gif) - Create `RestaurantCell` file under the `Cells` folder - Configure cell's Class and Identifier on the 'Identity Inspector' + 'Attribute Inspector' to `RestaurantCell`: ![Cell attributes](https://i.imgur.com/3dYeMwc.gif) - Create cell's label + image outlets on the `RestaurantCell` file: ![Cell Outlets](https://i.imgur.com/XKtmZVz.gif) </details> <br> <details> <summary style="font-size:1.25em; font-weight: 600"> Milestone 2: Hook up the Yelp API </summary> On the lab starter, take a look at the `exampleAPI.json` file to get yourself familiarized with the JSON format of the response data from the API. **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 are the next steps: :::warning :bulb: However, you can learn more about networks and APIs in our [course guides](https://guides.codepath.com/ios#networking-and-models) and in this [in-depth slide](https://docs.google.com/presentation/d/1XNZFyECpxZNzfCI-tXfGVxjaSRZQ3PmI9IKz0qJ2a_g/edit?usp=sharing) deck about API requests in Swift. ::: #### 1. [Create a Yelp Account](https://www.yelp.com/developers/v3/manage_app) and generate an API Key - Fill out the form to create a Yelp App to obtain an API Key. Once filled out you should your API key would look something like this: ![API Key](https://i.imgur.com/nY1Ale5.png) - Once the API key is generated, your `Network` folder and copy/paste your API key on the `API.swift`. - Next, lets add the logic to our API request: - Under the "TODO" section, type this: ![Getting API Data](https://i.imgur.com/yCZoGfd.png) Here is the flow of the code: - Traverse the data in JSON Format and convert it to a dictionary. - From the dictionary, the "businesses" value is an array of businesses, so we convert it to an array of dictionaries that represent the data of each restaurant - Return the array of dictionaries representing the restaurants - "What happens in closures, stay in closures." In order to get the data inside the closure, we use the @escaping method using the variable `completion` to return it. :::warning **Closures** are basically functions within a function. Make sure to review the [**Swift fundamentals**](https://guides.codepath.com/ios/Understanding-Swift#closures) to understand them! :bulb: Tip: If you are having trouble understanding it at first glance, don't worry! No one really understands code at first glance. It takes a while to fully get the flow of how code works. But be sure to always ask questions! ::: #### 2. Connect `RestaurantsViewController` to API data - Initialize your `restaurantsArray` on your `RestaurantsViewController` file. It should look something like this: ```Swift var restaurantsArray: [[String:Any?]] = [] ``` ![Initialize restaurantsArray](https://i.imgur.com/edjBpxl.jpg) The `restaurantsArray` is our placeholder for storing all the data we get from the API request - Create a function `getAPIData()` that retrieves the data from the API and stores the data in our `restaurantsArray` variable: ![getAPIData() Function](https://i.imgur.com/fFxsWND.png) </details> <br> <details> <summary style="font-size:1.25em; font-weight: 600"> Milestone 3: Build the basic Restaurants Feed </summary> #### 1. Configure tableView - Add the tableView.delegate + tableView.dataSource on `viewDidLoad()`: ```Swift // ––––– TODO: Add tableView datasource + delegate override func viewDidLoad() { super.viewDidLoad() tableView.delegate = self tableView.dataSource = self } ``` - Add the protocols for `UITableViewDelegate` and `UITableViewDataSource` to your `RestaurantsViewController` class: ```Swift class RestaurantsViewController: UIViewController, UITableViewDelegate, UITableViewDataSource { ... } ``` - An error should pop up. In that error, click on "fix" to automatically add the protocol stubs and place them at the bottom of all your code: ![Protocol Stubs](https://i.imgur.com/rShwj0M.jpg) - **==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 restaurants. We can get that number by calling the `count` method on our `restaurantsArray` array. So, instead of returning a hard coded number like `5` we will want to `return restaurantsArray.count`. This is where you can get into trouble if `restaurantsArray` contains `nil` which is why we initialized `restaurantsArray` as an empty array because although empty, it is not `nil`. #### 2. Configure tableView protocol stubs for cells - Configure `numberOfRowsInSection` protocol stub: ![num of Rows](https://i.imgur.com/kaZOQG4.png) - Configure `cellForRowAt` protocol stub: ![Cell forRowAt](https://i.imgur.com/vJuEfGK.png) ### ^^ What's happening here? - **References:** [Setting up 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 **RestaurantCell** class and not your RestaurantsViewController 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. - **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. - In the `tableView(_:cellForRowAt:)` method, pull out a single `restaurant` from our `restaurantsArray` array ```swift let restaurant = restaurantsArray[indexPath.row] ``` - Getting the image from the restaurant dictionary: 1. It's possible that we may get a `nil` value for an element in the `restaurantArray`, i.e. maybe no images exist for a given restaurant. We can check to make sure it is **not** `nil` before unwrapping. We can check using a shorthand swift syntax called **if let** 1. `restaurant` is a dictionary containing information about the restaurant. We can access the `restaurantArray` array of a `restaurant` using a **key** and **subscript syntax**. - Implementation to getting the image: - 💡 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 image url string from the restaurant dictionary 1. Get the convert url string –> url 1. set image using the image url with AlamofireImage ```swift // 1. if let imageUrlString = restaurant["image_url"] as? String { // 2. let imageUrl = URL(string: imageUrlString) // 3. cell.restaurantImage.af.setImage(withURL: imageUrl!) } ``` - Set the image view 1. We'll be bringing in a 3rd party library to help us display our restaurant 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.restaurantImage.af.setImage(withURL: imageUrl!) ``` #### 3. Last Step! Update the table view to display any new information - Our table view will likely be created before we get our 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 **getAPIData()** function, right after we load the data we got back into our `restaurantsArray` property. ```swift // ––––– TODO: Get data from API helper and retrieve restaurants func getAPIData() { API.getRestaurants() { (restaurants) in guard let restaurants = restaurants else { return } self.restaurantsArray = restaurants self.tableView.reloadData() // reload data! } } ``` - Call our `getAPIData()` from `viewDidLoad()` ``` Swift override func viewDidLoad() { ... getAPIData() } ``` 💡 **Tip:** Are your images looking like squished hamburgers like this? - <img src="https://i.imgur.com/tDOjRsI.png" height=100> #### Yes! How do I fix this? Inside your `viewDidLoad()` function, configure your tableView rowHeight property. Something like this, depending on your liking: ```swift override func viewDidLoad() { ... tableView.rowHeight = 150 } ``` :::success ### :tada: Congrats! You have finished the first part of Yelpy! ::: </details> <br> ## Bonus - [ ] Make your Yelpy app look like the one shown in the GIF introduction of the lab! - **Note:** You can download the star ratings images from Yelp [**here**](https://www.yelp.com/developers/display_requirements) ## Gotchas - If your app crashes with the exception: `Unknown class RestaurantsViewController 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" - Make sure you are in the `.xcworkspace` file - Try cleaning and building your project. <kbd>Command</kbd> + <kbd>Shift</kbd> + <kbd>K</kbd> and <kbd>Command</kbd> + <kbd>B</kbd> :::warning ☝️ **NO submission is required for labs** ::: # Yelpy - Part 2 <img src="https://i.imgur.com/SEyigmC.gif" width=200 /><br> ### Overview Extend your Yelpy 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 Download the Lab Starter below (we are working on the previous unit's lab): #### @[[assets/yelpy_starter_2.zip]] :::danger :warning: **Please do not move forward before creating your Yelp API Key for the project!** ::: #### How to get Yelp API Key 1. [Create a Yelp Account](https://www.yelp.com/developers/v3/manage_app) and generate an API Key - Fill out the form to create a Yelp App to obtain an API Key. Once filled out you should your API key would look something like this: ![API Key](https://i.imgur.com/nY1Ale5.png) - Once the API key is generated, go to your `Network` folder and copy/paste your API key on the `API.swift`. ## Milestone 1: Build the Restaurant Class ### Step 1: Create Restaurant Class File - Create a new Swift file under `Models` folder called `Restaurant.swift` ![Create Restaurant File](https://i.imgur.com/8BndnQv.gif) <br> ### Step 2: Configure Restaurant Class - Create variables + initializer method for the `Restaurant` class. It should look something like this: :::warning :bulb: Before viewing the code below, make sure to review your [Swift Fundamentals](https://guides.codepath.org/ios/Understanding-Swift#classes) about classes! ::: ![Variables + Initializer](https://i.imgur.com/tRJPUCt.png) ## Milestone 2: Set up Cell Properties using Restaurant Object ### Step 1: Configure Cell properties with didSet - On your `RestaurantCell.swift` file, lets configure the labels inside that file. It should like this: ![Didset](https://i.imgur.com/hKg1Zea.png) <br> :::warning :bulb: **Further readings:** In this section, we are implementing property observers, which allows the cells to update its properties more efficiently. Read more about [**Property Observers**](https://guides.codepath.org/ios/Understanding-Swift#property-observers). ::: ### Step 2: Configure API for Restaurant Array In our `Networks/API` file, change the code to return an array of restaurants`[Restaurant]`: - First, update the escaping method to return `[Restaurant]?` instead of `[[String:Any]]?` ```Swift // Update escaping method to [Restaurant]? static func getRestaurants(completion: @escaping ([Restaurant]?) -> Void) { ... } ``` - Next, update the code to convert the JSON Dictionary data to an array of Restaurants: ```Swift let dataDictionary = try! JSONSerialization.jsonObject(with: data, options: []) as! [String: Any] // Get array of restaurant dictionaries let restDictionaries = dataDictionary["businesses"] as! [[String: Any]] // Variable to store array of Restaurants var restaurants: [Restaurant] = [] // Use each restaurant dictionary to initialize Restaurant object for dictionary in restDictionaries { let restaurant = Restaurant.init(dict: dictionary) restaurants.append(restaurant) // add to restaurants array } return completion(restaurants) ``` :warning: In our `RestaurantsViewController`, make sure to update the variable `restaurantsArray` as well to an array of restaurants `[Restaurant]`. ### Setp 3: Configure Protocol Stub for cellForRowAt Remember this code? ![Old Code](https://i.imgur.com/wXYdGFV.png) We will be refactoring all this to **4 lines!** - Set the cell's `r` variable to the restaurant object from indexPath. Your refactored protocol stub should look something like this: ![New Code](https://i.imgur.com/CwfGDSQ.png) Run your app to see if it works before moving on to the next step! ## Milestone 3: 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. **Step 1 –** Create a new View Controller for the details screen(`RestaurantDetailViewController`): - In our `Controllers` folder, 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: ![CreateVC](https://i.imgur.com/pvij3Xi.gif) - Set the custom class of the View Controller to `RestaurantDetailViewController` (similar to what we did for the PhotosViewController above): ![Set VC Class](https://i.imgur.com/H4iWrIm.gif) **Step 2 -** Implement `RestaurantDetailViewController`: - The view should consist of a single `UIImageView`. - Your **Detail View Controller** should have a single public property for the photo Url. **Step 3 –** Embed the **Details View Controller** inside of a Navigation Controller to allow it to push the details screen onto the nav stack. ![Embed Navigation](https://i.imgur.com/ZgcMR2O.jpg) **Step 4 –** Make the cell take you to the RestaurantDetailViewController when tapped on a restaurant cell - In the storyboard, *ctrl-drag* from the cell to the RestaurantDetailViewController and choose "Show". ![Segue](https://i.imgur.com/cwfOEjy.gif) - In `RestaurantsViewController`, implement the `prepareForSegue` method to pass the restaurant object to the details screen under the tableView protocol stubs. Your code should should like this on the view controller: ![Override Segue](https://i.imgur.com/ABhGVYB.png) - On your `RestaurantDetailViewController`, setup the image using AlamoFireImage and the restaurant object: ![Set image from segue](https://i.imgur.com/ACcG168.png) ## 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. Implement SearchBar for your Restaurants - Follow this guide to learn implement search bar into your application: [SearchBar Guide](https://guides.codepath.org/ios/Search-Bar-Guide) <img src="https://i.imgur.com/NX1OS0j.gif" width="200"/><br> ### 2. 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. # Lab 3 - Auto Layout ## Overview This week we will be covering Auto Layout. This is a super important topic as we need out apps to work with a variety of screen sizes and orientations. This is a topic that will follow you thought your mobile career. ### Required User Stories We want our app to run on many devices so we need to make our UI adaptive in two ways. :::info - Orientation - Screen Size ::: We will do this by adding constraints and StackViews to our main storyboard. ## Intro to AutoLayout Auto Layout allows you to specify how your app looks based on the view and the orientation. Many of you have played around with this and it can be very intimidating. If you have done web development you can think of Auto Layout as a responsive design. Or if you have done Android development you will most likely have been exposed to Auto Layout because of the many devices. The goal of AutoLayout is to make the app look as intended depending on the size and orientation. Today we have many different iPhone iPad and watch sizes and we need to account for them. :::warning :heavy_check_mark: Check it out :exclamation: [Apple Documentation](https://developer.apple.com/library/archive/documentation/UserExperience/Conceptual/AutolayoutPG/index.html) [Outside Resource](https://www.twilio.com/blog/2018/05/xcode-auto-layout-swift-ios.html) [Outside Resource](https://www.appcoda.com/learnswift/auto-layout-intro.html) [Outside Video Resource](https://www.youtube.com/watch?v=27TFuaOpUsE&list=PL23Revp-82LI-MTPyLtvzTCDl-vJKwjlU) ::: ### How it works If you have taken algorithm classes then you may have heard of [***dynamic programming***](https://en.wikipedia.org/wiki/Dynamic_programming). If here is a quick overview, Dynamic Programming is trying to find the best solution given a list of constraints. Auto Layout uses this idea and tries to estimate the best placement of views based on the orientation and size of the device given the constraints we provide. Before we begin the lab, let's discuss different ways that AutoLayout can be implemented in Xcode... ### Implementing AutoLayout #### There are 4 ways to use AutoLayout **1 - Code** :face_with_head_bandage: This is a great way to handle AutoLayout if you are working in a large team or want full control of your views. Although this is more difficult to master it's worth taking a look into because it is still used in some large projects to avoid merge conflicts. To do this we have a few options: :::info - Creating instances of constraints(NSLayoutConstraint) and adding them to the view - Visual Format Code which is a micro language that is inserted into strings and passed into views - Creating constraints using the Interface Builder *NOTE: We will use this to make minor changes* ::: ```Swift // Get the superview's layout let margins = view.layoutMarginsGuide NSLayoutConstraint(item: myView, attribute: .leading, relatedBy: .equal, toItem: view, attribute: .leadingMargin, multiplier: 1.0, constant: 0.0).isActive = true func addConstraints() { //Collect Views to apply VFL let buttonsDictionary = ["button1": flagButton1, "button2": flagButton2, "button3": flagButton3] //Metrics establish Fixed Constants let metrics = ["topSpacing": 80, "bottomSpacing": 20, "buttonHeight": 20, "buttonSpacing": 20] //Note that priorities can be set using @. 1000 for Required. < 100 for Optional. Example: @999 //Horizontal constraints for buttonName in buttonsDictionary.keys { view.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("H:|-[\(buttonName)]-|", options: .allZeros, metrics: nil, views: buttonsDictionary)) } //Vertical constraints view.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:|-(==topSpacing)-[button1(>=buttonHeight@997)]-(==buttonSpacing@999)-[button2(==button1)]-(==buttonSpacing@999)-[button3(==button1)]-(>=bottomSpacing@998)-|", options: .allZeros, metrics: metrics, views: buttonsDictionary)) } ``` :::warning [Apple constraint example](https://developer.apple.com/library/archive/documentation/UserExperience/Conceptual/AutolayoutPG/ProgrammaticallyCreatingConstraints.html) ::: **2 - Xib Files** This was the standard before storyboards. They are much simpler to work with than storyboards because they only have one scene. It is good to read up and get some practice with Xib files because some apps still use them and are easy to use when reusing a view controller. :::warning [Outside Resource: What is a XIB?](https://medium.com/@tjcarney89/whats-a-xib-and-why-would-i-ever-use-one-58d608cd5e9b) [Outside Resource: Creating a XIB](https://medium.com/better-programming/swift-3-creating-a-custom-view-from-a-xib-ecdfe5b3a960) ::: **3 - Constraints Through StoryBoards** 🧐 This is the current standard and what you may have been using in our class we will use this method along with StackViews. They allow us to layout multiple views and scenes. Because storyboards are static meaning they don't change with the screen we need to add constraints to elements so they can be orientated correctly. :::warning [Apple Anatomy of a Constraint](https://developer.apple.com/library/archive/documentation/UserExperience/Conceptual/AutolayoutPG/AnatomyofaConstraint.html) [Apple Constraints in Interface Builder](https://developer.apple.com/library/archive/documentation/UserExperience/Conceptual/AutolayoutPG/WorkingwithConstraintsinInterfaceBuidler.html) ::: <img src="https://i.imgur.com/FIwF8I8.png"/> <br><br> **4 - Stack Views** :+1: :yum: This is the easiest way to implement AutoLayout. We will use this method first and then use the Interface Builder to help us get the look we want. **What is a StackView?** A StackView is a view that contains sub-views, and they allow us to all the views it contains. A StackView can organize views vertically and horizontally. :::info :bulb: *Tips:* - Use StackViews first then add constraints later - Work from the inside out. Focus on one area and work your way out. ::: **Properties of Views:** :::warning [**Outside Resource: View Properties**](https://medium.com/@abhimuralidharan/ios-content-hugging-and--compression-resistance-priorities-476fb5828ef) ::: - **Intrinsic Content Size:** The default size of the content displayed. Labels with different text lengths have different sizes - **Compression:** Determines how the view will behave and how likely it is to change when the display size decreases. - **Hugging:** Determines how the view resists being changed :::info [Outside Resource Hugging](https://medium.com/@abhimuralidharan/ios-content-hugging-and-content-compression-resistance-priorities-476fb5828ef) ::: **Properties of StackViews:** :::warning [**Apple StackView Properties**](https://developer.apple.com/documentation/uikit/uistackview) ::: - **Axis:** This Property allows the StackView to arrange the views vertically or horizontally. The default is horizontal, but this can be changed based on our needs. This property can be animated and changed in runtime. - **Spacing** Determines the spacing between the horizontal views this can be a positive or negative value. *Note:* Spacing takes priority over the intrinsic content size, meaning this it will shrink the view before the spacing. - **Distribution:** This allows you to control how the views behave on the axis - [***Learn The different properties of Distribution***](https://developer.apple.com/documentation/uikit/uistackview/distribution) - **Alignment:** Determines how the vies will take up vertical space. ## Project Setup - The checkpoints below should be implemented using the Pair Programming method - Use your Week 2 Yelpy project as a starting point. - You'll be using the same GitHub repository you've used for each iteration of your Yelpy App. Here is a project starter if you don't have the lab from last week :::success **@[[assets/yelpy_starter_3.zip]]** ::: ## Implementing Auto Layout <img src="https://i.imgur.com/M5fq70K.png" height=400/> <br> ### Step 1: How do you want it to look? 🤔 Determine the UI and layout of the subviews - How do you want the layout to look? - How do you want the app to look with different screen sizes? - What orientation do you want the app to be in? <img src="https://i.imgur.com/GRPnh0f.png" height=400/> ### Step 2: Configure the StackView Place the items into the StackView. It is best to start small work from then inside out. Place the StarsImage and the ReviewsLabel into a StackView by command clicking each element and then going to the embed in button and select stack view. :::info ***Note:*** Based on where the items are placed on the storyboard Xcode will estimate whether to put them into a vertical or horizontal StackView. You can always change it later. ::: ![Constraints](https://i.imgur.com/Ud6PRqv.gif) Description: Here is a gif showing how you add elements into a StackView <img src="https://imgur.com/ycsgpyq.png" width="200" /> <br> <br> Description: See how the Reviews label and the Stars Image changed size :::info ***Note***: Why did this change our item sizes?* They were changed back to there Intrinsic Content Size and the were distributed to the available empty space. We can fix this later. ::: ### Step 3: Continue Using StackViews when possible Now we can build out and embed all of our elements into StackViews. - Add more StackViews one for the - One containing the Resturant Image - One containing the Name Label, Category Label, and Phone label - One containing both StackViews <img src="https://i.imgur.com/Pqv6E5i.png"/> ### Step 4: Add constraints Now we have all of our stack Views we need to add constraints. --- - Remember that we need 4 constraints (2 Horizontal and 2 Vertical) :::warning :bulb:***Note:**** If out item has an intrinsic content size then we only need 2 (1 Horizontal and 1 Vertical) ::: To add a constraint click the item and control drag it. <img src="https://i.imgur.com/i1GtRbs.gif" /> ### Step 5: Making Minor Changes :mag: Play with the stack views and constraints until you have the desired look. Try clicking on the StackView and changing the Axis, Alignment, Distribution, and spacing until you have the look you want. ### Resources :books: This topic can be intimidating and can be frustrating, we recommend that you create a view with a few buttons and labels to get the hang of it. Here are some additional items that can help you. :::warning - [Apple AutoLayout Documentation](https://developer.apple.com/library/archive/documentation/UserExperience/Conceptual/AutolayoutPG/index.html#//apple_ref/doc/uid/TP40010853-CH7-SW1) - [Apple StackView Documentation](https://developer.apple.com/documentation/uikit/uistackview) - [Apple Constraints Documentation](https://developer.apple.com/library/archive/documentation/UserExperience/Conceptual/AutolayoutPG/WorkingwithConstraintsinInterfaceBuidler.html) - [CodePath Guide](https://guides.codepath.com/ios/Auto-Layout-Basics) ::: ### BONUS Try the app on a horizontal view. Figure put what happened to our AutoLayout. :::info It may have something to do with your constraints 🧐 ::: :::warning ☝️ **NO submission is required for labs** ::: # Lab 4 Animations ### Core Reading :::warning - [Animation](https://guides.codepath.org/ios/Animation) - [Using Gesture Recognizers](https://guides.codepath.org/ios/Using-Gesture-Recognizers) - [Moving and Transforming Views with Gestures](https://guides.codepath.org/ios/Moving-and-Transforming-Views-with-Gestures) - [Gesture Recognizer Overview](https://developer.apple.com/library/ios/documentation/EventHandling/Conceptual/EventHandlingiPhoneOS/GestureRecognizer_basics/GestureRecognizer_basics.html) ::: ### Why Animation? Open up an app and you are likely to see an animation either in the launch screen or when you swipe to refresh. Using animations in key areas like fetching data tells the user that things are happening therefore encouraging the user to wait. It also generally makes your app look professional. In the past, it took graphic designer skills to create animations and while that is still true for custom animations we now have a tool called [Lottie](https://airbnb.io/lottie/#/) that allows us to use professional animations in our apps. :::warning Here is a [**link**](https://medium.com/flawless-app-stories/animations-in-ios-30-beautiful-examples-80cb2663c559) with some examples of loading animations ::: ### Project Setup - The checkpoints below should be implemented using the Pair Programming method - Use your Week 3 Yelpy project as a starting point. - You'll be using the same GitHub repository you've used for each iteration of your Yelpy App. Here is a project starter if you don't have the lab from last week :::success **@[[assets/yelpy_starter_4.zip]]** ::: ### Lottie :::warning - Lottie iOS [Documentation ](http://airbnb.io/lottie/#/ios) - Lottie [Github](https://github.com/airbnb/lottie-ios) - Lottie[ tutorial](https://lottiefiles.com/blog/working-with-lottie/how-to-add-lottie-animation-ios-app-swift) ::: <img src="https://imgur.com/LQnFAG1.gif" /> ### 1. Install Lottie ```swift pod 'lottie-ios' pod install ``` For every file you use Lottie ```swift import Lottie ``` ### 2. Find the animation you want - Once you have selected the animation that you want download it and drag it into your project. :::info ***Note*** Download the animation as **JSON** file and drag them and drop them into your project. *I chose a [**food carousel**](https://lottiefiles.com/4762-food-carousel) animation that went well with the theme of yelp* ::: <img src="https://i.imgur.com/tS1JYOQ.png" /> ### 3. Calling the Animation :::warning Reference the documentation [Documentation](https://airbnb.io/lottie/#/ios) ::: To call the animation in General ```swift // create a AnimationView var animationView: AnimationView? override func viewDidLoad() { super.viewDidLoad() animationView = .init(name: "animationName") animationView?.frame = view.bounds animationView?.play() } ``` For this lab we are going to create a global variable so we can call the animation ```swift var animationView: AnimationView? ``` Because we will want to call animations multiple times during the program lets write a function called Start animation **What this function needs to do:** 1. Set the size of the frame 2. Set the animation loop 3. Set the animation speed 4. Start the animation <img src="https://imgur.com/UhxBkvm.png" /> <br><br> **We also need a stop function that needs:** 1. Stop the animation 2. Remove the animation overlay <img src="https://imgur.com/6aZ5sn6.png" /> <br><br> :::warning 🧐 Not working? :bulb: Did you call your start and stop functions? :anguished: Still not working? :bulb: Think about adding a time delay to your stop function ::: <img src="https://imgur.com/z8lKxMu.gif" width=250>&nbsp;<img src="https://imgur.com/oAHqfin.gif" width=250> :::info You can resize you animation any way you want take some time to play around with it ::: :::success :smile: You have completed this lab if you want to add extra style try adding in a Skeleton View view and a pull to refresh :smiley: ::: --- ## Skeleton View Want to take your app to the next Level? Skeleton View is a way to show the users that your page is loading and are fetching the data. We will implement one to display while the animation is playing. :::warning * Here is the [Documentation](https://github.com/Juanpe/SkeletonView) * Using a Skeleton View in a UITableView: [Link](https://medium.com/flawless-app-stories/skeletonview-animation-ade973655d03) * Another Guide on how to use a Skeleton View: [Link](https://medium.com/the-aesthetic-programmer/facebook-loading-labels-animation-simple-approach-for-skeleton-view-in-swift-4-4fcdfeffd121) ::: <img src="https://github.com/Juanpe/SkeletonView/blob/develop/Assets/tableview_scheme.png?raw=true" /> ### 1. Instal and import Skeleton View ```swift pod 'SkeletonView' pod install //For files that you want to use SkelitonView: import SkeletonView ``` ### 2. Call SkeletonView In general you will use: ```swift // This shows a Solid SkeletonViewview.showSkeleton() //Hide view.hideSkeleton() ``` Since we are working with table views we need to call the SkeletonTableViewDataSource instead of UITableViewDelegate, UITableViewDataSource and add in a function called collectionSkelitionView <img src="https://imgur.com/6WFYhUt.png" /> <br><br> :::info Note: because we removed UITableViewDelegate from our extension RestaurantsViewController we need to add UITableViewDelegate to our class UITableViewDelegate ::: Now that we have our TableView set we need to call the Skeleton View in our startAnimations function and hide it in the stop Animations function ### 3. Set the view to Skeletonable The last step is to go into each view and set the elements to skeletonable = on <img src='https://imgur.com/uhP6oVM.png' /> ### 4. Making sure the SkeletonView is called in the tableView Cell Now that we have turned everything on let's make sure the tableView is set to show the SkeletonView. Currently, it is only being applied outside the tableView ie. the search-bar. to do this create a global var refresh. This will be used to determine when the tableView is being refreshed. ```swift var refresh = true ``` Next inside the tableView function we will add an if statement that that will <img src='https://imgur.com/R4zmoRm.png'/> <br><br> Finally we need to set refresh to false at the end of the stopAnimations function. ```swift refresh = false ``` :::success :+1: :smile: You have completed THE SkeletonView :smiley: :+1: ::: <img src='https://imgur.com/EJGYjhl.gif'/> ### 4. Clean Up Now we have finished our code is a little sloppy and if we want to include a pull to refresh or call the animations in different parts of the program its best to refactor out code to separate the SkeletonView from the tableView to do this we will recreate a RestaurantsViewController class with a UITableViewDelegate, and a UITableViewDataSource. once we do that we will copy all of the table view functions into it. <img src='https://imgur.com/QpYhFub.png'/> <img src='https://imgur.com/WyUZpXe.png'/> Finally don't forget to remove the UITableViewDataSource from our RestaurantsViewController class <img src='https://imgur.com/DTJI583.png'/> ### Resources :book: :::warning Here are some helpful resources - [Documentation](https://github.com/Juanpe/SkeletonView) - [Hacking with Swift](https://www.hackingwithswift.com/articles/40/skeletonview-makes-loading-content-beautiful) - [Medium Article 1](https://medium.com/@osamakhan_92979/skeletonview-animation-in-swift-uitableview-d5508f2a736c) - [Medium Article 2](https://medium.com/the-aesthetic-programmer/facebook-loading-labels-animation-simple-approach-for-skeleton-view-in-swift-4-4fcdfeffd121) - [Cocoapods Article](https://cocoapods.org/pods/SkeletonView) ::: --- ## Bonus: Pull To Refresh: Now that we have done some animations our users may want to get updates data. Previously we only have the data refreshing on the startup. :::warning [UIRefreshControl Guide](https://guides.codepath.org/ios/Using-UIRefreshControl) ::: :::info Hint :bulb: Twitter part 2 goes over pull to refresh ::: ## Bonus: Launch Screen? In iOS we cant add lottie to the launch screen storyboard but there is a tool called Flow that allows us to edit and insert animations into the launch screen :::warning [Flow](https://createwithflow.com/) [Flow tutorial](https://createwithflow.com/tutorials/launchAnimationStepByStep/) ::: # Lab 5 Yelp Chat :::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](https://imgur.com/XIdD7Bb.gif) ### Getting Started - The checkpoints below should be implemented using the Pair Programming method ## Milestone 1. Project Setup and Initialize Parse 👷‍ 1. Use the Yelp Project Starter. - If you want to build off your old project you will need to implement a Tab Bar, Login View, and a Chat View along with their associated files to use them. :::success **@[[assets/yelpy_starter_5.zip]]** ::: :::info The storyboard and files are already updated and ready for you to implement your code. ***Note:*** The files we will use are - AppDelegate - SceneDelegate - LoginVC - ChatVC ::: 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: Login and Sign Up ✍️ 1. New user can tap "Sign Up" button to sign up :::warning 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) - [User Login](http://guides.codepath.org/ios/Building-Data-driven-Apps-with-Parse#user-login) - [Alert Controllers](https://guides.codepath.org/ios/Using-UIAlertController) ::: ### Sign Up In the sign up function we need to check to see that is the username and password is not empty. Once this is verified we need to store the user info to the `PFUDer()` by setting the username and password attributes. Next we can pass the user info to the `newUser.signUpInBackground` ![Sign Up](https://i.imgur.com/jrbB6ct.png) ### Log In Existing user can tap "Login" button to login this function will follow a similar format to the sign up function. First we will set the user info and pass it in to `PFUser.logInWithUsername` 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. User sees an alert with error description if there is a problem during sign up or login ![Log In](https://i.imgur.com/8g96duF.png) ## Milestone 3: Display the chatView 🖼 Because we only want to display the login screen when the user first signs in and we dont want to display a back button on all the views we will go into the ```SceneDelegate.swift``` and control all the scenes. 1. Create a listener In the `scene` function lets add a listener for when the user logs-in and lets have it call a function called `login()` when we receive a notification ![ChatView](https://imgur.com/rXYdziX.png) <br><br> :::info We will also need a listener for when the user logs out ::: 1. Change the view After a successful sign up or login from the Login View, we need to change the view programmatically. <img src="https://imgur.com/EN79bWi.png"/> <br><br> ## Milestone 4: Send a Chat Message 💬 ![Compose Chat|250](http://i.imgur.com/RIyFAHQ.png)<br> The Chat Screen will allow the user to compose and send a message. 1. We will need an array to store our messages and a chat message object ```swift var messages: [PFObject] = [] ``` 1. When the user taps the "Send" button, create a new Message of type PFObject and save it to Parse - Use the class name: `Message` (this is case sensitive). ```swift let chatMessage = PFObject(className: "Message") ``` - 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 ?? "" ``` - 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)") } } ``` - 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: 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 6: Persist Logged in User 1. On app launch, if current user is found in cache, user is taken directly to Chat Screen 1. In the SceneDelegate, check if there is a current logged in user in the scene function . - Parse automatically caches the current user on sign up or login. The current user can be accessed using, `PFUser.current()` ```swift if PFUser.current() != nil { login() } ``` 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 ``` :::success 🎉 Congrats! 🥳 You have finished all the required user stories. ::: ## Optional Stories 1. Create a Setting page where a user can logout 2. User sees an activity indicator while waiting for authentication. 3. User can pull to refresh Chat feed 4. 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. 5. 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> :::warning ☝️ **NO submission is required for labs** ::: # Lab 6 - Yelpy PhotoMap In this lab you will 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|250](https://i.imgur.com/Npz2m1A.gif) ### Getting Started - The checkpoints below should be implemented using the Pair Programming method ## Milestone 1: Project Setup and Initialize MapKit 1. Use the Yelp Project Starter. - If you want to build off your old project you will need to implement a new UIViewController with a custom class of `PostImageViewController` and create a segue from `RestaurantDetailViewController` to this new custom VC :::success **@[[assets/yelpy_starter_6.zip]]** ::: :::info The storyboard and files are already updated and ready for you to implement your code. ***Note:*** The files we will use are: - main.storyboard - Project Settings - Restaurant Model - PostImageViewController ::: :::warning **Guides** - <a href="https://guides.codepath.com/ios/Maps" target=blank>Maps - CodePath </a> - <a href="https://guides.codepath.com/ios/Location-Quickstart" target=blank>Retrieving Location -CodePath </a> - <a href="https://guides.codepath.com/ios/Camera-Quickstart" target=blank> Working with the Camera </a> - <a href="https://developer.apple.com/documentation/mapkit" target=blank> MapKit -Apple Documentation </a> - <a href="https://developer.apple.com/documentation/corelocation" target=blank> Apple Core Location </a> - <a href="https://developer.apple.com/documentation/corelocation/getting_the_user_s_location" target=blank>Retrieving Location - Apple Documentation </a> <a href="https://medium.com/flawless-app-stories/unwind-segues-in-swift-5-e392134c65fd" target=blank>Unwinding Segues - Medium Article </a> ::: ## Milestone 1: Configure MapKit + Storyboard Items **Files to be modified for M1: Project Settings, Restaurant Model, Storyboard** ### Step 1: Add MapKit to Project Add MapKit to project on the project configuration settings - Click on Project Yelpy -> General -> Frameworks, Libraries, and Embedded Content - click on (+) sign to add MapKit framework ### Step 2: Configure Restaurant Model - You will need to refactor the Restaurant model to include coordinate points since we will be displaying the locations of each resaurant on a map! :::info The Yelp API returns a **coordinates** property of type **[String:Double]** ::: ### Step 3: Add MapView to Storyboard 1. Add a MapView 1. Go to **RestaurantDetailVC** and change the height constraint of **HeaderImage** to `multiplier of 20/100` 1. Add **MapKitView** beneath the HeaderImage 1. Give constraints of 0 from top of MapKitView to bottom anchor of HeaderImageView, 0 to trailing and leading constraints, and 0 to **Safe Area** 1. Create an outlet for the MapView to set its initial visible region to the restaurant's location in `viewDidLoad:` ```swift // 1) get longitude and latitude from coordinates property let latitude = r.coordinates["latitude"]! let longitude = r.coordinates["longitude"]! // 2) initialize coordinate point for restaurant let locationCoordinate = CLLocationCoordinate2DMake(CLLocationDegrees.init(latitude), CLLocationDegrees.init(longitude)) // 3) initialize region object using restaurant's coordinates let restaurantRegion = MKCoordinateRegion(center: CLLocationCoordinate2DMake(latitude, longitude), span: MKCoordinateSpan(latitudeDelta: 0.1, longitudeDelta: 0.1)) // 4) set region in mapView to be that of restaurants mapView.setRegion(restaurantRegion, animated: true) ``` - At this point you should see a map centered in the restaurant's location! :::info 💡 We saved you some time and completed the following steps: 1. Imported MapKit 2. Conformed to MKMapViewDelegate 3. Set the mapView outlet's delegate to RestaurantDetailViewController :wink: ::: ## Milestone 2: Show Restaurant in MapView **File Editing: RestaurantDetailsViewController** ### Step 1: Drop Annotation (Pin) in Map Drop an annotation on the map and segue to PostImageViewController ```swift // 5) instantiate annotation object to show pin on map let annotation = MKPointAnnotation() // 6) set annotation's properties annotation.coordinate = locationCoordinate annotation.title = r.name // 7) drop pin on map using restaurant's coordinates mapView.addAnnotation(annotation) ``` At this point, your `configureOutlets()` function should look like this: ![ConfigureOutlets](https://i.imgur.com/oHiiipV.png) ### Step 2: MapView Methods In this section we will be configuring our mapView methods: ![MapView Methods](https://i.imgur.com/JsnPXFb.png) 1. Configure mapView method for `viewFor annotation`: - Implement `(mapView:_ viewFor annotation:_)` method - create a reuse identifier constant for your annotationView - set `annotationView` to `mapView.dequeueReusableAnnotationView(withIdentifier: reuseID)` - If `annotationView` is nil you want to configure it with an annotation and the reuse identifier from earlier then add an **accessoryView** with the camera image in your assets - Finally, return the **annotationView** ```swift if (annotationView == nil){ // MARK: USE MKPinAnnotationView and NOT MKAnnotationView annotationView = MKPinAnnotationView(annotation: annotation, reuseIdentifier: reuseID) annotationView?.canShowCallout = true // 9) Add info button to annotation view let annotationViewButton = UIButton(frame: CGRect(x: 0, y: 0, width: 50, height: 50)) annotationViewButton.setImage(UIImage(named: "camera"), for: .normal) annotationView?.leftCalloutAccessoryView = annotationViewButton } ``` This should be the end result: ![ViewFor Annotation](https://i.imgur.com/5eulDg7.png) :::info 💡 Make sure to use MKPinAnnotationView and not MKAnnotationView! ::: 1. `performSegue` using the identifier from the segue we made going to PostImageViewController inside the `calloutAccessoryControlTapped` method ![Segue](https://i.imgur.com/LxIWkQX.png) ## Milestone 3: Implement Protocol to Pass Data **File Editing: PostImageViewController** ### Step 1: Create your own Protocol - Create protocol named `PostImageViewControllerDelegate` above the class declaration - Add weak var for PostImageViewController delegate - Pass Image using Protocol Stub when you unwind your segue to return to DetailVC ![Pass Data](https://i.imgur.com/MOOqSYa.png) ### Step 2: Pass image back to RestaurantDetailVC **File Editing: RestaurantDetailViewController** In your `RestaurantDetailViewController`, using the protocol you created, conform to it in your Class and add it's `imageSelected()` protocol method: ![Img](https://i.imgur.com/MnQrePf.png) :::success 🎉 Congrats! 🥳 You have finished all the required user stories. ::: ## Optional Stories - [ ] Add a new ViewController and embed it in a TabBarController where you will display a map of with pins of **all** the restaurant locations <!-- ## 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. -->