これは続きです!
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への記述
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する
一覧が取得されていることが確認されました。次に今回書いたコードについて解説していきます。
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の構造を確認する
/*
{
"ResultSet":{
"Point":
[
{
"Station":{
"code":"00000",
"Name":"ほげ",
"Yomi":"ほげ"
},
"Prefecture":{
"Name":"ほげ県"
},
}
]
}
}
*/
今回受け取る駅すぱあとのJSONはこのような構造になっていました。これに合わせてstructを構築していったものが次になります。
それに合わせてstructを記述していく
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への記述
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しましょう
次に解説をしていきます。
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