PlayerPreferences.swift (2796B)
1 import Observation 2 import SwiftUI 3 4 /// Local, per-device player preferences (colour and display name). 5 /// 6 /// Persistence is layered: values are written to `UserDefaults` for fast local 7 /// reads and to `NSUbiquitousKeyValueStore` so they follow the user across 8 /// their devices. On launch we prefer the cloud copy if present, falling back 9 /// to the local copy, then to defaults. External changes (from another 10 /// device) are observed and applied live. 11 /// 12 /// This object is anchored in `RootView`'s `@State` and lives for the app's 13 /// lifetime; no `deinit` / observer teardown is required. 14 @Observable 15 @MainActor 16 final class PlayerPreferences { 17 private enum Keys { 18 static let colorID = "playerColorID" 19 static let name = "playerName" 20 static let isICloudSyncEnabled = "isICloudSyncEnabled" 21 } 22 23 private let local: UserDefaults 24 private let cloud: NSUbiquitousKeyValueStore 25 26 var colorID: String { 27 didSet { write(Keys.colorID, colorID) } 28 } 29 30 var name: String { 31 didSet { write(Keys.name, name) } 32 } 33 34 /// Debugging switch for comparing on-device responsiveness with CloudKit 35 /// work paused. Stored locally only so disabling sync on one device does 36 /// not silently disable it everywhere. 37 var isICloudSyncEnabled: Bool { 38 didSet { local.set(isICloudSyncEnabled, forKey: Keys.isICloudSyncEnabled) } 39 } 40 41 var color: PlayerColor { 42 get { PlayerColor.color(for: colorID) } 43 set { colorID = newValue.id } 44 } 45 46 init( 47 local: UserDefaults = .standard, 48 cloud: NSUbiquitousKeyValueStore = .default 49 ) { 50 self.local = local 51 self.cloud = cloud 52 self.colorID = cloud.string(forKey: Keys.colorID) 53 ?? local.string(forKey: Keys.colorID) 54 ?? PlayerColor.blue.id 55 self.name = cloud.string(forKey: Keys.name) 56 ?? local.string(forKey: Keys.name) 57 ?? "Player" 58 self.isICloudSyncEnabled = local.object(forKey: Keys.isICloudSyncEnabled) as? Bool ?? true 59 cloud.synchronize() 60 NotificationCenter.default.addObserver( 61 forName: NSUbiquitousKeyValueStore.didChangeExternallyNotification, 62 object: cloud, 63 queue: .main 64 ) { [weak self] _ in 65 MainActor.assumeIsolated { 66 self?.pullFromCloud() 67 } 68 } 69 } 70 71 private func write(_ key: String, _ value: String) { 72 local.set(value, forKey: key) 73 cloud.set(value, forKey: key) 74 } 75 76 private func pullFromCloud() { 77 if let id = cloud.string(forKey: Keys.colorID), id != colorID { 78 colorID = id 79 } 80 if let newName = cloud.string(forKey: Keys.name), newName != name { 81 name = newName 82 } 83 } 84 }