DocsiOS (Swift)

iOS Navigator - Swift + MapKit

MapKit is Apple's built-in map framework - no API key, no billing, available on every iOS device. We'll use it for tiles and rendering, and PickPoint for address search, routing, and optional live tracking.

Never embed your PickPoint API key in client-side code for geocoding and routing. Route those calls through your backend — see Server Integration. Server-side SDK code can call PickPoint directly with the key in env. Device tracking uses per-device client-id/client-secret credentials — safe to use directly from mobile/IoT apps. See the Integration guide for all three patterns.
Prerequisites: Xcode 15+, iOS deployment target 16+, a PickPoint API key. No additional map SDK needed.
1

Project setup

Create a new iOS SwiftUI project. MapKit is part of the SDK - no package needed. For live tracking, use the tracking.v2 protocol (see the Device Tracking guide).

// Package.swift - MapKit is built into iOS
// For live tracking, implement the tracking.v2 wire protocol from pickpoint-proto
// or bridge through a small local HTTP/WebSocket relay using @pickpoint/sdk on your backend

Add NSLocationWhenInUseUsageDescription to Info.plist.

2

Embed the map

Wrap MKMapView in a UIViewRepresentable. The coordinator renders route polylines in teal when they're added as overlays:

import MapKit
import SwiftUI

struct MapView: UIViewRepresentable {
    @Binding var region: MKCoordinateRegion
    var overlays: [MKOverlay] = []

    func makeUIView(context: Context) -> MKMapView {
        let map = MKMapView()
        map.showsUserLocation = true
        map.delegate = context.coordinator
        return map
    }

    func updateUIView(_ map: MKMapView, context: Context) {
        map.setRegion(region, animated: true)
        map.removeOverlays(map.overlays)
        map.addOverlays(overlays)
    }

    func makeCoordinator() -> Coordinator { Coordinator() }

    class Coordinator: NSObject, MKMapViewDelegate {
        func mapView(_ map: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer {
            let r = MKPolylineRenderer(overlay: overlay)
            r.strokeColor = .systemTeal; r.lineWidth = 4
            return r
        }
    }
}
3

Track the user's location

An ObservableObject wrapping CLLocationManager publishes the current coordinate and keeps the map region centred:

import CoreLocation

class LocationManager: NSObject, ObservableObject, CLLocationManagerDelegate {
    private let manager = CLLocationManager()
    @Published var coordinate: CLLocationCoordinate2D?
    @Published var region = MKCoordinateRegion(
        center: CLLocationCoordinate2D(latitude: 51.5, longitude: -0.12),
        span:   MKCoordinateSpan(latitudeDelta: 0.05, longitudeDelta: 0.05)
    )

    override init() {
        super.init()
        manager.delegate = self
        manager.desiredAccuracy = kCLLocationAccuracyBest
        manager.requestWhenInUseAuthorization()
        manager.startUpdatingLocation()
    }

    func locationManager(_ manager: CLLocationManager,
                         didUpdateLocations locations: [CLLocation]) {
        guard let loc = locations.last else { return }
        coordinate = loc.coordinate
        region.center = loc.coordinate
    }
}
4

Address search

Call /v2/address/search on each keystroke (debounce ~250 ms). Pass the current map region as viewbox to bias results toward what the user sees:

struct GeoResult: Decodable { let lat, lon, display_name: String }

func searchAddress(_ query: String,
                   near region: MKCoordinateRegion) async throws -> [GeoResult] {
    let span = region.span
    let c    = region.center
    let box  = "\(c.longitude - span.longitudeDelta/2),\(c.latitude - span.latitudeDelta/2),"
             + "\(c.longitude + span.longitudeDelta/2),\(c.latitude + span.latitudeDelta/2)"

    var comps = URLComponents(string: "https://api.pickpoint.io/v2/address/search")!
    comps.queryItems = [
        URLQueryItem(name: "q",       value: query),
        URLQueryItem(name: "limit",   value: "6"),
        URLQueryItem(name: "viewbox", value: box),
    ]
    var req = URLRequest(url: comps.url!)
    req.setValue(apiKey, forHTTPHeaderField: "X-Api-Key")

    let (data, _) = try await URLSession.shared.data(for: req)
    return try JSONDecoder().decode([GeoResult].self, from: data)
}

Show results in a List. On tap, use the result's lat/lon as the route destination.

5

Fetch the route

POST to /v2/route. Valhalla returns legs with a Google-encoded polyline in shape - use any Swift polyline decoder (e.g. Polyline on SPM) to convert it to CLLocationCoordinate2D:

struct RouteBody: Encodable {
    struct Loc: Encodable { let lat, lon: Double }
    let locations: [Loc]
    let costing: String
}

struct RouteResponse: Decodable {
    struct Trip: Decodable {
        struct Leg: Decodable { let shape: String }
        let legs: [Leg]
    }
    let trip: Trip
}

func fetchRoute(from origin: CLLocationCoordinate2D,
                to   dest:   CLLocationCoordinate2D) async throws -> MKPolyline {
    let body = RouteBody(
        locations: [.init(lat: origin.latitude, lon: origin.longitude),
                    .init(lat: dest.latitude,   lon: dest.longitude)],
        costing: "auto"
    )
    var req = URLRequest(url: URL(string: "https://api.pickpoint.io/v2/route")!)
    req.httpMethod = "POST"
    req.httpBody   = try JSONEncoder().encode(body)
    req.setValue("application/json", forHTTPHeaderField: "Content-Type")
    req.setValue(apiKey,             forHTTPHeaderField: "X-Api-Key")

    let (data, _) = try await URLSession.shared.data(for: req)
    let resp      = try JSONDecoder().decode(RouteResponse.self, from: data)

    // Valhalla returns a Google-encoded polyline; decode it:
    let coords = decodePolyline(resp.trip.legs[0].shape, precision: 6)
    return MKPolyline(coordinates: coords, count: coords.count)
}
6

Draw the route and fit the camera

Pass the MKPolyline as a binding to MapView's overlays, then fit the camera to the route's bounding rect with padding:

// After setting the overlay, fit camera to the route
func fitCamera(to polyline: MKPolyline, in mapView: MKMapView) {
    let rect = polyline.boundingMapRect
    mapView.setVisibleMapRect(rect, edgePadding: UIEdgeInsets(
        top: 60, left: 20, bottom: 60, right: 20
    ), animated: true)
}

What's next

  • Voice guidance - iterate trip.legs[0].maneuvers and feed each instruction string to AVSpeechSynthesizer as the user passes each waypoint.
  • Re-routing - watch location updates, compute cross-track distance to the polyline; if it exceeds ~50 m, trigger a new route request.
  • Live tracking - connect with device credentials via the tracking.v2 protocol. See the Device Tracking guide and pickpoint-proto for native iOS integration.
  • Vehicle profiles - change costing to bicycle, pedestrian, or truck.

← Navigator AppAndroid →Flutter →