[Swift MapKit Tutorial Series] How to search a place, address or POI on the map

Update September 2016: Fully updated for Xcode 8 and Swift 3.

I usually like how MapKit framework comes with a great set of API to empower each map app. The first app I have built for iOS was empowered with MapKit (it was google maps at that time), so I feel it’s good to write a series of tutorials about it, ’cause I have a special respect to Apple for this framework.

This is the first tutorial in a series of tutorials about MapKit. In this post, you will learn how to easily search for addresses and POI and annotate them in the map, the process is also known as “forward geocoding”. Along the way, you will learn how to present the search bar over the navigation bar, the same way many iOS apps did (youtube app for instance), using the new iOS 8 “UIPresentationController” class.

Without further ado, let’s do this 🙂

Fire up Xcode, select “File\New\Project” from the menu, and as usual, select the ‘Single View Application’ template from the iOS tab, name your project ‘MapLocator’ and make sure ‘Swift’ is the selected language, and ‘Universal’ is the devices family to build for.

Create new project with the new  Xcode 8 interface

Let’s start by building the screen and setting some Auto Layout constraints so it gets adaptive on all screen sizes and orientations.

Select ‘Main.storyboard’ from the Project navigator view and choose the iPhone 6s model from the list of devices at the bottom menu of the canvas.

Choose a device model to layout with from the "View as" panel in Xcode 8

Click on the view controller from the canvas and select ‘Editor\Embed In\Navigation Controller’ from the menu, now the screen become the first controller in the navigation stack you just added. You may notice a navigation bar is added to the view controller, you will need this to place the search button and to experience the search bar presentation animation over the navigation bar.

Select the navigation bar from the Document Outline view and set its title to ‘Map Places’ from the ‘Attributes inspector’ view.

change navigation bar title

Now, let’s add a button to the navigation bar, this button will respond to clicks and hence fire a function to show the search bar. From the ‘Object library’, drag a ‘Bar Button Item’ and place it to the right of the navigation bar. Make sure it’s selected, then switch to the ‘Attributes inspector’ view, select the ‘System Item’ menu and choose ‘Search’ from the drop down list.

Add Bar Button Item to the navigation bar in Xcode 8

Add the Map

Let’s add the map to the view, drag a ‘Map Kit View’ object from the object library to the view, switch to the ‘Size inspector’ view and set the X to 0, Y to 64, Width to 375 and Height to 603.

The map needs to be placed correctly below the navigation bar to fill in the remaining screen space for all sizes and orientations. To ensure that, select the ‘Pin’ menu, make sure the ‘Constrain to margins’ checkbox is UNchecked, then activate the top, leading, bottom and trailing spacing constraints with 0 as a margin value. Finally, select the ‘Add Constraints’ button to apply the changes.

Constraint to margins

Cool, that’s it for the UI, now it’s time to hook up the objects to the code. The map view will be referenced as an outlet while the bar button item will be associated to an action method.

Switch to the ‘Assistant editor’ view and load the ‘ViewController.swift’ file in the split area.

1/ First, hold a ctrl click and drag a line from the search button to the code file (just below the class name), set the Connection to ‘Action’ and name it ‘showSearchBar’.

Hook a button to the code with the Assistant editor
Click for better visualisation

2/ Repeat the same work with the Map, hold a ctrl click, and drag a line from the map view object to the code, but this time, set the Connection to ‘Outlet’ and name it ‘mapView’.

Note: You may get an error of kind “Use of undeclared type ‘MKMapView'”, such error is thrown because the compiler couldn’t find the root class of the object you just added. You will fix this right away by importing the MapKit framework (where MKMapView class is declared), in the top of the class so that all MapKit classes and API can be imported and used in your code.

Add the following import line above the class name in “ViewController.swift”:

import MapKit

So far, here is how the ‘ViewController.swift’ file should look like:

import UIKit
import MapKit

class ViewController: UIViewController {
    
    @IBAction func showSearchBar(sender: AnyObject) {
    }

    @IBOutlet var mapView: MKMapView!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }
}

Run the app and check the map is displaying correctly on all screen sizes and orientations 🙂

Now place to the code, switch back to the Standard editor if it’s not already, and select ViewController.swift file from the Project navigator view.

Start by declaring some variables that you will need along the way performing map searches, etc. Implement the following code right after the class name:

    var searchController:UISearchController!
    var annotation:MKAnnotation!
    var localSearchRequest:MKLocalSearchRequest!
    var localSearch:MKLocalSearch!
    var localSearchResponse:MKLocalSearchResponse!
    var error:NSError!
    var pointAnnotation:MKPointAnnotation!
    var pinAnnotationView:MKPinAnnotationView!

Let me explain what each variable is responsible for:

The ‘searchController’ variable will manage the presentation of the search bar and its animation, while the ‘annotation’ object will serve to reference any drawn annotation on the map and manage it. In order to perform a search of address or POI with MapKit, an ‘MKLocalSearchRequest’ object should be prepared and passed to the ‘MKLocalSearch’ object which will initiate the search process asynchronously and send back the search result stored in the ‘MKLocalSearchResponse’ object (NSError object is also delivered for any potential error stack). Finally the ‘MKPointAnnotation’ and ‘MKPinAnnotationView’ will work together to construct the pin and annotation which you will place at the location coordinates on the map 🙂

Next, copy the following code inside the ‘showSearchBar’ action method:

searchController = UISearchController(searchResultsController: nil)
searchController.hidesNavigationBarDuringPresentation = false
self.searchController.searchBar.delegate = self
present(searchController, animated: true, completion: nil)

The code above will instantiate the search controller object and present it above the navigation bar with an animation, the ‘hidesNavigationBarDuringPresentation’ property should be better set to false, but you can still hide the navigation bar if you want by setting it to true (although that will result in a bad transition in our case).

You also set the view controller as the delegate of the search bar, you will implement one more function related to the search bar delegate protocol in order to respond to the search button click event on the keyboard and fire the search process. But before, let’s make the class adopt to the search bar protocol to respond to such events. Go ahead and change the class declaration to the following:

class ViewController: UIViewController, UISearchBarDelegate {

Cool, finally, let’s implement the last snippet of code. Once you click the keyboard search button, the app will perform all the search work we talked about previously. Place the following code before the closing bracket of the class:

    func searchBarSearchButtonClicked(_ searchBar: UISearchBar){
        //1
        searchBar.resignFirstResponder()
        dismiss(animated: true, completion: nil)
        if self.mapView.annotations.count != 0{
            annotation = self.mapView.annotations[0]
            self.mapView.removeAnnotation(annotation)
        }
        //2
        localSearchRequest = MKLocalSearchRequest()
        localSearchRequest.naturalLanguageQuery = searchBar.text
        localSearch = MKLocalSearch(request: localSearchRequest)
        localSearch.start { (localSearchResponse, error) -> Void in
            
            if localSearchResponse == nil{
                let alertController = UIAlertController(title: nil, message: "Place Not Found", preferredStyle: UIAlertControllerStyle.alert)
                alertController.addAction(UIAlertAction(title: "Dismiss", style: UIAlertActionStyle.default, handler: nil))
                self.present(alertController, animated: true, completion: nil)
                return
            }
            //3
            self.pointAnnotation = MKPointAnnotation()
            self.pointAnnotation.title = searchBar.text
            self.pointAnnotation.coordinate = CLLocationCoordinate2D(latitude: localSearchResponse!.boundingRegion.center.latitude, longitude:     localSearchResponse!.boundingRegion.center.longitude)
            
            
            self.pinAnnotationView = MKPinAnnotationView(annotation: self.pointAnnotation, reuseIdentifier: nil)
            self.mapView.centerCoordinate = self.pointAnnotation.coordinate
            self.mapView.addAnnotation(self.pinAnnotationView.annotation!)
        }
    }

The code above is the last piece in the puzzle, let’s explain its main parts commented above:

//1: Once you click the keyboard search button, the app will dismiss the presented search controller you were presenting over the navigation bar. Then, the map view will look for any previously drawn annotation on the map and remove it since it will no longer be needed.

//2: After that, the search process will be initiated asynchronously by transforming the search bar text into a natural language query, the ‘naturalLanguageQuery’ is very important in order to look up for -even an incomplete- addresses and POI (point of interests) like restaurants, Coffeehouse, etc.

//3 Mainly, If the search API returns a valid coordinates for the place, then the app will instantiate a 2D point and draw it on the map within a pin annotation view. That’s what this part performs 🙂

That’s it, run the app and search for your town, your house address or your favourite place! Enjoy your work 🙂

As usual, here is the completed project for this tutorial. Feel free to comment below or tweet me, I am always looking to hear from you 😉

MapKit forward geocoding

Update in response to @Kevin Thulin comment below:

In order to init the map with an explicit zoom level, you will need to specify a span values. To do so, place the following code inside the viewDidLoad method (right after the super.viewDidLoad() call):

  // Init the zoom level
  let coordinate:CLLocationCoordinate2D = CLLocationCoordinate2D(latitude: 34.03, longitude: 118.14)
  let span = MKCoordinateSpanMake(100, 80)
  let region = MKCoordinateRegionMake(coordinate, span)
  self.mapView.setRegion(region, animated: true)

The code above will init the map to the East Asia region. Change the latitude/longitude of the coordinate constants above to modify the region, and change the values in the span constant to modify the zoom level to your preferences (a large span values results in a low zoom level, and a small span values reflect a high zoom level).


Discover more from SweetTutos

Subscribe to get the latest posts sent to your email.

Malek
Software craftsman with extensive experience in iOS and web development.