[Swift MapKit Tutorial Series] Working with MKMapItem to manage pin informations

Update October 2015: Fully updated for iOS 9 (Xcode 7 and Swift 2).

In the previous tutorial, you learned how to use the MKLocalSearch API powered by MapKit framework to search places, POI and addresses in the map. In this tutorial, you will learn how to get useful informations about the place you look for (name, address, postal code, etc) using another API from MapKit, called MKMapItem.

Along the way, you gonna master how to implement a pop over with the new iOS 8 UIPopoverPresentationController class, and how it is easy to adapt the pop over content to compact width and compact height size classes (AKA Adaptivity).


Without further ado, download the project from the previous tutorial and let’s build on it 🙂

As a first additional feature, you are going to implement some setting adjustments which will influence the map behaviour and appearance, the settings will be embedded in a pop over controller which will adapt its look based on the screen dimensions the app is running with.

Here is how the final popover will look like on both regular and compact width:

UIPopoverPresentationController on a regular width screen
UIPopoverPresentationController on a regular width screens
UIPopoverPresentationController on a compact width screen
UIPopoverPresentationController on a compact width screens

As you may notice, it’s the same content but different look and feel 🙂

Let’s add a bar button to the left of the navigation bar. Select Main.storyboard from the Project navigator, and drag a UIBarButtonItem from the Object library, place it to the left side of the navigation bar of the “Map Places” scene. Make sure the bar button is highlighted, then select the ‘Attributes inspector’ view and change the ‘System Item’ property to ‘Add’.

Add a UIBarButtonItem

Before coding, you need to hook the bar button to the class, switch to the ‘Assistant editor’ view, ctrl+drag from the bar button item to the view controller (just below the class name). Make sure it’s an ‘Outlet’ from the connection drop down list and name it ‘showOptionsBtn’.

Repeat the same step (ctrl+drag from the bar button to the class), but this time, make sure it’s an ‘Action’ (from the connection drop down list). Name the action method ‘showMapOptions’ and click the ‘Connect’ button to apply the changes.

Cool, now place to the code, start by adding the following properties declarations right after the class name:

var contentController:UITableViewController!
var tableMapOptions:UITableView!
var mapType:UISegmentedControl!
var showPointsOfInterest:UISwitch!

The contentController object will handle the pop up presented content, it will embed a UITableView object (tableMapOptions) with a segmented control and a UISwitch object to show/hide POI from the map view.

Locate the ‘showMapOptions’ method and place the following code inside:

 //1
contentController.modalPresentationStyle = UIModalPresentationStyle.Popover
//2
let popPC:UIPopoverPresentationController = contentController.popoverPresentationController!
popPC.barButtonItem = showOptionsBtn
popPC.permittedArrowDirections = UIPopoverArrowDirection.Any
popPC.delegate = self
presentViewController(contentController, animated: true, completion: nil)

Let’s explain the above code:

//1: Each view controller has a modal presentation style, it’s kind of setting the way you want to present your content, here you set it to pop over style, this is the preferred style especially for iPad screens. In a moment, you will see how to change this style for compact width .

//2: popPC is a UIPopoverPresentationController object, it will manage the display of a pop over content for you, it’s important to assign the pop over presentation controller declared previously to the popPC object. Also, you need to tell the object which bar button should the pop over start drawing its content from (showOptionsBtn bar button in this case). Furthermore, you set the view controller to be the delegate for the popPC, and you will implement some protocol methods to react to screen size changes and apply ‘Adaptivity’ right away. Finally, we presented the content modally with an animation to the screen 🙂

Next, you need to create the table view, which will hold the settings of the map and embed it into a table view controller object. To do so, place the following code inside the viewDidLoad function:

tableMapOptions = UITableView()
tableMapOptions.dataSource = self
contentController = UITableViewController()
contentController.tableView = tableMapOptions

Setting the datasource of the table view require implementing two main datasource protocol methods. Place the following code before the closing bracket of the view controller class:

 func tableView(tableView: UITableView,
        cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell{
            
            var cell:UITableViewCell! = tableView.dequeueReusableCellWithIdentifier("cellIdentifier")

            if cell == nil{
                cell = UITableViewCell(style: UITableViewCellStyle.Value1, reuseIdentifier: "cellIdentifier")
                if indexPath.row == 0{
                    mapType = UISegmentedControl(items: ["Standard","Satellite","Hybrid"])
                    mapType.center = cell.center
                    cell.addSubview(mapType)
                }
                if indexPath.row == 1{
                    showPointsOfInterest = UISwitch()
                    cell.textLabel?.text = "Show Points Of Interest"
                    cell.accessoryView = showPointsOfInterest
                }
            }
            return cell
    }
    
    func tableView(tableView: UITableView,
        numberOfRowsInSection section: Int) -> Int{
            return 2
    }

The code above will set up two rows for the table view containing a segmented control to switch between the type of the map to display, and a UISwitch control to show/hide points of interests while exploring the map.

Don’t forget to adopt the class to both UITableViewDataSource and UIPopoverPresentationControllerDelegate protocols. For that, replace the class declaration with the following:

class ViewController: UIViewController, UISearchBarDelegate, UITableViewDataSource, UIPopoverPresentationControllerDelegate {

Try to run the app on iPad simulator, and click the bar button to bring up the map options screen, the pop up screen is shown correctly. But, how about compact width screens ? Go ahead, run the app on iPhone simulator and bring up the map options pop over..

memes face

Neither I 🙁

The content is overlapped under the status bar which make it hard to interact with, also, there is no way to go back and dismiss the presented content.

Fortunately, iOS 8 has enough tools to fix that 🙂

Remember the ‘Adaptivity’ concept we talked about previously ? We will implement it right away. You will write some protocol methods in order to respond to screen size changes and adapt the pop over presented content accordingly. Basically, for compact width screens like the iPhone portrait, the pop over is better to be shown as a full screen controller, because iPhone users are more likely expecting such behaviour instead of the ordinary pop over shown on iPad screens.

So let’s go ahead and implement the Adaptivity mechanism. Place the following code before the class closing bracket

//1
func adaptivePresentationStyleForPresentationController(controller: UIPresentationController) -> UIModalPresentationStyle
 {
    return .FullScreen
 }
//2
func presentationController(controller: UIPresentationController,
      viewControllerForAdaptivePresentationStyle style: UIModalPresentationStyle) -> UIViewController?{
            
      let navController:UINavigationController = UINavigationController(rootViewController: controller.presentedViewController)
      controller.presentedViewController.navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: UIBarButtonSystemItem.Done, target: self, action:"done")
      return navController
}

Some explanations are needed here:

//1: You started by implementing the ‘adaptivePresentationStyleForPresentationController’ protocol method, this is where you give the delegate the new presentation style to use when the screen size is about to change, in this case, we want the content to be presented in full screen.

//2: This protocol method completes what ‘adaptivePresentationStyleForPresentationController’ method starts. Each time this method is called, it will ask for the layout you want to implement for adaptive styles, so here is the right place to tell the app that you want your presented content to be transformed to something else than a regular pop over, in order to better satisfy the iPhone portrait screens (compact width). In this case, it’s suitable to embed the presented content into a navigation controller with a dismiss button which will run a ‘done’ function to dismiss the presented controller and apply the map settings.

Before you run the app, let’s implement the ‘done’ function attached to the bar button item. Place the following code before the closing bracket of the view controller:

func done (){
        presentedViewController?.dismissViewControllerAnimated(true, completion: nil)
        
        if showPointsOfInterest.on{
            mapView.showsPointsOfInterest = true
        }else{
            mapView.showsPointsOfInterest = false
        }
        if mapType.selectedSegmentIndex == 0{
            mapView.mapType = MKMapType.Standard
        }else if mapType.selectedSegmentIndex == 1{
            mapView.mapType = MKMapType.Satellite
        }else if mapType.selectedSegmentIndex == 2{
            mapView.mapType = MKMapType.Hybrid
        }
    }
    
func popoverPresentationControllerDidDismissPopover(popoverPresentationController: UIPopoverPresentationController){
    done()
}

The ‘done’ function will detect any changes occurred on the map options screen and apply them to the map accordingly.

Implementing the ‘popoverPresentationControllerDidDismissPopover’ protocol method above was important to detect the pop over dismissing event, and call the ‘done’ function, since there will be no button to dismiss the pop over manually on regular width screens like iPad.

Cool, run the app on iPhone and iPad simulators and enjoy your new pop over presented controller 🙂

not bad memes

So far, you did a great work playing with UIPresentationController in the pop over context, you used it to show up some settings in order to customise the look and behaviour of the map.

As a second additional feature, you are going to use the MKMapItem class from the MapKit framework to request the place/address/POI details. MKMapItem has a great set of API which you need to get useful informations about the place you are searching on the map.

So far, when you search a place, it only shows up with a simple annotation displaying the name of that place. Let’s add an info button to the annotation view, place the following protocol method before the class closing bracket:

func mapView(mapView: MKMapView,
     viewForAnnotation annotation: MKAnnotation) -> MKAnnotationView?{
         self.pinAnnotationView.rightCalloutAccessoryView = UIButton(type: UIButtonType.InfoLight)
         self.pinAnnotationView.canShowCallout = true
         return self.pinAnnotationView
}

So far so good, the protocol method implemented above will customise the pin annotation view with an info light button and return it to the map view object to draw. Now, in order for this method to be called, you need to set the view controller as the delegate of the map view object and adopt the class to the MKMapViewDelegate protocol.

To do so, place the following statement inside the ‘viewDidLoad’ method:

mapView.delegate = self

Also, update the class name to look like the following:

class ViewController: UIViewController, UISearchBarDelegate,UITableViewDataSource, UIPopoverPresentationControllerDelegate, MKMapViewDelegate {

Build and run, search a place on the map and click on the pin to make sure the info button is displayed correctly inside the annotation view.

Next, you will implement the details controller in which you gonna show the map place informations. This controller will be pushed once you click the info button on the annotation view.

Select Main.storyboard from the Project navigator, and drag a UIViewController object from the Object library to the scene.

Now, we will make a triggered segue to detect the info button click and move to the next details controller. Ctrl+drag from the Map Places controller to the new controller, release the click and select ‘show’ from the list.

Create a triggered segue in storyboard
Create a triggered segue in storyboard

Select the segue from the storyboard scene, switch to the ‘Attributes inspector’ view and name it ‘PinDetails’, like shown in the following screenshot.

Give an identifier to the storyboard segue

Let’s add a table view object to the new controller, drag a UITableView object from the Object library to the controller scene, and place it below the navigation bar to fill the remaining space. Select the ‘Size inspector’ and make sure X=0, Y=64, Width=600 and Height=536.

Next, select the Pin menu, uncheck the ‘Constrain to margins’ checkbox, and activate the top, leading, bottom and trailing constraints to make the table view always fill the bounds of the remaining space of the screen.

Add Top, Leading, Bottom and Trailing constraints

To finish up with the UI, reproduce the following simple steps:

1- Select ‘File\New\File’ from the menu, choose iOS\Source tab and select ‘Cocoa Touch Class’. Name the class ‘PinDetailsViewController’ and make sure it’s a subclass of UIViewController and that the checkbox to make a xib file is UNchecked, like below:

Create a new View Controller in Swift

2- Switch back to Main.storyboard and select the View Controller scene, switch to the ‘Identity inspector’ view and name the custom class ‘PinDetailsViewController’.

Set the scene to a custom view controller in the Identity inspector

3- Select the ‘Attributes inspector’ view, and make sure the property ‘Adjust Scroll View Insets’ is UNchecked.

4- Switch to the ‘Assistant editor’. Make sure ‘PinDetailsViewController’ is loaded on the split editor along with the Main.storyboard file, then Ctrl+drag from the table view object in the scene to the code on ‘PinDetailsViewController’ file (right below the class name). Make sure the Connection is set to Outlet and name it ‘tableView’.

That’s all for the UI, now place to the final piece of code 🙂

Select PinDetailsViewController.swift from the Project navigator and make the following changes:

// Import MapKit (right above the class name)
import MapKit

// Place the following statement right under the class name
var mapItemData:MKMapItem!

// Set the datasource to self (place the following statement inside the viewDidLoad method)
tableView.dataSource = self

Make the class conforms to the table view data source protocol, update the class name to look like the following:

class PinDetailsViewController: UIViewController, UITableViewDataSource {

Place the following code before the closing bracket of the class:

func tableView(tableView: UITableView,
        cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell{
            
      var cell:UITableViewCell! = tableView.dequeueReusableCellWithIdentifier("cellIdentifier")

      if cell == nil{
          cell = UITableViewCell(style: UITableViewCellStyle.Value1, reuseIdentifier: "cellIdentifier")
           if indexPath.row == 0{
                cell.textLabel?.text = mapItemData.name
           }
           if indexPath.row == 1{
                cell.textLabel?.text = mapItemData.placemark.country
                cell.detailTextLabel?.text = mapItemData.placemark.countryCode
           }
           if indexPath.row == 2{
                cell.textLabel?.text = mapItemData.placemark.postalCode
           }
           if indexPath.row == 3{
                cell.textLabel?.text = "Phone number"
                cell.detailTextLabel?.text = mapItemData.phoneNumber
          }
     }
        return cell
}

func tableView(tableView: UITableView,
    numberOfRowsInSection section: Int) -> Int{
       return 4
}

Basically, mapItemData is an MKMapItem object type, which means it will handle all the place data (like phone number, address, postal code, and so on). Here you just asked mapItemData for some useful informations to display them inside the table view.

But, from where shall we initialise the mapItemData object and assign the pin data to it? Well, the previous controller should handle this, so let’s go back and adjust it accordingly.

Select ViewController.swift from the Project navigator, and make the following changes:

1- Declare the mapItemData object, place the following statement right under the class name:

var mapItemData:MKMapItem!

2- When searching a place on the map, the search response will return an array of map items. Here you just need the last item since it’s only one pin to show. Go ahead and locate the ‘localSearch.startWithCompletionHandler’ completion handler, and place the following code before its closing bracket:

self.mapItemData = localSearchResponse?.mapItems.last

3- Finally, you are going to implement the info button click handler and trigger the push segue to move to the next controller. The next controller has a property which you will assign it the item data before moving To the next screen. To do so, place the following code before the closing bracket of the class:

 
func mapView(mapView: MKMapView,
     annotationView view: MKAnnotationView,
     calloutAccessoryControlTapped control: UIControl){
         self.performSegueWithIdentifier("PinDetails", sender: self)
}
    
override func prepareForSegue(segue: UIStoryboardSegue,
     sender: AnyObject?){
         var pinDetailsVC = PinDetailsViewController()
         pinDetailsVC = segue.destinationViewController as! PinDetailsViewController
         pinDetailsVC.mapItemData = self.mapItemData
}

That’s it, you just assigned the mapItemData of the current controller (self.mapItemData) to the pin details controller property and triggered the segue you already set up in storyboard.

Build and run, search for a place, explore its data and most of all, enjoy your work 🙂

As usual, you can download the final project here.

Here is useful links to learn more about what you did in this tutorial:
Session 228 from the WWDC 2014: A Look Inside Presentation Controllers.
UIAdaptivePresentationControllerDelegate Protocol Reference
UIPresentationController class reference.

Feel free to leave a comment below, if you feel this tutorial has helped you, please consider sharing it!

Malek
iOS developer with over than 11 years of extensive experience working on several projects with different sized startups and corporates.