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

问题描述

我正在设置我的 Settings 类,它从 UserDefaults 获取/设置值。我希望尽可能多的代码是通用的,以尽量减少引入新设置时所涉及的工作(我有很多,我希望将来还会有更多),从而减少任何人为错误/错误的可能性.

我遇到了 this answer 来为 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"
}

在这个简化的 Settings 类示例中,您可以看到,当它被实例化时,我初始化了每个定义的设置的值。我检查它的设置键是否在 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 等。例如,在我的示例中为 enum 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
}

现在要添加新设置,您只需要为键编写一个新的枚举大小写,以及另外两行代码。