StorageValue.swift 1.9 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465
  1. // LoopFollow
  2. // StorageValue.swift
  3. import Combine
  4. import Foundation
  5. class StorageValue<T: Codable & Equatable>: ObservableObject, BFUReloadable {
  6. let key: String
  7. @Published var value: T {
  8. didSet {
  9. guard value != oldValue else { return }
  10. // Suspect BFU: keep in memory only — a poisoned-default write could be
  11. // flushed over real data on unlock. Hydration reconciles from disk.
  12. guard !StorageReadiness.isSuppressingWrites else {
  13. StorageReadiness.noteSuppressedWrite(key: key)
  14. return
  15. }
  16. if let encodedData = try? JSONEncoder().encode(value) {
  17. StorageValue.defaults.set(encodedData, forKey: key)
  18. }
  19. }
  20. }
  21. var exists: Bool {
  22. return StorageValue.defaults.object(forKey: key) != nil
  23. }
  24. private static var defaults: UserDefaults {
  25. return UserDefaults.standard
  26. }
  27. init(key: String, defaultValue: T) {
  28. self.key = key
  29. if let data = StorageValue.defaults.data(forKey: key),
  30. let decodedValue = try? JSONDecoder().decode(T.self, from: data)
  31. {
  32. value = decodedValue
  33. } else {
  34. value = defaultValue
  35. }
  36. StorageReadiness.register(self)
  37. }
  38. func remove() {
  39. StorageValue.defaults.removeObject(forKey: key)
  40. }
  41. /// Re-reads the value from UserDefaults, updating the @Published cache.
  42. /// Call this when the app foregrounds after a Before-First-Unlock background launch,
  43. /// where StorageValue was initialized while UserDefaults was locked (returning defaults).
  44. func reload() {
  45. if let data = StorageValue.defaults.data(forKey: key),
  46. let decodedValue = try? JSONDecoder().decode(T.self, from: data),
  47. decodedValue != value
  48. {
  49. value = decodedValue
  50. }
  51. }
  52. }