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

前端 未结 2 1289
南笙
南笙 2021-01-03 15:42

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?

<
相关标签:
2条回答
  • 2021-01-03 16:03

    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/

    0 讨论(0)
  • 2021-01-03 16:19

    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
    }
    
    0 讨论(0)
提交回复
热议问题