Есть ли лучший способ сохранить пользовательский класс в NSUserDefaults, чем кодирование и декодирование всего с помощью NSCoder?

В моем текущем классе около 50 строк только для кодирования и декодирования переменных, чтобы мой класс был совместимым с NSUserDefaults. Есть ли лучший способ справиться с этим?

Пример:

 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 ответ

Решение

Вы должны создать структуру или перечисление, чтобы организовать ваши ключи, потому что ваш путь просто склонен к опечаткам. Просто поместите это прямо над вашим классом

enum Key: String {
  case allSettings

  case lightEnabled
  case soundEnabled
}

и чем просто назвать ключи, как так

...forKey: Key.lightEnabled.rawValue)

Что касается вашего вопроса, я столкнулся с той же проблемой в моей игре, пытаясь сохранить свойства для 40 уровней (bestTimes, статус разблокировки уровня и т. Д.). Сначала я сделал то, что вы пытались, и это было чистое безумие.

В итоге я использовал массивы / словари или даже массивы словарей для своих данных, которые сократили мой код примерно на 80 процентов.

В этом также приятно то, что вам нужно сохранить что-то вроде LevelUnlock bools, это сделает вашу жизнь намного проще в дальнейшем. В моем случае у меня есть кнопка UnlockAllLevels, и теперь я могу просто выполнить цикл через мой словарь / массив и обновлять / проверять уровни levelUnlock в нескольких строках кода. Намного лучше, чем иметь массивные операторы if-else или switch для проверки каждого свойства в отдельности.

Например

 var settingsDict = [
      Key.lightEnabled.rawValue: false, 
      Key.soundEnabled.rawValue: false, 
      ...
 ]

Чем в методе декодера вы говорите это

Примечание. При этом будет учитываться, что вы можете добавить новые значения в SettingsDict, а затем при следующем запуске приложения эти значения не будут удалены, поскольку вы не заменяете весь словарь на сохраненный, вы обновляете только те значения, которые уже есть. существовать.

 // 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
    }
} 

Если вы используете несколько словарей, то ваш метод декодера снова станет грязным, и вы также будете повторять много кода. Чтобы избежать этого, вы можете создать расширение NSCoder, используя дженерики.

 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
     }
 }

и чем вы можете написать это в методе декодера для каждого словаря.

settingsDict = aDecoder.decodeObject(settingsDict, forKey: Key.allSettings.rawValue)

Ваш метод кодировщика будет выглядеть следующим образом.

 encoder.encodeObject(settingsDict, forKey: Key.allSettings.rawValue)

В вашей игре / приложении вы можете использовать их так

settingsDict[Key.lightEnabled.rawValue] = true

if settingsDict[Key.lightEnabled.rawValue] == true {
  /// light is turned on, do something
}

Этот способ также позволяет очень легко интегрировать хранилище iCloud KeyValue (просто создать словарь iCloud), опять же, главным образом потому, что его так легко сохранять и сравнивать много значений с очень небольшим кодом.

ОБНОВИТЬ:

Чтобы сделать их немного проще, мне нравится создавать удобные методы получения / установки в классе GameData. Преимущество этого заключается в том, что вы можете проще вызывать эти свойства в своем проекте (как в старом стиле), но ваш метод кодирования / декодирования все равно останется компактным. Вы также можете делать такие вещи, как цикл для сравнения значений.

 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 }
}

и чем вы можете назвать их как обычные свойства.

isLightEnabled = true

if isLightEnabled {
  /// light is turned on, do something
}

Смотреть на protocol codeable в Свифте 4.

Декодер и кодер будут автоматически сгенерированы для вас.

Проверьте: (начиная примерно на полпути) https://developer.apple.com/videos/play/wwdc2017/212/

Другие вопросы по тегам