129 lines
4.7 KiB
Swift
129 lines
4.7 KiB
Swift
import Combine
|
|
import Foundation
|
|
import WatchKit
|
|
|
|
@MainActor
|
|
public final class WatchAppModel: ObservableObject {
|
|
@Published public var hasCompletedOnboarding: Bool {
|
|
didSet { defaults.set(hasCompletedOnboarding, forKey: onboardingKey) }
|
|
}
|
|
@Published public var selectedTab: WatchHomeDestination?
|
|
|
|
public let store: WatchStore
|
|
public let measurement: WatchHealthMeasurementService
|
|
public let haptics: WatchHapticService
|
|
public let connectivity: WatchConnectivityService
|
|
|
|
private let defaults: UserDefaults
|
|
private let onboardingKey = "haptika.watch.onboarding.completed.v1"
|
|
private let deviceIDKey = "haptika.watch.device.id.v1"
|
|
private var sequence: UInt64
|
|
|
|
public var deviceID: String {
|
|
if let value = defaults.string(forKey: deviceIDKey) { return value }
|
|
let value = UUID().uuidString
|
|
defaults.set(value, forKey: deviceIDKey)
|
|
return value
|
|
}
|
|
|
|
public init(
|
|
defaults: UserDefaults = .standard,
|
|
store: WatchStore? = nil,
|
|
measurement: WatchHealthMeasurementService? = nil,
|
|
haptics: WatchHapticService? = nil,
|
|
connectivity: WatchConnectivityService? = nil
|
|
) {
|
|
self.defaults = defaults
|
|
self.store = store ?? WatchStore(defaults: defaults)
|
|
self.measurement = measurement ?? WatchHealthMeasurementService()
|
|
self.haptics = haptics ?? WatchHapticService()
|
|
self.connectivity = connectivity ?? WatchConnectivityService()
|
|
hasCompletedOnboarding = defaults.bool(forKey: onboardingKey)
|
|
sequence = UInt64(defaults.integer(forKey: "haptika.watch.sequence.v1"))
|
|
wireServices()
|
|
}
|
|
|
|
public func start() {
|
|
connectivity.activate()
|
|
connectivity.flush(outbox: store.outbox)
|
|
}
|
|
|
|
public func enqueueMeasurement(_ measurement: WatchMeasurementSummaryV1) {
|
|
guard let envelope = makeEnvelope(kind: .measurementUpsert, payload: measurement) else {
|
|
store.upsert(measurement)
|
|
return
|
|
}
|
|
store.upsert(measurement, enqueue: envelope)
|
|
connectivity.transfer(envelope)
|
|
}
|
|
|
|
public func enqueueSession(_ session: WatchRegulationSessionV1) {
|
|
guard let envelope = makeEnvelope(kind: .regulationSessionUpsert, payload: session) else {
|
|
store.upsert(session)
|
|
return
|
|
}
|
|
store.upsert(session, enqueue: envelope)
|
|
connectivity.transfer(envelope)
|
|
}
|
|
|
|
public func deleteMeasurement(_ measurement: WatchMeasurementSummaryV1) {
|
|
let tombstone = WatchDeletionTombstoneV1(entityID: measurement.id, entityKind: "measurement")
|
|
let envelope = makeEnvelope(kind: .tombstone, payload: tombstone)
|
|
_ = store.deleteMeasurement(id: measurement.id, envelope: envelope)
|
|
if let envelope { connectivity.transfer(envelope) }
|
|
}
|
|
|
|
public func saveFeeling(_ feeling: Int, for measurement: WatchMeasurementSummaryV1) {
|
|
var updated = measurement
|
|
updated.feeling = min(5, max(1, feeling))
|
|
updated.feelingRecordedAt = Date()
|
|
updated.updatedAt = Date()
|
|
enqueueMeasurement(updated)
|
|
}
|
|
|
|
private func wireServices() {
|
|
measurement.onCompleted = { [weak self] summary in self?.enqueueMeasurement(summary) }
|
|
measurement.onLiveHeartRate = { [weak self] sample in
|
|
guard let self,
|
|
let envelope = self.makeEnvelope(
|
|
kind: .liveHeartRate,
|
|
payload: WatchLiveHeartRateV1(bpm: sample.bpm, sampledAt: sample.date)
|
|
) else { return }
|
|
self.connectivity.sendLive(envelope)
|
|
}
|
|
connectivity.onAcknowledgement = { [weak self] ids in self?.store.acknowledge(eventIDs: ids) }
|
|
connectivity.onEnvelope = { [weak self] envelope in
|
|
guard let self else { return }
|
|
if envelope.kind == .preferences,
|
|
let preferences = try? envelope.decodePayload(WatchPreferencesV1.self) {
|
|
self.store.preferences = preferences
|
|
} else if envelope.kind == .measurementUpsert,
|
|
let measurement = try? envelope.decodePayload(WatchMeasurementSummaryV1.self) {
|
|
self.store.upsert(measurement)
|
|
} else if envelope.kind == .regulationSessionUpsert,
|
|
let session = try? envelope.decodePayload(WatchRegulationSessionV1.self) {
|
|
self.store.upsert(session)
|
|
} else if envelope.kind == .tombstone,
|
|
let tombstone = try? envelope.decodePayload(WatchDeletionTombstoneV1.self) {
|
|
self.store.apply(tombstone)
|
|
}
|
|
}
|
|
}
|
|
|
|
private func makeEnvelope<Payload: Encodable>(kind: WatchSyncKind, payload: Payload) -> WatchSyncEnvelopeV1? {
|
|
sequence += 1
|
|
defaults.set(Int(sequence), forKey: "haptika.watch.sequence.v1")
|
|
return try? .init(deviceID: deviceID, sequence: sequence, kind: kind, payload: payload)
|
|
}
|
|
}
|
|
|
|
public enum WatchHomeDestination: String, Hashable, Identifiable {
|
|
case measure
|
|
case coherence
|
|
case exercises
|
|
case cardiacMirror
|
|
case history
|
|
|
|
public var id: String { rawValue }
|
|
}
|