Skip to main content

Place Autocomplete with custom UI elements

This example shows how to add autocomplete search with a custom UI using the Mapbox Search SDK for iOS, providing an interactive search and result display experience. The PlaceAutocompleteMainViewController manages user input through a search bar, fetching location suggestions in real-time, and displaying them in a table view. It leverages the PlaceAutocomplete class to fetch suggestions based on user input, proximity, and filters, while also managing location access permissions. When a suggestion is selected, the result is passed to PlaceAutocompleteResultViewController for detailed display.

The PlaceAutocompleteResultViewController visualizes the selected result by displaying its details in a table view and adding corresponding annotations to a Mapbox map. It provides a smooth camera transition to the suggestion's coordinates and organizes key components such as name, type, distance, and categories into a user-friendly format. Together, these view controllers showcase how to integrate Place Autocomplete functionality into a user interface, combining search, map visualization, and structured result presentation.

iOS Demos App Available

This example code is part of the Search SDK for iOS Demos App, a working iOS project available on Github. iOS developers are encouraged to run the demos app locally to interact with this example in an emulator and explore other features of the Search SDK.

The code below may depend on additional classes that are not part of the Search SDK itself, but are part of the demo app. You can find the full source code for the demo app in the Mapbox Search iOS repository

PlaceAutocompleteMainViewController.swift
import MapboxSearch
import UIKit

final class PlaceAutocompleteMainViewController: UIViewController {
@IBOutlet private var tableView: UITableView!
@IBOutlet private var messageLabel: UILabel!

private lazy var placeAutocomplete = PlaceAutocomplete()

private var cachedSuggestions: [PlaceAutocomplete.Suggestion] = []

let locationManager = CLLocationManager()

override func viewDidLoad() {
super.viewDidLoad()

locationManager.requestWhenInUseAuthorization()
locationManager.startUpdatingLocation()
configureUI()
}
}

// MARK: - UISearchResultsUpdating

extension PlaceAutocompleteMainViewController: UISearchResultsUpdating {
func updateSearchResults(for searchController: UISearchController) {
guard let text = searchController.searchBar.text,
!text.isEmpty
else {
cachedSuggestions = []

reloadData()
return
}

placeAutocomplete.suggestions(
for: text,
proximity: locationManager.location?.coordinate,
filterBy: .init(types: [.POI], navigationProfile: .driving)
) { [weak self] result in
guard let self else { return }

switch result {
case .success(let suggestions):
cachedSuggestions = suggestions
reloadData()

case .failure(let error):
print(error)
}
}
}
}

// MARK: - UITableViewDataSource & UITableViewDelegate

extension PlaceAutocompleteMainViewController: UITableViewDataSource, UITableViewDelegate {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
cachedSuggestions.count
}

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cellIdentifier = "suggestion-tableview-cell"

let tableViewCell: UITableViewCell = if let cachedTableViewCell = tableView
.dequeueReusableCell(withIdentifier: cellIdentifier)
{
cachedTableViewCell
} else {
UITableViewCell(style: .subtitle, reuseIdentifier: cellIdentifier)
}

let suggestion = cachedSuggestions[indexPath.row]

tableViewCell.textLabel?.text = suggestion.name
tableViewCell.accessoryType = .disclosureIndicator

var description = suggestion.description ?? ""
if let distance = suggestion.distance {
description += "\n\(PlaceAutocomplete.Result.distanceFormatter.string(fromDistance: distance))"
}
if let estimatedTime = suggestion.estimatedTime {
description += "\n\(PlaceAutocomplete.Result.measurementFormatter.string(from: estimatedTime))"
}

tableViewCell.detailTextLabel?.text = description
tableViewCell.detailTextLabel?.textColor = UIColor.darkGray
tableViewCell.detailTextLabel?.numberOfLines = 3

return tableViewCell
}

func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)

placeAutocomplete.select(suggestion: cachedSuggestions[indexPath.row]) { [weak self] result in
switch result {
case .success(let suggestionResult):
let resultVC = PlaceAutocompleteResultViewController.instantiate(with: suggestionResult)
self?.navigationController?.pushViewController(resultVC, animated: true)

case .failure(let error):
print("Suggestion selection error \(error)")
}
}
}
}

// MARK: - Private

extension PlaceAutocompleteMainViewController {
private func reloadData() {
messageLabel.isHidden = !cachedSuggestions.isEmpty
tableView.isHidden = cachedSuggestions.isEmpty

tableView.reloadData()
}

private func configureUI() {
configureSearchController()
configureTableView()
configureMessageLabel()
}

private func configureSearchController() {
let searchController = UISearchController(searchResultsController: nil)
searchController.searchResultsUpdater = self
searchController.obscuresBackgroundDuringPresentation = false
searchController.searchBar.placeholder = "Search"
searchController.searchBar.returnKeyType = .done

navigationItem.searchController = searchController
}

private func configureMessageLabel() {
messageLabel.text = "Start typing to get autocomplete suggestions"
}

private func configureTableView() {
tableView.tableFooterView = UIView(frame: .zero)

tableView.delegate = self
tableView.dataSource = self

tableView.isHidden = true
}
}
PlaceAutocompleteDetailsViewController.swift
import MapboxMaps
import MapboxSearch
import MapKit
import UIKit

final class PlaceAutocompleteResultViewController: UIViewController {
@IBOutlet private var tableView: UITableView!
@IBOutlet private var mapView: MapView!
lazy var annotationsManager = mapView.makeClusterPointAnnotationManager()

private var result: PlaceAutocomplete.Result!
private var resultComponents: [(name: String, value: String)] = []

static func instantiate(with result: PlaceAutocomplete.Result) -> PlaceAutocompleteResultViewController {
let storyboard = UIStoryboard(
name: "Main",
bundle: .main
)

let viewController = storyboard.instantiateViewController(
withIdentifier: "PlaceAutocompleteResultViewController"
) as? PlaceAutocompleteResultViewController

guard let viewController else {
preconditionFailure()
}

viewController.result = result
viewController.resultComponents = result.toComponents()

return viewController
}

override func viewDidLoad() {
super.viewDidLoad()

prepare()
}

func showAnnotations(results: [PlaceAutocomplete.Result], cameraShouldFollow: Bool = true) {
annotationsManager.annotations = results.compactMap {
PointAnnotation.pointAnnotation($0)
}

if cameraShouldFollow {
cameraToAnnotations(annotationsManager.annotations)
}
}

func cameraToAnnotations(_ annotations: [PointAnnotation]) {
if annotations.count == 1, let annotation = annotations.first {
mapView.camera.fly(
to: .init(center: annotation.point.coordinates, zoom: 15),
duration: 0.25,
completion: nil
)
} else {
do {
let cameraState = mapView.mapboxMap.cameraState
let coordinatesCamera = try mapView.mapboxMap.camera(
for: annotations.map(\.point.coordinates),
camera: CameraOptions(cameraState: cameraState),
coordinatesPadding: UIEdgeInsets(top: 24, left: 24, bottom: 24, right: 24),
maxZoom: nil,
offset: nil
)

mapView.camera.fly(to: coordinatesCamera, duration: 0.25, completion: nil)
} catch {
_Logger.searchSDK.error(error.localizedDescription)
}
}
}
}

// MARK: - TableView data source

extension PlaceAutocompleteResultViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
result == nil ? .zero : resultComponents.count
}

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cellIdentifier = "result-cell"

let tableViewCell: UITableViewCell = if let cachedTableViewCell = tableView
.dequeueReusableCell(withIdentifier: cellIdentifier)
{
cachedTableViewCell
} else {
UITableViewCell(style: .value1, reuseIdentifier: cellIdentifier)
}

let component = resultComponents[indexPath.row]

tableViewCell.textLabel?.text = component.name
tableViewCell.detailTextLabel?.text = component.value
tableViewCell.detailTextLabel?.textColor = UIColor.darkGray

return tableViewCell
}

override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)

showSuggestionRegion()
}
}

// MARK: - Private

extension PlaceAutocompleteResultViewController {
/// Initial set-up
private func prepare() {
title = "Address"

updateScreenData()
}

private func updateScreenData() {
showAnnotations(results: [result])
showSuggestionRegion()

tableView.reloadData()
}

private func showSuggestionRegion() {
guard let coordinate = result.coordinate else { return }

let cameraOptions = CameraOptions(
center: coordinate,
zoom: 10.5
)

mapView.camera.ease(to: cameraOptions, duration: 0.4)
}
}

extension PlaceAutocomplete.Result {
static let measurementFormatter: MeasurementFormatter = {
let formatter = MeasurementFormatter()
formatter.unitOptions = [.naturalScale]
formatter.numberFormatter.roundingMode = .halfUp
formatter.numberFormatter.maximumFractionDigits = 0
return formatter
}()

static let distanceFormatter: MKDistanceFormatter = {
let formatter = MKDistanceFormatter()
formatter.unitStyle = .abbreviated
return formatter
}()

func toComponents() -> [(name: String, value: String)] {
var components = [
(name: "Name", value: name),
(name: "Type", value: "\(type == .POI ? "POI" : "Address")"),
]

if let address, let formattedAddress = address.formattedAddress(style: .short) {
components.append(
(name: "Address", value: formattedAddress)
)
}

if let distance {
components.append(
(name: "Distance", value: PlaceAutocomplete.Result.distanceFormatter.string(fromDistance: distance))
)
}

if let estimatedTime {
components.append(
(
name: "Estimated time",
value: PlaceAutocomplete.Result.measurementFormatter.string(from: estimatedTime)
)
)
}

if let phone {
components.append(
(name: "Phone", value: phone)
)
}

if let reviewsCount = reviewCount {
components.append(
(name: "Reviews Count", value: "\(reviewsCount)")
)
}

if let avgRating = averageRating {
components.append(
(name: "Rating", value: "\(avgRating)")
)
}

if !categories.isEmpty {
let categories = categories.count > 2 ? Array(categories.dropFirst(2)) : categories

components.append(
(name: "Categories", value: categories.joined(separator: ","))
)
}

if let mapboxId {
components.append(
(name: "Mapbox ID", value: mapboxId)
)
}

return components
}
}
Was this example helpful?