+1 (415) 993-7206

Location-Based AR with ARGeoAnchor: Outdoor Wayfinding That Actually Localizes

Every tutorial on this site so far has anchored content to something the camera can see: a plane, a poster, a scanned object, a face, a LiDAR mesh. Outdoor AR is different. There is no marker to point at, the "surface" is a city block, and the thing you want to pin content to — a storefront, a trailhead, a stadium gate — may be fifty metres away.

ARKit's answer is geo tracking: ARGeoTrackingConfiguration and ARGeoAnchor, which combine GPS, compass, and Apple's Look Around imagery to localize the device against the real world with far better accuracy than GPS alone. You give ARKit a latitude, longitude, and optional altitude; ARKit gives you back a tracked anchor in the AR session that stays put as the user walks around.

This tutorial builds a working wayfinding overlay: request authorization, check coverage, drop geo anchors from coordinates, render RealityKit content on them, and — the part most demos skip — handle the large fraction of the world where geo tracking is not available at all.

Targets iOS 18 or later, an A12 device or newer with GPS (cellular iPad or iPhone), and a location with Look Around coverage.

1. Understand the constraint before you scope the project

Geo tracking is not available everywhere. It works only in cities and regions Apple has mapped with Look Around imagery, and only outdoors with a clear view of buildings. Before you promise a client a citywide AR experience, answer three questions:

  1. Is the target location supported? Check at runtime with ARGeoTrackingConfiguration.checkAvailability(at:completionHandler:), and check manually on a device at the actual site during discovery. Do not rely on "the city is listed" — coverage is patchy at street level.
  2. Can the user stand outdoors, phone up, with buildings in view? Localization needs to match the camera feed against Look Around imagery. Open parks, dense tree cover, tunnels, parking garages, and indoor spaces fail.
  3. Is the content tolerant of a few metres of error? Geo anchors are excellent for "the entrance is over there" and unusable for "align this virtual pipe with that real pipe."

If any answer is no, the right tool is probably an image anchor, an object anchor, or a plain GPS list view — not geo tracking.

2. Permissions and capabilities

Geo tracking needs both camera and precise location. In the target's Info tab add:

  • NSCameraUsageDescription — "This app uses the camera to show directions in your surroundings."
  • NSLocationWhenInUseUsageDescription — "This app uses your location to place directions accurately in the world around you."

Precise location matters. If the user grants only approximate location, geo tracking will not work; you must ask for temporary full accuracy:

import CoreLocation

final class LocationGate: NSObject, CLLocationManagerDelegate {
    private let manager = CLLocationManager()

    func request() {
        manager.delegate = self
        manager.requestWhenInUseAuthorization()
    }

    func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
        guard manager.authorizationStatus == .authorizedWhenInUse else { return }
        if manager.accuracyAuthorization != .fullAccuracy {
            manager.requestTemporaryFullAccuracyAuthorization(
                withPurposeKey: "PreciseLocationForAR"
            )
        }
    }
}

PreciseLocationForAR is a key you add to NSLocationTemporaryUsageDescriptionDictionary in Info.plist, with a sentence explaining why. Skipping this step is the single most common reason a geo-tracking prototype "just doesn't work" on someone else's phone.

3. Check availability, then start the session

Geo tracking runs through ARKit's ARSession, so on iOS you use RealityKit's ARView rather than the RealityView + .spatialTracking shortcut from our getting started tutorial. You still build the content with RealityKit entities; only the session setup is different.

import ARKit
import RealityKit

func startGeoSession(in arView: ARView) {
    guard ARGeoTrackingConfiguration.isSupported else {
        // A12 or newer, GPS-capable device required.
        return showFallback(.unsupportedDevice)
    }

    ARGeoTrackingConfiguration.checkAvailability { available, error in
        DispatchQueue.main.async {
            guard available else {
                return showFallback(.noCoverageHere)
            }
            let config = ARGeoTrackingConfiguration()
            config.planeDetection = [.horizontal]        // optional, for ground content
            config.environmentTexturing = .automatic
            arView.session.run(config)
        }
    }
}

checkAvailability with no coordinate checks the device's current location; the at: variant checks a specific CLLocationCoordinate2D, which is what you want when the user is browsing destinations before travelling to them.

4. Wrap the SwiftUI layer

A thin UIViewRepresentable keeps the rest of the app in SwiftUI:

struct GeoARView: UIViewRepresentable {
    @Binding var destinations: [Destination]

    func makeUIView(context: Context) -> ARView {
        let arView = ARView(frame: .zero)
        arView.session.delegate = context.coordinator
        context.coordinator.arView = arView
        startGeoSession(in: arView)
        return arView
    }

    func updateUIView(_ arView: ARView, context: Context) {
        context.coordinator.sync(destinations)
    }

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

struct Destination: Identifiable, Equatable {
    let id = UUID()
    let name: String
    let coordinate: CLLocationCoordinate2D
    let altitude: Double?      // nil = let ARKit use ground level
}

5. Drop a geo anchor

Wait until geo tracking is actually localized before adding anchors — anchors added while the state is .initializing will jump around when localization completes.

func addAnchor(for destination: Destination, in arView: ARView) {
    let geoAnchor: ARGeoAnchor
    if let altitude = destination.altitude {
        geoAnchor = ARGeoAnchor(name: destination.name,
                                coordinate: destination.coordinate,
                                altitude: altitude)
    } else {
        // ARKit resolves ground-level altitude from its map data.
        geoAnchor = ARGeoAnchor(name: destination.name,
                                coordinate: destination.coordinate)
    }
    arView.session.add(anchor: geoAnchor)
}

Prefer the altitude-free initializer unless you genuinely know the elevation. Users type addresses, not metres above sea level, and ARKit's ground-level estimate is usually better than a guess.

6. Render RealityKit content on the anchor

Bridge the ARKit anchor to a RealityKit AnchorEntity in the session delegate:

final class Coordinator: NSObject, ARSessionDelegate {
    weak var arView: ARView?
    private var placed: [UUID: AnchorEntity] = [:]

    func session(_ session: ARSession, didAdd anchors: [ARAnchor]) {
        guard let arView else { return }
        for case let geoAnchor as ARGeoAnchor in anchors {
            let entity = AnchorEntity(anchor: geoAnchor)
            entity.addChild(makeMarker(named: geoAnchor.name ?? "Here"))
            arView.scene.addAnchor(entity)
            placed[geoAnchor.identifier] = entity
        }
    }

    private func makeMarker(named name: String) -> Entity {
        let mesh = MeshResource.generateText(
            name,
            extrusionDepth: 0.02,
            font: .systemFont(ofSize: 0.4),
            alignment: .center
        )
        var material = UnlitMaterial(color: .white)
        material.blending = .transparent(opacity: 0.95)
        let label = ModelEntity(mesh: mesh, materials: [material])
        label.position.y = 2.0                 // above head height
        label.components.set(BillboardComponent())   // always face the camera
        return label
    }
}

BillboardComponent is the small detail that makes street-scale AR legible: a label fifty metres away that is edge-on to the user is invisible. Billboard it, and size it generously — text authored for a tabletop demo disappears outdoors.

7. Coach the user through localization

Geo tracking has a state machine, and users need to be told what to do while it works. Implement session(_:didChangeGeoTrackingStatus:):

func session(_ session: ARSession, didChange status: ARGeoTrackingStatus) {
    switch status.state {
    case .initializing:
        hint = "Starting up…"
    case .localizing:
        hint = localizationHint(for: status.stateReason)
    case .localized:
        hint = nil
        flushPendingAnchors()
    case .notAvailable:
        hint = "AR directions aren't available at this location."
    @unknown default:
        hint = nil
    }
}

private func localizationHint(for reason: ARGeoTrackingStatus.StateReason) -> String {
    switch reason {
    case .notAvailableAtLocation: return "Move to a mapped street to continue."
    case .needLocationPermissions: return "Allow precise location in Settings."
    case .worldTrackingUnstable:   return "Move the phone slowly to look around."
    case .waitingForLocation:      return "Waiting for a GPS fix…"
    case .geoDataNotLoaded:        return "Loading map data — check your connection."
    case .devicePointedTooLow:     return "Point the phone at the buildings around you."
    case .visualLocalizationFailed:return "Point at buildings, not the sky or the ground."
    default:                       return "Look around to line things up."
    }
}

devicePointedTooLow and visualLocalizationFailed are the two you will see constantly in testing. The fix is always the same instruction: hold the phone up and point it at buildings. Put that on screen, with an arrow, before the user gives up.

Also watch status.accuracy.low, .medium, .high. Below .high, consider hiding precision-dependent content and showing distance text instead.

8. Converting between coordinates and AR space

Two conversions come up constantly:

// AR space → world coordinate (e.g. "save where the user is standing")
session.getGeoLocation(forPoint: transform.translation) { coordinate, altitude, error in
    guard error == nil else { return }
    save(coordinate, altitude)
}

Going the other way is just creating an ARGeoAnchor. To decide whether a destination is worth rendering at all, compute the distance in Core Location first and skip anything beyond your content budget:

let here = CLLocation(latitude: current.latitude, longitude: current.longitude)
let there = CLLocation(latitude: destination.coordinate.latitude,
                       longitude: destination.coordinate.longitude)
guard here.distance(from: there) < 500 else { continue }

Fifty to a few hundred metres is the practical range for readable content. Beyond that, an on-screen compass chip beats a 3D label.

9. Testing without walking the route

Xcode's location simulation works with geo tracking: run the app on a device connected to your Mac, then Debug → Simulate Location with a custom GPX file. You will not get a matching camera feed, so visual localization will not reach .localized, but you can exercise the availability checks, the anchor bookkeeping, and every branch of the status UI without leaving the office.

For the real thing, budget field testing time. Geo-tracking work is the one AR speciality where an afternoon on the actual street is not optional — coverage, glass façades, tree cover, and time of day all change the result.

10. The fallback is most of the product

Because coverage is limited, a shippable geo-AR feature is really two features:

  • Geo path. Coverage available, precise location granted, .localized reached, high accuracy: anchored AR labels.
  • Fallback path. Anything else: a heading-based overlay driven by CLLocationManager heading plus distance, or simply a map and a list. It is less magical, and it works everywhere.

Ship the fallback first. It is testable anywhere, it establishes the data model, and it means a coverage gap degrades the experience instead of breaking the app.

Where this pays off commercially

The projects that justify geo tracking share a shape: a fixed set of real-world places, a user standing outdoors, and information that is genuinely easier to understand in place than on a map.

  • Campus, venue, and stadium wayfinding — gate, entrance, and accessible-route guidance where signage is confusing.
  • Tourism and heritage — historical overlays and audio pinned to viewpoints.
  • Field operations — utility, telecom, and municipal crews marking asset locations for the next visit via getGeoLocation(forPoint:).
  • Retail and events — pop-up activations, queue routing, and outdoor product moments.

Note what is not on this list: indoor navigation. Geo tracking does not work indoors. Malls, airports, and warehouses need a different approach — image anchors at known fixed points, or object anchors, or a scanned-mesh relocalization scheme.

Next steps

Scoping an outdoor AR project and not sure whether the location is viable? RealityRogue's ARKit consultants run coverage and feasibility checks before anyone writes code. Get in touch.