はじめに
最近、少しずつRxSwift
の勉強をしています。
僕が勉強始めた時、いい塩梅のサンプルコードを探すのに、少しだけ時間がかかってしまったので、自分が書いたサンプルコードを記事にしようと思いました。
これからRxSwift
の門を叩く方のお役に立てれば幸いです。
前提条件
- Swiftが読めること
- Codableを利用したAPIリクエストについて理解していること
- MVVMの特徴を理解していること
- RxSwiftが概要レベルで分かること
サンプルコードの概要
アプリを起動すると、Qiitaの記事一覧の検索APIをコールして、そのレスポンスをTableViewに表示するだけのアプリです。
RxSwift
で動く簡単なアプリを第一目標にしたので、
- Protocolで抽象を参照するなどのリファクタリングはしてません。
- エラーハンドリングとか適当です。
↑ご容赦ください。
コード&解説
RxSwift
を利用するからには、データバインディングを活用したMVVMアーキテクチャを採用したいということで、今回は、MVVMで実装しました。
Model
QiitaAPIで表示したいデータをCodableを使ってデコードできるようにしたモデルクラスを定義ます。
// 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通信が相当お手軽ですね。
→ 通信部分の共通化とかやってみたい。
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
入門者であれば、このやり方が一番、シンプルなのかなぁとは個人的に思います。
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)
}
}
アプリの動作全体の流れとしては、
- 画面が開かれた時にリクエストを投げるように、ViewからViewModelに依頼する ←イベントの発火
- ViewModelでリクエストの処理結果を観察して、レスポンスを待つ。
- レスポンスが来たら
accept
を使って、Relay に.next
イベントを送信する -
articles
はTableViewとデータバインディングされているので、テーブルを更新する処理が走る。
といった感じだと思います。
個人的に、RxSwiftを用いたMVVMアーキテクチャは、
開発者が仕様書に沿って引いた導線(Observerとバインディング)に着火(ユーザーイベント)されたらゴールへ着地する(処理が流れる)
というイメージで勝手に捉えています。
最後に、RxSwift
無関係ですが、カスタムCellのコードも貼っておきます。
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つできたので、今度は、「検索機能」でも追加してみようと思います。
参考にさせていただいた記事・サンプル