Files
InvestmentTrackerApp/PortfolioJournalWatch/WatchDataStore.swift
T
alexandrev-tibco 464fbb2bad App de Apple Watch + 3 complicaciones (build 88) #48
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
2026-09-13 22:05:31 +02:00

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() }
}
}
}