:::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. :::warning ☝️ **NO submission is required for labs** ::: ![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** - [Maps - CodePath]("https://guides.codepath.com/ios/Maps") - [Retrieving Location -CodePath]("https://guides.codepath.com/ios/Location-Quickstart") - [Working with the Camera]("https://guides.codepath.com/ios/Camera-Quickstart") - [MapKit -Apple Documentation]("https://developer.apple.com/documentation/mapkit") - [Apple Core Location](https://developer.apple.com/documentation/corelocation) - [Retrieving Location - Apple Documentation]("https://developer.apple.com/documentation/corelocation/getting_the_user_s_location") - [Unwinding Segues - Medium Article]("https://medium.com/flawless-app-stories/unwind-segues-in-swift-5-e392134c65fd") ::: ## 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. -->