464fbb2bad
App de watchOS de solo consulta: resumen de cartera (total, cambio del mes y mayores posiciones), objetivos con su progreso y estado del check-in con la racha. Toda la edición sigue en el iPhone. Complicaciones (accessoryCircular / Rectangular / Inline / Corner): valor total de la cartera, progreso del objetivo más cercano a cumplirse y racha + estado del check-in. Datos: los App Groups no se comparten entre iOS y watchOS, así que el reloj no puede leer el store de CoreData que lee el widget de iOS. En su lugar, WatchSyncService construye un WatchPortfolioSnapshot ligero y lo envía como application context de WatchConnectivity, enganchado a CoreDataStack.refreshWidgetData() — el mismo punto que refresca el widget de iOS, así que cualquier repositorio que toque datos refresca el reloj. WatchDataStore lo cachea en el App Group del watch (WatchSnapshotCache) y recarga las timelines; la extensión de complicaciones solo lee ese caché. Proyecto: dos targets nuevos (watchOS app + widget extension) con grupos sincronizados de Xcode 16; Shared/ pertenece a la app de watch y se comparte con la app de iOS y el widget mediante exception sets, el mismo mecanismo que ya usaba el widget de iOS para el modelo de CoreData. Scheme compartido PortfolioJournalWatch. Localización propia por target en los 7 idiomas. Verificado: compilan los tres targets, la app de watch queda embebida en Watch/ con la extensión en PlugIns/, y en el par de simuladores iPhone 17 Pro Max + Apple Watch Series 11 el reloj recibe y muestra el snapshot enviado por el iPhone. 10 tests nuevos del payload. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YcDn5ccuRFV83q7xWWokBT
104 lines
3.4 KiB
Swift
104 lines
3.4 KiB
Swift
import Foundation
|
|
import WatchConnectivity
|
|
import WidgetKit
|
|
|
|
/// Receives portfolio snapshots from the iPhone and keeps them available to the
|
|
/// watch app and its complications.
|
|
///
|
|
/// Two paths feed it: the application context the phone pushes whenever data
|
|
/// changes (delivered even if the watch app is not running), and an explicit
|
|
/// request sent when the user opens the app while the phone is reachable.
|
|
@MainActor
|
|
final class WatchDataStore: NSObject, ObservableObject {
|
|
|
|
static let shared = WatchDataStore()
|
|
|
|
@Published private(set) var snapshot: WatchPortfolioSnapshot
|
|
@Published private(set) var isRefreshing = false
|
|
/// True once we know the phone can be asked for fresh data.
|
|
@Published private(set) var isPhoneReachable = false
|
|
|
|
private var session: WCSession? {
|
|
WCSession.isSupported() ? WCSession.default : nil
|
|
}
|
|
|
|
private override init() {
|
|
snapshot = WatchSnapshotCache.load() ?? .empty
|
|
super.init()
|
|
}
|
|
|
|
func activate() {
|
|
guard let session else { return }
|
|
session.delegate = self
|
|
if session.activationState != .activated {
|
|
session.activate()
|
|
} else {
|
|
isPhoneReachable = session.isReachable
|
|
}
|
|
}
|
|
|
|
/// Asks the phone for a fresh snapshot. Silently does nothing when the
|
|
/// phone is out of reach — the cached snapshot stays on screen.
|
|
func refresh() {
|
|
guard let session, session.activationState == .activated, session.isReachable else { return }
|
|
isRefreshing = true
|
|
session.sendMessage(
|
|
[WatchSyncMessage.requestSnapshot: true],
|
|
replyHandler: { [weak self] reply in
|
|
Task { @MainActor in
|
|
self?.isRefreshing = false
|
|
self?.apply(context: reply)
|
|
}
|
|
},
|
|
errorHandler: { [weak self] _ in
|
|
Task { @MainActor in
|
|
self?.isRefreshing = false
|
|
}
|
|
}
|
|
)
|
|
}
|
|
|
|
private func apply(context: [String: Any]) {
|
|
guard let received = WatchPortfolioSnapshot.from(applicationContext: context) else { return }
|
|
// Application contexts can arrive out of order after a reconnect.
|
|
guard received.generatedAt >= snapshot.generatedAt else { return }
|
|
snapshot = received
|
|
WatchSnapshotCache.save(received)
|
|
WidgetCenter.shared.reloadAllTimelines()
|
|
}
|
|
}
|
|
|
|
// MARK: - WCSessionDelegate
|
|
|
|
extension WatchDataStore: WCSessionDelegate {
|
|
|
|
nonisolated func session(
|
|
_ session: WCSession,
|
|
activationDidCompleteWith activationState: WCSessionActivationState,
|
|
error: Error?
|
|
) {
|
|
guard activationState == .activated else { return }
|
|
let context = session.receivedApplicationContext
|
|
let reachable = session.isReachable
|
|
Task { @MainActor in
|
|
self.isPhoneReachable = reachable
|
|
self.apply(context: context)
|
|
self.refresh()
|
|
}
|
|
}
|
|
|
|
nonisolated func session(_ session: WCSession, didReceiveApplicationContext applicationContext: [String: Any]) {
|
|
Task { @MainActor in
|
|
self.apply(context: applicationContext)
|
|
}
|
|
}
|
|
|
|
nonisolated func sessionReachabilityDidChange(_ session: WCSession) {
|
|
let reachable = session.isReachable
|
|
Task { @MainActor in
|
|
self.isPhoneReachable = reachable
|
|
if reachable { self.refresh() }
|
|
}
|
|
}
|
|
}
|