Swift4で新しく追加されたDecodableプロトコルを使ってJSONデータをパースする
Android開発におけるGsonライブラリに似たものはないか
Androidアプリを作る際、Gsonというライブラリを使っていて、
JSONのデータとJavaのオブジェクトを相互に変換しておりました。
(当エントリではJSON→Swiftのモデルの変換をしますので、
以降はこの方向の変換の話とさせていただきます。)
Gsonではどのように変換するかと言いますと
・JSONのデータ構造を表すJavaのモデルクラスを定義
その際JSONのキーをフィールド名にしておく
・JSONのデータが入っているStringオブジェクトをモデルクラスを使って変換
・変換が失敗した時の処理も書いておく
このような感じで、非常に短いコードで実現できます。
SwiftならDecodableが良さそう
これと似たようなことをiOSでもやろうとして調べたら、
Swift4で新しく追加された以下のプロトコルを実装すると大変便利に
エンコード・デコード出来ることがわかりました。
Encoding and Decoding Custom Types | Apple Developer Documentation
標準ライブラリに入っていることが嬉しいですし、
変換も非常に簡単です。
CodingKeyプロトコルを利用すれば
JSONのキーをそのままフィールド名にしなくても大丈夫です。
私にとってはGsonのやり方と似ているのでわかりやすいですし。
環境
サンプルコードの作成環境
Swift 4.0 Xcode 9.2
サンプルの仕様
Jsonファイルはこちらです。
https://github.com/sakurabird/ios-example-swift4-json-parse/blob/master/ExamJsonToTable/colors.json
実装
Decodableを実装したモデルの定義をする
JSONの構造を表す構造体を定義するのですが、JSONを貼り付けると
Swiftのモデルクラスの形式で出力してくれるサービスがありました。
json4swift.com | Online JSON to Swift Models Generator
オンライン上で出来るのが嬉しいです。
他に似たようなことが出来るライブラリも見かけたことがあるので探してみるのも良いかもしれません。
上記のサービスの出力結果を参考に定義したのがこちらです。
全てのstructにDecodableを記述する必要があります。
入ってくるデータは値が欠けているものがある前提で作っていますので
"?"を付けてOptional型にしています。
付けないと値が欠けていたら実行時にクラッシュしてしまいます。
struct ColorModel: Decodable { struct Color: Decodable { let color: String? let category: String? let type: String? let code: Code? } struct Code: Decodable { let rgba: [Int]? let hex: String? } let colors: [Color]? }
DataオブジェクトをパースしてSwiftのモデルオブジェクトを作成する
ローカルからJSONファイルを読み込みDataオブジェクトを作成します。
JSONDecoderのインスタンスよりdecodeメソッドを使用して変換します。
// パスの取得 guard let path = Bundle.main.path(forResource: "colors", ofType: "json") else { return } // URLの取得 let url = URL(fileURLWithPath: path) do { // JSONファイルを読み込みDataオブジェクトに格納する let data = try Data(contentsOf: url) // Dataオブジェクトをモデルオブジェクトにパースする let colors = try JSONDecoder().decode(ColorModel.self, from: data) // オブジェクトの中身を表示 // for color in (colors.colors)! { // print(color.color as Any) // print(color.category as Any) // print(color.type as Any) // print(color.code?.hex as Any) // print(color.code?.rgba?[0] as Any) // print(color.code?.rgba?[1] as Any) // print(color.code?.rgba?[2] as Any) // print(color.code?.rgba?[3] as Any) // } self.colors = colors.colors } catch { print(error) }
サンプル全体
以上のようにJSONは変換できました。
サンプルでは画面に表示するところまで書いております。
プロジェクト全体はこちらにあります。
処理のメインである「ViewController.swift」の全体を貼り付けておきます。
Swiftの経験が浅すぎるのでおかしな部分や、もっと綺麗に書き直せる
部分などございましたらご指摘いただけますと嬉しいです。
// // ViewController.swift // ExamJsonToTable // // Created by Sakura on 2017/12/19. // Copyright © 2017年 Sakura. All rights reserved. // import UIKit // colors.json用のモデル struct ColorModel: Decodable { struct Color: Decodable { let color: String? let category: String? let type: String? let code: Code? } struct Code: Decodable { let rgba: [Int]? let hex: String? } let colors: [Color]? } class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate { @IBOutlet weak var tableView: UITableView! var colors: [ColorModel.Color]? // リユースするセルのid let cellReuseIdentifier = "cell" override func viewDidLoad() { super.viewDidLoad() loadJsonFile() tableView.delegate = self tableView.dataSource = self } override func didReceiveMemoryWarning() { super.didReceiveMemoryWarning() // Dispose of any resources that can be recreated. } // ローカルのJSONファイルを読み込みモデルオブジェクトにパースする func loadJsonFile() { // パスの取得 guard let path = Bundle.main.path(forResource: "colors", ofType: "json") else { return } // URLの取得 let url = URL(fileURLWithPath: path) do { // JSONファイルを読み込みDataオブジェクトに格納する let data = try Data(contentsOf: url) // print(data) // byte数が表示される // Dataオブジェクトをモデルオブジェクトにパースする let colors = try JSONDecoder().decode(ColorModel.self, from: data) // オブジェクトの中身を表示 // for color in (colors.colors)! { // print(color.color as Any) // print(color.category as Any) // print(color.type as Any) // print(color.code?.hex as Any) // print(color.code?.rgba?[0] as Any) // print(color.code?.rgba?[1] as Any) // print(color.code?.rgba?[2] as Any) // print(color.code?.rgba?[3] as Any) // } self.colors = colors.colors } catch { print(error) } } func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return (colors!.count) } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { // セルを取得する guard let cell = tableView.dequeueReusableCell(withIdentifier: cellReuseIdentifier, for: indexPath) as? TableViewCell else { fatalError("The dequeued cell is not an instance of TableViewCell.") } // セルにjsonの中身を表示する。Viewに対応するフィールドに値がなければ"nil"と表示する。 if let color = colors![indexPath.row].color { cell.color!.text = color } else { cell.color!.text = "nil" } if let category = colors![indexPath.row].category { cell.category!.text = category } else { cell.category!.text = "nil" } if let type = colors![indexPath.row].type { cell.type!.text = type } else { cell.type!.text = "nil" } if let hex = colors![indexPath.row].code?.hex { cell.hex!.text = hex } else { cell.hex!.text = "nil" } if let rgba1 = colors![indexPath.row].code?.rgba![0] { cell.rgba1!.text = String(rgba1) } else { cell.rgba1!.text = "nil" } if let rgba2 = colors![indexPath.row].code?.rgba![1] { cell.rgba2!.text = String(rgba2) } else { cell.rgba2!.text = "nil" } if let rgba3 = colors![indexPath.row].code?.rgba![2] { cell.rgba3!.text = String(rgba3) } else { cell.rgba3!.text = "nil" } if let rgba4 = colors![indexPath.row].code?.rgba![3] { cell.rgba4!.text = String(rgba4) } else { cell.rgba4!.text = "nil" } return cell } }