Search for places using MKLocalSearchRequest and display results with UISearchController
Apple provides a powerful native map API called MapKit. You can display a map, show the user’s current location, and drop annotation pins. And you don’t need to rely on any third-party SDKs.
For example, if you’re building an app that shows branch locations, displays geotagged content, or does anything involving a map, MapKit is the tool for you.
Maps contain a lot of information, so it’s natural for users to search for places. Just check out any popular app with a map like Yelp, Zillow, or Uber and you’ll see a search bar with a results table.
In this tutorial, you’re going to implement a map search that displays location results in a table.
Selecting a table row will cause a pin to drop on the map. The pin will have a callout button that app-switches to Apple Maps for driving directions.
This tutorial is broken up into several sections. While there are great resources out there that cover each topic individually, the goal here is to integrate them into a single cohesive feature.
Here’s a table of contents if you want to jump straight to a particular section:
- Get user location
- Set up a Map Kit View
- Use UISearchController to display search results
- Search for locations using MKLocalSearchRequest
- Drop a map pin and create a callout button
Get User Location
It’s reasonable for an app with a map to ask for the user’s current location. You can conveniently zoom to the local area. And map searches can prioritize results by proximity.
Let’s get started with a new project:
1. Create a new project
Create a new Single View Application in Swift.
a. Within Xcode, go to File New Project…
b. Select iOS Application Single View Application
c. Name the project MapKitTutorial
d. Select Swift as the language
For this tutorial, uncheck the boxes for Use Core Data, Include Unit Tests, and Include UI Tests just to keep things simple.
2. Configure the location permission prompt message
Before you can get the user’s location, you first need permission. You’ll trigger a permission dialog in a later step. But for now, you can customize the description text that will appear in the alert dialog.
a. Select Info.plist from the Project navigator
b. Hover your mouse any item and click the plus button
c. For the Key, paste in NSLocationWhenInUseUsageDescription
d. Make sure the Type is set to String
e. Set the Value to anything you want, such as This tutorial wants to use your location
Configuring this key within Info.plist is a mandatory step. If this key is missing, iOS will simply not prompt the user, and the user wont get the chance to allow permission.
3. Set up the Location Manager
CLLocationManager is the object you’ll use for pretty much anything location related. You will use it to prompt the user for permission, and ask the device for the current location. Delegate methods are used to handle any responses.
To set up the Location Manager:
a. Open ViewController.swift
b. Replace the contents of ViewController.swift with the following code:
import UIKit import MapKit class ViewController : UIViewController { let locationManager = CLLocationManager() override func viewDidLoad() { super.viewDidLoad() } }
The import statement gives you access to classes that begin with MK (Map Kit) and CL (Core Location).
The CLLocationManager variable gives you access to the location manager throughout the scope of the controller.
c. Replace viewDidLoad() with the following code:
override func viewDidLoad() { super.viewDidLoad() locationManager.delegate = self locationManager.desiredAccuracy = kCLLocationAccuracyBest locationManager.requestWhenInUseAuthorization() locationManager.requestLocation() }
This is what’s going on in this code:
locationManager.delegate: It could take some time for requested information to come back, so delegate methods are used to handle responses asynchronously. You’ll implement these delegate methods momentarily.
locationManager.desiredAccuracy: This step is optional, but you might want to override the default accuracy level with an explicit value, like kCLLocationAccuracyBest. Or you could use something less accurate like kCLLocationAccuracyHundredMeters to conserve battery life.
locationManager.requestWhenInUseAuthorization(): This triggers the location permission dialog. The user will only see the dialog once.
locationManager.requestLocation(): This API call was introduced in iOS 9 and triggers a one-time location request.
Note: For iOS 8 and prior, you’ll have to use locationManager.startUpdatingLocation(), which is a little more difficult to use. This API continuously requests the current location, and leaves it up to the developer to figure out when to stop location updates. I highly recommend reading iOS Apprentice by Ray Wenderlich to learn more about using this method.
At this point, you should see a compiler error because the delegate methods haven’t been implemented yet:
4. Set up the Location Manager Delegate
The Location Manager Delegate methods process Location Manager responses. There will be responses to the authorization and location requests you sent earlier in viewDidLoad.
Add this class extension to the bottom of ViewController.swift, outside of the class body:
extension ViewController : CLLocationManagerDelegate { func locationManager(manager: CLLocationManager, didChangeAuthorizationStatus status: CLAuthorizationStatus) { if status == .AuthorizedWhenInUse { locationManager.requestLocation() } } func locationManager(manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { if let location = locations.first { print("location:: (location)") } } func locationManager(manager: CLLocationManager, didFailWithError error: NSError) { print("error:: (error)") } }
Here is what is going on:
extension: You put everything into a class extension rather than the class body. This is just a code organization technique to group related delegate methods.
locationManager(_:didChangeAuthorizationStatus:): This method gets called when the user responds to the permission dialog. If the user chose Allow, the status becomes CLAuthorizationStatus.AuthorizedWhenInUse.
You also trigger another requestLocation() because the first attempt would have suffered a permission failure.
locationManager(_:didUpdateLocations:): This gets called when location information comes back. You get an array of locations, but you’re only interested in the first item. You don’t do anything with it yet, but eventually you will zoom to this location.
locationManager(_:didFailWithError:): For now, just print out the error.
Note: Although this last method is optional, leaving it out will cause the app to crash with this error:
Build and run
This is what the code looks like so far:
You should be prompted with a location permission dialog. The alert description uses the body copy you set earlier within Info.plist.
Tap Allow, and you should see the following logged to the console, thanks to your print statement inside didUpdateLocations:
Note: Occasionally, you’ll run into the following error:
Error Domain=kCLErrorDomain Code=0 "The operation couldn't be completed. (kCLErrorDomain error 0.)"
The first thing is to make sure your Simulator has a location set under Debug > Location > Custom Location…
If you’re still getting the error, try quitting the Simulator and re-running the app in Xcode.
Section Recap
First, you created a location manager and triggered a location request. Then you implemented delegate methods to handle incoming location data.
You also had to juggle permissions. You requested location permission from the user. The user was presented with an alert dialog which used body copy from the Info.plist file. Then you implemented a delegate method that listened for an authorization change so you could re-attempt the location request.
For further reading about user location, check out this NSHipster article which covers requestLocation. Also check out iOS Apprentice by Ray Wenderlich part 3 which covers location in-depth.
Set up a Map Kit View
Now that you have the user’s location, it’s time to set up the map. In this section, you will wire up auto-layout constraints, a delegate, and an outlet to a map view within the Storyboard.
1. Wire up auto-layout constraints
a. Open Main.storyboard from the Project navigator
b. Drag a Map Kit View from the Object Library onto the View Controller Scene.
c. Select the Pin icon near the bottom right of the screen
d. Uncheck Constrain to margins
e. Set the constraint values to zero in all four directions. Make sure the orange beams are highlighted.
f. Select Update Frames > Items of New Constraints
g. Click Add Constraints
These constraints pin the map to the edges of the main View Controller screen.
2. Wire up the map delegate
The Map View Delegate is responsible for managing annotations. You’ll be using delegate methods to customize pins and callouts later on.
a. Hold down Control and drag from the Map Kit View onto the yellow Scene icon
b. Select delegate from the pop-up menu
3. Enable the user’s location on the map
The Map Kit View comes with a blue pulsing dot that shows the user’s current location. A checkbox in the Storyboard enables this.
a. Select the Map View within the View Hierarchy
b. In the Attributes Inspector, check the box for User Location
4. Create an IBOutlet
The IBOutlet is your handle to the Map Kit View object. Later on, you’ll be using it to drop pins and zoom to a pin’s location.
a. Within the Project Navigator, hold down Alt, and click on ViewController.swift
This opens the ViewController.swift file side-by-side with the Storyboard.
b. Control-drag from the Map Kit View onto the ViewController class, directly below the locationManager variable.
You should see a pop-over dialog for creating an outlet.
c. Type mapView as the Outlet name.
d. Click Connect
Xcode should automatically generate and wire up the following code for you:
@IBOutlet weak var mapView: MKMapView!
5. Zoom to the user’s current location
Now that you have a handle to the Map Kit View, you can zoom to the user’s location.
a. Open ViewController.swift
b. Replace the code for locationManager(_:didUpdateLocations:) with the following:
func locationManager(manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { if let location = locations.first { let span = MKCoordinateSpanMake(0.05, 0.05) let region = MKCoordinateRegion(center: location.coordinate, span: span) mapView.setRegion(region, animated: true) } }
locationManager(_:didUpdateLocations:) gives you an array of locations, but you’re only interested in the first one.
In order to zoom to this location, you need to perform a few intermediate steps. You define a map region, which is a combination of the map center (coordinate) and zoom level (span). The coordinate is property from the first location object. And the span is an arbitrary area of 0.05 degrees longitude and latitude.
Once you combine the coordinate and span into a region, you can zoom using setRegion(_:animated:).
Build and run
This is what the code looks like so far:
You should see the map zoom toward a blue pulsing dot at your current location.
Section Recap
In this section, you wired up the Map Kit View constraints, delegate, and outlet in the Storyboard. Then you used the map view outlet to zoom to the user’s location.
I found this article helpful when learning Map Kit for the first time. It covers coordinates, spans, regions, and basic annotations.
Use UISearchController to display search results
UISearchController is a special UI element that is optimized for displaying search results. It has a search bar that shows or hides a results table, based on the search bar state. It was introduced in iOS 8, and it replaces the deprecated UISearchDisplayController.
The search bar is accessible from both the current page and the search results page. Typically, you embed the search bar in the navigation bar or table view header. When you tap inside the search bar, a semi-transparent modal appears over the current screen. As you start typing, a table view appears within the modal. The search bar’s cancel button dismisses the modal overlay.
UISearchController is made up of three major parts:
searchBar: A search bar comes included with UISearchController.
searchResultsController: This is a custom View Controller you provide to display search results. In this tutorial, you will use a Table View Controller.
searchResultsUpdater: This is a delegate that responds to search bar text entry. In this tutorial, the Table View Controller will implement this delegate and update the table view data source accordingly.
Note: UISearchController does not exist in the Storyboard object library. This means that you will have to a lot of things in code, such as position the search bar UI and instantiate the searchResultsController.
Let’s start with something familiar, and configure a Table View Controller in the Storyboard
1. Set up the Table View Controller
a. Drag a Table View Controller from the Object Library onto the Storyboard.
Make sure you use the Table View Controller with the yellow icon, and not the Table View with the gray icon.
b. Create a new file: FileNewFile…
c. Choose iOSSourceSwift File
d. Name the file LocationSearchTable and click Create
e. Replace the contents of the LocationSearchTable.swift file with the following:
import UIKit class LocationSearchTable : UITableViewController { }
f. Go back to the Storyboard, and select the Table View Controller scene
g. In the Identity Inspector, set the class to LocationSearchTable
h. Set the Storyboard ID to LocationSearchTable. Just in case you’re just skimming, pay attention to this particular step.
i. Select the Table View Cell in the view hierarchy
j. In the Attributes Inspector, set the cell Style to subtitle
k. Set the Identifier to cell
This is just a typical Table View Controller setup. One important difference though is the use of a Storyboard ID, which you’ll use in a later step.
2. Embed the View Controller inside a Navigation Controller
a. In the Storyboard, select the View Controller scene (the one containing the map)
b. Go to EditorEmbed InNavigation Controller
You should now see something like this in the Storyboard. The Navigation Controller gives you a free navigation bar which will become the future home of the search bar.
3. Set up the UISearchController
Now it’s time to write some UISearchController code.
a. Create a property for the UISearchController
Within the ViewController class body, add the following variable:
var resultSearchController:UISearchController? = nil
This variable has controller-level scope to keep the UISearchController in memory after it’s created.
b. Set up the search results table
Append the following code to the contents of viewDidLoad:
let locationSearchTable = storyboard!.instantiateViewControllerWithIdentifier("LocationSearchTable") as! LocationSearchTable resultSearchController = UISearchController(searchResultsController: locationSearchTable) resultSearchController?.searchResultsUpdater = locationSearchTable
The locationSearchTable is the Table View Controller you set up earlier in the Storyboard. Thanks to the Storyboard ID, you can instantiate it programmatically.
The locationSearchTable will also serve as the searchResultsUpdater delegate. For now, you will see the following compiler error, which you’ll fix momentarily:
c. Set up the UISearchController delegate
In the Project navigator, open LocationSearchTable.swift.
Add the following class extension to the bottom, outside of the class body:
extension LocationSearchTable : UISearchResultsUpdating { func updateSearchResultsForSearchController(searchController: UISearchController) { } }
You will fully implement this method in a later section. But for now, this is just a stub to silence the compiler error.
d. Set up the search bar
Switch back to ViewController.swift, and append the following code to the contents of viewDidLoad:
let searchBar = resultSearchController!.searchBar searchBar.sizeToFit() searchBar.placeholder = "Search for places" navigationItem.titleView = resultSearchController?.searchBar
This configures the search bar, and embeds it within the navigation bar.
e. Configure the UISearchController appearance
Add some more code to viewDidLoad:
resultSearchController?.hidesNavigationBarDuringPresentation = false resultSearchController?.dimsBackgroundDuringPresentation = true definesPresentationContext = true
hidesNavigationBarDuringPresentation determines whether the Navigation Bar disappears when the search results are shown. Set this to false, since we want the search bar accessible at all times.
dimsBackgroundDuringPresentation gives the modal overlay a semi-transparent background when the search bar is selected.
Setting definesPresentationContext to true is an important but easily overlooked step. By default, the modal overlay will take up the entire screen, covering the search bar. definesPresentationContext limits the overlap area to just the View Controller’s frame instead of the whole Navigation Controller.
Note: There’s also a corresponding checkbox in the Storyboard called Defines Context in the Attributes Inspector that also sets the definesPresentationContext flag.
Build and run
This is what the code looks like so far:
You should see a search bar inside the Navigation Bar. As you type, an empty table will appear. Hit Cancel and the table disappears.
Your viewDidLoad method is starting to look like a massive block of code. This can feel intimidating at first. But it’s actually kind of convenient, because you can just copy and paste this block between projects.
Section Recap
Don’t worry if UISearchController is confusing — it could take a while to get used to.
In this section, you used UISearchController to display a search results table overlay. UISearchController comes with a built-in search bar which you embedded inside the navigation bar.
You also created a Table View Controller that acts as both the searchResultsController and searchResultsUpdater. In the next section, you’ll use Apple’s local search API to populate the table with real data.
There is more than one way to configure UISearchController. For example, your content might already be in table format and you just want to filter it in-place. For this use case, I recommend checking out this UISearchController tutorial on raywenderlich.com.
Search for locations using MKLocalSearchRequest
Apple provides a location search API for querying places. This API is free, which is one reason you might use native MapKit over a 3rd party SDK like Google Maps.
1. Wire up map elements
a. At the top of LocationSearchTable.swift, add the following line:
import MapKit
You did this in ViewController.swift, but you’ll also need it for LocationSearchTable.swift.
b. Within the LocationSearchTable class body, add the following variables:
var matchingItems:[MKMapItem] = [] var mapView: MKMapView? = nil
matchingItems: You will use this later on to stash search results for easy access.
mapView: Search queries rely on a map region to prioritize local results. The mapView variable is a handle to the map from the previous screen. You’ll wire this up in the next step.
c. Switch to ViewController.swift, and append the following line to the contents of viewDidLoad:
locationSearchTable.mapView = mapView
This passes along a handle of the mapView from the main View Controller onto the locationSearchTable.
2. Set up the API call
Switch back again to LocationSearchTable.swift, and replace updateSearchResultsForSearchController(_:) with the following code:
func updateSearchResultsForSearchController(searchController: UISearchController) { guard let mapView = mapView, let searchBarText = searchController.searchBar.text else { return } let request = MKLocalSearchRequest() request.naturalLanguageQuery = searchBarText request.region = mapView.region let search = MKLocalSearch(request: request) search.startWithCompletionHandler { response, _ in guard let response = response else { return } self.matchingItems = response.mapItems self.tableView.reloadData() } }
The guard statement unwraps the optional values for mapView and the search bar text.
MKLocalSearchRequest: A search request is comprised of a search string, and a map region that provides location context. The search string comes from the search bar text, and the map region comes from the mapView.
MKLocalSearch performs the actual search on the request object. startWithCompletionHandler() executes the search query and returns a MKLocalSearchResponse object which contains an array of mapItems. You stash these mapItems inside matchingItems, and then reload the table.
3. Set up the Table View Data Source
Now that data is coming back from the API, it’s time to wire up the UI.
Add another class extension to the bottom of LocationSearchTable.swift:
extension LocationSearchTable { override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return matchingItems.count } override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCellWithIdentifier("cell")! let selectedItem = matchingItems[indexPath.row].placemark cell.textLabel?.text = selectedItem.name cell.detailTextLabel?.text = "" return cell } }
This extension groups all the UITableViewDataSource methods together.
The matchingItems array determines the number of table rows.
Each cell was configured with an identifier of cell in a previous section. The cell’s built-in textLabel is set to the placemark name of the Map Item.
The cell’s detailTextLabel is set to an empty string for now. You will populate this with the address later on.
Build and run
This is what the code looks like so far:
Select the search bar and start searching for something like “Target”. You should see search results populate the table.
4. Add the placemark address
So far, you’ve only used the placemark name. Placemarks have other properties like subThoroughfare and administrativeArea which correspond to street number and state. You can piece together these properties to form a full postal address.
In this section, you will squeeze the postal address into a single line so that it fits inside the cell’s detailTextLabel.
a. Add the following method within the LocationSearchTable class body:
func parseAddress(selectedItem:MKPlacemark) -> String { // put a space between "4" and "Melrose Place" let firstSpace = (selectedItem.subThoroughfare != nil && selectedItem.thoroughfare != nil) ? " " : "" // put a comma between street and city/state let comma = (selectedItem.subThoroughfare != nil || selectedItem.thoroughfare != nil) && (selectedItem.subAdministrativeArea != nil || selectedItem.administrativeArea != nil) ? ", " : "" // put a space between "Washington" and "DC" let secondSpace = (selectedItem.subAdministrativeArea != nil && selectedItem.administrativeArea != nil) ? " " : "" let addressLine = String( format:"%@%@%@%@%@%@%@", // street number selectedItem.subThoroughfare ?? "", firstSpace, // street name selectedItem.thoroughfare ?? "", comma, // city selectedItem.locality ?? "", secondSpace, // state selectedItem.administrativeArea ?? "" ) return addressLine }
This method converts the placemark to a custom address format like: “4 Melrose Place, Washington DC”.
It’s not important to know how the above code works. But basically, it adds commas and spaces, even if some of the properties come back as nil. For example, it doesn’t make sense to split up the string with a comma if the street information is missing.
Note: there’s a method ABCreateStringWithAddressDictionary(_:_:) that is part of the AddressBookUI framework which parses an address dictionary into a string. However, this was deprecated in iOS 9 and doesn’t give us exactly what we need.
b. Populate the cell with the address
Now that we have a postal address, it’s time to add it to the subtitle cell. Within cellForRowAtIndexPath, replace the detailTextLabel text assignment with the following code:
cell.detailTextLabel?.text = parseAddress(selectedItem)
Build and run
This is what the code looks like so far:
- ViewController.swift (no change)
- LocationSearchTable.swift
Now when you search for “McDonalds”, you should see the address.
Section Recap
MKLocalSearchRequest is a free API that returns locations based on a search string and map region. You used the UISearchResultsUpdating delegate to call this API each time the search text changed. You then displayed the name and address of the search results in the table view.
Drop a Map Pin and create a callout button
In this section, you will drop a pin on the map whenever the user taps on a search result. Since the map and table view are in two separate controllers, you’ll use a protocol to communicate between the two. You’ve been using protocols and delegates throughout this tutorial. But now you’ll be creating your own custom protocol.
Think of a protocol as a job description, and a delegate as an employee. The delegate is the one that actually performs the job, and it can assume multiple roles as long as he has the skills. Imagine an understaffed fast food joint where employees need to wear multiple “hats”. The guy taking your order is both the cashier, and the french fry guy. In this analogy, one delegate conforms to two protocols.
In this section, you will create a special job duty, or protocol, for the map controller. This role has just one responsibility, which is to drop a pin on the map. The table view controller will call this method whenever the user selects a search result.
1. Create the protocol
a. At the top of ViewController.swift, add the following protocol underneath the import statements, but above the class body:
protocol HandleMapSearch { func dropPinZoomIn(placemark:MKPlacemark) }
This declares a custom protocol named HandleMapSearch. Anything that conforms to this protocol has to implement a method called dropPinZoomIn(_:).
Note: There aren’t any curly braces on the method. The protocol definition is only interested in the method signatures — the implementation goes somewhere else.
b. Within the ViewController class body, add the following variable:
var selectedPin:MKPlacemark? = nil
You’ll use this later on to cache any incoming placemarks. More on this later.
c. At the bottom of ViewController.swift, add another class extension:
extension ViewController: HandleMapSearch { func dropPinZoomIn(placemark:MKPlacemark){ // cache the pin selectedPin = placemark // clear existing pins mapView.removeAnnotations(mapView.annotations) let annotation = MKPointAnnotation() annotation.coordinate = placemark.coordinate annotation.title = placemark.name if let city = placemark.locality, let state = placemark.administrativeArea { annotation.subtitle = "(city) (state)" } mapView.addAnnotation(annotation) let span = MKCoordinateSpanMake(0.05, 0.05) let region = MKCoordinateRegionMake(placemark.coordinate, span) mapView.setRegion(region, animated: true) } }
This extension implements the dropPinZoomIn() method in order to adopt the HandleMapSearch protocol.
The incoming placemark is cached in the selectedPin variable. This will be useful later when you create the callout button.
removeAnnotations() clears the map of any existing annotations. This step is to ensure we’re only dealing with one annotation pin on the map at a time.
MKPointAnnotation is a map pin that contains a coordinate, title, and subtitle. The placemark has similar information like a coordinate and address information. Here you populate the title and subtitle with information that makes sense.
mapView.addAnnotation() adds the above annotation to the map.
setRegion() zooms the map to the coordinate. You create a span to specify a zoom level, just like you did in a previous section.
2. Wire up the protocol
a. Append the following code to the contents of viewDidLoad:
locationSearchTable.handleMapSearchDelegate = self
The parent (ViewController) passes a handle of itself to the child controller (LocationSearchTable). This variable doesn’t exist yet, so you’ll create it in the next step.
b. Switch to LocationSearchTable.swift, and add the following variable:
var handleMapSearchDelegate:HandleMapSearch? = nil
This should silence the compiler error from the previous step.
c. At the bottom of LocationSearchTable.swift, add another class extension:
extension LocationSearchTable { override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) { let selectedItem = matchingItems[indexPath.row].placemark handleMapSearchDelegate?.dropPinZoomIn(selectedItem) dismissViewControllerAnimated(true, completion: nil) } }
This class extension groups UITableViewDelegate methods together.
When a search result row is selected, you find the appropriate placemark based on the row number. Then you pass the placemark to the map controller via the custom protocol method. Finally, you close the search results modal so the user can see the map.
Build and run
This is what the code looks like so far:
Type “Target” into the search bar and tap on one of the search result rows. The search table should dismiss and you should see a red pin with the name and city of the selected item.
3. Wire a button to the pin callout
You’re almost done! The last step is to customize the map pin callout with a button that takes you to Apple Maps for driving directions.
In ViewController.swift, add the following class extension:
extension ViewController : MKMapViewDelegate { func mapView(mapView: MKMapView, viewForAnnotation annotation: MKAnnotation) -> MKAnnotationView?{ if annotation is MKUserLocation { //return nil so map view draws "blue dot" for standard user location return nil } let reuseId = "pin" var pinView = mapView.dequeueReusableAnnotationViewWithIdentifier(reuseId) as? MKPinAnnotationView pinView = MKPinAnnotationView(annotation: annotation, reuseIdentifier: reuseId) pinView?.pinTintColor = UIColor.orangeColor() pinView?.canShowCallout = true let smallSquare = CGSize(width: 30, height: 30) let button = UIButton(frame: CGRect(origin: CGPointZero, size: smallSquare)) button.setBackgroundImage(UIImage(named: "car"), forState: .Normal) button.addTarget(self, action: "getDirections", forControlEvents: .TouchUpInside) pinView?.leftCalloutAccessoryView = button return pinView } }
mapView(_:viewForAnnotation:) is a MKMapViewDelegate method that customizes the appearance of map pins and callouts. There’s a lot happening here, so let’s take this one step at a time.
MKUserLocation: The blue pulsing dot is a map annotation just like the map pins. You return nil so that you don’t interfere with the user’s blue location dot.
Pins are dequeued and reused just like table view cells. The reuseIdentifier is set to “pin” in this example.
MKPinAnnotationView is the map pin UI. You can set the pinTintColor, or determine whether it can show a callout. Although this is beyond the scope of this tutorial, you can also use a custom icon instead of the default pin.
leftCalloutAccessoryView: This is set to an UIButton that you instantiate programmatically. The button is set to a size of 30×30. The button image is set to an asset catalog image named car, and the button’s action is wired to a custom method called getDirections().
Note: you need to remember to create the car image, and implement the getDirections() method. Otherwise, the app will crash at runtime.
4. Implement the getDirections() method
Add the following method to the ViewController class:
func getDirections(){ if let selectedPin = selectedPin { let mapItem = MKMapItem(placemark: selectedPin) let launchOptions = [MKLaunchOptionsDirectionsModeKey : MKLaunchOptionsDirectionsModeDriving] mapItem.openInMapsWithLaunchOptions(launchOptions) } }
Here you use an API call that launches the Apple Maps app with driving directions. You convert the cached selectedPin to a MKMapItem. The Map Item is then able to open up driving directions in Apple Maps using the openInMapsWithLaunchOptions() method.
5. Create image assets for the car button icon
In the Project Navigator, open Assets.xcassets and create a New Image Set named car. You will need PNG files of various sizes (30×30, 60×60, and 90×90) to fill up the 1x, 2x, and 3x slots, respectively.
Go to fa2png.io and type in fa-car as the icon name. Then download the icon sizes listed above and drag them into the corresponding slots within the Xcode asset catalog.
Build and run
This is what the code looks like so far:
- ViewController.swift
- LocationSearchTable.swift (No change)
Type “Target” into the search bar. Tap on a search result and look for the pin on the map. Tap the pin to reveal the callout, and tap the car button to app-switch to Apple Maps for driving directions.
Section Recap
In this section, you used the custom protocol to pass the placemark information back to the map page. Then you loaded up an annotation with the placemark data and dropped a pin on the map.
You implemented a Map View delegate method that customizes the appearance of the pin when it is rendered. Besides making the pin orange, you created a callout button that takes the user to Apple Maps for driving directions.
Conclusion
You can download the final version of the project here. This is a really long tutorial, so good job for reading this far!
When adding a map feature to your app, chances are that you’ll need user location, map search, and some mechanism for displaying search results. There are good tutorials out there on how each component works individually. But piecing everything together takes a little bit of work. Hopefully this tutorial will help you build out your next map search feature.
Got any tips for working with maps and location? Have any suggestions for future tutorial topics? Feel free to add your thoughts to the comments.
Like this post? Please share it! Then follow us on Twitter @thorntech or join our mailing list below for future updates.
Hi, a great tutorial as all the pieces have been put together.But im working on a project in objective c.Can you please post the objective C version of code.I havent worked on swift yet.Thanks
I did a quick and dirty translation of the sample project:
https://github.com/ThornTechPublic/MapKitTutorialObjC
One thing: I didn’t know how to do nil coalescing in Objective-C, so there’s a lot of verbose ternary operators littering the code.
Please let me know if you run into any issues. Also, feel free to submit a pull request with code improvements. Thanks!
Rob
very nice tutorial, but in am working in swift 1.2, can you please convert this code for swift 1.0
thanks,
ios dev
I am working in swift 1.0, can you please convert this code for swift 1.0
thanks,
ios dev
Unfortunately, I wont be converting any tutorials to earlier versions of Swift. If you want to hire us, we’d be happy to help you update your Swift code =) But seriously, keeping up to date is the main trade-off of using Swift over Objective-C. And we’ll have to update all the code yet again when Swift 3.0 comes out later this year…
You can view the changes for Swift 3 in
https://github.com/waverun/MapKitTutorial-swit_3-xcode_8/tree/master
Thanks Shay!
I am not sure.
Please let me know correct method you have.
Thanks
Hi I want to detect cancel button but can’t figure out how to do it.
Can you help me with this problem?
Thanks in advance.
Ton
Hi Ton,
Within ViewController.swift, append the following line to viewDidLoad():
resultSearchController?.delegate = self
Then, at the bottom of ViewController.swift, add the following Extension:
extension ViewController : UISearchControllerDelegate {
func willDismissSearchController(searchController: UISearchController) {
print(“User dismissed the search controller by either 1) tapping the Cancel button, or 2) the dark gray area”)
}
}
The UISearchControllerDelegate protocol can detect the Cancel button, among other things.
Hope this helps!
Rob
Hi, a great tutorial as all the pieces have been put together. i’m working on a project in maps using coredata i am storing some places in coredata how to display in maps
Can you help me with this problem?
Thanks in advance.
I can try my best to help. Are you running into issues with fetching from core data? Or with adding annotations to the map?
Im trying to modify the code to work for say all targets.need some inputs
When constructing the MKLocalSearchRequest, you can try setting the region to a very large bounding box. However, I’ve found that this API tends to return just a handful of results near the center of the region. If I simply don’t set the region, the API tends to fall back on my current location as the center point.
If you want to get all Target or Starbucks or whatever stores, this sounds like something outside the scope of the MKLocalSearch API. You may need to use a Google search API.
How would I have it search a plist file? I do not want it to search api just a plist file that contains latitude longitude and a title. I would like it to search for the matching title. Any help would be greatly appreciated. I am very new to this but am learning 🙂 thanks!
Hi Ed,
I’ve personally never used plist files as a backing store, but this is where I would start…
This looks like a pretty good article on hydrating a plist file using Swift: http://rebeloper.com/read-write-plist-file-swift/
Once you have your coordinate array in memory, you can use NSPredicate to match on title. Here’s a semi-related stack article on filtering plist data, and the last comment has some NSPredicate code:
http://stackoverflow.com/questions/28197648/read-array-from-plist-and-only-return-item-if-match-was-found
If you happen to own iOS Apprentice by Ray Wenderlich, they have a checklist app (Part 2, page 130-140) that uses a plist file as a backing store. That particular example doesn’t filter results with NSPredicate though.
One alternative example to using NSPredicate is to use string-contains-string:
let filteredResults = arrayData.filter { thePlace in
return thePlace.title.lowercaseString.containsString(searchText.lowercaseString)
}
This example is adapted from the Ray Wenderlich article referenced in the tutorial body:
https://www.raywenderlich.com/113772/uisearchcontroller-tutorial
Good luck,
Robert
Thank you so much for your quick and very thorough response. I will certainly be looking and hopefully learning from all the information you have provided. Thanks again!
Great tutorial 🙂 Im now working on project that receives two inputs (Start and Destination) Are there possible ways to add multiple searchbars?
Thank you in advance
I don’t think UISearchController supports multiple inputs. You might have to do something similar to Apple Maps. There’s a “driving direction” mode where tapping on the search bar opens a modal with fields for Start and Destination. In this mode, you would have to hook into the UISearchBarDelegate and open the modal upon user interaction. Then in the search bar, you would display something like “Home to Work“. This is how I would approach the issue, anyhow.
Just wanted to say thank you for putting this tutorial together! The last time I had taken a look for a MapKit search tutorial there was nothing close to this out there. You have saved me a lot of time trying to hack the same outcome together from multiple sources. Keep up the good work!
hey i was wondering if indian maps has some issue?
i tried searching for a route using ur project from trivandrum airport to kovalam kerala . i ain’t receiving a routing in this case, but then i tried California to some place near it everything worked well.. why such a problem?
It doesn’t look like Apple Maps support driving directions in India. I’m seeing the following error message: “Directions Not Available, A route could not be determined between these locations.” According to this StackOverflow article, this has been an issue since 2013: http://stackoverflow.com/questions/19035752/display-route-between-two-points-on-map-with-mapkit-ios7. Apple’s own documentation shows that India is missing from the Maps:Directions section: http://www.apple.com/ios/feature-availability/#maps-directions. This is my first time learning of this, and this issue is something I’ll have to consider moving forward when deciding between Apple Maps and the Google Maps SDK.
okay thanks for the info.. i will have to find an alternative thing as well then.. thanks again:) Mean while if there is any progress regarding the same do inform me as well..
Hi, I was just wondering is there any way to remove the cancel button? I tried calling searchBar!.showsCancelButton = false but nothing happens.
Also how can I make it so that the searchBar shows a specific string when the cancel button or gray area is pressed to dismiss the search? I tried using your code in the manner below but it does not set the searchBar text after dismissing.
func willDismissSearchController(searchController: UISearchController) {
print(“User dismissed the search controller by either 1) tapping the Cancel button, or 2) the dark gray area”)
self.searchBar!.text = “Hello”
}
Nevermind, I forgot there was another function didDismissSearchController so I figured out the second part of my question.
Is there anyway to not put the searchBar in the navigation bar and use a searchBar created through storyboard?
Most of the time, people tend to put the search bar inside the nav bar or table view header. It’s possible to put the search bar elsewhere, but it doesn’t seem to work well with the magic built into UISearchController.
I created a GitHub branch called “jonathan” with a first pass at doing this:
https://github.com/ThornTechPublic/MapKitTutorial/tree/jonathan
It wires up a container view on the Storyboard, and then drops in the search bar from the search controller. (I tried wiring up an UISearchBar object in the Storyboard and setting it to the search controller’s search bar but the delegates didn’t seem to work). If you try running the project, it looks a little awkward though.
TIL: I think the search controller automatically creates like a scrollview top inset or something so that the top result row lines up with the bottom of where the search bar is on the screen.
Thank you for the quick response!
And thank you so much for trying to implement it and making me a project just for my cause. I really appreciate it.
What I am trying to do is something like what Uber did. In the picture you provided, it shows that for Uber they did not put the SearchBar in the NavigationBar. But instead it almost looks like they connect a UITextField to a TableView and as the user typed into the Textfield, they searched for similar addresses and populated the TableView. Do you think they did this using the UISearchController as well?
Sorry, I would have responded earlier but I spent the last week playing with the files you gave me and trying to make it look/perform similar to how Uber does it. But I just started leraning xcode and swift so I can’t seem to figure it out.
Hi Jonathan,
was wondering if you had managed to get it to work? I’ve got a searchbar in a stackview on a MapView and I would like to link this search bar to a tableview that would filter through all the annotations I have on my map.
Did you manage to get this to work?
Hi Jonathan,
I struggled with that for quite a while too.
You can play with the following settings:
1. Hide searchBar & show title:
navigationItem.titleView = nil
2. Show searchBar & hide title:
navigationItem.titleView = resultSearchController?.searchBar
Great tutorial, is there any chance that you can show how this can be done using Firebase and GeoFire? I’m stuck at the part where the table list only nearby locations. Thanks in advance!
Thanks for the suggestion — making a Firebase and GeoFire tutorial is a great idea. Unfortunately, I won’t be able to get to this anytime soon. I haven’t used GeoFire yet, but I can’t seem to find any API calls other than queries within a region (center + radius) either.
How can the user save the location he/she searched to another view controller? Display the saved location in some way?
Thank you!
If you’re getting the following error:
Terminating app due to uncaught exception ‘NSInvalidUnarchiveOperationException’, reason: ‘Could not instantiate class named MKMapView’
Make sure you go to TARGETS > Capabilities > Maps. Then flip the switch from OFF to ON.
The error happens at runtime, right after you add an MKMapView to the Storyboard.
hi.
i’m Muhammad Zia-Ur-Rehman
your work is awesome.
i’m working on a project which is ios application in swift 2.1 Xcode 7.2
one component of may app is about user location i just want to find nearby hospital’s at my location.
please help me. this is important for me.
Sorry to keep you waiting. How can I help?
If you’re searching for Hospitals in particular, you can try to use Apple MapKit’s search for the String “hospital”. Although, this doesn’t seem like a real solution.
So, you might be better off using a real API that supports Places. For example, the Google Places API has a place “type”, which includes Hospital.
https://developers.google.com/places/supported_types
There’s a Ray Wenderlich tutorial that covers using the Google Places API with MapKit, although you might need to mentally translate some Objective-C.
I can’t seem to get this to work. I write the code but when I run the app there is no search bar. I should mention that I’m trying to add a searchbar to 1 of 2 views that are embedded in a tab view controller.
Thoughts?
Thanks!
Giselle
NM I figured it out, I need to put that VC in its own Nav controller. Thanks!
This is incredible. You have changed my whole mentality about articled tutorials. i thought they were not very effective because i never got a single one of them working. but i have gone through all this and i faced not even a single challenge. Thank you very much my friend. you’ve done a great job.
Thanks for the Tutorial, it is great!
I just have one question, which is how to place a search bar from the object library to the view controller and connect it to the resultSearchController’s searchBar. I’ve tried several ways, all failed.
Although in the tutorial, navigationItem.titleView is used to show the search bar, I’ve seen others like tableView.TableHeadView. Is there anyway I can attach the search bar to the top of mapView?
Thanks
Zack
Dear Zack,
Got your library search bar (or any view) connect to an IBOutlet var in View Controller and add this line of code:
self.searchBar.addSubview(resultSearchController!.searchBar)
I got this solution on this post: https://stackoverflow.com/questions/30756825/how-do-i-set-the-uisearchcontrollers-searchbar-to-a-view-that-isnt-the-tablehe
Tks!
Am using your objective c code for search location.
When I search particular place with name, then it shows single result only… which is correct result.. but i want list of suggested names of what I searched…. how can i achieve that?
I created a new branch to get you started:
https://github.com/ThornTechPublic/MapKitTutorial/tree/searchHistory
It does the following:
1. Creates a searchHistory struct that saves search terms to a NSUserDefaults
array, and fetches them as well. The fetch uses Set to discard duplicates.
2. When the user taps on a search result, whatever is inside the search bar is saved to the history.
3. I add a section to the search results table that contains search history terms. These are filtered against the search bar text.
4. Tapping on a search history item replaces the search bar text, which triggers a search results table refresh.
Again, this is just a first pass — you might want to polish it up some.
Hi, I am having trouble with getting directions to the place. The car wont pop up in the view, so thats the start.
Lukas, thanks for spotting this regression. I have pushed up a commit that addresses this issue: https://github.com/ThornTechPublic/MapKitTutorial/commit/725fa57cd0e50e9234696c20fdeff54611f62c41. The pins were getting dequeued, but never created in the first place.
I just tried your solutions yet no luck :/ I am running on Swift 3 btw.
If you email me what you have at robert.chen@thorntech.com, I can take a look. Thanks!
Just sent it to you.
I have the same question as Dan E from 4/24/16.
“How can the user save the location he/she searched to another view controller? Display the saved location in some way?”
If you have a button on the annotation view, you could pass the placemark in prepareForSegue. The child detail view could have a small map that zooms to that placemark and drops a pin.
If you want to save locations, like Work or Home, you could maybe add a star to the search results table row. And in the search results table, maybe have a section zero that shows the user’s favorites (if there are any). For starters, you could persist these favorite locations to NSUserDefaults.
Sorry if this sounds kind of vague.
this is the best, most thorough and clear ios/swift tutorial i have ever seen. thank you!
Thanks Robert for this great work. Just fantastic!
In my experimentation, I wanted to trigger creation of the search bar when I clicked a text field. So I moved the search bar creation code from under viewDidLoad into a func block. But I was having to click it twice before the search bar appeared. Any idea as to why the delay? How do I get around this?
This has been solved. Basically, I wanted to call the function that creates the search bar when I clicked on a UITextField. I also wanted to surpress the keyboard from showing when I clicked on the UITextField.
override func viewDidAppear(_ animated: Bool)
{
myTextField.addTarget(self, action: #selector(ViewController.textFieldShouldBeginEditing(textField:)), for: UIControlEvents.editingDidBegin)
// prevent default keyboard from appearing when UITextField is clicked
myTextField.inputView = UIView()
}
func textFieldShouldBeginEditing(textfield: UITextField)
{
showSearchBar()
}
func showSearchBar
{
// your docde to show search bar
…
}
Hi, this tutorial is great. Thank you for sharing. I am working on a project where the users’ location and the search location are both in a table view controller. I can get the lat/long for the users location but unable to get the lat, long from the searched location, which is the destination, how can I extract the lat/long for the searched location?
Thanks for your kind words Liz! The MKLocalSearchRequest completion handler should give back a response with an array of mapItems. Each mapItem should have a placemark, containing a coordinate.
Tutorial is very nice but direction function is not working why can u give me suggestion how to get direction one place to another in swift 3.0
This is one of the best tutorials I have ever seen! Love the animation to show how the completed code should work and the modularity in the code from the use of extensions. Great job!
Hi thank you for this great code and explanation.
Just a quick code update. You original code :
extension LocationSearchTable : UISearchResultsUpdating {
func updateSearchResultsForSearchController(searchController: UISearchController) {
}
}
needs to be changed to :
extension LocationSearchTable : UISearchResultsUpdating {
func updateSearchResults(for searchController: UISearchController) {
}
}
in order not to get a compiler error.
Thanks
Thanks for your feedback! I need to update this tutorial for Swift 3 one of these days…
Please suggest how to drag and drop pin and get longitude and latitude
I’m not sure about dragging and dropping pins, but I do know that the latitude and longitude can be accessed through the placemark. ‘placemark’ has attributes that can give you the latitude and longitude.
Hi, how i can put the search bar under the navigation controller?
Hello, How can I put the search bar outside navigation controller? Like uber app, the searcher is on top the map
I have only experimented with this approach a little bit, but you could try adding a UIView onto the Storyboard, and using that as a container for the search bar. I posted an answer containing a code snippet on stackoverflow, but it’s kind of dated:
http://stackoverflow.com/questions/26417591/uisearchcontroller-in-a-uiviewcontroller/32275537#32275537
Awesome tutorial it was almost exactly what I needed but I need an extra part, could you help me implement that?
I am showing some pins on map that I am getting from an array (that I populate the array by downloading some Json files).
how can I search through this pins?
I tried a lot by changing your code but didn’t work. (one main problem is getting the filled-array from view Controller)
Abed,
If they are truly pins (annotations) on the map:
let allPins = self.map.annotations as [MKAnnotation] // This returns an array of all annotations
Then you can use a simply for annotation in allPins loop to sift through them.
Thank you, my problem is solved. but now I have only one problem,
I am not putting my searchbar in navigation bar, instead, a bit lower, because I have a logo and a menu button in NavigationBar.
the problem is that when I tap the searchbar everything is fine, but as soon as I start typing the navigation bar (with the logo and button) disappears and I just see white space instead.
what can I do?
I won’t be able to give you a good answer without actually debugging the project. But I would start with using Debug View Hierarchy to see what’s actually going on. I would also keep an eye on the definesPresentationContext setting for involved view controllers. Good luck!
if i want to show the tableview without covering the background view what i supposed to do…
You could try opening a modal with a transparent background (containing the tableview) and present it over the current context. If UISearchController wont let you do this, you might have to do things manually.
Wonderful tutorial however it it outdated for Swift 3.. I’ve managed to fix everything except for the final extension in the LocationSearchTable.swift file.
This function:
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return matchingItems.count
}
Produces this error:
“Argument names for method ‘tableView(tableView:numberOfRowsInSection:) do not match those of overridden method”
This function:
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: “cell”)!
let selectedItem = matchingItems[indexPath.row].placemark
cell.textLabel?.text = selectedItem.name
cell.detailTextLabel?.text = parseAddress(selectedItem: selectedItem)
return cell
}
Produces this error:
“Method does not override any method from its superclass”
I would appreciate any help on this matter.
Just delete the override keyword, if you haven’t already.
If anyone got this working for Swift 3, please let me know, as I got stuck over halfway when it won’t load a table view cell when I type into search bar.
You do need to override those methods, they just have different signatures in Swift3/4. Here is a version of LocationSearchTable in Swift 4: https://gist.github.com/iannase/b509f188586f72941ba5e47ce51b0373
Is there a way to search cities only without business or POIs?
I want to set a label on a whole different view controller as the address selected by the user. Is there any way I can do this?
Hey very nice Tutorial!
Hi, this is a great tutorial. I’m using Xcode 8.3.3 with Swift 3, and applying the steps here to my existing app. I’m getting locations, the map view is displaying, but the search bar is not displaying. The setup code, in viewDidLoad is running, and the objects created dynamically are non-nil (resultSearchController, searchBar). Can you suggest an approach to attacking this problem?
Thank you.
I ran into a problem regarding the table view not updating the search results. I have followed the code as accurately as possible but I still don’t know what the problem is. What are the key areas to look at when solving this bug? I can give anyone additional information if they need it.
Hi, great tutoria, thanks for this 🙂
I have only one question (for now):
Is it possible to show in the result table only those locations/objects/results that are nearest to the user location ?
Thank you Sir! very good tutorial, love it!
Can you pls update this for swift 3, Xcode 8.3.3? 🙂
Great tut!
Xcode helps you with most of the steps needed to transform this to Swift4 (oct 2017)
One thing I overlooked that caused my search results not to update was that I had:
`func updateSearchResultsForSearchController(searchController: UISearchController)`
in my LocationSearchTable.swift file, swap this function for the new:
`func updateSearchResults(for searchController: UISearchController)` and voila!
Hello,
I have having problems with mapView.
I put this in mapView.setRegion(region, animated: true) and it gives me an error of this:
Thread 1: Fatal error: Unexpectedly found nil while unwrapping an Optional
I am working on Xcode 9 and Swift 4.
Could you please help?
Thanks
Great tutorial. It saved tons of time, efforts going through and assembling different codes, and solving errors. Thanks 🙂
Can someone upload the swift 4 version, I have tried several times and I cannot convert it to swift 4.
Thank you
Hello,
Is it possible to have the annotation view with the car show without having to tap on the pin. similar to how the native maps app does it. Any help will be appreciated.
Thank you
Just thank you. This is amazing!
Hi,
when i opened default apple map from my app, it is possible to set an images in annotation pin that is opening in default apple map.Because of i am able to send direction, but not able to change default pin.
For iOS 11 do not use
navigationItem.titleView = resultSearchController?.searchBar
use instead:
navigationItem.searchController = resultSearchController
Looks much nicer and won’t end in strange layouted additional BarButtonitems
u made my day
thank you
how to use google api for “place type” with same code (swift) that you have explained
.
Two years later and still a great tutorial. Thank you Robert!
Swift 4 Version of LocationSearchTable: https://gist.github.com/iannase/b509f188586f72941ba5e47ce51b0373
Great tutorial ! I translated it to Objective-C and works like charm 🙂
I am getting an warning message and the app is crashing when I try to type inside the search bar. I put a print statement inside the “updateSearchResults” function to catch the warning message and this is what I get “could not execute support code to read Objective-C class in the process. This may reduce the quality of type information available”. Since I am using Swift 4 its method may no longer be supported.
Any help is greatly appreciated!
can i get the same code for Xcode Version 8.2.1 (8C1002)
and swift 3.0
Thank you for the tutorial. How do I add “custom” pins to nearby places such as businesses?
It’s been a while since I’ve worked with MapKit. There’s a pretty good tutorial on Ray Wenderlich: https://www.raywenderlich.com/160517/mapkit-tutorial-getting-started with a section on “Customizing Annotations”. I would check that out. Good luck!