概要
この記事は初心者の自分がRESTfulなAPIとswiftでiPhone向けのクーポン配信サービスを開発した手順を順番に記事にしています。技術要素を1つずつ調べながら実装したため、とても遠回りな実装となっています。
前回のDBに格納したデータを返すwebAPIをDjangoとSQLiteで開発するでWebAPIの仕様を変えたので、新しいAPIの仕様に合わせて、swiftでwebAPIを呼び出してjsonデータを表示させる で作成したswiftのコードを改造します。大きな変更点は、レスポンスのjsonオブジェクトが配列になった点です。これにより複数のクーポン情報が取得出来るようになりました。
これまでのjson (例)
{“name”:”beer”,”price”:500}
新しいjson (例)
[
{“name”:”beer”,”price”:500}
{“name”:”vodka”,”price”:700}
{“name”:”scotch”,”price”:800}
]
jsonのデータを取り出してパースする処理を配列型に対応させる必要があります。
参考
- やさしくはじめる iPhoneアプリ作りの教科書 森 巧尚 著 マイナビ
- 【swift入門】apiを叩いてTableViewに表示させる
- JSON (JavaScript Object Notation)の書式、エンコーディング、MIME Type について
環境
Mac OS 10.15
Swift5
Xcode11.1
手順
- レスポンスの内容をData型からjson形式に変換しコンソールで確認する
- jsonをAny型にキャストする
- 配列に格納されているjsonのオブジェクトを一つずつ String型にキャストする
レスポンスのjsonの形式を確認する
webAPI側はjsonをオブジェクトの配列で返す仕様になっていますが、念のためどの様なjsonが返ってくるか確認します。
まずはブラウザでAPIのURLにアクセスしてjsonの形式を確認します。
次にターミナルでも確認してみます。ターミナルの場合は curl コマンドで APIのURLにアクセスします。
curlコマンドを使う方が改行がされていて確認しやすいですね。もっと複雑なJSONの確認をする場合は専用のツールを使うの方法があるそうです。
想定した通り、配列型の下記の様な形式のjsonになっています。
[
{...}
{...}
{...}
]
次にXcodeでData型をjsonに変換しただけのデータをコンソールに出力させてみます。swiftでwebAPIを呼び出してjsonデータを表示させる で作成したコードを使う場合は、ViewController.swift
のviewDidLoad
の一部を下記の通りコメントアウトや修正します。
- APIのURLを
http://127.0.0.1:8000/coupon/
に変更 -
JSONSerialization〜
の後ろのas! [String: Any]
は型違いでエラーになるので削除 - TableViewとデータをやり取りするためのグルーバル変数に代入する処理をコメントアウト
修正後のviewDidLoadはこちらです。
override func viewDidLoad() {
super.viewDidLoad()
let url: URL = URL(string: "http://127.0.0.1:8000/coupon/")! // URLの変更
let task: URLSessionTask = URLSession.shared.dataTask(with: url, completionHandler: {(data, response, error) in
do {
let couponData = try JSONSerialization.jsonObject(with: data!, options: JSONSerialization.ReadingOptions.allowFragments)
print(couponData)
/* ここはコメントアウト
DispatchQueue.main.async() { () -> Void in
self.couponBenefit = couponData["coupon_benefits"] as! String
self.couponDeadline = couponData["coupon_deadline"] as! String
}
*/
}
catch {
print(error)
}
})
task.resume()
}
この状態で実行してコンソールに出力されるjsonを確認します。すると、下記の様に出力されます。
ブラウザやcurlで表示されるjsonは外側のかっこが[ ]
ですが、アプリ上ではかっこが( )
になっています。このままでは配列として扱えず、中のデータが取り出せません。そこで、Any型にキャストします。
JSONをAny型にキャストする
JSONSerialization.jsonObject(with: data!, options: JSONSerialization.ReadingOptions.allowFragments)
の後ろに as! [Any]
を付けるだけです。修正前のコードではas! [String: Any]
が付いていたところです。
do {
let couponData = try JSONSerialization.jsonObject(with: data!, options: JSONSerialization.ReadingOptions.allowFragments) as! [Any] //Any型にキャスト
print(couponData)
//...中略...
}
catch {
print(error)
}
すると、コンソールに出力されたjsonの外側のかっこが( )
から[ ]
に変わりました。これで配列として扱えるようになりました。
配列に格納されているjsonのオブジェクトを一つずつ String型にキャストする
jsonクラスのmapという関数を使い、jsonの配列のオブジェクトに対して[String: Any]型にキャストしたものを新しい配列couponData
に格納するようにします。
JSONSErialization
の処理の下に以下の処理を追加します。
let couponData = couponDataArray.map { (couponData) -> [String: Any] in
return couponData as! [String: Any]
}
ここまでの処理で個々のjsonオブジェクトの情報を取り出せるようになりました。情報を取り出す際、変数couponData
はjsonオブジェクトと、オブジェクトに格納されているモデルフィールドの二重配列となっているので、
個々のデータは、couponData[オブジェクトの番号(0~)][“モデルフィールド名(jsonのキーと同じ)”]で指定できます。
但しそのままだと[String: Any]型なので、取り出すモデルフィールドの型に合わせてキャストします。文字型の場合はas! String
でキャストします。下記のようなコードでデータを取り出していきます。
couponData[オブジェクトの番号(0~)][“JSONのキー名”] as! [モデルフィールドの型]
試しに以下のコードを追加し、クーポン2つ分のクーポン特典をコンソールに出力させてみます。
print(couponData[0]["coupon_benefits"] as! String)
print(couponData[1]["coupon_benefits"] as! String)
実行してコンソールを見ると、クーポン2つ分のクーポン特典が出力されているので処理が成功しています。
以上でオブジェクトの配列形式のjsonから個々の情報を取り出せるようになりました。
複数のクーポン情報をTableViewで表示するように改造する。
複数のクーポンの情報をレスポンス出来るようになったので、TableViewに複数のクーポンの情報を表示できるようにします。
まず、TableViewにデータを渡す方法を変え、jsonオブジェクトの配列を[String: Any]型にパースした 変数couponData
をそのままTableViewに渡すようにします。そのためメンバー変数の定義部分を下記のように修正します。
- メンバー変数の型を[String: Any]型にし、couponDataをそのまま格納出来るようにする
- オブジェクトの項目毎のメンバ変数は不要なので削除
var coupons: [[String: Any]] = [] { //パースした[String: Any]型のクーポンデータを格納するメンバ変数
didSet{
tableView.reloadData()
}
}
次にテーブルの行数を渡している部分を改造します。配列のcount
メソッドを使ってデータを格納するメンバ変数(coupons)の配列の行数を渡すようにします。
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.coupons.count
}
次にテーブルに表示するデータを渡している部分を改造します。
辞書型変数のcoupon
を定義し、couponsの配列の1行(jsonの{...}で囲まれたオブジェクト)ごとにcouponに格納します。そして辞書型のcoupon
からモデルフィールドの名前(jsonのキー)でデータを取り出し、データをString型にキャストしてセルに渡します。
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = UITableViewCell(style: .subtitle, reuseIdentifier: "couponCell")
//辞書型変数のcouponを定義
let coupon = self.coupons[indexPath.row]
//モデルフィールドの名前でデータを取り出し、String型にキャストしてセルに渡す
cell.textLabel?.text = (coupon["coupon_benefits"] as! String)
cell.detailTextLabel?.text = "有効期限:" + (coupon["coupon_deadline"] as! String)
return cell
}
最後にviewDidLoad
でjsonをパースしてメンバ変数に格納する部分を改造します。
String型の変数にjsonオブジェクト内のデータを直接格納していましたが、[String: Any]
型のままTableViewに渡す仕様となったため、jsonをパースした[String: Any]型のデータをそのままメンバ変数coupons
に渡すようにします。
DispatchQueue.main.async() { () -> Void in
self.coupons = couponData
}
アプリを起動すると、先にjsonで確認した3件つのクーポンがTableViewで表示されるようになりました。
修正後のViewControllerクラスは下記の通りです。
import UIKit
class ViewController: UIViewController,UITableViewDataSource,UITableViewDelegate {
@IBOutlet weak var tableView: UITableView!
var coupons: [[String: Any]] = [] { //パースした[String: Any]型のクーポンデータを格納するメンバ変数
didSet{
tableView.reloadData()
}
}
override func viewDidLoad() {
super.viewDidLoad()
let url: URL = URL(string: "http://127.0.0.1:8000/coupon/")!
let task: URLSessionTask = URLSession.shared.dataTask(with: url, completionHandler: {(data, response, error) in
do {
let couponDataArray = try JSONSerialization.jsonObject(with: data!, options: JSONSerialization.ReadingOptions.allowFragments) as! [Any]
let couponData = couponDataArray.map { (couponData) -> [String: Any] in
return couponData as! [String: Any]
}
DispatchQueue.main.async() { () -> Void in
self.coupons = couponData
}
}
catch {
print(error)
}
})
task.resume()
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.coupons.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = UITableViewCell(style: .subtitle, reuseIdentifier: "couponCell")
//辞書型変数のcouponを定義
let coupon = self.coupons[indexPath.row]
//モデルフィールドの名前でデータを取り出し、String型にキャストしてセルに渡す
cell.textLabel?.text = (coupon["coupon_benefits"] as! String)
cell.detailTextLabel?.text = "有効期限:" + (coupon["coupon_deadline"] as! String)
return cell
}
}
以上です。