QiitaのAPIを叩いて記事を取得して表示するサンプルアプリを書いてみた その4（Xcode9.3＆Swift4対応、Codable対応、標準API切り替え対応）


[[Swift]QiitaのAPIを叩いて記事を取得して表示するサンプルアプリを書いてみた](http://qiita.com/BlackCat/items/0f5313b5a60ca39d0b12)シリーズの続き。

## 概要
[[Swift]QiitaのAPIを叩いて記事を取得して表示するサンプルアプリを書いてみた その3](https://qiita.com/BlackCat/items/92ee91f990c9e8774b5e)の続き。
上記記事の内容がSwift3の頃のものなので、更新することにした。
また、考えなしにライブラリを導入していたので、これを機会にiOSの標準APIに切り替えてみることにした。

## 改修対象のソースコード
前回記事公開時点のものを改修する。
https://github.com/macneko-ayu/QiitaAPISample/releases/tag/1.0.0

改修後は下記からどうぞ。
https://github.com/macneko-ayu/QiitaAPISample/releases/tag/1.0.1

## やったこと
* Xcode9.3＆Swift4対応
* ObjectMapperからCodableへの切り替え
* Alamofireから標準APIへの切り替え
* Viewの改修

## Xcode9.3＆Swift4対応
### Xcode9.3で実行できるか確認
1. プロジェクトを `git clone` して取得する
2. ターミナルでプロジェクトのディレクトリに移動して `pod install` を実行
3. 作成された `QiitaAPISample.xcworkspace` を開き、シミュレータをデバイスとして指定してRunを実行
4. シミュレータで起動することを確認

問題なく実行できた。

### XcodeのRecommended settingsを適用する
1. Xcodeの `Issue Navigator` に表示されている「Update to recommended settings」をクリック
2. ダイアログが表示されるので、すべてにチェックを入れて、「Perform Changes」ボタンをクリック
3. 「The working copy QiitaAPISample has uncommitted changes.」っていうアラートが表示されるので、「Countinue」ボタンをクリック
4. シミュレータで起動することを確認

### Swiftのバージョンを変更する
1. プロジェクトの `Build Settings＞Swift Language Version` を「Swift 4.1」に変更する
2. シミュレータで起動することを確認

## ObjectMapperからCodableへの切り替え
ObjectMapper の利用をやめ、Swift4から採用されたCodableを使うことにした。

完成形がこちら。
モデルは `Item` という名称がふさわしくなかったので、改修を機に `Article ` にリネームした。

```swift:モデル
struct Article: Codable {
    var title: String?
    var userId: String?
    
    private enum UserKeys: String, CodingKey {
        case userId = "id"
    }
    
    private enum ArticleKeys: String, CodingKey {
        case title
        case user
    }
    
    init(from decoder: Decoder) throws {
        let values = try decoder.container(keyedBy: ArticleKeys.self)
        self.title = try values.decode(String.self, forKey: .title)
        let user = try values.nestedContainer(keyedBy: UserKeys.self, forKey: .user)
        self.userId = try user.decode(String.self, forKey: .userId)
    }
}
```

```swift:JSON→モデル
let data = """
 [
   {
     "title": "仮のタイトル",
     "user": {
       "id": "macneko_ayu"
     }
   },
   {
     "title": "仮のタイトル2",
     "user": {
       "id": "macneko_ayu"
     }
   }
 ]
""".data(using: .utf8)!

let articles = try! JSONDecoder().decode([Article].self, from: data)
```

切り替えるにあたって、つまづいたところが二つあって、結構悩んだ。

### RootがArrayの場合に、JSONをモデルに変換する方法がわからない
RootがArrayじゃない場合は下記コードで変換できる。

```swift:JSON→モデル
let data = """
   {
     "title": "仮のタイトル",
     "user": {
       "id": "macneko_ayu"
     }
   }
""".data(using: .utf8)!

let articles = try! JSONDecoder().decode(Article.self, from: data)
```

そしてRootがArrayの場合は、`Article.self` の部分を、`[Article].self` とすれば良いだけだった。

### ネストした構造の場合に、JSONをフラットなモデルに変換する方法がわからない
モデルは改修前のフラット構造をそのまま使いたかった。

```swift:改修前のフラット構造のモデル
struct Article: Codable {
    var title: String?
    var userId: String?
}
```

この構造のモデルに変換するには、ネスト構造をフラット構造にする必要があるんだけど、その方法がわからなかったので、手始めにネスト構造のまま変換することにした。
`Article` モデル内に、JSONの `user` キーに相当する `User` モデルを持つようにする。

```swift:ネスト構造のモデル
struct Article: Codable {
    var title: String?
    var user: User
    
    struct User: Codable {
        var id: String?
    }
}
```

サンプルアプリではこの構造でもいいんだけど、実際の案件ではフラット構造にしたいことも出てくるだろう。
そこでCodingKeyを用いて、フラット構造に変換できるようにした。

```swift:完成形
struct Article: Codable {
    var title: String?
    var userId: String?
    
    private enum UserKeys: String, CodingKey {
        case userId = "id"
    }
    
    private enum ArticleKeys: String, CodingKey {
        case title
        case user
    }
    
    init(from decoder: Decoder) throws {
        let values = try decoder.container(keyedBy: ArticleKeys.self)
        self.title = try values.decode(String.self, forKey: .title)
        let user = try values.nestedContainer(keyedBy: UserKeys.self, forKey: .user)
        self.userId = try user.decode(String.self, forKey: .userId)
    }
}
```

`init(from decoder: Decoder) throws` メソッドで `user` キーの内容を一旦デコードし、デコードした中の `id` を `userId` とする処理を行っている。
それによって、ネスト構造をフラット構造に変換している。
先にネスト構造をネストを保ったまま変換するコードを書いたことによって見えてきたけど、いきなり書くのは難しいな、これ。

## Alamofireから標準APIへの切り替え
「通信するなら通信ライブラリを使う」という盲目的な思考で導入したAlamofire。
でも、サンプルアプリではGETしか使わないし、導入している意味がほぼなかったので、標準APIに切り替えた。

完成形がこちら。

```swift:APIClient
struct APIClient {

    static func fetchArticle(_ completion: @escaping ([Article]) -> Void) {
        let components = URLComponents(string: "https://qiita.com/api/v2/items")
        guard let url = components?.url else {
            return
        }
        let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
            if let data = data {
                let decorder = JSONDecoder()
                do {
                    let articles = try decorder.decode([Article].self, from: data)
                    print(articles)
                    completion(articles)
                } catch {
                    print(error.localizedDescription)
                }
            } else {
                print(error ?? "Error")
            }
        }
        task.resume()
    }
}
```

## Viewの改修
モデル名を変更したのと、メインキューで APIから取得したデータをUITableViewにセットするようにした。
また、デリゲートをExtensionで適用するようにした。

```swift:ArticleListViewController
class ArticleListViewController: UIViewController {

    @IBOutlet weak var tableView: UITableView!
    var articles: [Article] = []
    
    override func viewDidLoad() {
        super.viewDidLoad()
        title = "新着記事"
        tableView.dataSource = self

        APIClient.fetchArticle { (articles) in
            self.articles = articles
            DispatchQueue.main.sync {
                self.tableView.reloadData()
            }
        }
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
    }
}

extension ArticleListViewController: UITableViewDataSource {
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return articles.count
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell: UITableViewCell = UITableViewCell(style: UITableViewCellStyle.subtitle, reuseIdentifier: "Cell")
        let article = articles[indexPath.row]
        if let title = article.title {
            cell.textLabel?.text = title
        }
        if let userId = article.userId {
            cell.detailTextLabel?.text = userId
        }
        return cell
    }
}
```

## 感想
すぐできると思って手をつけたけど、意外とてこずった。

## 参考
### Codable
[Encoding and Decoding Custom Types](https://developer.apple.com/documentation/foundation/archives_and_serialization/encoding_and_decoding_custom_types)
[“SwiftでJSON作成、読込みする方法（ Swift4 Codableを利用）” by digitalnauts](https://medium.com/@digitalnauts/swift4-codable-json-encode-17eaa95372d1)
[ルートが配列のJSONをCoadableでカスタムモデルにマッピングする](https://qiita.com/matsuda/items/ee6d02570f716da765e9)
[Ultimate Guide to JSON Parsing with Swift 4](https://benscheirman.com/2017/06/swift-json/)
[イケてない JSON  を Swift の Decodable で扱いやすいモデルにデコードする - star__hoshi's diary](https://starhoshi.hatenablog.com/entry/2018/02/18/024211)
[iOS開発: 再入門 apiを叩いてtableViewに表示する (Qiita編)](https://qiita.com/abouch/items/aca979b71ad792478687)

### 通信
[Swift の HTTP ライブラリで苦しまないための自作 API クライアント設計](https://qiita.com/Kuniwak/items/a972ff2ade643799d1fe)
[iOSでライブラリに頼らず、URLSessionを使ってHTTP通信する](https://qiita.com/yutailang0119/items/ab400cb7158295a9c171)