crossmate

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

GameCursorStore.swift (2062B)


      1 import Foundation
      2 
      3 /// Persists the local cursor (row, column, direction) per game in
      4 /// `UserDefaults`.
      5 ///
      6 /// The cursor is device-local by design — it is never synced to CloudKit. The
      7 /// coarser collaborator-visible "cursor track" is published separately by
      8 /// `PlayerSession`; this store only remembers where *this* player left off so
      9 /// reopening a puzzle (even after a cold launch) restores their position.
     10 ///
     11 /// Layout under the `"gameCursors"` key:
     12 ///   `[gameID.uuidString: ["row": Int, "col": Int, "dir": Int]]`
     13 ///   where `dir` is `0` for across and `1` for down.
     14 @MainActor
     15 final class GameCursorStore {
     16     struct Cursor: Equatable {
     17         var row: Int
     18         var col: Int
     19         var direction: Puzzle.Direction
     20     }
     21 
     22     private let defaults: UserDefaults
     23     private let defaultsKey = "gameCursors"
     24 
     25     init(defaults: UserDefaults = .standard) {
     26         self.defaults = defaults
     27     }
     28 
     29     // MARK: - Read
     30 
     31     func cursor(forGame gameID: UUID) -> Cursor? {
     32         guard let entry = rawStore[gameID.uuidString],
     33               let row = entry["row"],
     34               let col = entry["col"],
     35               let dir = entry["dir"] else { return nil }
     36         return Cursor(row: row, col: col, direction: dir == 1 ? .down : .across)
     37     }
     38 
     39     // MARK: - Write
     40 
     41     func setCursor(_ cursor: Cursor, forGame gameID: UUID) {
     42         if self.cursor(forGame: gameID) == cursor { return }
     43         var s = rawStore
     44         s[gameID.uuidString] = [
     45             "row": cursor.row,
     46             "col": cursor.col,
     47             "dir": cursor.direction == .down ? 1 : 0
     48         ]
     49         rawStore = s
     50     }
     51 
     52     func clearCursor(forGame gameID: UUID) {
     53         var s = rawStore
     54         guard s[gameID.uuidString] != nil else { return }
     55         s.removeValue(forKey: gameID.uuidString)
     56         rawStore = s
     57     }
     58 
     59     // MARK: - Private
     60 
     61     private var rawStore: [String: [String: Int]] {
     62         get { defaults.dictionary(forKey: defaultsKey) as? [String: [String: Int]] ?? [:] }
     63         set { defaults.set(newValue, forKey: defaultsKey) }
     64     }
     65 }