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
- •Error Handling - Handle errors gracefully
- •Examples - Complete code examples