HyperbasisHyperbasisDocs
Home

Examples

Complete code examples for common use cases.

Minimum Viable Integration

The simplest possible Hyperbasis integration:

import Hyperbasis
import ARKit

// 1. Create storage
let storage = HBStorage()

// 2. Create space from world map
let worldMap = try await arSession.currentWorldMap
let space = try HBSpace(worldMap: worldMap)
try await storage.save(space)

// 3. Save anchors
let anchor = HBAnchor(
    spaceId: space.id,
    transform: entity.transform.matrix,
    metadata: [:]
)
try await storage.save(anchor)

// 4. Load on next launch
let spaces = try await storage.loadAllSpaces()
let loadedWorldMap = try spaces.first?.arWorldMap()
let anchors = try await storage.loadAnchors(spaceId: spaces.first!.id)

Sticky Notes App

A complete sticky notes AR app example:

Model

struct StickyNote: Identifiable {
    let id: UUID
    var text: String
    var color: Color
    var transform: simd_float4x4
    let createdAt: Date

    enum Color: String, CaseIterable {
        case yellow, blue, green, pink

        var uiColor: UIColor {
            switch self {
            case .yellow: return .systemYellow
            case .blue: return .systemBlue
            case .green: return .systemGreen
            case .pink: return .systemPink
            }
        }
    }
}

extension StickyNote {
    init?(from anchor: HBAnchor) {
        guard let text = anchor.stringMetadata(forKey: "text"),
              let transform = anchor.validSimdTransform else {
            return nil
        }

        let colorString = anchor.stringMetadata(forKey: "color") ?? "yellow"
        let color = Color(rawValue: colorString) ?? .yellow

        self.init(
            id: anchor.id,
            text: text,
            color: color,
            transform: transform,
            createdAt: anchor.createdAt
        )
    }

    func toAnchor(spaceId: UUID) -> HBAnchor {
        HBAnchor(
            id: id,
            spaceId: spaceId,
            transform: transform,
            metadata: [
                "type": .string("sticky_note"),
                "text": .string(text),
                "color": .string(color.rawValue)
            ]
        )
    }
}

View Model

@MainActor
class NotesViewModel: ObservableObject {
    private let storage = HBStorage()
    private var currentSpace: HBSpace?

    @Published var notes: [StickyNote] = []
    @Published var isLoading = false
    @Published var error: Error?

    func loadOrCreateSpace(worldMap: ARWorldMap) async {
        isLoading = true
        defer { isLoading = false }

        do {
            // Try to load existing
            let spaces = try await storage.loadAllSpaces()
            if let space = spaces.first {
                currentSpace = space
                notes = try await loadNotes()
            } else {
                // Create new
                let space = try HBSpace(worldMap: worldMap)
                try await storage.save(space)
                currentSpace = space
            }
        } catch {
            self.error = error
        }
    }

    func addNote(text: String, transform: simd_float4x4) async {
        guard let space = currentSpace else { return }

        let note = StickyNote(
            id: UUID(),
            text: text,
            color: .yellow,
            transform: transform,
            createdAt: Date()
        )

        do {
            try await storage.save(note.toAnchor(spaceId: space.id))
            notes.append(note)
        } catch {
            self.error = error
        }
    }

    func updateNote(_ note: StickyNote) async {
        guard let space = currentSpace,
              let index = notes.firstIndex(where: { $0.id == note.id }) else { return }

        do {
            try await storage.save(note.toAnchor(spaceId: space.id))
            notes[index] = note
        } catch {
            self.error = error
        }
    }

    func deleteNote(_ note: StickyNote) async {
        do {
            try await storage.deleteAnchor(id: note.id)
            notes.removeAll { $0.id == note.id }
        } catch {
            self.error = error
        }
    }

    private func loadNotes() async throws -> [StickyNote] {
        guard let space = currentSpace else { return [] }
        let anchors = try await storage.loadAnchors(spaceId: space.id)
        return anchors.compactMap { StickyNote(from: $0) }
    }
}

Cloud Sync Setup

struct AppConfig {
    static let supabaseUrl = "https://your-project.supabase.co"
    static let supabaseKey = "your-anon-key"

    static var storageConfig: HBStorageConfig {
        #if DEBUG
        return .default // Local only in debug
        #else
        return HBStorageConfig(
            backend: .supabase(url: supabaseUrl, anonKey: supabaseKey),
            syncStrategy: .onSave,
            compression: .balanced
        )
        #endif
    }
}

class PersistenceManager {
    let storage = HBStorage(config: AppConfig.storageConfig)

    func syncIfNeeded() async {
        guard storage.isCloudEnabled else { return }

        if storage.pendingOperationCount > 0 {
            do {
                try await storage.sync()
                print("Synced \(storage.pendingOperationCount) operations")
            } catch {
                print("Sync failed: \(error)")
            }
        }
    }
}

Multi-Space Management

Handle multiple spaces for different locations:

@MainActor
class SpaceManager: ObservableObject {
    private let storage = HBStorage()

    @Published var spaces: [HBSpace] = []
    @Published var currentSpace: HBSpace?

    func loadSpaces() async throws {
        spaces = try await storage.loadAllSpaces()
        spaces.sort { $0.updatedAt > $1.updatedAt }
    }

    func selectSpace(_ space: HBSpace) {
        currentSpace = space
    }

    func createSpace(name: String, worldMap: ARWorldMap) async throws {
        let space = try HBSpace(name: name, worldMap: worldMap)
        try await storage.save(space)
        spaces.insert(space, at: 0)
        currentSpace = space
    }

    func deleteSpace(_ space: HBSpace) async throws {
        try await storage.deleteSpace(id: space.id)
        spaces.removeAll { $0.id == space.id }

        if currentSpace?.id == space.id {
            currentSpace = spaces.first
        }
    }

    func renameSpace(_ space: HBSpace, to name: String) async throws {
        var updated = space
        updated.update(name: name)
        try await storage.save(updated)

        if let index = spaces.firstIndex(where: { $0.id == space.id }) {
            spaces[index] = updated
        }
    }
}

Saving on App Background

@main
struct MyARApp: App {
    @StateObject private var viewModel = ARViewModel()
    @Environment(\.scenePhase) private var scenePhase

    var body: some Scene {
        WindowGroup {
            ContentView()
                .environmentObject(viewModel)
        }
        .onChange(of: scenePhase) { oldPhase, newPhase in
            switch newPhase {
            case .background:
                Task {
                    await viewModel.saveBeforeBackground()
                }
            case .active:
                Task {
                    await viewModel.syncOnForeground()
                }
            default:
                break
            }
        }
    }
}

extension ARViewModel {
    func saveBeforeBackground() async {
        guard let space = currentSpace,
              canCaptureWorldMap else { return }

        do {
            let worldMap = try await arSession.getCurrentWorldMap()
            var updated = space
            try updated.update(worldMap: worldMap)
            try await storage.save(updated)
        } catch {
            print("Failed to save on background: \(error)")
        }
    }

    func syncOnForeground() async {
        if storage.isCloudEnabled {
            try? await storage.sync()
        }
    }
}

Time Machine

Browse through the history of your AR space with a timeline scrubber:

@MainActor
class TimeMachineViewModel: ObservableObject {
    private let storage = HBStorage()
    private var timeline: HBTimeline?

    @Published var scrubberDates: [Date] = []
    @Published var currentDate: Date = Date()
    @Published var anchors: [HBAnchor] = []
    @Published var isTimeTraveling = false

    func loadTimeline(spaceId: UUID) async throws {
        timeline = try await storage.timeline(spaceId: spaceId)

        guard let timeline = timeline,
              let start = timeline.startDate,
              let end = timeline.endDate else { return }

        // Generate scrubber positions
        scrubberDates = timeline.scrubberDates(from: start, to: end, steps: 50)
        currentDate = end
    }

    func scrub(to date: Date) async {
        guard let timeline = timeline else { return }
        isTimeTraveling = date < (timeline.endDate ?? Date())
        currentDate = date
        anchors = timeline.state(at: date)
    }

    func exitTimeMachine() async throws {
        guard let timeline = timeline else { return }
        isTimeTraveling = false
        currentDate = timeline.endDate ?? Date()
        anchors = try await storage.loadAnchors(spaceId: timeline.spaceId)
    }
}

// SwiftUI View
struct TimeMachineView: View {
    @StateObject var viewModel: TimeMachineViewModel

    var body: some View {
        VStack {
            if viewModel.isTimeTraveling {
                Text("Viewing: \(viewModel.currentDate.formatted())")
                    .foregroundStyle(.orange)
            }

            Slider(
                value: Binding(
                    get: { dateToProgress(viewModel.currentDate) },
                    set: { progress in
                        let date = progressToDate(progress)
                        Task { await viewModel.scrub(to: date) }
                    }
                ),
                in: 0...1
            )
            .disabled(viewModel.scrubberDates.isEmpty)

            if viewModel.isTimeTraveling {
                Button("Exit Time Machine") {
                    Task { try? await viewModel.exitTimeMachine() }
                }
            }
        }
    }

    private func dateToProgress(_ date: Date) -> Double {
        guard let first = viewModel.scrubberDates.first,
              let last = viewModel.scrubberDates.last else { return 1.0 }
        let total = last.timeIntervalSince(first)
        guard total > 0 else { return 1.0 }
        return date.timeIntervalSince(first) / total
    }

    private func progressToDate(_ progress: Double) -> Date {
        guard let first = viewModel.scrubberDates.first,
              let last = viewModel.scrubberDates.last else { return Date() }
        let total = last.timeIntervalSince(first)
        return first.addingTimeInterval(total * progress)
    }
}

Undo/Redo

Implement undo/redo functionality using anchor versioning:

@MainActor
class UndoRedoManager: ObservableObject {
    private let storage = HBStorage()
    private var undoStack: [(anchorId: UUID, version: Int)] = []
    private var redoStack: [(anchorId: UUID, version: Int)] = []

    @Published var canUndo = false
    @Published var canRedo = false

    // Call this after every user action
    func recordAction(anchorId: UUID) async {
        let events = try? await storage.history(anchorId: anchorId)
        guard let currentVersion = events?.last?.version else { return }

        // Record the version before this action (for undo)
        if currentVersion > 1 {
            undoStack.append((anchorId, currentVersion - 1))
            redoStack.removeAll()
            updateState()
        }
    }

    func undo() async throws -> HBAnchor? {
        guard let action = undoStack.popLast() else { return nil }

        // Get current version before undo
        let events = try await storage.history(anchorId: action.anchorId)
        if let currentVersion = events.last?.version {
            redoStack.append((action.anchorId, currentVersion))
        }

        let anchor = try await storage.rollback(
            anchorId: action.anchorId,
            toVersion: action.version
        )
        updateState()
        return anchor
    }

    func redo() async throws -> HBAnchor? {
        guard let action = redoStack.popLast() else { return nil }

        // Get current version before redo
        let events = try await storage.history(anchorId: action.anchorId)
        if let currentVersion = events.last?.version {
            undoStack.append((action.anchorId, currentVersion))
        }

        let anchor = try await storage.rollback(
            anchorId: action.anchorId,
            toVersion: action.version
        )
        updateState()
        return anchor
    }

    private func updateState() {
        canUndo = !undoStack.isEmpty
        canRedo = !redoStack.isEmpty
    }
}

Change Detection

Detect what changed between sessions and notify users:

@MainActor
class ChangeDetector: ObservableObject {
    private let storage = HBStorage()

    @Published var changes: HBDiff?
    @Published var hasUnseenChanges = false

    private let lastViewedKey = "lastViewedDate"

    func checkForChanges(spaceId: UUID) async throws {
        let lastViewed = UserDefaults.standard.object(forKey: lastViewedKey) as? Date
            ?? Date.distantPast

        let diff = try await storage.diff(
            spaceId: spaceId,
            from: lastViewed,
            to: Date()
        )

        changes = diff
        hasUnseenChanges = diff.hasChanges
    }

    func markAsSeen() {
        UserDefaults.standard.set(Date(), forKey: lastViewedKey)
        hasUnseenChanges = false
    }
}

// Display changes to user
struct ChangeSummaryView: View {
    let diff: HBDiff

    var body: some View {
        VStack(alignment: .leading, spacing: 12) {
            if !diff.added.isEmpty {
                Label("\(diff.added.count) new", systemImage: "plus.circle.fill")
                    .foregroundStyle(.green)
            }

            if !diff.removed.isEmpty {
                Label("\(diff.removed.count) removed", systemImage: "minus.circle.fill")
                    .foregroundStyle(.red)
            }

            if !diff.moved.isEmpty {
                Label("\(diff.moved.count) moved", systemImage: "arrow.up.and.down.circle.fill")
                    .foregroundStyle(.blue)
            }

            if !diff.updated.isEmpty {
                Label("\(diff.updated.count) updated", systemImage: "pencil.circle.fill")
                    .foregroundStyle(.orange)
            }
        }
    }
}

Testing with Mocks

class MockStorage: HBStorage {
    var savedSpaces: [HBSpace] = []
    var savedAnchors: [HBAnchor] = []
    var shouldFail = false

    override func save(_ space: HBSpace) async throws {
        if shouldFail { throw HBStorageError.fileSystemError(underlying: MockError.intentional) }
        savedSpaces.append(space)
    }

    override func loadAllSpaces() async throws -> [HBSpace] {
        if shouldFail { throw HBStorageError.fileSystemError(underlying: MockError.intentional) }
        return savedSpaces
    }

    override func save(_ anchor: HBAnchor) async throws {
        if shouldFail { throw HBStorageError.fileSystemError(underlying: MockError.intentional) }
        savedAnchors.append(anchor)
    }

    override func loadAnchors(spaceId: UUID, includeDeleted: Bool = false) async throws -> [HBAnchor] {
        if shouldFail { throw HBStorageError.fileSystemError(underlying: MockError.intentional) }
        return savedAnchors.filter { $0.spaceId == spaceId }
    }

    enum MockError: Error {
        case intentional
    }
}

// Usage in tests
@Test func testSaveAnchor() async throws {
    let mockStorage = MockStorage()
    let viewModel = ARViewModel(storage: mockStorage)

    try await viewModel.placeNote(text: "Test", at: .identity)

    #expect(mockStorage.savedAnchors.count == 1)
    #expect(mockStorage.savedAnchors.first?.stringMetadata(forKey: "text") == "Test")
}