NSCodingをSwiftyにしてみる

NSCodingについて

Swiftにはクラスをシリアライズ, デシリアライズするためにNSCodingが用意されている。
そのクラスをシリアライズ, デシリアライズするには、NSCodingに準拠させ、2つのメソッド(func encode(with aCoder: NSCoder)init?(coder aDecoder: NSCoder))を実装する必要がある。

例えばこのような感じ。

class User: NSObject, NSCoding {
    var id: String!
    var name: String?

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

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

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

シリアライズするときは、func encode(with aCoder: NSCoder)が呼ばれ、aCoder: NSCodeに対して値とそのキーを指定して、エンコードする。
シリアライズするときは、init?(coder aDecoder: NSCoder)が呼ばれ、aDecoder: NSCodeからキーを指定して値をデコードし、クラスのストアドプロパティを満たす形にする。

NSCodingをSwiftyでないポイント

自分は上の例でキーをStringリテラルで指定しているのがSwiftらしくないなと思います。Stringリテラルだとタイポをしていてもコンパイルが通ってしまい、意図しない挙動をする可能性があります。なので、Swiftならばenumでキーを指定したいです。

// デシリアライズ
id  = aDecoder.decodeObject(forKey: "id") as! String
// シリアライズ
aCoder.encode(id, forKey: "id")

ならばSwiftyに

// NSCoderのメソッドをラップ
extension NSCoder {

    // RawRepresentableでそのRawValueがStringであるCodingKeyをジェネリクスで指定
    func encode<CodingKey: RawRepresentable>(_ object: Any?, forKey key: CodingKey)
        where CodingKey.RawValue == String {
        // CodingKeyのRawValueがStringなので、 key.rawValueでキーを指定する
        encode(object, forKey: key.rawValue)
    }

    func decodeObject<CodingKey: RawRepresentable>(forKey key: CodingKey) -> Any?
        where CodingKey.RawValue == String {
        return decodeObject(forKey: key.rawValue)
    }
}

protocol NSCodingKeyHandleable: NSCoding { // NScodingを継承
    // RawRepresentableでそのRawValueがStringであるNSCodingKeyを持たせる
    associatedtype NSCodingKey: RawRepresentable where NSCodingKey.RawValue == String
}

class User: NSObject, NSCodingKeyHandleable {
    var id: String!
    var name: String?

    // NSCodingKeyHandleableのNSCodingKeyをenumで定義
    enum NSCodingKey: String {
        case id
        case name
    }

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

    required init?(coder aDecoder: NSCoder) {
        // NSCodingKeyでキーを指定
        id  = aDecoder.decodeObject(forKey: NSCodingKey.id) as! String
        name = aDecoder.decodeObject(forKey: NSCodingKey.name) as? String
    }

    func encode(with aCoder: NSCoder) {
        // NSCodingKeyでキーを指定
        aCoder.encode(id, forKey: NSCodingKey.id)
        aCoder.encode(name, forKey: NSCodingKey.name)
    }
}

classにenum NSCodingKey: Stringをキーを定義させ、それを使用して、シリアライズ, デシリアライズできるようにした。キーをStringにさせるように、各箇所でRawRepresentableかつそのRawValueStringになるように制約をつけている。

ちなみに実行はこんな感じ。

let user = User(id: "123", name: "culumn")
let userData = NSKeyedArchiver.archivedData(withRootObject: user)
let decodedUser = NSKeyedUnarchiver.unarchiveObject(with: userData) as! User
print(decodedUser.id) // 123
print(decodedUser.name) // Optional("culumn")

まとめ

実はこれを考えてる途中でNSCodingを使うのならSwift4で追加されたCodable使ったほうがいいのでは気づいてしまった。また、調べてみるとCodableを使ったほうがシリアライズしたときのバイナリサイズが小さいらしい。 qiita.com

そうは言うものの、どうしたらSwiftyにできるかを考えること自体は楽しかったので良しとしたい。今後NSCodingを使う機会があればこのパターンを使ってみたいと思う。