My current class has around 50 lines just encoding and decoding variables in order for my class to be NSUserDefaults compatible. Is there a better way to handle this?
<Look at protocol codeable
in Swift 4.
The decoder and encoder will be auto-generated for you.
Check out: (starting about half way through) https://developer.apple.com/videos/play/wwdc2017/212/
You should create a struct or enum to organise your keys, because your way is just prone to typos. Just put it right above your class
enum Key: String {
case allSettings
case lightEnabled
case soundEnabled
}
and than just call the keys like so
...forKey: Key.lightEnabled.rawValue)
Now in regards to your question, I was facing the same issue with my game trying to save properties for 40 levels (bestTimes, Level unlock status etc). I initially did what you tried and it was pure madness.
I ended up using arrays/dictionaries or even arrays of dictionaries for my data which cut down my code by like 80 percent.
Whats also nice about this is that say you need to save something like LevelUnlock bools, it will make your life so much easier later on. In my case I have a UnlockAllLevels button, and now I can just loop trough my dictionary/array and update/check the levelUnlock bools in a few lines of code. So much better than having massive if-else or switch statements to check each property individually.
For example
var settingsDict = [
Key.lightEnabled.rawValue: false,
Key.soundEnabled.rawValue: false,
...
]
Than in the decoder method you say this
Note: This way will take into account that you might add new values to the SettingsDict and than on the next app launch those values will not be removed because you are not replacing the whole dictionary with the saved one, you only update the values that already exist.
// If no saved data found do nothing
if var savedSettingsDict = decoder.decodeObjectForKey(Key.allSettings.rawValue) as? [String: Bool] {
// Update the dictionary values with the previously saved values
savedSettingsDict.forEach {
// If the key does not exist anymore remove it from saved data.
guard settingsDict.keys.contains($0) else {
savedSettingsDict.removeValue(forKey: $0)
return
}
settingsDict[$0] = $1
}
}
If you use multiple dictionaries than your decoder method will become a messy again and you will also repeat alot of code. To avoid this you can create an extension of NSCoder using generics.
extension NSCoder {
func decodeObject<T>(_ object: [String: T], forKey key: String) -> [String: T] {
guard var savedData = decodeObject(forKey: key) as? [String: T] else { return object }
var newData = object
savedData.forEach {
guard object.keys.contains($0) else {
savedData[$0] = nil
return
}
newData[$0] = $1
}
return newData
}
}
and than you can write this in the decoder method for each dictionary.
settingsDict = aDecoder.decodeObject(settingsDict, forKey: Key.allSettings.rawValue)
Your encoder method would look like this.
encoder.encodeObject(settingsDict, forKey: Key.allSettings.rawValue)
In your game/app you you can use them like so
settingsDict[Key.lightEnabled.rawValue] = true
if settingsDict[Key.lightEnabled.rawValue] == true {
/// light is turned on, do something
}
This way makes it also very easy to integrate iCloud KeyValue storage (just create an iCloud dictionary), again mainly because its so easy to save and compare a lot of values with very little code.
UPDATE:
To make calling these a bit easier I like to create some convenience getters/setters in the GameData class. This has the benefit that you can more easily call these properties in your project (like your old way) but your encode/decode method will still stay compact. You can also still do things such as looping to compare values.
var isLightEnabled: Bool {
get { return settingsDict[Key.lightEnabled.rawValue] ?? false }
set { settingsDict[Key.lightEnabled.rawValue] = newValue }
}
var isSoundEnabled: Bool {
get { return settingsDict[Key.soundEnabled.rawValue] ?? false }
set { settingsDict[Key.soundEnabled.rawValue] = newValue }
}
and than you can call them like normal properties.
isLightEnabled = true
if isLightEnabled {
/// light is turned on, do something
}