I’m setting up my Settings class which gets/sets values from UserDefaults. I wish for as much of the code to be generic to minimise effort involved whenever a new setting is introduced (I have many as it is, and I expect many more in the future too), thereby reducing the probability of any human errors/bugs.
I came across this answer to create a wrapper for 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
}
}
I’ve created an enum to store all the setting keys:
enum SettingKeys: String {
case TimeFormat = "TimeFormat"
// and many more
}
And each setting has their own enum:
enum TimeFormat: String, Codable {
case ampm = "12"
case mili = "24"
}
In this simplified Settings class example, you can see that when it’s instantiated I initialise the value of every setting defined. I check if its setting key was found in UserDefaults: if yes, I use the value found, otherwise I set it a default and save it for the first time to 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
}
}
This is working fine, and pretty straightforward. However, thinking ahead, this will be tedious (and therefore error-prone) to replicate for every setting in this app (and every other I look to do in the future). Is there a way for the init<name of setting>()
and set<name of setting>()
to be generalised, whereby I pass it a key for a setting (and nothing more), and it handles everything else?
I’ve identified every setting to have these shared elements:
Thanks as always!
UPDATE:
This may or may not make a difference, but considering all settings will have a default value, I realised they don’t need to be a non-optional optional but can have the default set at initialisation.
That is, change:
var timeFormat: TimeFormat!
func initTimeFormat() {
guard let format: TimeFormat = UserDefaultsManager.get(forKey: SettingKeys.TimeFormat.rawValue) else {
self.setTimeFormat(to: .ampm)
return
}
self.timeFormat = format
}
To:
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
}
The setTimeFormat()
function is still needed when its value is changed by the user in-app.
Note that your settings don't have to be stored properties. They can be computed properties:
var timeFormat: TimeFormat {
get {
UserDefaultsManager.get(forKey: SettingKeys.TimeFormat.rawValue) ?? .ampm
}
set {
UserDefaultsManager.set(newValue, forKey: SettingKeys.TimeFormat.rawValue)
}
}
You don't have to write as much code this way, when you want to add a new setting. Since you will just be adding a new setting key enum case, and a computed property.
Further more, this computed property can be wrapped into a property wrapper:
@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)
}
}
}
Usage:
class Settings {
@FromUserDefaults(key: .TimeFormat, defaultValue: .ampm)
var timeFormat: TimeFormat
}
Now to add a new setting, you just need to write a new enum case for the key, and two more lines of code.