Skip to main content

Category Search Within a Map's Viewport

This example shows the integration the Mapbox Search SDK for iOS with a Mapbox map to provide a category-based search experience with dynamic map annotations. It initializes a MapView centered on a default location and allows users to search for specific categories like parking, restaurants, or museums within the visible map area. The search functionality is powered by the Search Box API, which fetches relevant results. The search results data are then displayed as clustered annotations using the PointAnnotationManager. A segmented control enables users to toggle between categories, and a search button triggers the category-based search within the current map bounding box.

When search results are returned, the controller dynamically adjusts the map's camera view to fit all the annotations while ensuring padding for UI elements like the segmented control and search button. User location is also displayed on the map using a 2D location puck. This view controller highlights an effective implementation of category-based discovery, combining Mapbox's mapping and search capabilities to deliver an interactive and visually engaging experience.

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

DiscoverViewController.swift
import MapboxMaps
import MapboxSearch
import MapboxSearchUI
import UIKit

final class DiscoverViewController: UIViewController {
private var mapView = MapView(frame: .zero, mapInitOptions: defaultMapOptions)
@IBOutlet private var segmentedControl: UISegmentedControl!
@IBOutlet private var searchButton: UIButton!

private let category = Discover()
lazy var annotationsManager = mapView.makeClusterPointAnnotationManager()

override func viewDidLoad() {
super.viewDidLoad()

mapView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(mapView)
view.sendSubviewToBack(mapView)

NSLayoutConstraint.activate([
mapView.topAnchor.constraint(equalTo: view.topAnchor),
mapView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
mapView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
mapView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor),
])

// Show user location
mapView.location.options.puckType = .puck2D()
}
}

// MARK: - Actions

extension DiscoverViewController {
@IBAction
private func handleSearchInRegionAction() {
let regionResultsLimit = switch category.apiType {
case .geocoding:
/// Geocoding has a limit of 10 results
10
default:
/// You can request up to 100 results for SBS and SearchBox API types
/// For this demo we will request fewer for this UI output
20
}

category.search(
for: currentSelectedCategory,
in: currentBoundingBox,
options: .init(limit: regionResultsLimit)
) { result in
switch result {
case .success(let results):
self.showCategoryResults(results)

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

// MARK: - Private

extension DiscoverViewController {
private var currentBoundingBox: MapboxSearch.BoundingBox {
let bounds = mapView.mapboxMap.coordinateBounds(for: mapView.bounds)
return MapboxSearch.BoundingBox(bounds.southwest, bounds.northeast)
}

private var currentSelectedCategory: Discover.Query {
let allDemoCategories: [Discover.Query] = [
.Category.parking,
.Category.restaurant,
.Category.museum,
]

return allDemoCategories[segmentedControl.selectedSegmentIndex]
}

private static var defaultMapOptions: MapInitOptions {
let cameraOptions = CameraOptions(
center: CLLocationCoordinate2D(latitude: 40.730610, longitude: -73.935242),
zoom: 10.5
)

return MapInitOptions(cameraOptions: cameraOptions)
}

private 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 inset: CGFloat = 24
let insets = UIEdgeInsets(
top: inset + segmentedControl.frame.height,
left: inset,
bottom: inset + searchButton.frame.height,
right: inset
)
let cameraState = mapView.mapboxMap.cameraState
let coordinatesCamera = try mapView.mapboxMap.camera(
for: annotations.map(\.point.coordinates),
camera: CameraOptions(cameraState: cameraState),
coordinatesPadding: insets,
maxZoom: nil,
offset: nil
)

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

private func showCategoryResults(_ results: [Discover.Result], cameraShouldFollow: Bool = true) {
annotationsManager.annotations = results.map {
PointAnnotation.pointAnnotation($0)
}

if cameraShouldFollow {
cameraToAnnotations(annotationsManager.annotations)
}
}
}
Was this example helpful?