Edited at

Swiftオブジェクトの配列をNSUserDefaultsに保存する

More than 3 years have passed since last update.

swift初心者です。

struct MyLogData{

let date:NSDate
let message:String
init(date:NSDate,message:String){
self.date = date
self.message = message
}
}

このシンプルな構造体の配列を、NSUserDefaultsに保存しようと思いました。

Objective-Cの場合、NSCodingを継承してNSKeyedArchiverを使うパターンでなんとでもなったので、まずこれでやってみます。


NSCodingとNSKeyedArchiverを使ったパターン

// NSKeyedUnarchiverを使う為に、NSObjectとNSCodingを継承

class MyLogData:NSObject,NSCoding{
var date:NSDate=NSDate()
var message:String=""

init(date:NSDate,message:String){
self.date=date
self.message=message
}

// 以下NSCodingのための処理
@objc required init(coder aDecoder: NSCoder) {
let date = aDecoder.decodeObjectForKey("date") as? NSDate
let message = aDecoder.decodeObjectForKey("message") as? String
if let unwrapDate=date,unwrapMessage=message{
self.date = unwrapDate
self.message=unwrapMessage
}
}
@objc func encodeWithCoder(aCoder: NSCoder) {
aCoder.encodeObject(self.date, forKey: "date")
aCoder.encodeObject(self.message as NSString, forKey: "message")
}
}

extension NSUserDefaults {
var logDatas:[MyLogData]{
get{
let rowData:NSData = self.objectForKey("MyLogDatas") as? NSData ?? NSData()
let datas = NSKeyedUnarchiver.unarchiveObjectWithData(rowData) as? [MyLogData] ?? []
return datas
}
set(newDatas){
let archive = NSKeyedArchiver.archivedDataWithRootObject(newDatas)
self.setObject(archive, forKey: "MyLogDatas")
}
}
}

ちゃんと動きます。動きますが、気になります。

- 構造体だったMyLogDataが、NSCodingを継承するためにクラスに化けた。

- NSCodingのメソッド2つを、extensionで分離しようとしたのだけど、init部分が分離できなかったので1つになってしまう。

- 本来MyLogDataと関係無いメソッドで太ってしまった。


NSCodingとNSKeyedArchiverを使わないパターン

struct MyLogData{

let date:NSDate
let message:String
init(date:NSDate,message:String){
self.date = date
self.message = message
}
}

extension NSUserDefaults {
var logDataArray:[MyLogData]{
set(datas){
// Swiftのオブジェクトを、NSObjectなオブジェクトに変換する
let newDatas:[NSDictionary] = datas.map{
["message":$0.message,
"date":$0.date] as NSDictionary
}
// NSObjectなオブジェクトのみになったから、setObjectできる
self.setObject(newDatas,forKey:"MyLogData")
}
get{
// NSDictionaryの配列として、データを取得
let datas = self.objectForKey("MyLogData") as? [NSDictionary] ?? []
// 保存されたデータから復元出来無い場合もあり得るので、
// mapではなくreduceを使う
let array = datas.reduce([]){ (ary, d:NSDictionary) -> [MyLogData] in
// dateやmessageがnilでないなら、MyLogDataを作って足し込む
if let date = d["date"] as? NSDate,
message = d["message"] as? String{
return ary + [MyLogData(date: date, message: message)]
}else{
return ary
}
}
return array
}
}
}


  • MyLogDataは、元の素朴な構造体に戻りました。

  • 代わりにNSUserDefaultsのextensionが太りました。ただ元々「MyLogDataのために拡張した」部分なので、ここが太るのは良いことに思う。

  • もしもMyLogDataが「他のSwiftオブジェクトを内包する」ならば、ここのロジックはとことん肥大化していくので、NSCodingとNSKeyedArchiverを使ったパターンの方が見通しが良くなると思う。


結局は場合によりけりなんでしょうね。

個人的には「使わない」パターンが好みです。

ですが、複数のオブジェクトの組み合わせでできてるものを保存する時には、NSCodingとNSKeyedArchiverを使わないとめんどくさいのかもしれません。

あと、Swift純正のシリアライザがあれば、悩むことも無くなるかもしれませんね。


追記:MyLogDataとNSDictionaryを相互変換するinitを定義するパターン

記事を書いた後、そういえば「暗黙の型変換」があったように思い調べたのですが、今現在使えないようです。

Swiftで__conversionメソッドを使ってカスタムの型変換を定義する方法 から引用:

Xcode 6.0 beta 6以降、__conversion()を使った暗黙的なas演算子を用いた型変換はサポートされていません。Xcode 6.1(Swift 1.1)現在、暗黙的な型変換を行う手段はないため、型変換を行いたい場合はイニシャライザを定義する方法を取るのが通例として良いと思います。

暗黙の型変換は使えなくても、「イニシャライザを定義する方法」がなかなか良さそうなので、試しました。


// MARK: 構造体の本体
struct MyLogData{
let date:NSDate
let message:String
init(date:NSDate,message:String){
self.date = date
self.message = message
}
}

// MARK: 暗黙の型変換は今使えなくなってるらしいから、
// 相互変換のコンストラクタで代用する
// http://akisute.com/2014/06/swift-conversion.html
extension NSDictionary{
convenience init(_ data:MyLogData){
let dic = ["message":data.message, "date":data.date];
self.init(dictionary:dic)
}
}
extension MyLogData{
init(_ dic:NSDictionary){
let date = dic["date"] as? NSDate ?? NSDate()
let message = dic["message"] as? String ?? ""
self.init(date:date, message:message)
}
}

// MARK: NSUserDefaults本体
extension NSUserDefaults {
var logDataArray:[MyLogData]{
set(datas){
let newDatas:[NSDictionary] = datas.map{ NSDictionary.init($0) }
self.setObject(newDatas,forKey:"MyLogData")
}
get{
// NSDictionaryの配列として、データを取得
let datas = self.objectForKey("MyLogData") as? [NSDictionary] ?? []
return datas.map{ MyLogData.init($0) }
}
}
}

このパターンは、役割がくっきり分離してて読みやすくて、なかなか良さそうです。