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. Browser JavaScript, iOS/Android apps, and Flutter bundles can all be inspected or decompiled. Always route geocoding and routing calls through your own backend. Exception: device tracking uses per-device client-id/client-secret credentials - safe to use directly from mobile/IoT apps. See the Backend Examples for production-ready proxy implementations.
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. Add Socket.IO only if you plan to stream live tracking:

// Package.swift - no map SDK needed, MapKit is built into iOS
// Only add Socket.IO if you want live tracking
.package(url: "https://github.com/socketio/socket.io-client-swift", from: "16.0.0")

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 - emit location:add via Socket.IO on each CLLocation update. See the Device Tracking guide.
  • Vehicle profiles - change costing to bicycle, pedestrian, or truck.

← Navigator AppAndroid →Flutter →