[iOSアプリ開発] ファイルを抽象化した構造体を作る(8)

4 min read読了の目安(約4000字

こんにちは。
ZennではiOSアプリ開発の普段自分が使っているちょっとしたTipsなどを書いていければと思っております。

今回はSwiftでは少し冗長になりがちなファイルの操作を簡単にするために "ファイルを抽象化した構造体" を作っていきます。
当記事だけで終わりというわけではなく、続き物にしていく予定なのでよろしくおねがいします。

前回まではこちら

https://zenn.dev/nkysyuichi/articles/467ddcc1041d4e
https://zenn.dev/nkysyuichi/articles/7269df712386ed
https://zenn.dev/nkysyuichi/articles/2c44e971f7afac
https://zenn.dev/nkysyuichi/articles/bcc71f9eeabf1f
https://zenn.dev/nkysyuichi/articles/0689c909bcffb4
https://zenn.dev/nkysyuichi/articles/1ffb9ef3a0af64
https://zenn.dev/nkysyuichi/articles/03043bbcb1939a

ファイルを取り扱う構造体

8回目にしてFile構造体の実装は終わりです。
最後はCodable+JSONファイルの処理を簡潔にできるような実装を書いていきます。

Codable

話の前提としてCodableについてですが、およそ3年半くらい前に登場したあたりで一度ブログにまとめております。

https://dev.classmethod.jp/articles/json-decoding-without-swiftyjson/

構造化されたデータを扱うためには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文字列に書き起こしてファイルに保存する」という挙動を実現できるようになりました。

デコードはメソッドを分けないの?

ここまでエンコードについてはjsonEncodewriteEncodedJsonというメソッドに分けました。繰り返しますが「ファイルデータを生成する」と「ファイルに書き込む」の責務に分けたかったからです。

しかし、デコードはjsonDecodedの1つしか定義していません。

これは既に作っていたdataプロパティが「ファイルを読み込む」責務を果たしているからです。わざわざloadDecodedJsonのようなメソッドを作る必要はありません。今まで作っていた処理で「ファイルを読み込む」と「オブジェクトを生成する」の責務の切り分けが充分できているというわけです。

まとめ

  • 構造化されたデータはCodableを使用して扱う
  • CodableなデータをJSONファイルを介して取得・保存できるようにした
  • データ生成とファイル処理の責務は切り分けた

ということでFile構造体については以上になります。次回は今までのソースコードをまとめて書きたいと思います。

では、また。