Is there a better way to save a custom class to NSUserDefaults than encoding and decoding everything with NSCoder?

别等时光非礼了梦想. 提交于 2019-12-18 18:31:30

问题


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?

Example:

 init(coder aDecoder: NSCoder!) {
    lightEnabled = aDecoder.decodeBoolForKey("lightEnabled")
    soundEnabled = aDecoder.decodeBoolForKey("soundEnabled")
    vibrateEnabled = aDecoder.decodeBoolForKey("vibrateEnabled")
    pulseEnabled = aDecoder.decodeBoolForKey("pulseEnabled")
    songs = aDecoder.decodeObjectForKey("songs") as! [Song]
    currentSong = aDecoder.decodeIntegerForKey("currentSong")
    enableBackgroundSound = aDecoder.decodeBoolForKey("enableBackgroundSound")
    mixSound = aDecoder.decodeBoolForKey("mixSound")
    playSoundInBackground = aDecoder.decodeBoolForKey("playSoundInBackground")
    duckSounds = aDecoder.decodeBoolForKey("duckSounds")
    BPMBackground = NSKeyedUnarchiver.unarchiveObjectWithData(aDecoder.decodeObjectForKey("BPMBackgorund") as! NSData) as! UIColor!
    BPMPulseColor = NSKeyedUnarchiver.unarchiveObjectWithData(aDecoder.decodeObjectForKey("BPMPulseColor") as! NSData) as! UIColor!
    TempoBackGround = NSKeyedUnarchiver.unarchiveObjectWithData(aDecoder.decodeObjectForKey("TempoBackGround") as! NSData) as! UIColor!
    TempoPulseColor = NSKeyedUnarchiver.unarchiveObjectWithData(aDecoder.decodeObjectForKey("TempoPulseColor") as! NSData) as! UIColor!
    TimeBackGround = NSKeyedUnarchiver.unarchiveObjectWithData(aDecoder.decodeObjectForKey("TimeBackGround") as! NSData) as! UIColor!
    TimeStrokeColor = NSKeyedUnarchiver.unarchiveObjectWithData(aDecoder.decodeObjectForKey("TimeStrokeColor") as! NSData) as! UIColor!
    TextColor = NSKeyedUnarchiver.unarchiveObjectWithData(aDecoder.decodeObjectForKey("TextColor") as! NSData) as! UIColor!
}

func encodeWithCoder(aCoder: NSCoder!) {
    aCoder.encodeBool(lightEnabled, forKey: "lightEnabled")
    aCoder.encodeBool(soundEnabled, forKey: "soundEnabled")
    aCoder.encodeBool(vibrateEnabled, forKey: "vibrateEnabled")
    aCoder.encodeBool(pulseEnabled, forKey: "pulseEnabled")
    aCoder.encodeObject(songs, forKey: "songs")
    aCoder.encodeInteger(currentSong, forKey: "currentSong")
    aCoder.encodeBool(enableBackgroundSound, forKey: "enableBackgroundSound")
    aCoder.encodeBool(mixSound, forKey: "mixSound")
    aCoder.encodeBool(playSoundInBackground, forKey: "playSoundInBackground")
    aCoder.encodeBool(duckSounds, forKey: "duckSounds")
    aCoder.encodeObject(BPMBackground.archivedData(), forKey: "BPMBackground")
    aCoder.encodeObject(BPMPulseColor.archivedData(), forKey: "BPMPulseColor")
    aCoder.encodeObject(TempoBackGround.archivedData(), forKey: "TempoBackGround")
    aCoder.encodeObject(TempoPulseColor.archivedData(), forKey: "TempoPulseColor")
    aCoder.encodeObject(TimeBackGround.archivedData(), forKey: "TimeBackGround")
    aCoder.encodeObject(TimeStrokeColor.archivedData(), forKey: "TimeStrokeColor")
    aCoder.encodeObject(TextColor.archivedData(), forKey: "TextColor")
}

回答1:


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
}



回答2:


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/



来源:https://stackoverflow.com/questions/35821732/is-there-a-better-way-to-save-a-custom-class-to-nsuserdefaults-than-encoding-and

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!