crossmate

A collaborative crossword app for iOS
Log | Files | Refs | LICENSE

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 }