如何设计一个设置模型来为所有 UserDefaults 键一般获取+设置值

How to design a Settings model to generically get + set values for all UserDefaults keys

我正在设置我的设置 class,其中 gets/sets 值来自 UserDefaults。我希望尽可能多的代码是通用的,以尽量减少引入新设置时所涉及的工作量(我现在有很多,我希望将来会有更多),从而减少任何人的可能性 errors/bugs.

我发现 为 UserDefaults 创建包装器:

struct UserDefaultsManager {
    static var userDefaults: UserDefaults = .standard
    
    static func set<T>(_ value: T, forKey: String) where T: Encodable {
        if let encoded = try? JSONEncoder().encode(value) {
            userDefaults.set(encoded, forKey: forKey)
        }
    }
    
    static func get<T>(forKey: String) -> T? where T: Decodable {
        guard let data = userDefaults.value(forKey: forKey) as? Data,
            let decodedData = try? JSONDecoder().decode(T.self, from: data)
            else { return nil }
        return decodedData
    }
}

我创建了一个枚举来存储所有设置键:

enum SettingKeys: String {
    case TimeFormat = "TimeFormat"
    // and many more
}

每个设置都有自己的枚举:

enum TimeFormat: String, Codable  {
    case ampm = "12"
    case mili = "24"
}

在这个简化的设置 class 示例中,您可以看到在实例化时我初始化了每个已定义设置的值。我检查它的设置键是否在 UserDefaults 中找到:如果是,我使用找到的值,否则我将其设置为默认值并首次将其保存到 UserDefaults.

class Settings {

    var timeFormat: TimeFormat!
    
    init() {
        self.initTimeFormat()
    }

    func initTimeFormat() {
        guard let format: TimeFormat = UserDefaultsManager.get(forKey: SettingKeys.TimeFormat.rawValue) else {
            self.setTimeFormat(to: .ampm)
            return
        }
        
        self.timeFormat = format
    }

    func setTimeFormat(to format: TimeFormat) {
        UserDefaultsManager.set(format, forKey: SettingKeys.TimeFormat.rawValue)
        self.timeFormat = format
    }
}

这工作正常,而且非常简单。然而,提前考虑,为这个应用程序中的每个设置(以及我希望在未来做的所有其他设置)复制这将是乏味的(因此容易出错)。有没有一种方法可以使 init<name of setting>()set<name of setting>() 通用化,从而我将它传递给一个设置键(仅此而已),它会处理其他所有事情?

我已经确定每个设置都有这些共享元素:

  1. 设置键(例如,在我的示例中为 SettingsKey.TimeFormat)
  2. 唯一类型(可以是 AnyObject、String、Int、Bool 等。例如我的示例中的枚举 TimeFormat)
  3. 唯一 属性(例如我的示例中的 timeFormat)
  4. 默认值(例如在我的示例中为 TimeFormat.ampm)

一如既往的感谢!

更新:

这可能会或可能不会有所不同,但考虑到所有设置都有默认值,我意识到它们不需要是非可选的可选但可以在初始化时设置默认值。

即变化:

var timeFormat: TimeFormat!

func initTimeFormat() {
    guard let format: TimeFormat = UserDefaultsManager.get(forKey: SettingKeys.TimeFormat.rawValue) else {
        self.setTimeFormat(to: .ampm)
        return
    }
    
    self.timeFormat = format
}

收件人:

var timeFormat: TimeFormat = .ampm

func initTimeFormat() {
    guard let format: TimeFormat = UserDefaultsManager.get(forKey: SettingKeys.TimeFormat.rawValue) else {
        UserDefaultsManager.set(self.timeFormat, forKey: SettingKeys.TimeFormat.rawValue)
        return
    }
    
    self.timeFormat = format
}

当用户在应用程序内更改其值时,仍然需要 setTimeFormat() 函数。

请注意,您的设置不必是存储属性。它们可以计算属性:

var timeFormat: TimeFormat {
    get {
        UserDefaultsManager.get(forKey: SettingKeys.TimeFormat.rawValue) ?? .ampm
    }
    set {
        UserDefaultsManager.set(newValue, forKey: SettingKeys.TimeFormat.rawValue)
    }
}

当您想添加新设置时,您不必用这种方式编写那么多代码。因为您将只添加一个新的设置键枚举大小写和计算的 属性.

此外,这个计算的 属性 可以包装到 属性 包装器中:

@propertyWrapper
struct FromUserDefaults<T: Codable> {
    let key: SettingKeys
    let defaultValue: T

    init(key: SettingKeys, defaultValue: T) {
        self.key = key
        self.defaultValue = defaultValue
    }

    var wrappedValue: T {
        get {
            UserDefaultsManager.get(forKey: key.rawValue) ?? defaultValue
        }
        set {
            UserDefaultsManager.set(newValue, forKey: key.rawValue)
        }
    }
}

用法:

class Settings {
    @FromUserDefaults(key: .TimeFormat, defaultValue: .ampm)
    var timeFormat: TimeFormat
}

现在要添加一个新的设置,你只需要为key写一个新的enum case,再写两行代码。