質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

ただいまの
回答率

90.03%

NSMutableArrayに格納した独自クラスごと、シリアライズしたい

解決済

回答 2

投稿

  • 評価
  • クリップ 2
  • VIEW 2,584

bizkit.kit

score 28

独自クラスを作成して、それをNSMutableArrayに格納しているのですが、このNSMutableArray型の変数をシリアライズして、UserDefaultsに保存したいです。(Swift3でコーディングしています)

独自クラスは以下の様に定義しました。

class DisplayNutritionData :NSObject, NSCoding{

    // 栄養素名
    var name :String = ""

    // 表示するかどうか
    var display :Bool = true

    //  表示内容
    var value :String = ""

    init(name :String){
        self.name = name
    }

    func encode(with aCoder: NSCoder) {
        aCoder.encode(name, forKey: "name")
        aCoder.encode(display, forKey: "display")
        aCoder.encode(value, forKey: "value")
    }

    required init?(coder aDecoder: NSCoder) {
        name = aDecoder.decodeObject(forKey: "name") as! String
        display = aDecoder.decodeBool(forKey: "display")
        value = aDecoder.decodeObject(forKey: "value") as! String
    }
}

保存処理は次の様に書きました。

// itemsがNSMutableArray型の変数で、DisplayNutritionDataのインスタンスをいくつか追加してあります。
let userDefault = UserDefaults.standard
        let data :Data = NSKeyedArchiver.archivedData(withRootObject: items) as Data
        userDefault.set(data, forKey: "displayNutrition")
        userDefault.synchronize()

このコードだと、NSKeyedArchiver.archivedDataを呼んだ時点で落ちてしまいます。

Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[_SwiftValue encodeWithCoder:]: unrecognized selector sent to instance 0x600000052840'

Arrayに格納してある独自クラスにNSCodingのプロトコルを実装しておけばそれが呼ばれるのだと思ったのですがうまくいきませんでした。

Swift3でコーディングしているのですが、まだネットを探してもあまり情報がなく、困っています。
※Swift3というよりももっと根本的な問題かも知れませんが。。。

以上、よろしくお願い致します。

  • 気になる質問をクリップする

    クリップした質問は、後からいつでもマイページで確認できます。

    またクリップした質問に回答があった際、通知やメールを受け取ることができます。

    クリップを取り消します

  • 良い質問の評価を上げる

    以下のような質問は評価を上げましょう

    • 質問内容が明確
    • 自分も答えを知りたい
    • 質問者以外のユーザにも役立つ

    評価が高い質問は、TOPページの「注目」タブのフィードに表示されやすくなります。

    質問の評価を上げたことを取り消します

  • 評価を下げられる数の上限に達しました

    評価を下げることができません

    • 1日5回まで評価を下げられます
    • 1日に1ユーザに対して2回まで評価を下げられます

    質問の評価を下げる

    teratailでは下記のような質問を「具体的に困っていることがない質問」、「サイトポリシーに違反する質問」と定義し、推奨していません。

    • プログラミングに関係のない質問
    • やってほしいことだけを記載した丸投げの質問
    • 問題・課題が含まれていない質問
    • 意図的に内容が抹消された質問
    • 広告と受け取られるような投稿

    評価が下がると、TOPページの「アクティブ」「注目」タブのフィードに表示されにくくなります。

    質問の評価を下げたことを取り消します

    この機能は開放されていません

    評価を下げる条件を満たしてません

    評価を下げる理由を選択してください

    詳細な説明はこちら

    上記に当てはまらず、質問内容が明確になっていない質問には「情報の追加・修正依頼」機能からコメントをしてください。

    質問の評価を下げる機能の利用条件

    この機能を利用するためには、以下の事項を行う必要があります。

質問への追記・修正、ベストアンサー選択の依頼

  • k_katsumi

    2016/09/18 14:26

    このコードで問題なく`UserDefaults`に保存、復元できましたよ。まっさらなプロジェクトで確かめてみたらどうですか?

    キャンセル

  • bizkit.kit

    2016/09/19 00:57

    実際に試していただけたのですね。ありがとうございます。新しいプロジェクトで試してみましたが、結果は同じでした。。。

    キャンセル

回答 2

checkベストアンサー

0

以下のようにすると配列の形で保存できます。
※ Swift3

class DisplayNutritionData :NSObject, NSCoding {

    // 栄養素名
    var name :String = ""

    // 表示するかどうか
    var display :Bool = true

    //  表示内容
    var value :String = ""

    init(name :String, display: Bool = true, value: String = "") {
        self.name = name
        self.display = display
        self.value = value
    }

    func encode(with aCoder: NSCoder) {
        aCoder.encode(name, forKey: "name")
        aCoder.encode(display, forKey: "display")
        aCoder.encode(value, forKey: "value")
    }

    required init?(coder aDecoder: NSCoder) {
        name = aDecoder.decodeObject(forKey: "name") as! String
        display = aDecoder.decodeBool(forKey: "display")
        value = aDecoder.decodeObject(forKey: "value") as! String
    }
}


extension UserDefaults {
    var nutritionDataArray: [DisplayNutritionData] {
        get{
            let rowData: NSData = self.object(forKey: "nutritionData") as? NSData ?? NSData()
            let datas = NSKeyedUnarchiver.unarchiveObject(with: rowData as Data) as? [DisplayNutritionData] ?? []
            return datas
        }
        set(newDatas) {
            let archive = NSKeyedArchiver.archivedData(withRootObject: newDatas)
            self.set(archive, forKey: "nutritionData")
        }
    }
}


// 使い方

let userDefault = UserDefaults.standard
let nutritionData = DisplayNutritionData(name: "test", display: false, value: "value1")
userDefault.nutritionDataArray = [nutritionData]
userDefault.synchronize()


let nutritionData1 = userDefault.nutritionDataArray
print(nutritionData1[0].name, nutritionData1[0].display, nutritionData1[0].value)
//=> test false value1

投稿

編集

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2016/09/19 01:07

    ありがとうございます。教えていただいた方法で実装できました。

    ただ、イマイチ何をやっているのかよくわかっていません(コピペしたので(^_^;)
    extensionを使ってUserDefaultsを拡張している・・・ということくらいはわかるのですが。。。

    実際、配列に格納されている独自クラスをシリアライズすることは割とよくありそうなのですが、その場合は普通はどういう風にするものなのでしょうか?
    #ご教示いただいた内容は結構テクニカルな感じの印象を受けました。

    お手すきでしたら、少しその辺を教えていただけると幸いです。

    キャンセル

  • 2016/09/20 08:51

    > 配列に格納されている独自クラスをシリアライズすることは割とよくありそうなのですが、その場合は普通はどういう風にするものなのでしょうか

    データ量が多くなければ簡単に扱えてよいと思いますが、一般的にはDatabaseを使うと思います。
    UserDefaultsは全て読みだして、全て書き込むので一部更新や検索して取得などができませんので自分はあまり使用しませんね。

    キャンセル

  • 2016/09/20 11:05

    なるほど。

    実は扱っているデータが幾つかあって、量が多くて検索が必要なモノはRealmを使ってみています。

    中途半端な量(50項目くらい)のものはUserDefaultsでもいいかな、と思って試していた次第です。

    回答ありがとうございました!助かりました!

    キャンセル

0

func encode 内にブレークポイントは設定してみましたでしょうか。
何をエンコードしようとした時に落ちるのかが分かれば手がかりになるかもしれません。

Objective-Cだと items に格納されているモノが想定と違っていた・・・なんてことがすごくありがちですが
Swiftでもあり得なくはないですね。あと、SwiftだとOptional値が紛れ込んだりするとおかしくなることがありますね。

投稿

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2016/09/19 01:04

    デバッグログにインスタンスのアドレスの様なものは表示されているのですが、実際にどのインスタンスをエンコードしようとして例外が発生しているのかがわかりませんでした。確かにその方法なら何かわかりそうですね。

    itemsに入っているものが違うというのは、ジェネリクスとかそんな(すいません、Javaだとジェネリクスというのですが、Swiftでなんというのかわかりません)話でしょうか。

    アドバイスありがとうございました。

    キャンセル

  • 2016/09/19 07:51

    配列の型が [Any] とか [AnyObject] だと、何でも入れられちゃうので必ずしも DisplayNutritionData のインスタンスが入っているかどうかわからない、ということですね。

    Swiftで怪しいのは、Optional型が入っている可能性ですね。
    Optional型はNSKeyedArchiverでシリアライズできない仕様なんですよ。
    つまり、DisplayNutritionData でなく、DisplayNutritionData? (=Optional<DisplayNutritionData>)が入ってるとシリアライズできないです。

    NSKeyedArchiver.archivedData の前にブレークポイントを置いて、デバッガコマンドで po items とやると、何が出てきますかね。
    もし、Optional<...> みたいなモノが配列の中に入ってるような雰囲気でしたらそれが怪しいです。

    キャンセル

  • 2016/09/19 13:06

    ありがとうございます。

    (lldb) po items
    ▿ Optional<NSMutableArray>
    ▿ some : 47 elements
    ▿ 0 : <SerializeTest.DisplayNutritionData: 0x600000094730>
    ▿ 1 : <SerializeTest.DisplayNutritionData: 0x600000094000>
    ▿ 2 : <SerializeTest.DisplayNutritionData: 0x6000000945a0>
    (つづく)

    こんな感じでした。自作クラスが問題なのか、そもそもシリアライズ処理の書き方が問題なのか、、、その辺切り分けないと本質はわからなさそうですね。まだ全然初心者(始めて一ヶ月くらい)なので、勉強しないといけないですね。

    キャンセル

  • 2016/09/20 13:25

    解決なさったようですが、そもそも items 自体が Optinal でラップされてるので、これ自身がエンコードできてなかったかもですね。if-let 文で囲ってアンラップしてからエンコード、で良かったかもしれません。

    キャンセル

15分調べてもわからないことは、teratailで質問しよう!

  • ただいまの回答率 90.03%
  • 質問をまとめることで、思考を整理して素早く解決
  • テンプレート機能で、簡単に質問をまとめられる