[iOSアプリ開発] ファイルを抽象化した構造体を作る(8)
こんにちは。
ZennではiOSアプリ開発の普段自分が使っているちょっとしたTipsなどを書いていければと思っております。
今回はSwiftでは少し冗長になりがちなファイルの操作を簡単にするために "ファイルを抽象化した構造体" を作っていきます。
当記事だけで終わりというわけではなく、続き物にしていく予定なのでよろしくおねがいします。
前回まではこちら
ファイルを取り扱う構造体
8回目にしてFile構造体の実装は終わりです。
最後はCodable+JSONファイルの処理を簡潔にできるような実装を書いていきます。
Codable
話の前提としてCodableについてですが、およそ3年半くらい前に登場したあたりで一度ブログにまとめております。
構造化されたデータを扱うためにはSwiftではもはや必須な機能ですね。
Codableは色々なデータフォーマットで使用できますが、今回は一番よく使われるであろうJSON形式での話に絞ります。
ファイルからオブジェクトを生成する
ファイルの内容がJSONであるとして、その内容からCodable(正しくはDecodable)のオブジェクトが生成できると便利です。早速作ってみましょう。
extension File {
    
    func jsonDecoded<T>(_ type: T.Type) throws -> T? where T : Decodable {
        guard let data = self.data else { return nil }
        let decoder = JSONDecoder()
        decoder.keyDecodingStrategy = .convertFromSnakeCase
        return try decoder.decode(type, from: data)
    }
}
メソッドの3行目 decoder.keyDecodingStrategy = .convertFromSnakeCase は、JSONキーがスネークケースである前提で書いているものなので、たとえばキャメルケース前提である場合は外してください。
また、メソッドの1行目はdataが取れない場合はnilを返すようにしていますが、そもそもdataが取れないこと自体は異常ケースだと思うので、例外を吐くようにしてもいいかもしれません。(今回はそうしませんでしたが・・・)
使い方
以下のようにUserというCodableな構造体が定義されているとして
struct User: Codable {
    let name: String
    let age: Int
    let email: String
}
ファイルからUserの配列をこのように取得することができます。
let file = File.mainBundle + "data.json"
let users = try? file.jsonDecoded([User].self)
このように「ファイルからオブジェクトを生成する」という処理を2行ほどで書けるようになるのです。
オブジェクトからファイルデータを生成する
では、逆にCodable(正しくはencodable)のオブジェクトをファイルに落とし込む処理も作りましょう。こうすることで先程作った処理と併せて、JSONファイルを介して双方向にやりとりができるようになるはずです。
ここでは2つのステップでオブジェクト=>JSONファイルという流れを作っていきます。まずは「オブジェクトからファイルデータを生成する」までを1メソッドとしてまとめます。
「ファイルデータを生成する」と「ファイルに書き込む」は責務を分けておきたいからです。
extension File {
    
    func jsonEncode<T>(_ value: T) throws -> Data where T : Encodable {
        let encoder = JSONEncoder()
        encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
        encoder.keyEncodingStrategy = .convertToSnakeCase
        return try encoder.encode(value)
    }
}
メソッドの2〜3行目はデコード時と同様に、アプリで扱おうとするJSONの構造によって変わるところだと思いますので、よしなに変更してください。上記の通りに使用すると人の目に見やすいようなスネークケースのJSONデータになります。
オブジェクトからファイルに書き込む
前項で書いたとおり「ファイルデータを生成する」と「ファイルに書き込む」はメソッドを分けて定義します。
extension File {
    
    func jsonEncode<T>(_ value: T) throws -> Data where T : Encodable {
        let encoder = JSONEncoder()
        encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
        encoder.keyEncodingStrategy = .convertToSnakeCase
        return try encoder.encode(value)
    }
    
+   func writeEncodedJson<T>(_ value: T, encoding: String.Encoding = .utf8) throws where T : Encodable {
+       let encoded = try jsonEncode(value)
+       let jsonString = String(data: encoded, encoding: encoding) ?? ""
+       try parentDirectory.makeDirectory()
+       try jsonString.write(to: url, atomically: false, encoding: encoding)
+   }
}
使い方
let user = User(name: "佐藤さん", age: 21, email: "sato@hoge.com")
let file = File.mainBundle + "data.json"
try? file.writeEncodedJson([user])
ここもシンプルな実装で「構造化されたデータをJSON文字列に書き起こしてファイルに保存する」という挙動を実現できるようになりました。
デコードはメソッドを分けないの?
ここまでエンコードについてはjsonEncodeとwriteEncodedJsonというメソッドに分けました。繰り返しますが「ファイルデータを生成する」と「ファイルに書き込む」の責務に分けたかったからです。
しかし、デコードはjsonDecodedの1つしか定義していません。
これは既に作っていたdataプロパティが「ファイルを読み込む」責務を果たしているからです。わざわざloadDecodedJsonのようなメソッドを作る必要はありません。今まで作っていた処理で「ファイルを読み込む」と「オブジェクトを生成する」の責務の切り分けが充分できているというわけです。
まとめ
- 構造化されたデータはCodableを使用して扱う
- CodableなデータをJSONファイルを介して取得・保存できるようにした
- データ生成とファイル処理の責務は切り分けた
ということでFile構造体については以上になります。次回は今までのソースコードをまとめて書きたいと思います。
では、また。



Discussion