yynsmk's tech blog

何でもできる=何にもできない

【Swift】UserDefaultsに自作クラスのデータを保存する方法(iOS12対応)

UserDefaultsを使っていてハマったことがあったのでメモ。

環境

Swift4.2.1、Xcode10.1

やりたいこと

UserDefaultsに自作クラスの配列を保存したい。

UserDefaultsとは?

データベースを使うことなく、iPhone上にデータを保存できるようにしてくれる機能です。 ただ、アプリをアンイストールするとデータは消えるので注意。 また、UserDefaultsで保存できる型は決まっており、以下の11個の型のみが利用できます。

Any, URL, [Any], [String: Any], String, [String], Data, Bool, Int, Float, Double

UserDefaultsの基本的な使い方

キーを用いて値を保存したり、参照したりします。 例えば、String型の配列を保存する場合は以下のようになります。

// 保存
UserDefaults.standard.set(["test1", "test2", "test3"], forKey: "tests")

// 参照
if let tests = UserDefaults.standard.stringArray(forKey: "tests") {
    print(tests)
}

UserDefaultsで自作クラスのデータを保存する方法

上で説明したように、UserDefaultsで保存できる型は決まっており、自作クラスは保存できません。 また、公式ドキュメントでも以下のような説明があります。

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 store any other type of object, you should typically archive it to create an instance of NSData.

つまり、自作クラスのデータを保存したいなら一度Data型に変換する必要があります。 今回は、Articleクラスのインスタンスの配列を保存する場合を例にとってサンプルコードを書きました。

保存したい自作クラス

// NSObjectを継承し、NSCodingを批准する必要があります
class Article: NSObject, NSCoding {
    var title = ""
    var date = ""

    init(_ title: String, _ date: String) {
        self.title = title
        self.date = date
    }

    // NSKeyedArchiverに呼び出されるシリアライズ処理(NSCodingで定義されている)
    func encode(with aCoder: NSCoder) {
        aCoder.encode(self.title, forKey: "title")
        aCoder.encode(self.date, forKey: "date")
    }

    // NSKeyedArchiverに呼び出されるデシリアライズ処理(NSCodingで定義されている)
    required init?(coder aDecoder: NSCoder) {
        title = aDecoder.decodeObject(forKey: "title") as! String
        date = aDecoder.decodeObject(forKey: "date") as! String
    }
}

iOS11までの保存と参照の処理

// 保存したい配列
var articles = [Article]()
let article = Article("Swift", "1月1日")
articles.append(article)

// 保存
let archivedData = NSKeyedArchiver.archivedData(withRootObject: articles)
UserDefaults.standard.set(archivedData, forKey: "articles")

// 参照
var articles = [Article]()
if let storedData = UserDefaults.standard.object(forKey: "articles") as? Data {
    if let unarchivedObject = NSKeyedUnarchiver.unarchiveObject(with: storedData) as? [Article] {
        articles = unarchivedObject
    }
}

iOS12での保存と参照の処理

// 保存したい配列
var articles = [Article]()
let article = Article("Swift", "1月1日")
articles.append(article)

// 保存
let archivedData = try! NSKeyedArchiver.archivedData(withRootObject: articles, requiringSecureCoding: false)
UserDefaults.standard.set(archivedData, forKey: "articles")

// 参照
var articles = [Article]()
if let storedData = UserDefaults.standard.object(forKey: "articles") as? Data {
    if let unarchivedObject = try! NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(storedData) as? [Article] {
        articles = unarchivedObject
    }
}

NSKeyedUnarchiver.unarchivedObjectを使う方法もあるようなのですが、エラーが出てうまくいきませんでした。 誰か分かる方いればコメントください。