[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