Help us understand the problem. What is going on with this article?

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

More than 5 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) }
        }
    }
}

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

paming
Android修行中
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした