DamusUserDefaults.swift (5037B)
1 // 2 // DamusUserDefaults.swift 3 // damus 4 // 5 // Created by Daniel D’Aquino on 2023-11-25. 6 // 7 8 import Foundation 9 10 /// # DamusUserDefaults 11 /// 12 /// This struct acts like a UserDefaults object, but is also capable of automatically mirroring values to a separate store. 13 /// 14 /// It works by using a specific store container as the main source of truth, and by optionally mirroring values to a different container if needed. 15 /// 16 /// This is useful when the data of a UserDefaults object needs to be accessible from another store container, 17 /// as it offers ways to automatically mirror information over a different container (e.g. When using app extensions) 18 /// 19 /// Since it mirrors items instead of migrating them, this object can be used in a backwards compatible manner. 20 /// 21 /// The easiest way to use this is to use `DamusUserDefaults.standard` as a drop-in replacement for `UserDefaults.standard` 22 /// Or, you can initialize a custom object with customizable stores. 23 struct DamusUserDefaults { 24 25 // MARK: - Helper data structures 26 27 enum Store: Equatable { 28 case standard 29 case shared 30 case custom(UserDefaults) 31 32 func get_user_defaults() -> UserDefaults? { 33 switch self { 34 case .standard: 35 return UserDefaults.standard 36 case .shared: 37 return UserDefaults(suiteName: Constants.DAMUS_APP_GROUP_IDENTIFIER) 38 case .custom(let user_defaults): 39 return user_defaults 40 } 41 } 42 } 43 44 enum DamusUserDefaultsError: Error { 45 case cannot_initialize_user_defaults 46 case cannot_mirror_main_user_defaults 47 } 48 49 // MARK: - Stored properties 50 51 private let main: UserDefaults 52 private let mirrors: [UserDefaults] 53 54 // MARK: - Initializers 55 56 init?(main: Store, mirror mirrors: [Store] = []) throws { 57 guard let main_user_defaults = main.get_user_defaults() else { throw DamusUserDefaultsError.cannot_initialize_user_defaults } 58 let mirror_user_defaults: [UserDefaults] = try mirrors.compactMap({ mirror_store in 59 guard let mirror_user_default = mirror_store.get_user_defaults() else { 60 throw DamusUserDefaultsError.cannot_initialize_user_defaults 61 } 62 guard mirror_store != main else { 63 throw DamusUserDefaultsError.cannot_mirror_main_user_defaults 64 } 65 return mirror_user_default 66 }) 67 68 self.main = main_user_defaults 69 self.mirrors = mirror_user_defaults 70 } 71 72 // MARK: - Functions for feature parity with UserDefaults 73 74 func string(forKey defaultName: String) -> String? { 75 let value = self.main.string(forKey: defaultName) 76 self.mirror(value, forKey: defaultName) 77 return value 78 } 79 80 func set(_ value: Any?, forKey defaultName: String) { 81 self.main.set(value, forKey: defaultName) 82 self.mirror(value, forKey: defaultName) 83 } 84 85 func removeObject(forKey defaultName: String) { 86 self.main.removeObject(forKey: defaultName) 87 self.mirror_object_removal(forKey: defaultName) 88 } 89 90 func object(forKey defaultName: String) -> Any? { 91 let value = self.main.object(forKey: defaultName) 92 self.mirror(value, forKey: defaultName) 93 return value 94 } 95 96 // MARK: - Mirroring utilities 97 98 private func mirror(_ value: Any?, forKey defaultName: String) { 99 for mirror in self.mirrors { 100 mirror.set(value, forKey: defaultName) 101 } 102 } 103 104 private func mirror_object_removal(forKey defaultName: String) { 105 for mirror in self.mirrors { 106 mirror.removeObject(forKey: defaultName) 107 } 108 } 109 } 110 111 // MARK: - Default convenience objects 112 113 /// # Convenience objects 114 /// 115 /// - `DamusUserDefaults.standard`: will detect the bundle identifier and pick an appropriate object. You should generally use this one. 116 /// - `DamusUserDefaults.app`: stores things on its own container, and mirrors them to the shared container. 117 /// - `DamusUserDefaults.shared`: stores things on the shared container and does no mirroring 118 extension DamusUserDefaults { 119 static let app: DamusUserDefaults = try! DamusUserDefaults(main: .standard, mirror: [.shared])! // Since the underlying behavior is very static, the risk of crashing on force unwrap is low 120 static let shared: DamusUserDefaults = try! DamusUserDefaults(main: .shared)! // Since the underlying behavior is very static, the risk of crashing on force unwrap is low 121 static var standard: DamusUserDefaults { 122 get { 123 switch Bundle.main.bundleIdentifier { 124 case Constants.MAIN_APP_BUNDLE_IDENTIFIER: 125 return Self.app 126 case Constants.NOTIFICATION_EXTENSION_BUNDLE_IDENTIFIER: 127 return Self.shared 128 default: 129 return Self.shared 130 } 131 } 132 } 133 }