Files
workouts/Shared/Connectivity/WCPayload.swift
T
rzen 85d0eaddbb Workouts 2.0: re-base persistence on iCloud Drive documents
Replace Core Data + NSPersistentCloudKitContainer + App-Group store +
WatchConnectivity dictionary sync with the QuickRabbit iCloud-documents
architecture:

- iCloud Drive JSON documents are the sole source of truth (one file per
  aggregate: Splits/<ULID>.json, Workouts/YYYY/MM/<ULID>.json), with a
  rebuildable SwiftData cache populated only by an NSMetadataQuery observer
  and a connect-time reconcile. Soft-delete tombstones; hard iCloud gate.
- Shared model layer (ULID, Codable *Documents + stateless mappers, @Model
  cache entities, SwiftData container) compiled into both targets.
- New iPhone<->Watch bridge over WatchConnectivity keyed by ULIDs; the phone
  is the sole writer of iCloud Drive, the watch round-trips documents.
- AppServices DI + iCloud-required root gate; Swift 6 strict concurrency.
- Starter splits generated on demand from the bundled YAML catalogs.
- Migrate to XcodeGen (project.yml), iOS 26 / watchOS 26; CloudDocuments
  entitlement (drop CloudKit/App Group/aps-environment).
- Duration stored as Int seconds (was a Date epoch hack); fix workout
  end-on-create, undismissable delete dialog, toolbar-hiding nav stacks,
  and the Settings placeholder.
- Add README/CHANGELOG/LICENSE, .gitignore, refreshed REQUIREMENTS, and the
  Scripts/ TestFlight pipeline (release.sh + ASC API scripts).

MARKETING_VERSION 2.0.
2026-06-19 14:25:27 -04:00

50 lines
2.2 KiB
Swift

import Foundation
/// Wire format for the iPhoneWatch bridge. The phone is the only device that
/// touches iCloud Drive; the watch round-trips domain documents through the phone.
/// Payloads carry the shared `*Document` types (JSON-encoded `Data` blobs, which
/// WatchConnectivity allows) keyed by stable ULIDs no name/date reconciliation.
enum WCPayload {
static let typeKey = "type"
static let splitsKey = "splits"
static let workoutsKey = "workouts"
static let workoutKey = "workout"
static let workoutUpdateType = "workoutUpdate" // watch phone (one workout)
static let requestSyncType = "requestSync" // watch phone (please push state)
// MARK: - Phone Watch (application context: latest-state-wins)
static func encodeState(splits: [SplitDocument], workouts: [WorkoutDocument]) -> [String: Any] {
var dict: [String: Any] = [:]
if let s = try? DocumentCoder.encoder.encode(splits) { dict[splitsKey] = s }
if let w = try? DocumentCoder.encoder.encode(workouts) { dict[workoutsKey] = w }
return dict
}
static func decodeSplits(_ dict: [String: Any]) -> [SplitDocument] {
guard let data = dict[splitsKey] as? Data else { return [] }
return (try? DocumentCoder.decoder.decode([SplitDocument].self, from: data)) ?? []
}
static func decodeWorkouts(_ dict: [String: Any]) -> [WorkoutDocument] {
guard let data = dict[workoutsKey] as? Data else { return [] }
return (try? DocumentCoder.decoder.decode([WorkoutDocument].self, from: data)) ?? []
}
// MARK: - Watch Phone (a single updated workout)
static func encodeWorkoutUpdate(_ workout: WorkoutDocument) -> [String: Any] {
var dict: [String: Any] = [typeKey: workoutUpdateType]
if let w = try? DocumentCoder.encoder.encode(workout) { dict[workoutKey] = w }
return dict
}
static func decodeWorkoutUpdate(_ dict: [String: Any]) -> WorkoutDocument? {
guard let data = dict[workoutKey] as? Data else { return nil }
return try? DocumentCoder.decoder.decode(WorkoutDocument.self, from: data)
}
static func requestSyncMessage() -> [String: Any] { [typeKey: requestSyncType] }
}