HyperbasisHyperbasisDocs
Home

Integration Patterns

Recommended patterns for integrating Hyperbasis into SwiftUI apps.

PersistenceManager Pattern

Create a dedicated manager class that wraps HBStorage:

@MainActor
class PersistenceManager: ObservableObject {
    private let storage: HBStorage

    @Published private(set) var currentSpace: HBSpace?
    @Published private(set) var isSaving = false
    @Published private(set) var isLoading = false
    @Published var lastError: Error?

    init(config: HBStorageConfig = .default) {
        self.storage = HBStorage(config: config)
    }

    func createSpace(name: String?, worldMap: ARWorldMap) async throws -> HBSpace {
        isSaving = true
        defer { isSaving = false }

        let space = try HBSpace(name: name, worldMap: worldMap)
        try await storage.save(space)
        self.currentSpace = space
        return space
    }

    func updateSpace(worldMap: ARWorldMap) async throws {
        guard var space = currentSpace else {
            throw PersistenceError.noActiveSpace
        }

        isSaving = true
        defer { isSaving = false }

        try space.update(worldMap: worldMap)
        try await storage.save(space)
        self.currentSpace = space
    }

    func loadMostRecentSpace() async throws -> HBSpace? {
        isLoading = true
        defer { isLoading = false }

        let spaces = try await storage.loadAllSpaces()
        let mostRecent = spaces.max(by: { $0.updatedAt < $1.updatedAt })
        self.currentSpace = mostRecent
        return mostRecent
    }

    func saveAnchor(_ anchor: HBAnchor) async throws {
        isSaving = true
        defer { isSaving = false }
        try await storage.save(anchor)
    }

    func loadAnchors() async throws -> [HBAnchor] {
        guard let space = currentSpace else { return [] }

        isLoading = true
        defer { isLoading = false }

        return try await storage.loadAnchors(spaceId: space.id)
    }
}

enum PersistenceError: LocalizedError {
    case noActiveSpace

    var errorDescription: String? {
        switch self {
        case .noActiveSpace:
            return "No active space. Create or load a space first."
        }
    }
}

Anchorable Protocol

Create a protocol for objects that can be persisted as anchors:

protocol Anchorable: Identifiable where ID == UUID {
    var simdTransform: simd_float4x4 { get }
    func toMetadata() -> [String: AnyCodableValue]
    init?(from anchor: HBAnchor)
}

// Extension on PersistenceManager
extension PersistenceManager {
    func saveAnchor<T: Anchorable>(_ item: T) async throws {
        guard let space = currentSpace else {
            throw PersistenceError.noActiveSpace
        }

        let anchor = HBAnchor(
            id: item.id,
            spaceId: space.id,
            transform: item.simdTransform,
            metadata: item.toMetadata()
        )
        try await storage.save(anchor)
    }

    func loadAnchors<T: Anchorable>() async throws -> [T] {
        guard let space = currentSpace else { return [] }

        let anchors = try await storage.loadAnchors(spaceId: space.id)
        return anchors.compactMap { T(from: $0) }
    }
}

Model Extension Pattern

Extend your domain models to convert to/from HBAnchor:

// Your app's model
struct StickyNote: Identifiable {
    let id: UUID
    var text: String
    var color: NoteColor
    var transform: simd_float4x4
    let createdAt: Date

    enum NoteColor: String {
        case yellow, blue, green, pink
    }
}

// Conform to Anchorable
extension StickyNote: Anchorable {
    var simdTransform: simd_float4x4 { transform }

    func toMetadata() -> [String: AnyCodableValue] {
        [
            "type": .string("sticky_note"),
            "text": .string(text),
            "color": .string(color.rawValue)
        ]
    }

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

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

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

ViewModel Pattern

Combine AR session management with persistence:

@MainActor
class ARViewModel: ObservableObject {
    private let arSession: ARSessionManager
    private let persistence: PersistenceManager

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

    init() {
        self.arSession = ARSessionManager()
        self.persistence = PersistenceManager()
    }

    func start() async {
        arSession.start()

        // Try to load existing space
        do {
            if let space = try await persistence.loadMostRecentSpace() {
                let worldMap = try space.arWorldMap()
                arSession.relocalize(with: worldMap)

                // Wait for relocalization
                for await isRelocalized in arSession.$isRelocalized.values {
                    if isRelocalized {
                        try await loadNotes()
                        break
                    }
                }
            }
        } catch {
            self.error = error
        }
    }

    func placeNote(text: String, at transform: simd_float4x4) async throws {
        // Ensure we have a space
        if persistence.currentSpace == nil {
            let worldMap = try await arSession.getCurrentWorldMap()
            _ = try await persistence.createSpace(name: nil, worldMap: worldMap)
        }

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

        try await persistence.saveAnchor(note)
        notes.append(note)
    }

    func loadNotes() async throws {
        notes = try await persistence.loadAnchors()
    }

    func deleteNote(_ note: StickyNote) async throws {
        try await persistence.storage.deleteAnchor(id: note.id)
        notes.removeAll { $0.id == note.id }
    }
}

SwiftUI Integration

Environment Object

@main
struct MyARApp: App {
    @StateObject private var viewModel = ARViewModel()

    var body: some Scene {
        WindowGroup {
            ContentView()
                .environmentObject(viewModel)
        }
    }
}

struct ContentView: View {
    @EnvironmentObject var viewModel: ARViewModel

    var body: some View {
        ZStack {
            ARViewContainer()

            if viewModel.persistence.isLoading {
                LoadingOverlay()
            }

            VStack {
                Spacer()
                ToolbarView()
            }
        }
        .task {
            await viewModel.start()
        }
        .alert("Error", isPresented: .constant(viewModel.error != nil)) {
            Button("OK") { viewModel.error = nil }
        } message: {
            Text(viewModel.error?.localizedDescription ?? "")
        }
    }
}

Loading States

struct LoadingOverlay: View {
    var body: some View {
        ZStack {
            Color.black.opacity(0.5)
            VStack(spacing: 16) {
                ProgressView()
                    .scaleEffect(1.5)
                Text("Loading space...")
                    .foregroundColor(.white)
            }
        }
        .ignoresSafeArea()
    }
}

Dependency Injection

For testability, use dependency injection:

protocol StorageProtocol {
    func save(_ space: HBSpace) async throws
    func loadAllSpaces() async throws -> [HBSpace]
    func save(_ anchor: HBAnchor) async throws
    func loadAnchors(spaceId: UUID) async throws -> [HBAnchor]
}

extension HBStorage: StorageProtocol {}

class MockStorage: StorageProtocol {
    var spaces: [HBSpace] = []
    var anchors: [HBAnchor] = []

    func save(_ space: HBSpace) async throws {
        spaces.append(space)
    }

    func loadAllSpaces() async throws -> [HBSpace] {
        return spaces
    }

    func save(_ anchor: HBAnchor) async throws {
        anchors.append(anchor)
    }

    func loadAnchors(spaceId: UUID) async throws -> [HBAnchor] {
        return anchors.filter { $0.spaceId == spaceId }
    }
}

Next Steps