Objective-Cで保存したアーカイブデータをSwiftで読み込む

アプリをObjective-CからSwiftに書き換える際、少し悩むことに、Objective-Cで保存したアーカイブデータ(ユーザーのデバイスに保存されている)をSwiftで読み込むにはどうすればいいのかということがあります。
ここでは、NSKeyedUnarchiverやNSKeyedArchiverクラスを用いて利用するアーカイブデータがデバイスに保存されている場合に限定して、アプリをSwiftに書き換える時の手順などをまとめます。


ファイルの場所の指定
データの保存と読み込みではファイルの場所を指定します。ファイルの場所の指定はObjective-CとSwiftでほとんど同じ感覚でいけます。
var fileName = ""
let paths: [String] = NSSearchPathForDirectoriesInDomains(.DocumentDirectory, .UserDomainMask, true)
if 0 < paths.count {
    fileName = paths[0]
    fileName.appendContentsOf("/targetObject.dat")
    //print(fileName)
}
if 0 < paths.countは一応入れといた。
ちなみにstringByAppendingPathComponentというNSStringクラスのメソッドは使えなくなったようです。追加するものの前に自動的に/を加えるメソッドです。URLクラスに同じ機能のメソッドがありそっちを使えと表示が出ます。使えなくなったのが意図的だとすればStringとして扱うかURLとして扱うか中途半端ということでしょうか。


アプリにアーカイブ機能を付けるためにソースを書く部分
主に3つあります。
1 : データを保存するところ
2 : データを読み込むところ
3 : データ自体の定義


データの保存/読み込み
データの保存/読み込みの仕方は
A : オブジェクトをファイルに直接アーカイブする
B : NSDataを介してオブジェクトをファイルにアーカイブする
と2つあります。保存時と読み込み時でやり方が「対」になっていると余計な問題が出ることを避けられると思います。Objective-CでAの方法でアーカイブを行ったものはSwiftでもAのやり方で取り出すとうまくいきます。

A : オブジェクトをファイルに直接アーカイブする
//読み込み
if fileName != "" {
    if let obj = NSKeyedUnarchiver.unarchiveObjectWithFile(fileName) as? TargetObject {

    } else {
        //print("失敗")
    }
}
//書き込み
if fileName != "" {
    //objが保存したいオプジェクト
    let success = NSKeyedArchiver.archiveRootObject(obj, toFile: fileName)  
    if success {
        //print("保存に成功")
    }
}

B : NSDataを介してオブジェクトをファイルにアーカイブする
//読み込み
if let data = NSData.init(contentsOfFile: fileName) {
    let unarchiver = NSKeyedUnarchiver.init(forReadingWithData: data)
    if let targetObject = unarchiver.decodeObjectForKey("targetObject") as? TargetObject {
        //print("データ型OK")
    }
    unarchiver.finishDecoding()
} else {
    //print("データ取得失敗")
}
//書き込み
let data = NSMutableData.init()
//objが入れるデータ
let archiver: NSKeyedArchiver = NSKeyedArchiver.init(forWritingWithMutableData: data)
archiver.encodeObject(obj, forKey: "targetObject")
archiver.finishEncoding()
data.writeToFile(fileName, atomically: true)


データ自体の定義
ここではObjective-Cで設計したクラスをSwift側でも同じように再度設計するとします(このほかにObjective-Cで設計したクラスをSwift側からアクセスする方法もあります)。
BoolひとつとIntふたつの場合
@objc(TargetObject)
class TargetObject :NSObject, NSCoding {

    var boolValue: Bool = false
    var intValue1: Int = 0
    var intValue2: Int = 0
 
    @objc func encodeWithCoder(aCoder: NSCoder) {
 
        aCoder.encodeBool(boolValue, forKey: "boolValue")
        aCoder.encodeInteger(intValue1, forKey: "intValue1")
        aCoder.encodeInteger(intValue2, forKey: "intValue2")
    }
 
    @objc required init?(coder aDecoder: NSCoder) {
        super.init()
  
        boolValue = aDecoder.decodeBoolForKey("boolValue")
        intValue1 = aDecoder.decodeIntegerForKey("intValue1")
        intValue2 = aDecoder.decodeIntegerForKey("intValue2")
    }// NS_DESIGNATED_INITIALIZER
 
    override init() {
        super.init()
    }
}
ポイントは頭の@objc(TargetObject)です。これが必要です。NSObjectの継承も必要です。NSCodingも付けといてください。

データのビット数について
Objective-CとSwiftでは数値系データの名前やらビット数やらが別のものになったので確認しておきます。
Int32とInt64のような名前にビット数が付く指定制はObjective-CとSwiftで名前とバイト数の混乱はないですが、Objective-Cのint、NSIntegerと、SwiftのIntはちょっと変則的で

Objective-Cでは
int 32ビット
NSInteger 32ビット環境では32ビット、64ビット環境では64ビット

Swiftでは
Int 32ビット環境では32ビット、64ビット環境では64ビット

となっています。つまりビット数を揃えようとすると
Objective-Cでintに対応するものはSwiftではInt32しかなく、
Objective-CでNSIntegerに対応するものがSwiftではIntになる。
となります。

これを受けてSwiftではエンコード時、デコード時のメソッド名のビット数と名前の組み合わせはObjective-Cのものを採用しています。つまりデコードは
decodeIntForKey(32ビットを扱う)
decodeIntegerForKey(32ビット環境では32ビット、64ビット環境では64ビットを扱う)
となります。encode側も同じネーミングの考え方をしています。
となると
Objective-Cでintでencodeしたものは、Swiftで値を入れるときにdecodeIntForKeyを使い、SwiftのInt32に入れる。
Objective-CでIntegerでencodeしたものは、Swiftで値を入れるときにdecodeIntegerForKeyを使い、SwiftのIntに入れる。
とする全ての過程でビットが統一されます。
しかし、

Objective-CでintのものはSwiftでもIntにしたいよね。シンプルな型名のものがいいんだ。

という気持ちもあると思います(あることにしてください)。
そこで、整数をencode時とdecode時で違うビット数として扱ったらエラーが出るかどうか確認しました。
するとなぜかエラーは出ずに正常に実行されました。調べるとNSCoder Class Referenceの中に

A coder object stores object type information along with the data

とありました。データと一緒に型の情報を入れてエンコーディングするということでしょうか。
さらに調べるとArchives and Serializations Programming GuideのType Coercionsという項目に

NSKeyedUnarchiver supports limited type coercion. A value encoded as any type of integer, be it a standard int or an explicit 32-bit or 64-bit integer, can be decoded using any of the integer decode methods. Likewise, a value encoded as a float or double can be decoded as either a float or a double value.

を発見。limited type coercionがちょっとわかりませんが、2文目以降を読むとどうやらInt系、float系さえ守っていれば任意のバイト数での取り出しが大丈夫のようです。ただし、あふれは当然自己責任になります。たしかにこの仕様になっていると32bitデバイスからiCloudを経由して64bitデバイスにデータを移す際のトラブルも防ぐことが出来そうですね。

Arrayなど
さて次はオブジェクト型です。Objective-CとSwiftで型名が違うもののうちArrayとStringについて調べました。
NSString型からStringもNSArrayからArrayもいけました。互換性あります。なのでObjective-CでNSArrayに入れたNSStringは、SwiftでArrayに入ったStringとして取り出せます。
SwiftのデコードメソッドはInt系やFloat系は非オプショナルとして取り出せますが、AnyObject系は取り出したときにオプショナルとなるのでアンラップの処理が必要になります。
if let stringValueCandidate = aDecoder.decodeObjectForKey("stringValue") as? String {
    stringValue = stringValueCandidate
}
if let sampleArrayCandidate = aDecoder.decodeObjectForKey("array") as? [String] {
    sampleArray = sampleArrayCandidate
}

アンアーカイブで落ちる
Swift内でアーカイブ対象のクラスを書くときに@objc(クラス名)が必要ですが、そのクラス名はアーカイブされているものの(アーカイブされた時点での)クラス名を書きます。これがないとNSKeyedUnarchiver.unarchiveObjectWithFile()やNSKeyedUnarchiverのdecodeObjectForKey()メソッドで落ちます。
Swiftでは@objc(クラス名)は見ていないのかと思ったのですが、保存するときに@objc(クラス名)が書いてある場合は、取り出すときにも同名の@objc(クラス名)が必要のようです。

コメント