Androidはワンツーパンチ 三歩進んで二歩下がる

プログラミングやどうでもいい話

Swift4で新しく追加されたDecodableプロトコルを使ってJSONデータをパースする

Android開発におけるGsonライブラリに似たものはないか

Androidアプリを作る際、Gsonというライブラリを使っていて、
JSONのデータとJavaのオブジェクトを相互に変換しておりました。
(当エントリではJSON→Swiftのモデルの変換をしますので、
以降はこの方向の変換の話とさせていただきます。)

Gsonではどのように変換するかと言いますと

JSONのデータ構造を表すJavaのモデルクラスを定義
 その際JSONのキーをフィールド名にしておく
JSONのデータが入っているStringオブジェクトをモデルクラスを使って変換
・変換が失敗した時の処理も書いておく

このような感じで、非常に短いコードで実現できます。

SwiftならDecodableが良さそう

これと似たようなことをiOSでもやろうとして調べたら、
Swift4で新しく追加された以下のプロトコルを実装すると大変便利に
エンコード・デコード出来ることがわかりました。

  • DecodableJSON→Swiftのオブジェクト
  • Encodable :Swiftのオブジェクト→JSON
  • Codable上記の両方

Encoding and Decoding Custom Types | Apple Developer Documentation

標準ライブラリに入っていることが嬉しいですし、
変換も非常に簡単です。
CodingKeyプロトコルを利用すれば
JSONのキーをそのままフィールド名にしなくても大丈夫です。
私にとってはGsonのやり方と似ているのでわかりやすいですし。

環境

サンプルコードの作成環境
Swift 4.0 Xcode 9.2

サンプルの仕様

f:id:sakura_bird1:20171226024346p:plain

  • Jsonファイルをローカルから読み込む
  • Array構造になっているデータなので、UITableViewで中身を表示する
  • データが部分的に欠けている場合"nil"と表示する

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は変換できました。
サンプルでは画面に表示するところまで書いております。
プロジェクト全体はこちらにあります。

github.com

処理のメインである「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
  }

}