RxSwift入門に丁度いいサンプルコード書いてみた

## はじめに
最近、少しずつ`RxSwift`の勉強をしています。
僕が勉強始めた時、いい塩梅のサンプルコードを探すのに、少しだけ時間がかかってしまったので、自分が書いたサンプルコードを記事にしようと思いました。

これから`RxSwift`の門を叩く方のお役に立てれば幸いです。

## 前提条件
- Swiftが読めること
- Codableを利用したAPIリクエストについて理解していること
- MVVMの特徴を理解していること
- RxSwiftが概要レベルで分かること

## サンプルコードの概要
アプリを起動すると、Qiitaの記事一覧の検索APIをコールして、そのレスポンスをTableViewに表示するだけのアプリです。

`RxSwift`で動く簡単なアプリを第一目標にしたので、

- Protocolで抽象を参照するなどのリファクタリングはしてません。
- エラーハンドリングとか適当です。

↑ご容赦ください。

## コード＆解説

`RxSwift`を利用するからには、データバインディングを活用したMVVMアーキテクチャを採用したいということで、今回は、MVVMで実装しました。

#### Model
QiitaAPIで表示したいデータをCodableを使ってデコードできるようにしたモデルクラスを定義ます。

```Swift:QiitaArticleModel.swift
// MARK: - QiitaArticleModel
struct QiitaArticleModel: Codable {
    let title: String
    let url: String
    let user: User
}

// MARK: - User
struct User: Codable {
    let id: String
    let profileImageURL: String

    enum CodingKeys: String, CodingKey {
        case id
        case profileImageURL = "profile_image_url"
    }
}
```

#### ViewModel
最初は知らなくて進めてたのですが、`RxSwfit`を導入することで、`URLSession`を使ったAPI通信もこんな感じで書けるようです。

まだ完全にキャッチアップしきれてませんが、熟練度の高いサンプルコードを見ると、もう少し異なる書き方をされていたりするので、まだまだ奥が深そうです。。。

これ使いこなせたらAPI通信が相当お手軽ですね。
→ 通信部分の共通化とかやってみたい。

```Swift:QiitaArticleViewModel.swift
import Foundation
import RxSwift
import RxCocoa

class QiitaArticleViewModel {
    private var disposeBag = DisposeBag()
    
    var articles = BehaviorRelay<[QiitaArticleModel]>(value: [])
    
    func requestQiitaArticle() {
        guard let url = URL(string: "https://qiita.com/api/v2/items?page=1&per_page=20") else { return }
        let urlRequest = URLRequest(url: url)
        
        URLSession.shared.rx.response(request: urlRequest)
            .subscribe { [weak self] response, data in
                
                guard let articles = try? JSONDecoder().decode([QiitaArticleModel].self, from: data) else { return }
                
                self?.articles.accept(articles)
                
            } onError: { error in
                print(error.localizedDescription)
            }
            .disposed(by: disposeBag)
    }
}
```

#### View(ViewController)
個人的に生のSwiftしか書いてこなかった人間が一番戸惑ったのは、TableViewのところです。
`RxSwift`を使わない場合、DataSourceの準拠が漏れると、データが表示されなかったりしますが、`RxSwift`を使えば、DataSourceに準拠しなくてもいいみたいです。

調べたら、TableViewにデータを表示するための書き方は何通りかあるみたいです。
`RxSwfit`入門者であれば、このやり方が一番、シンプルなのかなぁとは個人的に思います。

```swift:QiitaArticleViewController.swift
import UIKit
import RxSwift

class QiitaArticleViewController: UIViewController {
    
    private var viewModel = QiitaArticleViewModel()
    private var disposeBag = DisposeBag()
    
    @IBOutlet private weak var tableView: UITableView! {
        didSet {
            tableView.register(UINib(nibName: QiitaArticleCell.identifier, bundle: nil), forCellReuseIdentifier: QiitaArticleCell.identifier)
        }
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        
        bind()
    }
    
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
                
        viewModel.requestQiitaArticle()
    }
    
    private func bind() {
        viewModel.articles
            .bind(to: tableView.rx.items(cellIdentifier: QiitaArticleCell.identifier, cellType: QiitaArticleCell.self)) { row, element, cell in
                cell.configureCell(model: element, row: row)
            }
            .disposed(by: disposeBag)

    }
}
```

アプリの動作全体の流れとしては、

1. 画面が開かれた時にリクエストを投げるように、ViewからViewModelに依頼する ←イベントの発火
2. ViewModelでリクエストの処理結果を観察して、レスポンスを待つ。
3. レスポンスが来たら`accept`を使って、Relay に`.next`イベントを送信する
4. `articles`はTableViewとデータバインディングされているので、テーブルを更新する処理が走る。

といった感じだと思います。

個人的に、RxSwiftを用いたMVVMアーキテクチャは、
`開発者が仕様書に沿って引いた導線（Observerとバインディング）に着火（ユーザーイベント）されたらゴールへ着地する（処理が流れる）`というイメージで勝手に捉えています。


最後に、`RxSwift`無関係ですが、カスタムCellのコードも貼っておきます。

```swift:QiitaArticleCell.swift
import UIKit

class QiitaArticleCell: UITableViewCell {
    
    static var identifier = "QiitaArticleCell"
    
    @IBOutlet private weak var userNameLabel: UILabel!
    @IBOutlet private weak var titleLabel: UILabel!
    
    override func awakeFromNib() {
        super.awakeFromNib()
        // Initialization code
    }

    override func setSelected(_ selected: Bool, animated: Bool) {
        super.setSelected(selected, animated: animated)

        // Configure the view for the selected state
    }
    
    func configureCell(model: QiitaArticleModel, row: Int) {
        userNameLabel.text = model.user.id
        titleLabel.text = model.title
        self.contentView.backgroundColor = (row % 2 == 0) ? .black : .gray
    }
    
}
```

## おわりに
とりあえず、サンプルアプリ1つできたので、今度は、「検索機能」でも追加してみようと思います。

## 参考にさせていただいた記事・サンプル
https://qiita.com/usamik26/items/444d6dd7386b2949c06b

https://qiita.com/HikaruKuroda/items/86fa7aba3eed6cadbd0b

https://qiita.com/hironytic/items/71bc729abe73ab9f0879

https://github.com/ReactiveX/RxSwift/tree/main/RxExample/RxExample/Examples