swiftで配列型のJSONから値を取り出す

# 概要

　この記事は初心者の自分がRESTfulなAPIとswiftでiPhone向けのクーポン配信サービスを開発した手順を順番に記事にしています。技術要素を1つずつ調べながら実装したため、とても遠回りな実装となっています。

　前回の[DBに格納したデータを返すwebAPIをDjangoとSQLiteで開発する](https://qiita.com/Ajyarimochi/items/0835a2719b9b017459e2)でWebAPIの仕様を変えたので、新しいAPIの仕様に合わせて、[swiftでwebAPIを呼び出してjsonデータを表示させる](https://qiita.com/Ajyarimochi/items/5a6e7a44d3fb2ab37f25)　で作成した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に表示させる](https://fukatsu.tech/api-tableview)
* [JSON (JavaScript Object Notation)の書式、エンコーディング、MIME Type について](https://www.ipentec.com/document/json-format)


### 環境
Mac OS 10.15
Swift5
Xcode11.1


# 手順

* レスポンスの内容をData型からjson形式に変換しコンソールで確認する
* jsonをAny型にキャストする
* 配列に格納されているjsonのオブジェクトを一つずつ String型にキャストする


### レスポンスのjsonの形式を確認する

　webAPI側はjsonをオブジェクトの配列で返す仕様になっていますが、念のためどの様なjsonが返ってくるか確認します。

まずはブラウザでAPIのURLにアクセスしてjsonの形式を確認します。
![check-api-response-by-safari-1200.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/526594/269c9003-6149-a7c3-ad40-b46782295c0a.png)


次にターミナルでも確認してみます。ターミナルの場合は curl コマンドで APIのURLにアクセスします。
<img width="570" alt="check-api-response-by-curl-mask.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/526594/df07bb3c-252a-f8c2-77f6-315b79c25b95.png">

curlコマンドを使う方が改行がされていて確認しやすいですね。もっと複雑なJSONの確認をする場合は専用のツールを使うの方法があるそうです。

想定した通り、配列型の下記の様な形式のjsonになっています。

```
[
  {...}
  {...}
  {...}
]
```


次にXcodeでData型をjsonに変換しただけのデータをコンソールに出力させてみます。[swiftでwebAPIを呼び出してjsonデータを表示させる](https://qiita.com/Ajyarimochi/items/5a6e7a44d3fb2ab37f25)　で作成したコードを使う場合は、`ViewController.swift`の`viewDidLoad`の一部を下記の通りコメントアウトや修正します。

* APIのURLを`http://127.0.0.1:8000/coupon/`に変更
* `JSONSerialization〜`の後ろの `as! [String: Any]`は型違いでエラーになるので削除
* TableViewとデータをやり取りするためのグルーバル変数に代入する処理をコメントアウト


修正後のviewDidLoadはこちらです。

```swift:ViewController.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を確認します。すると、下記の様に出力されます。

<img width="791" alt="check-api-response-by-console.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/526594/d49d409b-e707-7e77-ee18-e57e3afdec98.png">



ブラウザやcurlで表示されるjsonは外側のかっこが`[  ]`ですが、アプリ上ではかっこが`(  )`になっています。このままでは配列として扱えず、中のデータが取り出せません。そこで、Any型にキャストします。


### JSONをAny型にキャストする

`JSONSerialization.jsonObject(with: data!, options: JSONSerialization.ReadingOptions.allowFragments)`の後ろに `as! [Any]` を付けるだけです。修正前のコードでは`as! [String: Any]`が付いていたところです。


```swift

do  {
     let couponData = try JSONSerialization.jsonObject(with: data!, options: JSONSerialization.ReadingOptions.allowFragments) as! [Any] //Any型にキャスト
     print(couponData)

			//...中略...

     }
     catch {
         print(error)
         }
```

すると、コンソールに出力されたjsonの外側のかっこが`(   )`から`[   ]`に変わりました。これで配列として扱えるようになりました。
<img width="792" alt="check-api-resopnse-after-cast.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/526594/247f6a4f-81fc-9c4e-6f0b-84e885826307.png">




### 配列に格納されているjsonのオブジェクトを一つずつ String型にキャストする

　jsonクラスのmapという関数を使い、jsonの配列のオブジェクトに対して[String: Any]型にキャストしたものを新しい配列`couponData`に格納するようにします。

`JSONSErialization`の処理の下に以下の処理を追加します。

```swift

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つ分のクーポン特典をコンソールに出力させてみます。

```swift

print(couponData[0]["coupon_benefits"] as! String)
print(couponData[1]["coupon_benefits"] as! String)
```

実行してコンソールを見ると、クーポン２つ分のクーポン特典が出力されているので処理が成功しています。
<img width="263" alt="out-put-data-by-2objects.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/526594/98e041be-26cd-6409-1062-da1f55ba5d47.png">


以上でオブジェクトの配列形式のjsonから個々の情報を取り出せるようになりました。


### 複数のクーポン情報をTableViewで表示するように改造する。

　複数のクーポンの情報をレスポンス出来るようになったので、TableViewに複数のクーポンの情報を表示できるようにします。


まず、TableViewにデータを渡す方法を変え、jsonオブジェクトの配列を[String: Any]型にパースした 変数`couponData`をそのままTableViewに渡すようにします。そのためメンバー変数の定義部分を下記のように修正します。

* メンバー変数の型を[String: Any]型にし、couponDataをそのまま格納出来るようにする
* オブジェクトの項目毎のメンバ変数は不要なので削除

```swift

    var coupons: [[String: Any]] = [] { //パースした[String: Any]型のクーポンデータを格納するメンバ変数
        didSet{
            tableView.reloadData()
        }
    }
```


次にテーブルの行数を渡している部分を改造します。配列の`count`メソッドを使ってデータを格納するメンバ変数（coupons）の配列の行数を渡すようにします。

```swift

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


次にテーブルに表示するデータを渡している部分を改造します。

辞書型変数の`coupon`を定義し、couponsの配列の1行（jsonの{...}で囲まれたオブジェクト）ごとにcouponに格納します。そして辞書型の`coupon`からモデルフィールドの名前（jsonのキー）でデータを取り出し、データをString型にキャストしてセルに渡します。

```swift

    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`に渡すようにします。

```swift

      DispatchQueue.main.async() { () -> Void in
          self.coupons = couponData
      }
```


アプリを起動すると、先にjsonで確認した3件つのクーポンがTableViewで表示されるようになりました。
<img width="381" alt="test-ami-coupon-02.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/526594/ebb70339-d0ab-1987-077a-9c867da482c3.png">


修正後のViewControllerクラスは下記の通りです。

```swift:ViewController.swift

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
    }
    
}
```

以上です。

次回は [TableViewを改造して、取得したクーポン情報をアプリ画面に表示出来るようにします。](https://qiita.com/Ajyarimochi/items/0154030bde239d703806)