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

Zeba Rahman
Mar 27, 2019 · 4 min read

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])
}


Originally published at Fabcoding.

fabcoding

app & web development tutorials & tips

Zeba Rahman

Written by

App Developer | fabcoding.com

fabcoding

fabcoding

app & web development tutorials & tips

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade