Help us understand the problem. What is going on with this article?

SwiftyJSON/Decodableを利用してAPI通信を行い、UITableViewCellに表示する

More than 1 year has passed since last update.

これは続きです!
PART1: https://qiita.com/ostk0069/items/8bd4173ff4f083e35951

この記事はPARTが2つに分かれています。これはPART2に該当します。PART1ではPART2のベースのコードとなるUITableViewの実装を行なっています。ぜひ確認してみてください。

今回実装にあたって実装したものをGitHubに作成しておいたのでコードだけ見たい人はこちらからどうぞ。

とりあえずswift内のデータを表示させたもの(PART1の内容)*
https://github.com/takumaosada/customCellTutorial

*終了後、Alamofire, SwiftyJSONを用いてAPI通信を行い表示させたもの
https://github.com/takumaosada/customCellTutorial/tree/feature/swifty_json

*終了後、Alamofire, Decodable(codable)を利用してAPI通信を行い表示させたもの
https://github.com/takumaosada/customCellTutorial/tree/feature/decodable

Carthageからpackageをインストールする

という話をするにあたって人によっては「CocoaPodsにしよう」という人もいると思います。僕はCarthageでやってと言われたのでCarthageでやっただけのことなので好きな方を選んでください。
インストール方法に関しては他の方の記事を参考にした方がわかりやすいと思うので載せておきます。

  • Carthage

Carthageを使ってビルド時間を短縮しよう
【Swift】Carthage導入手順
Carthage について

  • CocoaPods

【Swift】CocoaPods導入手順
iOSライブラリ管理ツール「CocoaPods」の使用方法

今回インストールするのはAlamofire, SwiftyJSONの2つです。(decodableはswiftが標準で使うことのできるものです)

ちなみに僕はカーセッジと呼びたい派です。

使用するAPIについて

今回使用するのは駅すぱあとのAPIです。利用方法は以下の記事に詳細に書かれています。
駅すぱあとAPIを使ってみた
これを使用する上で申請が必要なので申請するのがめんどいのは他のAPIを使うことを推奨します。
この後わかることなのですが、駅すぱあとのAPIは俗に言うnestedJSON(出力されるJSONが複雑で扱いにくい)のでむしろ他のAPIを使う方が苦労しません。
[2018] 個人でも使える!おすすめAPI一覧を参考にするといいと思います。

APIからJSONを受け取る前に

実装していくにあたって駅すぱあとのAPIを利用する場合はデフォルトの設定のXcodeがHTTPSではなく、HTTPのURLのAPIを利用することを拒否してきます。なので解消することをお忘れなく!
解消方法: 「http://」のAPIを実行できるようにする(Swift)

SwiftJSONとDecodableを利用するにあたって

これはどちらもJSONをいい感じに変換して使用する方法です。なのでどちらか(あるいは他にも選択肢はあると思いますが)選ぶ必要があります。
どっちがいいのかと言う正解みたいなのはないとは思いまずが、最近ではdecodableの方が主流と聞きました。持ち間違っていたらコメントください。

1. SwiftyJSONを利用する方法

他の言語でJSONの処理をやったことがある人はdecodableよりもこちらの方が考え方として慣れているかなと思います。(そもそもcodableの考え方はswiftにしかないっぽい)今回はとりあえずこれ以上の機能を考えていないのでViewControllerに記述しちゃいたいと思います。

ViewControllerへの記述

ViewController.swift
import UIKit
import Alamofire
import SwiftyJSON

class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {

  @IBOutlet weak var stationList: UITableView!
  var stations:[Station] = [Station]()


  override func viewDidLoad() {
    super.viewDidLoad()
    stationList.dataSource = self
    stationList.delegate = self
    stationList.register(UINib(nibName: "StationTableViewCell", bundle: nil), forCellReuseIdentifier: "StationTableViewCell")
    self.setupStations()
  }

  override func didReceiveMemoryWarning() {
    super.didReceiveMemoryWarning()
    // Dispose of any resources that can be recreated.
  }

  func setupStations() {
    // stations = [Station(name: "飯田橋", prefecture: "東京都新宿区"), Station(name: "九段下", prefecture: "東京都千代田区"), Station(name: "御茶ノ水", prefecture: "東京都文京区") ];
    let url = URL(string: "http://api.ekispert.jp/v1/json/station?key=[駅すぱあとのKEY]")!
    Alamofire.request(url, method: .get).responseJSON { response in
      switch response.result {
      case .success:
        let json:JSON = JSON(response.result.value ?? kill)
        let resData:JSON = json["ResultSet"]["Point"]
        var stationInfo: [Station] = []
        resData.forEach { (_, resData) in
          let station: Station = Station.init(name: resData["Station"]["Name"].string!, prefecture: resData["Prefecture"]["Name"].string!)
          stationInfo.append(station)
        }
        self.stations = stationInfo
        self.stationList.reloadData()

      case .failure(let error):
        print(error)
      }
    }
  }

  func numberOfSectionsInTableView(tableView: UITableView) -> Int {
    return 1
  }

  func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    return stations.count
  }

  func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCell(withIdentifier: "StationTableViewCell", for: indexPath ) as! StationTableViewCell

    cell.setCell(station: stations[indexPath.row])

    return cell
  }
}

その後、Buildする

一覧が取得されていることが確認されました。次に今回書いたコードについて解説していきます。

ViewController.swift
Alamofire.request(url, method: .get).responseJSON { response in
      switch response.result {
      case .success:
        let json:JSON = JSON(response.result.value ?? kill)
        let resData:JSON = json["ResultSet"]["Point"]
        var stationInfo: [Station] = []
        resData.forEach { (_, resData) in
          let station: Station = Station.init(name: resData["Station"]["Name"].string!, prefecture: resData["Prefecture"]["Name"].string!)
          stationInfo.append(station)
        }
        self.stations = stationInfo
        self.stationList.reloadData()

      case .failure(let error):
        print(error)
      }
    }

今回で変わっているのはここだけです。Alamofireの使い方については今回は省略します。受け取ったデータをresDataと置き、その後forEach文で取ってきた値を1つ1つ代入しています。最後にそれをstationsとしてまとめることで他のファイルの記述に合わせています。最後にself.stationList.reloadData()を記述することを忘れないようにしましょう。

今回使用したコードの詳細はこちらにあります。
https://github.com/takumaosada/customCellTutorial/tree/feature/swifty_json

decodableを利用する方法

こちらがdecodable(codable)の公式のドキュメントです。
https://developer.apple.com/documentation/swift/swift_standard_library/encoding_decoding_and_serialization
僕の解釈としては受け取ったJSONを自分がやりやすいようにカスタマイズして出力するのがSwiftyJSONだとすると、受け取ったJSONを、受け取ったJSONの構造のまま使う制約を設けることでデータを出力させやすくするのがCodableだと思っています。そのためチーム開発するときにメンバーがstructの構造を見ればすぐにどんなJSONを受け取ったのか理解することができるメリットがあります。ここで言うstructの構造というのは受け取るJSONの設計図みたいなものです。じゃあ実際にそれをコードに起こしていくのが次になります。

structの構造を確認する

Station.swift
/*
 {
   "ResultSet":{
     "Point":
     [
       {
         "Station":{
           "code":"00000",
           "Name":"ほげ",
           "Yomi":"ほげ"
         },
         "Prefecture":{
           "Name":"ほげ県"
         },
       }
     ]
   }
 }
*/

今回受け取る駅すぱあとのJSONはこのような構造になっていました。これに合わせてstructを構築していったものが次になります。

それに合わせてstructを記述していく

Station.swift
import Foundation

class StationSet : NSObject {
  var code: String
  var name: String
  var prefecture: String

  init(code: String, name: String, prefecture: String){
    self.code = code
    self.name = name
    self.prefecture = prefecture
    }
}

struct Data: Decodable {
  var ResultSet: ResultSet
}

public struct ResultSet: Decodable {
  var Point: [Point]
}

public struct Point: Decodable {
  public let Station: Station
  public let Prefecture: Prefecture
}

public struct Station: Decodable {
  public let code: String
  public let name: String
  public let yomi: String

  public enum CodingKeys: String, CodingKey {
    case code
    case name = "Name"
    case yomi =  "Yomi"
  }
}

public struct Prefecture: Decodable  {
  public let name: String

  public enum CodingKeys: String, CodingKey {
    case name = "Name"
  }
}

今まで作っていたmodelのStationは名前が被っているのでやむなくStationSetに変更しました。それに応じて他のファイルでの記述も変更が必要です。nested JSONにおけるstructの構造を理解する上でCodingKey理解は必須です。 (自分はまだ完全に理解してないです。)

ViewControllerへの記述

ViewController.swift
import UIKit
import Alamofire
import SwiftyJSON

class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {

  @IBOutlet weak var stationList: UITableView!
  var stations:[StationSet] = [StationSet]()


  override func viewDidLoad() {
    super.viewDidLoad()
    stationList.dataSource = self
    stationList.delegate = self
    stationList.register(UINib(nibName: "StationTableViewCell", bundle: nil), forCellReuseIdentifier: "StationTableViewCell")
    self.setupStationSets()
  }

  override func didReceiveMemoryWarning() {
    super.didReceiveMemoryWarning()
    // Dispose of any resources that can be recreated.
  }

  func setupStationSets() {
    //stations = [StationSet(name: "飯田橋", prefecture: "東京都新宿区"), StationSet(name: "九段下", prefecture: "東京都千代田区"), StationSet(name: "御茶ノ水", prefecture: "東京都文京区") ];
    let url = URL(string: "http://api.ekispert.jp/v1/json/station?key=[駅すぱあとのKEY]")!
    Alamofire.request(url, method: .get).responseJSON { response in
      switch response.result {
      case .success:
        do {
          let resultSetInfo = try JSONDecoder().decode(Data.self, from: response.data!)

          let stations = resultSetInfo.ResultSet.Point.map({ (Point) -> StationSet in
            let name = Point.Station.name
            let code = Point.Station.code
            let prefecture = Point.Prefecture.name
            let stationArray = StationSet(code: code, name: name, prefecture: prefecture)
            return stationArray
          })

          self.stations = stations
          DispatchQueue.main.async {
            self.stationList.reloadData()
          }
             } catch {
          print(error)
        }
      case .failure(let error):
        print(error)
      }
    }
  }

  func numberOfSectionsInTableView(tableView: UITableView) -> Int {
    return 1
  }

  func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    return self.stations.count
  }

  func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCell(withIdentifier: "StationTableViewCell", for: indexPath ) as! StationTableViewCell

    cell.setCell(station: self.stations[indexPath.row])

    return cell
  }
}

Buildしましょう


無事に取得することができました。

次に解説をしていきます。

ViewController.swift
Alamofire.request(url, method: .get).responseJSON { response in
      switch response.result {
      case .success:
        do {
          let resultSetInfo = try JSONDecoder().decode(Data.self, from: response.data!)

          let stations = resultSetInfo.ResultSet.Point.map({ (Point) -> StationSet in
            let name = Point.Station.name
            let code = Point.Station.code
            let prefecture = Point.Prefecture.name
            let stationArray = StationSet(code: code, name: name, prefecture: prefecture)
            return stationArray
          })

          self.stations = stations
          DispatchQueue.main.async {
            self.stationList.reloadData()
          }
             } catch {
          print(error)
        }
      case .failure(let error):
        print(error)
      }

SwiftyJSONの時と結構似てます。forEach文ではなくmapで一つ一つに代入してまたもやstationsとしてまとめている感じです。

このコードの詳細はこちらにあるのでぜひ見てみてください。
https://github.com/takumaosada/customCellTutorial/tree/feature/decodable

ostk0069
Swift / Rails Engineer https://www.wantedly.com/users/31623094
https://github.com/takumaosada
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away