import Foundation import Combine /// View model for the presence experience. /// /// Exposes the state the UI needs to render the "am I present?" moment, the /// radius tier picker, and the live feed of encounters (silhouettes). @MainActor final class PresenceViewModel: ObservableObject { @Published var isPresent: Bool = false @Published var radiusTier: DistanceTier = .rightHere @Published private(set) var activeEngines: Set = [] @Published private(set) var encounters: [Encounter] = [] @Published private(set) var lastScan: Date? private let engine: PresenceEngine private var cancellables = Set() init(engine: PresenceEngine) { self.engine = engine engine.$isPresent .assign(to: &$isPresent) engine.$radiusTier .assign(to: &$radiusTier) engine.$activeEngines .assign(to: &$activeEngines) engine.$recentEncounters .assign(to: &$encounters) engine.$lastScan .assign(to: &$lastScan) } // MARK: - User actions func togglePresence() { isPresent.toggle() Task { await engine.setPresent(isPresent) } } func setRadius(_ tier: DistanceTier) { radiusTier = tier Task { await engine.setRadiusTier(tier) } } /// Send a "wave" to a silhouette — the first step toward mutual reveal. func wave(to encounter: Encounter) { // In production: POST /v1/wave with the encounter token. Task { await engine.wave(to: encounter) } } func block(_ encounter: Encounter) { Task { await engine.block(encounter) } } }