Home iOS & Swift Books UIKit Apprentice

29
Maps Written by Matthijs Hollemans & Fahim Farook

Showing the locations in a table view is useful, but not very visually appealing. Given that the iOS SDK comes with an awesome map view control, it would be a shame not to use it :]

In this chapter, you will add a third tab to the app that will look like this when you are finished:

The completed Map screen
The completed Map screen

This is what you’ll do in this chapter:

  • Add a map view: Learn how to add a map view to your app and get it to show the current user location or pins for a given set of locations.
  • Make your own pins: Learn to create custom pins to display information about points on a map.

Add a map view

First visit: the storyboard.

➤ From the Objects Library, drag a View Controller on to the canvas.

➤ Control-drag from the Tab Bar Controller to this new View Controller to add it to the tabs – choose Relationship segue – view controllers.

➤ The new view controller now has a Tab Bar Item. Change its title to Map via the Attributes inspector.

➤ Drag a Map Kit View into the view controller. Make it cover the entire area of the screen, so that the lower part of the map view sits under the tab bar – the size of the Map View should be 375 × 667 points.

➤ Add left, top, right, and bottom Auto Layout constraints to the Map View via the Add New Constraints menu, pinning it to the main view.

➤ In the Attributes inspector for the Map View, enable Shows: User Location. That will put a blue dot on the map at the user’s current coordinates.

Enable show user location for the Map View
Enable show user location for the Map View

➤ Select the new view controller and select Editor ▸ Embed In ▸ Navigation Controller. This wraps your view controller in a navigation controller, and makes the new navigation controller the view controller displayed by the Tab Bar Controller.

➤ Change the view controller’s — not the new navigation controller, but its root view controller — Navigation Item title to Map.

➤ Drag a Bar Button Item into the left-hand slot of the navigation bar and set the title to Locations. Drag another into the right-hand slot and set its title to User. Later on you’ll use nice icons for these buttons, but for now these labels will do.

This part of the storyboard should look like this:

The design of the Map screen
The design of the Map screen

➤ Run the app. Choose a location in the Simulator’s Features menu and switch to the Map. The screen should look something like this — the blue dot shows the current location:

The map shows the user’s location
The map shows the user’s location

Sometimes, the map might show a different location than the current user location and you might not see the blue dot. If that happens, you can pan the map by clicking the mouse and dragging it across the Simulator window. Also, to zoom in or out, hold down the Alt/Option key while dragging the mouse.

Zoom in

Next, you’re going to show the user’s location in a little more detail because that blue dot could be almost anywhere in California!

import UIKit
import MapKit
import CoreData

class MapViewController: UIViewController {
  @IBOutlet var mapView: MKMapView!

  var managedObjectContext: NSManagedObjectContext!

  // MARK: - Actions
  @IBAction func showUser() {
    let region = MKCoordinateRegion(
      center: mapView.userLocation.coordinate, 
      latitudinalMeters: 1000,
      longitudinalMeters: 1000)    
    mapView.setRegion(
      mapView.regionThatFits(region), 
      animated: true)
  }

  @IBAction func showLocations() {
  }
}

extension MapViewController: MKMapViewDelegate {
}
Pressing the User button zooms in to the user’s location
Vpaqdakd tpu Imoj ternad yaoxh es ke cfi aluy’n tapofoor

Show pins for locations

The other button, Locations, is going to show the region that contains all the user’s saved locations. Before you can do that, you first have to fetch those locations from the data store.

var locations = [Location]()
// MARK: - Helper methods
func updateLocations() {
  mapView.removeAnnotations(locations)

  let entity = Location.entity()

  let fetchRequest = NSFetchRequest<Location>()
  fetchRequest.entity = entity

  locations = try! managedObjectContext.fetch(fetchRequest)
  mapView.addAnnotations(locations)
}
override func viewDidLoad() {
  super.viewDidLoad()
  updateLocations()
}
// Third tab
navController = tabViewControllers[2] as! UINavigationController
let controller3 = navController.viewControllers.first as! MapViewController
controller3.managedObjectContext = managedObjectContext
public class Location: NSManagedObject, MKAnnotation {
public var coordinate: CLLocationCoordinate2D {
  return CLLocationCoordinate2DMake(latitude, longitude)
}

public var title: String? {
  if locationDescription.isEmpty {
    return "(No Description)"
  } else {
    return locationDescription
  }
}

public var subtitle: String? {
  return category
}
let s = location.title
location.title = "Time for a change"
func title() -> String? {
  if locationDescription.isEmpty {
    return "(No Description)"
  } else {
    return locationDescription
  }
}
The map shows pins for the saved locations
Qsu mab qcojn xubb mep lfi tutin fiqiyaunx

Show a region

Tapping the User button makes the map zoom to the user’s current coordinates, but the same thing doesn’t happen yet for the location pins.

func region(for annotations: [MKAnnotation]) -> MKCoordinateRegion {
  let region: MKCoordinateRegion

  switch annotations.count {
  case 0:
    region = MKCoordinateRegion(
      center: mapView.userLocation.coordinate, 
      latitudinalMeters: 1000, 
      longitudinalMeters: 1000)

  case 1:
    let annotation = annotations[annotations.count - 1]
    region = MKCoordinateRegion(
      center: annotation.coordinate, 
      latitudinalMeters: 1000, 
      longitudinalMeters: 1000)

  default:
    var topLeft = CLLocationCoordinate2D(
      latitude: -90, 
      longitude: 180)
    var bottomRight = CLLocationCoordinate2D(
      latitude: 90, 
      longitude: -180)

    for annotation in annotations {
      topLeft.latitude = max(topLeft.latitude, 
                             annotation.coordinate.latitude)
      topLeft.longitude = min(topLeft.longitude, 
                              annotation.coordinate.longitude)
      bottomRight.latitude = min(bottomRight.latitude, 
                                 annotation.coordinate.latitude)
      bottomRight.longitude = max(
        bottomRight.longitude,
        annotation.coordinate.longitude)
    }

    let center = CLLocationCoordinate2D(
      latitude: topLeft.latitude - (topLeft.latitude - bottomRight.latitude) / 2,
      longitude: topLeft.longitude - (topLeft.longitude - bottomRight.longitude) / 2)

    let extraSpace = 1.1
    let span = MKCoordinateSpan(
      latitudeDelta: abs(topLeft.latitude - bottomRight.latitude) * extraSpace,
      longitudeDelta: abs(topLeft.longitude - bottomRight.longitude) * extraSpace)

    region = MKCoordinateRegion(center: center, span: span)
  }

  return mapView.regionThatFits(region)
}
@IBAction func showLocations() {
  let theRegion = region(for: locations)
  mapView.setRegion(theRegion, animated: true)
}
override func viewDidLoad() {
  . . .
  if !locations.isEmpty {
    showLocations()
  }
}
The map view zooms in to fit all your saved locations
Jli rop raaf siayk ic ti buq ulx taip salid rawuduezf

Make your own pins

You made the MapViewController conform to the MKMapViewDelegate protocol, but so far, you haven’t done anything with that.

Create custom annotations

➤ Add the following code to the extension at the bottom of MapViewController.swift:

func mapView(
  _ mapView: MKMapView, 
  viewFor annotation: MKAnnotation
) -> MKAnnotationView? {
  // 1
  guard annotation is Location else {
    return nil
  }
  // 2
  let identifier = "Location"
  var annotationView = mapView.dequeueReusableAnnotationView(
    withIdentifier: identifier)
  if annotationView == nil {
    let pinView = MKPinAnnotationView(
      annotation: annotation,
      reuseIdentifier: identifier)
    // 3
    pinView.isEnabled = true
    pinView.canShowCallout = true
    pinView.animatesDrop = false
    pinView.pinTintColor = UIColor(
      red: 0.32, 
      green: 0.82, 
      blue: 0.4, 
      alpha: 1)

    // 4
    let rightButton = UIButton(type: .detailDisclosure)
    rightButton.addTarget(
      self,
      action: #selector(showLocationDetails(_:)),
      for: .touchUpInside)
    pinView.rightCalloutAccessoryView = rightButton

    annotationView = pinView
  }

  if let annotationView = annotationView {
    annotationView.annotation = annotation

    // 5
    let button = annotationView.rightCalloutAccessoryView as! UIButton
    if let index = locations.firstIndex(of: annotation as! Location) {
      button.tag = index
    }
  }

  return annotationView
}
@objc func showLocationDetails(_ sender: UIButton) {
}
The annotations use your own view
Fye olsexidiond eke doan ijq xaix

Guard

In the map view delegate method, you wrote the following:

guard annotation is Location else {
  return nil
}
if annotation is Location {
  // do all the other things
  . . .
} else {
  return nil
}
if condition1 {
  if condition2 {
    if condition3 {
      . . .
    } else {
      return nil  // condition3 is false
    }
  } else {
    return nil    // condition2 is false
  }
} else {
  return nil      // condition1 is false
}
guard condition1 else {
  return nil             // condition1 is false
}
guard condition2 else {
  return nil             // condition2 is false
}
guard condition3 else {
  return nil             // condition3 is false
}
. . .

Add annotation actions

Tapping a pin on the map now brings up a callout with a blue ⓘ button. What should this button do? Show the Edit Location screen, of course!

The Location Details screen is connected to all three screens
Bzi Neciqouc Qatuomn mmziaz az barwesder bi egb rlyuu kgboulw

func showLocationDetails(sender: UIButton) {
  performSegue(withIdentifier: "EditLocation", sender: sender)
}
// MARK: - Navigation
override func prepare(
  for segue: UIStoryboardSegue, 
  sender: Any?
) {
  if segue.identifier == "EditLocation" {
    let controller = segue.destination as! LocationDetailsViewController
    controller.managedObjectContext = managedObjectContext

    let button = sender as! UIButton
    let location = locations[button.tag]
    controller.locationToEdit = location
  }
}

Live-updating annotations

The way you’re going to fix this for the Map screen is by using notifications. Recall that you have already put NotificationCenter to use for dealing with Core Data save errors.

var managedObjectContext: NSManagedObjectContext! {
  didSet {
    NotificationCenter.default.addObserver(
      forName: Notification.Name.NSManagedObjectContextObjectsDidChange,
      object: managedObjectContext,
      queue: OperationQueue.main
    ) { _ in
      if self.isViewLoaded {
        self.updateLocations()
      }
    }
  }
}
if self.isViewLoaded {
 self.updateLocations()
}

Wildcard

Have another look at that closure. The _ in bit is the parameter list for the closure. Like functions and methods, closures can take parameters.

if let dictionary = notification.userInfo {
  print(dictionary[NSInsertedObjectsKey])
  print(dictionary[NSUpdatedObjectsKey])
  print(dictionary[NSDeletedObjectsKey])
}

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.

Have feedback to share about the online reading experience? If you have feedback about the UI, UX, highlighting, or other features of our online readers, you can send them to the design team with the form below:

© 2020 Razeware LLC

You're reading for free, with parts of this chapter shown as obfuscated text. Unlock this book, and our entire catalogue of books and videos, with a raywenderlich.com Professional subscription.

Unlock Now

To highlight or take notes, you’ll need to own this book in a subscription or purchased by itself.