【Swift3】RxSwift + APIKit + Himotokiで作るAPIクライアント

# 【Swift3】 RxSwift + APIKit + Himotokiで作るAPIクライアント
(おそらくn番煎じですが...)

## 前置き
春休みに企業のインターンに参加させてもらったときに、APIKitとHimotokiを教えていただきました。
今まではAlamofireとObjectMapperを使っていたのですが、APIKitが書きやすくて可読性も良いのですごく気に入りました。
別の企業のインターンに参加したときは、MVVM+RxSwiftを使用しているプロジェクトに参加させていただいたのですが、なかなか難しかったので頑張って勉強しています。

今回、RxSwiftの勉強をしながらGitHubのリポジトリビューアーを作ったので、記事にしたいと思います。

あと、RxSwiftとAPIKitを使用した記事で、Swift3に対応したものが少なかった(気がする)ので、参考になればと思います。

## 作ったもの
<img src="https://qiita-image-store.s3.amazonaws.com/0/84888/94cf943c-f78f-5be0-ba52-6a6a266b0bf5.gif" width="40%" height="40%"></img>
https://github.com/natmark/GitHubRepositoryViewer

GitHubのユーザIDを入力すると、ユーザの公開リポジトリ一覧を表示できるようにしました。

## 環境
Mac OS X El Capitan 10.12.3
Xcode 8.2.1
Swift 3.0.2

## 実装
### ライブラリのインポート
```swift:Cartfile
github "ishkawa/APIKit" ~> 3.0
github "ikesyo/Himotoki" ~> 3.0
github "ReactiveX/RxSwift" ~> 3.0
github "pinterest/PINRemoteImage"
```

Carthageの導入についてはこちらを参考にしてください。
http://qiita.com/kobad/items/dddab651c91b3dee9fbf

以下のライブラリをインポートしました。

- **ishkawa/APIKit**
タイプセーフな軽量HTTPクライアントライブラリ

- **ikesyo/Himotoki**
タイプセーフなJSONデコーダライブラリ

- **ReactiveX/RxSwift**
Rx(Reactive Extensions)のSwift実装ライブラリ
> observableのシークエンスを使って非同期でイベントベースのプログラムを実現するためのライブラリ。
http://qiita.com/jollyjoester/items/c4013c60acd453ea7248

- **pinterest/PINRemoteImage**
スレッドセーフな画像フェッチライブラリ

### Model
今回GitHub APIの `/users/[username]/repos` を使用しました。
使用したい項目は、以下になります。

- `full_name`
ユーザID / リポジトリ名
- `owner.avatar_url`
ユーザのアバター画像のURL
- `language`
リポジトリの主要言語(optional)
- `url`
リポジトリのURL

上記を取得できるようなモデルをHimotokiを用いて以下のように作成しました。

```swift:Repository.swift
import Himotoki

struct Repository: Decodable {
    let fullName: String
    let ownerAvatarUrl: String
    let language: String?
    let url: String

    static func decode(_ e: Extractor) throws -> Repository {
        return try Repository(
            fullName: e <| "full_name",
            ownerAvatarUrl: e <| ["owner", "avatar_url"], //nested
            language: e <|? "language", //optional
            url: e <| "url"
        )
    }
}
```

### APIKitとRxSwiftの連携
>前述の APIKit の Session.sendRequest(request) には Observable を返すインターフェースではないので、別で用意する必要があります。 (http://qiita.com/pm11/items/57b2dff4b1ac19bd89ba)


こちらの記事を参考にしました。
http://h3poteto.hatenablog.com/entry/2016/05/16/000351

```swift:SessionRx.swift
import APIKit
import RxSwift

extension Session {
    func rx_sendRequest<T: Request>(request: T) -> Observable<T.Response> {
        return Observable.create { observer in
            let task = self.send(request) { result in
                switch result {
                case .success(let res):
                    observer.on(.next(res))
                    observer.on(.completed)
                case .failure(let err):
                    observer.onError(err)
                }
            }
            return Disposables.create {
                task?.cancel()
            }
        }
    }

    class func rx_sendRequest<T: Request>(request: T) -> Observable<T.Response> {
        return shared.rx_sendRequest(request: request)
    }
}
```

参考にした記事はSwift2のものだったので、Swift3で使用できるように一部書き換えました。

`AnonymousDisposable`ですが、`Disposables.create()`　で代替できるとのことでした。
http://stackoverflow.com/questions/40936295/what-is-the-rxswift-3-0-equivalent-to-anonymousdisposable-from-rxswift-2-x

### API Client
`APIKit`を使用してAPIクライアントを作成しました。
[開発者のIshkawaさんもおっしゃているように](https://speakerdeck.com/ishkawa/introducing-apikit?slide=31)、まるでドキュメントを写したかのように定義することができます。

```swift:API.swift
import APIKit
import Himotoki

protocol GitHubRequest: Request {

}

extension GitHubRequest {
    var baseURL: URL {
        return URL(string: "https://api.github.com")!
    }

    func intercept(object: Any, urlResponse: HTTPURLResponse) throws -> Any {
        guard (200..<300).contains(urlResponse.statusCode) else {
            throw ResponseError.unacceptableStatusCode(urlResponse.statusCode)
        }
        return object
    }
}

extension GitHubRequest where Response: Decodable {
    func response(from object: Any, urlResponse: HTTPURLResponse) throws -> Self.Response {
        return try decodeValue(object)
    }
}

struct FetchRepositoryRequest: GitHubRequest {
    var userName: String
    var path: String {
        return "/users/\(self.userName)/repos"
    }
    typealias Response = [Repository]

    var method: HTTPMethod {
        return .get
    }

    init(userName: String) {
        self.userName = userName
    }
    func response(from object: Any, urlResponse: HTTPURLResponse) throws -> FetchRepositoryRequest.Response {
        return try decodeArray(object)
    }
}

```

今回ですが、`/users/[username]/repos` はルートがArrayのJSONを返却するAPIなので、
`decodeArray(_ JSON: Any)`でレスポンスをデコードするようにしました。

(`decodeValue(_ JSON: Any)`でデコードしている記事が多く、ルートがArrayのものをデコードするためにモデルをいろいろいじってハマりました。これなかなか気づかなかったです。)

### ViewModel
```swift:ListViewModel.swift
import UIKit
import RxSwift
import APIKit

class ListViewModel: NSObject, UITableViewDataSource {

    private var cellIdentifier = "ListCell"

    private(set) var repos = Variable<[Repository]>([])

    private(set) var error = Variable<Error?>(nil)

    let disposeBag = DisposeBag()

    override init() {
        super.init()
    }

    func reloadData(userName: String) {
        let request = FetchRepositoryRequest(userName: userName)
        Session.rx_sendRequest(request: request)
            .subscribe {
                [weak self] event in
                switch event {
                case .next(let repos):
                   self?.repos.value = repos
                case .error(let error): break
                    self?.error.value = error
                default: break
                }
            }
            .addDisposableTo(disposeBag)
    }

    // MARK: - TableView
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return repos.value.count
    }
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: self.cellIdentifier, for: indexPath) as! ListCell
        cell.configureCell(repo: repos.value[indexPath.row])
        return cell
    }
}
```

`reloadData(userName: String)`内で、リクエストを発行し
`repos`に値を格納するようにしました。

### TableViewCell
<img width="375" alt="スクリーンショット 2017-04-07 17.45.38.png" src="https://qiita-image-store.s3.amazonaws.com/0/84888/0764b816-7e54-d1dd-7a05-08936f396b67.png">

```swift:ListCell.swift
import UIKit
import PINRemoteImage
class ListCell: UITableViewCell {

    @IBOutlet weak var fullNameLabel: UILabel!
    @IBOutlet weak var avatarImageView: UIImageView!
    @IBOutlet weak var languageLabel: UILabel!
    @IBOutlet weak var urlLabel: UILabel!

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

    func configureCell(repo: Repository) {
        fullNameLabel.text = repo.fullName
        avatarImageView.pin_setImage(from: URL(string: repo.ownerAvaterUrl), completion: nil)
        languageLabel.text = repo.language ?? ""
        urlLabel.text = repo.url
    }

    override func setSelected(_ selected: Bool, animated: Bool) {
        super.setSelected(selected, animated: animated)
        // Configure the view for the selected state
    }
}
```

```swift
avatarImageView.pin_setImage(from: URL(string: repo.ownerAvatarUrl), completion: nil)
```

`avatarImageView`に表示する画像を`PINRemoteImage`を使用して非同期でフェッチしています。

### ViewController
<img width="502" alt="スクリーンショット 2017-04-07 17.55.36.png" src="https://qiita-image-store.s3.amazonaws.com/0/84888/414fd322-4aa6-7e50-e0c4-87faaf879949.png">

```swift:ViewController
import UIKit
import APIKit
import RxSwift
import RxCocoa

class ViewController: UIViewController, UITableViewDelegate, UISearchBarDelegate {

    @IBOutlet weak var tableView: UITableView!
    @IBOutlet weak var searchBar: UISearchBar!

    private let viewModel = ListViewModel()

    let disposeBag = DisposeBag()

    override func viewDidLoad() {
        super.viewDidLoad()
        searchBar.delegate = self
        tableView.delegate = self

        tableView.dataSource = viewModel

        bind()
    }

    func bind() {
        // Connection
        viewModel.repos.asObservable()
            .filter { x in
                return !x.isEmpty
            }
            .subscribe(onNext: { [unowned self] x in
                self.tableView.reloadData()
            }, onError: { error in
            }, onCompleted: { () in
            }, onDisposed: { () in
            })
            .addDisposableTo(disposeBag)

        //search
        searchBar.rx.text
            .subscribe(onNext: { [unowned self] q in
                self.navigationItem.title = q!
                self.viewModel.reloadData(userName: q!)
            }, onError: { error in
            }, onCompleted: { () in
            }, onDisposed: { () in
            })
            .addDisposableTo(disposeBag)
    }
    // MARK: - TableView
    func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
        return 130
    }
    // MARK: - SearchBar
    func searchBarCancelButtonClicked(_ searchBar: UISearchBar) {
        searchBar.resignFirstResponder()
    }
    func searchBarSearchButtonClicked(_ searchBar: UISearchBar) {
        searchBar.resignFirstResponder()
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }
}
```
```swift
viewModel.repos.asObservable()
    .filter { x in
        return !x.isEmpty
    }
    .subscribe(onNext: { [unowned self] x in
        self.tableView.reloadData()
    }, onError: { error in
    }, onCompleted: { () in
    }, onDisposed: { () in
    })
    .addDisposableTo(disposeBag)
```
`viewModel.repos`をsubscribeして、変化があった場合に`tableView.reloadData()`を呼び出すようにしています。

```swift
searchBar.rx.text
    .subscribe(onNext: { [unowned self] q in
        self.navigationItem.title = q!
        self.viewModel.reloadData(userName: q!)
    }, onError: { error in
    }, onCompleted: { () in
    }, onDisposed: { () in
    })
    .addDisposableTo(disposeBag)
```
searchBarの文字を監視して、変化があった場合に
navigationBarのタイトルを変更して、`viewModel.reloadData(userName: String)`を呼び出すようにしました。

## まとめ
普段MVCばかりなので、MVVMになかなか慣れません。
ただ、MVVMを使うことでModel、View、ViewModelを区別できるので、頑張って勉強したいです。(MVCが悪いわけではないですが、MVCを使っていて`Fat View Controller`になることも多く、課題も感じているので...)


## 参考
http://qiita.com/pm11/items/57b2dff4b1ac19bd89ba
http://h3poteto.hatenablog.com/entry/2016/05/16/000351