Creating an iOS Place Picker / Places Autocomplete / Search UI using Mapbox API

Although not as popular as Google, Mapbox provides a very useful Places API which many of you may prefer since Google APIs introduced billing on their services. Mapbox doesn’t provide an iOS SDK for Places, so we will be using the endpoints based on their Geocoder API, to make our requests and fetch places.

Getting Started

Before you begin, Sign up for an account at mapbox.com/signup. Find your access token on your account page.

Open info.plist file and the following key, with value as the access token you received from MapBox in the first step.

<key>MGLMapboxAccessToken</key>
<string>YOUR_TOKEN</string>

Assuming you have created your XCode project, add these pods to your Podfile.

pod 'Mapbox-iOS-SDK', '~> 4.9'
pod 'MapboxGeocoder.swift', '~> 0.10'
pod 'Alamofire'
pod 'Alamofire-SwiftyJSON'

We will be using Alamofire for our requests to Mapbox API endpoints, and SwiftyJSON for parsing the responses.

In your terminal, run ‘pod install’.

Setting up the UI

Assuming you have created your Xcode project, add a UIViewController, make sure it is embedded inside a UINavigationController, since we will be adding our UISearchBar to the Navigation Bar.

Add a UITableView to completely fill the view, and create an outlet for it in your ViewController Swift class file.

Import the necessary modules

import UIKit
import Alamofire
import SwiftyJSON
import Alamofire_SwiftyJSON

Implement the delegates

class PlacesSearchVC: UIViewController, UITableViewDelegate, UITableViewDataSource, UISearchBarDelegate {
    override func viewDidLoad() {
        super.viewDidLoad()
    }
}

Inside the class, declare the objects

@IBOutlet var tableView: UITableView!      //outlet to the tableview you created in storyboard
var searchActive : Bool = false  //for controlling search states
var searchBar:UISearchBar?   //the searchbar to be added in navigation bar
var searchedPlaces: NSMutableArray = []   //array to store the places returned in response
let decoder = JSONDecoder()    //for decoding data returned by API

Add searchBar to the navigation bar in viewDidLoad()

if self.searchBar == nil {
    self.searchBar = UISearchBar()
    self.searchBar!.searchBarStyle = UISearchBarStyle.prominent
    self.searchBar!.tintColor = Helper.UIColorFromRGB(rgbValue: 0x000000)
    self.searchBar!.barTintColor = Helper.UIColorFromRGB(rgbValue: 0xffffff)
    self.searchBar!.delegate = self
    self.searchBar!.placeholder = "Search for place";
}        
self.navigationItem.titleView = searchBar

Create delegate functions of the UISearchBar to handle the events

    func searchBarCancelButtonClicked(_ searchBar: UISearchBar) {
        self .cancelSearching()
        searchActive = false;
    }
    
    func searchBarSearchButtonClicked(_ searchBar: UISearchBar) {
        self.view.endEditing(true)
    }
    
    func searchBarTextDidBeginEditing(_ searchBar: UISearchBar) {
        self.searchBar!.setShowsCancelButton(true, animated: true)
    }
    
    func searchBarTextDidEndEditing(_ searchBar: UISearchBar) {
        self.searchBar!.setShowsCancelButton(false, animated: false)
    }
    
    func cancelSearching(){
        searchActive = false;
        self.searchBar!.resignFirstResponder()
        self.searchBar!.text = ""
    }

Now let’s add the main method that will call the search function. We want to wait a moment until the user pauses typing, and then fire the method, and in doing so, cancel all previous requests that may have been triggered.

    func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {        
        NSObject.cancelPreviousPerformRequests(withTarget: self, selector: #selector(self.searchMe), object: nil)
        self.perform(#selector(self.searchMe), with: nil, afterDelay: 0.5)     
        if(searchBar.text!.isEmpty){
            searchActive = false;
        } else {
            searchActive = true;
        }
    }

    @objc func searchMe() {
        if(searchBar?.text!.isEmpty)!{ } else {
            self.searchPlaces(query: (searchBar?.text)!)
        }
    }

Searching

For searching, create the API URL first. Define the mapbox endpoint and the access token as strings

 static var mapbox_api = "https://api.mapbox.com/geocoding/v5/mapbox.places/"
 static var mapbox_access_token = "YOUR_ACCESS_TOKEN_HERE"

Create a new class Feature.swift and define the structure of the response, so that we are able to parse the response directly.

import UIKit

struct Feature: Codable {
    var id: String!
    var type: String?
    var matching_place_name: String?
    var place_name: String?
    var geometry: Geometry
    var center: [Double]
    var properties: Properties
}

struct Geometry: Codable {
    var type: String?
    var coordinates: [Double]
}

struct Properties: Codable {
    var address: String?
}

Now create the search function.

    @objc func searchPlaces(query: String) {
        let urlStr = "\(mapbox_api)\(query).json?access_token=\(mapbox_access_token)"

        Alamofire.request(urlStr, method: .get, parameters: nil, encoding: URLEncoding.default, headers: nil).responseSwiftyJSON { (dataResponse) in
            
            if dataResponse.result.isSuccess {
                let resJson = JSON(dataResponse.result.value!)
                if let myjson = resJson["features"].array {
                    for itemobj in myjson ?? [] {
                        try? print(itemobj.rawData())                   
                        do {
                            let place = try self.decoder.decode(Feature.self, from: itemobj.rawData())                            
                            self.searchedPlaces.add(place)
                            self.tableView.reloadData()
                        } catch let error  {
                            if let error = error as? DecodingError {
                                print(error.errorDescription)
                            }
                        }
                    }
                }
            }
            
            if dataResponse.result.isFailure {
                let error : Error = dataResponse.result.error!                
            }
        }
    }

That’s it! We have fetched the data and parsed it and saved it into the array. Now let’s write the code to display it into the table.

Displaying the data in the table

Write the UITableView delegate methods

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = UITableViewCell.init(style: .subtitle, reuseIdentifier: "cell")
        cell.detailTextLabel?.textColor = UIColor.darkGray
    
            let pred = self.searchedPlaces.object(at: indexPath.row) as! Feature
            cell.textLabel?.text = pred.place_name!
            if let add = pred.properties.address {
                cell.detailTextLabel?.text = add
            } else { }
        cell.imageView?.image = UIImage.init(icon: .fontAwesome(.mapMarker), size: CGSize(width:30,height:30))
        
        return cell
    }
    
    func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
        return 60.0
    }

If you want to get coordinates from this Feature object, here’s how you do it

func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
    let cell = tableView.cellForRow(at: indexPath)
    let pred = self.searchedPlaces.object(at: indexPath.row) as! Feature
    let coord = CLLocationCoordinate2D.init(latitude: pred.geometry.coordinates[1], longitude: pred.geometry.coordinates[0])
}


Also published on Medium.

By |2019-04-01T19:22:23+00:00March 27th, 2019|Categories: iOS Tutorials|Tags: , , , , , |3 Comments

3 Comments

  1. Jammu March 28, 2019 at 10:31 am - Reply

    Thanks for the tutorials. Really Appriciate your work Mam.

  2. Ali Zuberi September 4, 2019 at 9:41 pm - Reply

    hey does this also naviagtite it the to the adress

    • Zeba Rahman November 5, 2019 at 3:40 pm - Reply

      That is not covered here. But you have the address coordinates and other data, you can implement navigation or anything according to your requirement.

Leave A Comment