Edited at

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

これは続きです!

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