HyperbasisHyperbasisDocs
Home

AR Session Integration

Integrate Hyperbasis with ARKit sessions.

Overview

Hyperbasis works with ARKit's ARSession and ARWorldMap to provide spatial persistence. This guide covers the key integration points.

Capturing World Maps

Check Mapping Status

Only capture world maps when the environment is well-mapped:

func session(_ session: ARSession, cameraDidChangeTrackingState camera: ARCamera) {
    switch camera.trackingState {
    case .normal:
        // Good mapping, can capture world map
        canCaptureWorldMap = true
    case .limited(let reason):
        canCaptureWorldMap = false
        switch reason {
        case .initializing:
            print("AR session initializing")
        case .relocalizing:
            print("Relocalizing to previous world map")
        case .excessiveMotion:
            print("Move device more slowly")
        case .insufficientFeatures:
            print("Point at more textured surfaces")
        @unknown default:
            break
        }
    case .notAvailable:
        canCaptureWorldMap = false
    }
}

Capture World Map

func captureWorldMap() async throws -> ARWorldMap {
    return try await arSession.currentWorldMap
}
Tip

Best practice is to capture the world map after the user places their first anchor, or after significant environment scanning.

Relocalization

Load and Apply World Map

func relocalize(with worldMap: ARWorldMap) {
    let config = ARWorldTrackingConfiguration()
    config.planeDetection = [.horizontal, .vertical]
    config.environmentTexturing = .automatic
    config.initialWorldMap = worldMap

    arSession.run(config, options: [.resetTracking, .removeExistingAnchors])
}

Monitor Relocalization

class ARSessionManager: NSObject, ARSessionDelegate {
    @Published var isRelocalizing = false
    @Published var isRelocalized = false

    func session(_ session: ARSession, cameraDidChangeTrackingState camera: ARCamera) {
        switch camera.trackingState {
        case .normal:
            if isRelocalizing {
                isRelocalized = true
                isRelocalizing = false
            }
        case .limited(let reason):
            if case .relocalizing = reason {
                isRelocalizing = true
            }
        case .notAvailable:
            break
        }
    }
}
Note

Relocalization may take a few seconds. Prompt the user to look around the environment to help the AR session find matching features.

Complete Integration Example

@MainActor
class ARSessionManager: NSObject, ObservableObject {
    let session = ARSession()

    @Published var trackingState: ARCamera.TrackingState = .notAvailable
    @Published var isRelocalizing = false
    @Published var isRelocalized = false
    @Published var canCaptureWorldMap = false

    override init() {
        super.init()
        session.delegate = self
    }

    func start() {
        let config = ARWorldTrackingConfiguration()
        config.planeDetection = [.horizontal, .vertical]
        config.environmentTexturing = .automatic
        session.run(config)
    }

    func relocalize(with worldMap: ARWorldMap) {
        isRelocalizing = true
        isRelocalized = false

        let config = ARWorldTrackingConfiguration()
        config.planeDetection = [.horizontal, .vertical]
        config.environmentTexturing = .automatic
        config.initialWorldMap = worldMap

        session.run(config, options: [.resetTracking, .removeExistingAnchors])
    }

    func getCurrentWorldMap() async throws -> ARWorldMap {
        return try await session.currentWorldMap
    }
}

extension ARSessionManager: ARSessionDelegate {
    nonisolated func session(_ session: ARSession, cameraDidChangeTrackingState camera: ARCamera) {
        Task { @MainActor in
            trackingState = camera.trackingState

            switch camera.trackingState {
            case .normal:
                if isRelocalizing {
                    isRelocalized = true
                    isRelocalizing = false
                }
                canCaptureWorldMap = true
            case .limited(let reason):
                canCaptureWorldMap = false
                if case .relocalizing = reason {
                    isRelocalizing = true
                }
            case .notAvailable:
                canCaptureWorldMap = false
            }
        }
    }
}

Placing Anchors

Get Transform from Hit Test

func placeAnchor(at screenPoint: CGPoint, in arView: ARView) -> simd_float4x4? {
    let results = arView.raycast(
        from: screenPoint,
        allowing: .estimatedPlane,
        alignment: .any
    )

    return results.first?.worldTransform
}

Save Anchor with Hyperbasis

func placeAndSave(at screenPoint: CGPoint) async throws {
    guard let transform = placeAnchor(at: screenPoint, in: arView),
          let space = currentSpace else { return }

    let anchor = HBAnchor(
        spaceId: space.id,
        transform: transform,
        metadata: [
            "type": "marker",
            "createdAt": .string(ISO8601DateFormatter().string(from: Date()))
        ]
    )

    try await storage.save(anchor)
}

Recreating Anchors After Relocalization

func recreateAnchors(from savedAnchors: [HBAnchor]) {
    for anchor in savedAnchors {
        guard let transform = anchor.validSimdTransform else { continue }

        // Create your entity
        let entity = createEntity(for: anchor)
        entity.transform.matrix = transform

        // Add to scene
        let anchorEntity = AnchorEntity(world: anchor.position)
        anchorEntity.addChild(entity)
        arView.scene.addAnchor(anchorEntity)
    }
}

func createEntity(for anchor: HBAnchor) -> Entity {
    let type = anchor.stringMetadata(forKey: "type") ?? "default"

    switch type {
    case "sticky_note":
        return createStickyNote(anchor: anchor)
    case "marker":
        return createMarker()
    default:
        return createDefaultEntity()
    }
}

App Lifecycle

Save on Background

@main
struct MyApp: App {
    @Environment(\.scenePhase) private var scenePhase

    var body: some Scene {
        WindowGroup {
            ContentView()
        }
        .onChange(of: scenePhase) { oldPhase, newPhase in
            if newPhase == .background {
                NotificationCenter.default.post(name: .saveWorldMap, object: nil)
            }
        }
    }
}

// In your manager
init() {
    NotificationCenter.default.addObserver(
        forName: .saveWorldMap,
        object: nil,
        queue: .main
    ) { [weak self] _ in
        Task {
            await self?.saveCurrentWorldMap()
        }
    }
}

Next Steps

  • Patterns - Integration patterns for SwiftUI
  • Examples - Complete code examples