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
かつそのRawValue
がString
になるように制約をつけている。
ちなみに実行はこんな感じ。
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
を使う機会があればこのパターンを使ってみたいと思う。