読者です 読者をやめる 読者になる 読者になる

Timers Tech Blog

カップル専用アプリ「Pairy」, 子育て夫婦アプリ「Famm」を運営している Timers inc. の公式Tech Blogです。

SwiftでStructとNSDataを相互変換するライブラリを作りました

こんにちは、iOSエンジニアのちぎらです。

SwiftでStructのデータを永続化したい時、どうしたらいいでしょう。Classに書き換えてNSCodingに準拠させればNSDataに変換できます。でもStructは使いたいしなんか悔しいですね。Structが保持している変数をそれぞれバイト列にして繋げてしまって一つのデータにするという方法があります。方法としてはこれでいいですが、NSCodingに準拠させるよりもはるかに頭を使うし、もっと簡単にできないものでしょうか...。

StructとNSDataを相互変換するライブラリを作りました。 github.com

使い方

1. Struct を定義する

// CustomArchivable プロトコルを指定
struct SampleStruct: CustomArchivable {

    let title: String
    let timestamp: Double

    // これだけ実装する必要があります
    public static var restoreProcedure: ArchiveRestoreProcedure {

        return { (dictionary: ArchivableDictionary) in
            // プロパティ名と値のセットが含まれたDictionaryが渡ってくるので、Structを生成して返す
            if let title = dictionary["title"] as? String, let timestamp = dictionary["timestamp"] as? Double {
                return SampleStruct(title: title, timestamp: timestamp)
            }
            return SampleStruct(title: "", timestamp: 0.0)
        }
    }
}

2. AppDelegate に以下の記述を追加

func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
    
    StructArchiver.activateStandardArchivables(withCustomStructActivations: {
        // 先ほど定義したStructのactivateArchive()を呼ぶ
        SampleStruct.activateArchive() 
    })
    

3. 変換する

// SampleStruct を生成して NSData に変換
let archivedStruct: SampleStruct = SampleStruct(title: title, timestamp: timestamp)
let archivedStructData: NSData = archivedStruct.archivedData

// ArrayやDictionaryごと変換することもできます
let archivedArray: Archivables = [SampleStruct(title: title1, timestamp: timestamp1), SampleStruct(title: title2, timestamp: timestamp2)]
let archivedArrayData: NSData = archivedArray.archivedData

NSData に変換した後は、ファイルやUserDefaultsに書き込んだりしていつも通り永続化をしてください。

実装

Swiftの勉強を兼ねて、Protocol Oriented Programming を目指してみます。仮に、NSDataとStructの相互変換の事をアーカイブと呼びます。ライブラリ内ではアーカイブ可能な型に共通する Archivable というプロトコルを実装しています。Archivable の実装を順を追って説明を試みてみましょう。

NSDataのバイト列から型を識別する為に必要な事

個別の型のアーカイブの仕方を考える前に、何のデータがアーカイブされているかを識別する仕組みが必要そうです。そこで、バイト列の頭に型の名前をつける事にして、データの形式を考えます。データから型名を読み取る時にはデータから型名が格納されている領域だけ切り出したいので、型名を格納する為のバイト数があらかじめ分かっている必要があります。

[型名の格納に必要なバイト数(UInt8)][型名(String)][...]

このデータの先頭のデータを Identifier と呼ぶ事にします。(Identifier の格納に必要なバイト数は sizeof(UInt8)+typeName.characters.count です。)次に、個別の型について考えます。

Int のデータ形式

まず、Int です。Int はただ1つの数字ですから、Identifier の後に Int のデータを1つ付ければ十分そうです。

[Identifier][値(Int)]

Float や Double も同様のやり方でいけそうです。

String のデータ形式

次に、String を考えてみます。String のデータは0個以上の文字が連続したデータです。なので、型名を格納した時と同じように、あらかじめバイト列の長さがわかっている必要があります。

[Identifier][文字列の格納に必要なバイト長(Int)][値(String)]

ここで、String と Int を比べてみますと、String の方には Int の時にはなかった[文字列の格納に必要なバイト長(Int)]という部分があります。これは値に関する情報なので、Header と呼ぶ事にします。[値(String)]の部分は Body と呼びましょう。

ここまで考えると、格納するデータは

[Identifier][Header][Body]

の形式で表せそうです。

Array の形式

上の形式に合わせて Array を考えてみます。Array は複数の、長さのあるデータを格納しますから、Header としてはデータの個数とそれぞれのデータのバイト長が必要です。Body は各要素のデータです。

[Header] = [要素の個数(Int)][要素1のバイト長]...[要素Nのバイト長]
[Body] = [要素1]...[要素N]

要素もまた、Archivable である必要があります。

Dictionary の形式

あと一息。Dictionary は Array とほぼ同じで、要素の数だったところをキーと値の組にすれば良いですね。

[Header] = [データの組の個数(Int)][キー1のバイト長]...[キーNのバイト長[値1のバイト長]...[値Nのバイト長]
[Body] = [キー1]...[キーN][値1]...[値N]

Archivable プロトコル

上の話を眺めて、アーカイブするものに共通して必要なものは以下で良さそうです。(解凍については長くなってしまうので記述を省きます。)

protocol Archivable {
    
    /// Identifier を表す文字列
    var archivedIdentifier: String { get }
    
    /// Identifier 部分のバイト長
    var archivedIDLength: Int { get }
    
    /// データの全体のバイト長
    var archivedDataLength: Int { get }
    
    /// Header データ
    var archivedHeaderData: [NSData] { get }
    
    /// Body データ
    var archivedBodyData: [NSData] { get }
    
    /// Identifier + Header + Body
    var archivedData: NSData { get }
    
    /// データの解凍をするためのクロージャ
    static var unarchiveProcedure: ArchiveUnarchiveProcedure { get }
}

上で考えた型以外も、Archivable プロトコルに準拠することでアーカイブ可能になります。

Struct の変換

Struct の変換は、実は一度 Dictionary に変換してから NSData に変換をしています。Mirror という関数を使って、Struct に宣言されている変数の名前と値の組を取得することができます。

var children: ArchivableDictionary = ArchivableDictionary()
Mirror(reflecting: self).children.forEach { label, value in
    // label が変数名、value が値
    if let label = label, value = value as? Archivable {
        children[label] = value
    }
}

ですので、Struct は Dictionary から復元するためのクロージャさえ提供されれば、アーカイブ可能になります。アーカイブしたい Struct が準拠する必要があるプロトコルは以下です。

protocol CustomArchivable: Archivable {
    /// Dictionary からの変換の為のクロージャ
    static var restoreProcedure: ArchiveRestoreProcedure { get }
}

最後に

エラーハンドリング、Archibavle な型が少ないなど、まだ不十分です。おかしなところなどもありましたらぜひ御指摘をお願い致します! ありがとうございました。


子育て家族アプリFamm、カップル専用アプリPairyを運営する Timers inc. では、現在エンジニアを積極採用中! 急成長中のサービスの技術の話を少しでも聞いてみたい方、スタートアップで働きたい方など、是非お気軽にご連絡ください! 採用HP : http://timers-inc.com/engineerings