【Swift】UserDefaultsにカスタムクラスの配列を保存する方法

Swiftでコードを書いていてデータを永久的に保存したい場合はUserDefaultsを使うと良い。

アプリを再起動した時でもデータを保存しておける。

UserDefaultsは便利な機能だが、カスタムクラスの配列を保存する場合は少し使い方に工夫が必要だ。

その辺りの説明をしていきたいと思う。

  • iOSバージョン:13.4
  • Swift:5.2

クラス内のエンコード、デコード

以下のようにItemと言うクラスを作った場合、そのクラスは NSSecureCodingとNSObjectを継承し、クラス内の変数のエンコード、デコードの処理を書く必要がある。

なぜかと言うと、あとで出てくる`NSKeyedArchiver`はオブジェクト自体をエンコードしてくれるのだが、オブジェクト内の変数まではエンコードできないため、自分でエンコードする処理を書く必要がある。(デコードも同様)

class Item:NSObject, NSSecureCoding{
    
    static var supportsSecureCoding: Bool = true
    
    var title = ""
    var link = ""
    var isFavorite = Bool()
    
    override init() {
    }
    
    required init?(coder decoder: NSCoder) {
        if let title = decoder.decodeObject(forKey: "title") as? String{
            self.title = title
        }
        if let link = decoder.decodeObject(forKey: "link") as? String{
            self.link = link
        }
        
        self.isFavorite = decoder.decodeBool(forKey: "isFavorite")
    }
    
    func encode(with coder: NSCoder) {
        coder.encode(self.title, forKey: "title")
        coder.encode(self.link, forKey: "link")
        coder.encode(self.isFavorite, forKey: "isFavorite")
    }
』

注意するべきなのはデコードする対象がBoolの場合は`decodeBool`を使うこと。

(エンコードの場合はencodeで統一されているのになぜデコードの場合は違う関数なのか。)

UserDefaultsへの保存・ロード

次にオブジェクトのエンコード、デコード処理とUserDefaultsの処理を書いていく。

iOS12から以下のメソッドが非推奨になったのでご注意されたい。

  • archivedData(withRootObject:)
  • unarchiveObject(with:)

(参考:https://qiita.com/rcftdbeu/items/2de95d1bc8f520f590ef

非推奨は警告が出るだけなので動作に問題はない。

私のように警告が気になる場合は以下のメソッドを代わりに使えば良い。

  • archivedData(withRootObject: fav, requiringSecureCoding: true)
  • unarchiveTopLevelObjectWithData(data)
    func loadData() -> [Item]{
        if let data = UserDefaults.standard.data(forKey: "key"){
            return try! NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(data) as! [Item]
        }else{
            let items = [Item]()
            return items
        }
    }
    
    func saveData(fav: [Item]){
        let userDefaults = UserDefaults.standard
        guard let encodedData = try? NSKeyedArchiver.archivedData(withRootObject: fav, requiringSecureCoding: true)else{
            fatalError()
        }
        userDefaults.set(encodedData, forKey: "key")
    }

追記:2021年5月24日

  1. Codableを継承したGymクラスを作る。Gym:Codable
  2. UserDefaulstsを拡張する
extension UserDefaults {
    func object<T: Codable>(_ type: T.Type, with key: String, usingDecoder decoder: JSONDecoder = JSONDecoder()) -> T? {
        guard let data = self.value(forKey: key) as? Data else { return nil }
        return try? decoder.decode(type.self, from: data)
    }

    func set<T: Codable>(object: T, forKey key: String, usingEncoder encoder: JSONEncoder = JSONEncoder()) {
        let data = try? encoder.encode(object)
        self.set(data, forKey: key)
    }
}

3. 保存とロード

ロードする時は管理用クラスのinitで以下のようにすると良い。

    init() {
        if let data = UserDefaults.standard.object([Date:[Gym]].self, with: "trainings") {
            self.dict = data
        }else{
            self.dict = [:]
        }
    }

保存は以下の通り。

UserDefaults.standard.set(object: self.dict, forKey: "trainings")