swift4
Codable

Swift4のCodableに喜び勇んで飛びついたらとんでもない落とし穴が開いていた

More than 1 year has passed since last update.

もともとNSCodingでチマチマとencode/decodeを書くのに本当に辟易していた上に、さらにJSON書き出しも実装しなきゃ、みたいな状況が重なって鬱になってたところに、Swift4のCodableを使うとJSON書き出しも永続化もイッパツ、みたいな情報が入ってきたんですよ。そんな自分のニーズにドンピシャな機能があればそりゃあまあ飛びつくじゃないですか。

ということで、早速使ってみたらまあコーディングの楽なこと。やっぱり最新機能はええべな・・・と思ってたわけですが、そこにはとんでもない落とし穴があったわけです。

これは今作ってるシーケンサーアプリのデータ構造の一部なんですが、ポイントは、シーケンスの中には音符イベント(SequenceEventNote)とピッチベンドイベントみたいな違った種類のイベントがあって、それをSequenceEventというスーパークラスで抽象化しているところです。これにより、すべてのイベントをひっくるめてシンプルなArrayとして扱えるようにしたわけです。

class Song : Codable {
    var events : [SequenceEvent] 
}

class SequenceEvent : Codable {
    var time : Double
}

class SequenceEventNote : SequenceEvent {
    var noteNo : Int
    var velocity : Float
    var duration : Float
}

class SequenceEventPitchBend : SequenceEvent {
    var depth : Float
}

で、こういうコードを追加してJSON化・保存するとどうなるか。

class Song : Codable {
    var events : [SequenceEvent] 

    func save() {
        let encoder = JSONEncoder.init()
        let data = try? encoder.encode(self)
        if let d = data {
            try? d.write(to: Const.fileURL, options: [.atomic]);
        }
    }
}

eventsとして、SequenceEventNoteをいくつか追加、結果を見てみると

{
  "events": [
        {
          "time": 0
        },
        {
          "time": 120
        },
        {
          "time": 240
        },
        ...
    ]
}

ノーーー!!!! スーパークラスの情報しか入ってないじゃん!!!
そりゃねえよ・・・。

もちろん何か解決策はあるだろう、と調べます。するとこういうのが出てきます。

http://benscheirman.com/2017/06/ultimate-guide-to-json-parsing-with-swift-4/

まじか・・・。スーパークラスとサブクラスにencode/decodeメソッドを書かなきゃいかんのか・・・。でもまあNSCodingよりはまだいいか・・・と思ってたらさらにこんな話が。

↓その対策をやったところで、それがアレイに入っていた場合ガン無視されるという問題の報告。
https://bugs.swift.org/browse/SR-5331

↓そして、それに対するコメント

This is by design — if you need the dynamism required to do this, we recommend that you adopt NSSecureCoding and use NSKeyedArchiver/NSKeyedUnarchiver, which will allow you to decode based on the class found in the archive rather than what is requested at runtime. See https://developer.apple.com/documentation/foundation/nscoding and https://developer.apple.com/documentation/foundation/nssecurecoding for more info.

し・・・「仕様」・・・だと・・・!?

一応対策みたいのは提案されていて、unkeyed container から、まずサブクラスへのデコードを試みて、失敗したらスーパークラスにデコードするということらしい。
だが、これサブクラスごとに全部やるのか・・・。

Alternatively, if you know the possible subclasses which can be decoded from the payload, you can get an unkeyed container (instead of requesting Array) and attempt to decode instances of the subclasses out of the container; if those fail, you can decode the superclass successfully.

やっぱNSCodingに戻そうかな・・・。それはそれで各データ要素ごとにencode/decode/JSON書き出しを実装しなきゃだし・・・。NSCodingを自動化するライブラリを今更入れるか・・・。

新機能はやはりリスクが高いな・・・。

追記

逃げの対策を書きました
http://qiita.com/Yokemura/items/11440a9ab87183db3bb7