initial commit: Proximity iOS app proposal, full Swift code scaffold, reference backend, testing docs, generated Xcode project
This commit is contained in:
171
Proximity/App/AppState.swift
Normal file
171
Proximity/App/AppState.swift
Normal file
@@ -0,0 +1,171 @@
|
||||
import Foundation
|
||||
import Combine
|
||||
|
||||
/// Root application state. Owns the long-lived services and wires them together.
|
||||
///
|
||||
/// The central philosophy: **Presence is foreground-first.** The app is not a
|
||||
/// passive background listener — it is an active instrument you hold while you
|
||||
/// are out in the world. This is a feature, not a limitation.
|
||||
@MainActor
|
||||
final class AppState: ObservableObject {
|
||||
|
||||
// MARK: - Published state
|
||||
|
||||
/// Whether the user is currently "open to connection" and the app is
|
||||
/// actively broadcasting + listening. This is the master switch.
|
||||
@Published var isPresent: Bool = false
|
||||
|
||||
/// The user's chosen radius tier. Defaults to "right here" (UWB).
|
||||
@Published var radiusTier: DistanceTier = .rightHere
|
||||
|
||||
/// The current proximity engine (UWB / BLE / beacon) in use.
|
||||
@Published private(set) var activeEngine: ProximityEngine = .uwb
|
||||
|
||||
// MARK: - Debug / simulation state
|
||||
#if DEBUG
|
||||
/// Whether we're using the simulated adapter (no hardware).
|
||||
@Published var isSimulated = true
|
||||
/// When true, the simulated adapter emits nearby users on a timer.
|
||||
@Published var autoBroadcast = false {
|
||||
didSet { configureSimulation() }
|
||||
}
|
||||
let simulatedAdapter = SimulatedAdapter()
|
||||
#endif
|
||||
|
||||
// MARK: - Services
|
||||
|
||||
let presenceEngine: PresenceEngine
|
||||
let tokenManager: TokenManager
|
||||
let cryptoManager: CryptoManager
|
||||
let apiClient: APIClient
|
||||
|
||||
let authService: AuthService
|
||||
let profileImportService: ProfileImportService
|
||||
|
||||
let graphStore: GraphStore
|
||||
let realtime: RealtimeClient
|
||||
let revealFlow: RevealFlow
|
||||
let chatViewModel: ChatViewModel
|
||||
|
||||
let presenceViewModel: PresenceViewModel
|
||||
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
|
||||
init() {
|
||||
let tokenManager = TokenManager()
|
||||
let cryptoManager = CryptoManager()
|
||||
|
||||
#if DEBUG
|
||||
// In the simulator, point at the local reference server.
|
||||
let apiClient = APIClient.local()
|
||||
#else
|
||||
let apiClient = APIClient()
|
||||
#endif
|
||||
|
||||
self.tokenManager = tokenManager
|
||||
self.cryptoManager = cryptoManager
|
||||
self.apiClient = apiClient
|
||||
|
||||
let authService = AuthService(apiClient: apiClient)
|
||||
self.authService = authService
|
||||
let profileImportService = ProfileImportService()
|
||||
self.profileImportService = profileImportService
|
||||
|
||||
let graphStore = GraphStore()
|
||||
self.graphStore = graphStore
|
||||
|
||||
let realtime = RealtimeClient()
|
||||
self.realtime = realtime
|
||||
|
||||
let revealFlow = RevealFlow(
|
||||
apiClient: apiClient,
|
||||
cryptoManager: cryptoManager,
|
||||
profileService: profileImportService,
|
||||
graphStore: graphStore,
|
||||
realtime: realtime
|
||||
)
|
||||
self.revealFlow = revealFlow
|
||||
|
||||
let chatViewModel = ChatViewModel(
|
||||
apiClient: apiClient,
|
||||
cryptoManager: cryptoManager,
|
||||
graphStore: graphStore,
|
||||
realtime: realtime
|
||||
)
|
||||
self.chatViewModel = chatViewModel
|
||||
|
||||
let presenceEngine: PresenceEngine
|
||||
#if DEBUG
|
||||
// In DEBUG (simulator), drive the engine with the simulated adapter
|
||||
// so the whole interaction loop can be tested without hardware.
|
||||
presenceEngine = PresenceEngine(
|
||||
tokenManager: tokenManager,
|
||||
cryptoManager: cryptoManager,
|
||||
apiClient: apiClient,
|
||||
uwbAdapter: simulatedAdapter,
|
||||
bleAdapter: simulatedAdapter,
|
||||
beaconAdapter: simulatedAdapter
|
||||
)
|
||||
#else
|
||||
presenceEngine = PresenceEngine(
|
||||
tokenManager: tokenManager,
|
||||
cryptoManager: cryptoManager,
|
||||
apiClient: apiClient
|
||||
)
|
||||
#endif
|
||||
self.presenceEngine = presenceEngine
|
||||
|
||||
self.presenceViewModel = PresenceViewModel(presenceEngine: presenceEngine)
|
||||
|
||||
// When the user toggles presence, drive the engine and open the
|
||||
// live socket so mutual waves and messages arrive in real time.
|
||||
$isPresent
|
||||
.sink { [weak presenceEngine, weak realtime] present in
|
||||
Task {
|
||||
await presenceEngine?.setPresent(present)
|
||||
if present {
|
||||
realtime?.connect(token: tokenManager.currentToken())
|
||||
} else {
|
||||
realtime?.disconnect()
|
||||
}
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
$radiusTier
|
||||
.sink { [weak presenceEngine] tier in
|
||||
Task { await presenceEngine?.setRadiusTier(tier) }
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
#if DEBUG
|
||||
configureSimulation()
|
||||
#endif
|
||||
}
|
||||
|
||||
// MARK: - Debug / simulation
|
||||
#if DEBUG
|
||||
/// Forward the simulated adapter's events into the engine and set up
|
||||
/// auto-broadcast when enabled.
|
||||
private func configureSimulation() {
|
||||
simulatedAdapter.autoBroadcast = autoBroadcast
|
||||
// The engine already wired onEncounter/onBroadcast/onRegion to the
|
||||
// simulated adapter at init. Auto-broadcast is handled internally.
|
||||
}
|
||||
|
||||
/// Used by the debug menu to simulate a single nearby user.
|
||||
func simulateEncounter() {
|
||||
simulatedAdapter.simulateEncounter(remoteToken: "sim-\(Int.random(in: 100_000...999_999))")
|
||||
}
|
||||
|
||||
/// Used by the debug menu to simulate a crowd.
|
||||
func simulateCrowd() {
|
||||
simulatedAdapter.simulateCrowd(count: 20)
|
||||
}
|
||||
|
||||
/// Reconnect the realtime socket (e.g. after starting the local server).
|
||||
func restartRealtime() {
|
||||
realtime.connect(token: tokenManager.currentToken())
|
||||
}
|
||||
#endif
|
||||
}
|
||||
Reference in New Issue
Block a user