はじめに
以下のようなカスタムクラスのリストをUserdefaultsに保存しようとすると、ビルドは通りますがエラーが出てしまいます。
import Foundation
class Task: Codable {
var body: String
var isCompleted: Bool
init(body: String, isCompleted: Bool) {
self.body = body
self.isCompleted = isCompleted
}
}
// インスタンス化して保存
let task = Task(body: "こんにちは", isCompleted: false)
UserDefaults.standard.setValue(task, forKey: "hoge")
// エラー文
// A default object must be a property list—that is, an instance of (or for collections,
// a combination of instances of) NSData, NSString, NSNumber, NSDate, NSArray, or NSDictionary.
// If you want to stfore any other type of object, you should typically archive it to create an instance of NSData.
これはなぜかというと、UserDefaultsに保存できるのは、エラー分の通りNSData, NSString, NSNumber, NSDate, NSArray, NSDictionaryとなっているからで、カスタムクラスはこれに準拠していないからです。
解決策として、カスタムクラス型をNSData型に変換して保存し、読み出すときはNSData型が読み出されるので、それを再度カスタムクラス型に変換する、ということを考えます。実装したコードは次のとおりです。
実装
まず、先程のカスタムクラスにCodableに準拠させます。名前の通り「NSData型に変換できるよ!」みたいなイメージです。
class Task: Codable {
そして、次のファイルを作成します。
class JsonEncoder {
class func saveItemsToUserDefaults<T: Codable>(list: [T], key: String) {
let data = list.map { try! JSONEncoder().encode($0) }
UserDefaults.standard.set(data as [Any], forKey: key)
UserDefaults.standard.synchronize()
}
class func readItemsFromUserUserDefault<T: Codable>(key: String) -> [T] {
guard let items = UserDefaults.standard.array(forKey: key) as? [Data] else { return [T]() }
let decodedItems = items.map { try! JSONDecoder().decode(T.self, from: $0) }
return decodedItems
}
}
そして次のように呼び出すことで、カスタムクラスのリストをUserdefaultsに保存することが出来ます。
// 保存処理
JsonEncoder.saveItemsToUserDefaults(list: taskList, key: "hoge")
// 読み出し処理
let taskList: [Task] = JsonEncoder.readItemsFromUserUserDefault(key: "hoge")
解説
saveItemsToUserDefaultsはカスタムクラスのリストを受け取り、JSONに変換して保存していて、eadItemsFromUserUserDefaultはその逆の処理をしているのがわかると思います。
またT: CodableというふうにTが型みたいになっているのは、これはジェネリクスという概念で、引数の型を複数指定することを可能にします。今回では、「CodableならなんでもOKだよ!」というふうなイメージです。ジェネリクスで共通化をしたので、どのようなカスタムクラスでも保存ができるようになっています。
さらに引数の型にはCodableに準拠したカスタムクラスはもちろん、CodableはEncodableプロトコルとDecodableプロトコルを合わせて作られたので、これに準拠した方も引数に取ることが出来ます。
また読み出し処理において、定義する変数の型を指定してあげないと、ジェネリクス側が型推論できなくなってしまい、コンパイルエラーが起きるので注意してください。