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.
client-id/client-secret credentials - safe to use directly from mobile/IoT apps. See the Backend Examples for production-ready proxy implementations.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.
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
}
}
}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
}
}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.
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)
}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].maneuversand feed eachinstructionstring toAVSpeechSynthesizeras 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:addvia Socket.IO on eachCLLocationupdate. See the Device Tracking guide. - Vehicle profiles - change
costingtobicycle,pedestrian, ortruck.