feat: implement Haptika PPG and Watch V1
This commit is contained in:
@@ -0,0 +1,129 @@
|
||||
import Combine
|
||||
import Foundation
|
||||
import WatchConnectivity
|
||||
|
||||
@MainActor
|
||||
public final class WatchConnectivityService: NSObject, ObservableObject {
|
||||
@Published public private(set) var activationState: WCSessionActivationState = .notActivated
|
||||
@Published public private(set) var isReachable = false
|
||||
@Published public private(set) var lastSynchronizedAt: Date?
|
||||
@Published public private(set) var lastError: String?
|
||||
|
||||
public var onEnvelope: ((WatchSyncEnvelopeV1) -> Void)?
|
||||
public var onAcknowledgement: ((Set<UUID>) -> Void)?
|
||||
|
||||
private let session: WCSession?
|
||||
private let encoder = WatchCoding.encoder
|
||||
private let decoder = WatchCoding.decoder
|
||||
|
||||
public override init() {
|
||||
session = WCSession.isSupported() ? .default : nil
|
||||
super.init()
|
||||
}
|
||||
|
||||
public func activate() {
|
||||
guard let session else { return }
|
||||
session.delegate = self
|
||||
session.activate()
|
||||
activationState = session.activationState
|
||||
isReachable = session.isReachable
|
||||
}
|
||||
|
||||
public func sendLive(_ envelope: WatchSyncEnvelopeV1) {
|
||||
guard let session, session.isReachable else { return }
|
||||
do {
|
||||
let data = try encoder.encode(envelope)
|
||||
session.sendMessageData(data, replyHandler: nil) { [weak self] error in
|
||||
Task { @MainActor in self?.lastError = error.localizedDescription }
|
||||
}
|
||||
} catch {
|
||||
lastError = error.localizedDescription
|
||||
}
|
||||
}
|
||||
|
||||
/// Guaranteed delivery for durable mutations. WatchConnectivity persists these while offline.
|
||||
public func transfer(_ envelope: WatchSyncEnvelopeV1) {
|
||||
guard let session else { return }
|
||||
do {
|
||||
session.transferUserInfo(["envelope": try encoder.encode(envelope)])
|
||||
} catch {
|
||||
lastError = error.localizedDescription
|
||||
}
|
||||
}
|
||||
|
||||
public func flush(outbox: [WatchSyncEnvelopeV1]) {
|
||||
for envelope in outbox { transfer(envelope) }
|
||||
}
|
||||
|
||||
public func updateApplicationContext(preferences: WatchPreferencesV1) {
|
||||
guard let session else { return }
|
||||
do {
|
||||
try session.updateApplicationContext(["preferences": try encoder.encode(preferences)])
|
||||
} catch {
|
||||
lastError = error.localizedDescription
|
||||
}
|
||||
}
|
||||
|
||||
private func receive(_ data: Data) {
|
||||
do {
|
||||
let envelope = try decoder.decode(WatchSyncEnvelopeV1.self, from: data)
|
||||
lastSynchronizedAt = Date()
|
||||
if envelope.kind == .acknowledgement,
|
||||
let acknowledgement = try? envelope.decodePayload(WatchAcknowledgementV1.self) {
|
||||
onAcknowledgement?(Set(acknowledgement.eventIDs))
|
||||
} else {
|
||||
onEnvelope?(envelope)
|
||||
}
|
||||
} catch {
|
||||
lastError = error.localizedDescription
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension WatchConnectivityService: WCSessionDelegate {
|
||||
nonisolated public func session(
|
||||
_ session: WCSession,
|
||||
activationDidCompleteWith activationState: WCSessionActivationState,
|
||||
error: Error?
|
||||
) {
|
||||
Task { @MainActor [weak self] in
|
||||
self?.activationState = activationState
|
||||
self?.isReachable = session.isReachable
|
||||
self?.lastError = error?.localizedDescription
|
||||
}
|
||||
}
|
||||
|
||||
nonisolated public func sessionReachabilityDidChange(_ session: WCSession) {
|
||||
Task { @MainActor [weak self] in self?.isReachable = session.isReachable }
|
||||
}
|
||||
|
||||
nonisolated public func session(_ session: WCSession, didReceiveMessageData messageData: Data) {
|
||||
Task { @MainActor [weak self] in self?.receive(messageData) }
|
||||
}
|
||||
|
||||
nonisolated public func session(_ session: WCSession, didReceiveUserInfo userInfo: [String: Any] = [:]) {
|
||||
guard let data = userInfo["envelope"] as? Data else { return }
|
||||
Task { @MainActor [weak self] in self?.receive(data) }
|
||||
}
|
||||
|
||||
nonisolated public func session(
|
||||
_ session: WCSession,
|
||||
didReceiveApplicationContext applicationContext: [String: Any]
|
||||
) {
|
||||
guard let data = applicationContext["preferences"] as? Data else { return }
|
||||
Task { @MainActor [weak self] in
|
||||
guard let self,
|
||||
let preferences = try? self.decoder.decode(WatchPreferencesV1.self, from: data) else { return }
|
||||
let payload = try? self.encoder.encode(preferences)
|
||||
if let payload {
|
||||
self.onEnvelope?(.init(
|
||||
deviceID: "iphone",
|
||||
sequence: 0,
|
||||
kind: .preferences,
|
||||
payload: payload
|
||||
))
|
||||
}
|
||||
self.lastSynchronizedAt = Date()
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user