GlucoseSnapshotStore.swift 2.5 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576
  1. // LoopFollow
  2. // GlucoseSnapshotStore.swift
  3. import Foundation
  4. /// Persists the latest GlucoseSnapshot into the App Group container so that:
  5. /// - the Live Activity extension can read it
  6. /// - future Watch + CarPlay surfaces can reuse it
  7. ///
  8. /// Uses an atomic JSON file write to avoid partial/corrupt reads across processes.
  9. final class GlucoseSnapshotStore {
  10. static let shared = GlucoseSnapshotStore()
  11. private init() {}
  12. private let fileName = "glucose_snapshot.json"
  13. private let queue = DispatchQueue(label: "com.loopfollow.glucoseSnapshotStore", qos: .utility)
  14. // MARK: - Public API
  15. func save(_ snapshot: GlucoseSnapshot) {
  16. queue.async {
  17. do {
  18. let url = try self.fileURL()
  19. let encoder = JSONEncoder()
  20. encoder.dateEncodingStrategy = .iso8601
  21. let data = try encoder.encode(snapshot)
  22. try data.write(to: url, options: [.atomic])
  23. } catch {
  24. // Intentionally silent (extension-safe, no dependencies).
  25. }
  26. }
  27. }
  28. func load() -> GlucoseSnapshot? {
  29. do {
  30. let url = try fileURL()
  31. guard FileManager.default.fileExists(atPath: url.path) else { return nil }
  32. let data = try Data(contentsOf: url)
  33. let decoder = JSONDecoder()
  34. decoder.dateDecodingStrategy = .iso8601
  35. return try decoder.decode(GlucoseSnapshot.self, from: data)
  36. } catch {
  37. // Intentionally silent (extension-safe, no dependencies).
  38. return nil
  39. }
  40. }
  41. func delete() {
  42. queue.async {
  43. do {
  44. let url = try self.fileURL()
  45. if FileManager.default.fileExists(atPath: url.path) {
  46. try FileManager.default.removeItem(at: url)
  47. }
  48. } catch {
  49. // Intentionally silent (extension-safe, no dependencies).
  50. }
  51. }
  52. }
  53. // MARK: - Helpers
  54. private func fileURL() throws -> URL {
  55. let groupID = AppGroupID.current()
  56. guard let containerURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: groupID) else {
  57. throw NSError(
  58. domain: "GlucoseSnapshotStore",
  59. code: 1,
  60. userInfo: [NSLocalizedDescriptionKey: "App Group containerURL is nil for id=\(groupID)"],
  61. )
  62. }
  63. return containerURL.appendingPathComponent(fileName, isDirectory: false)
  64. }
  65. }