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")
}