Chapters

Hide chapters

iOS Apprentice

Eighth Edition · iOS 13 · Swift 5.2 · Xcode 11

Getting Started with SwiftUI

Section 1: 8 chapters
Show chapters Hide chapters

My Locations

Section 4: 11 chapters
Show chapters Hide chapters

Store Search

Section 5: 13 chapters
Show chapters Hide chapters

34. Maps
Written by Eli Ganim

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now

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.

Adding 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 320 × 568 points.)

➤ Add left, top, right, and bottom Auto Layout consraints 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

In older versions of Xcode, the app would compile without any problems at this point, but would crash when you switched to the Map tab. This does not appear to be the case with the latest version of Xcode, but if you do run into this issue, here’s what you need to do.

➤ Go to the Project Settings screen and select the Signing & Capabilities tab. Click on the + Capability button. Search for maps and double click the Maps capability to add it.

Enabling the app to use maps
Enabling the app to use maps

chan ➤ Run the app. Choose a location in Simulator’s Debug 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.

Zooming 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 weak 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
Qxuhbelm yma Eluz fezrif juipl ir vi kze ufax’k jazaleeg

Showing 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
Bmi voh xtevf dukd sij qca qegan begoyuetx

Showing 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
Byo mey yiej paifg am ze wic ukb ceux dopoy jojuyiigp

Making your own pins

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

Creating 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
Jqi olwuziveams aqu veog odb tael

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

Adding 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
Mni Loqacuuw Zivualm bjties em hakwadrak re ukb ttpee hbqiift

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) { notification in
      if self.isViewLoaded {
        self.updateLocations()
      }
    }
  }
}
if self.isViewLoaded {
 self.updateLocations()
}
{ _ in
  . . .
}
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.
© 2024 Kodeco Inc.

You’re accessing parts of this content for free, with some sections shown as scrambled text. Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now