サンプルの各クラスを作成する(2)
Viewの作成
今回のサンプルでは、作成するクラスがビューコントローラーとコレクションのセルの2つあります、先にビューコントローラーから作成します。次のようにUIとViewModelのインスタンスを定義します。
class ViewController: UIViewController { @IBOutlet weak var textField: UITextField! // 入力欄 @IBOutlet weak var searchButton: UIButton! // 検索ボタン @IBOutlet weak var collectionView: UICollectionView! // コレクション @IBOutlet weak var indicatorView: UIActivityIndicatorView! // インジゲーター private let viewModel = SearchViewModel() // ViewModel private let disposeBag = DisposeBag() override func viewDidLoad() { super.viewDidLoad() self.bind() # 略
SearchViewModelクラスの各プロパティとは、bind()メソッドの中で行います。
func bind() { // 入力欄の値とViewModelのinputs.searchWordをbind self.textField.rx.text.orEmpty .bind(to: self.viewModel.inputs.searchWord).disposed(by: self.disposeBag) // 検索ボタンのタップでViewModelのinputs.searchTriggerを起動 self.searchButton.rx.tap.asDriver() .drive(self.viewModel.searchTrigger).disposed(by: self.disposeBag) // ViewModelのoutputs.isSearchButtonEnabledと検索ボタンの押下可否をbind self.viewModel.outputs.isSearchButtonEnabled.asObservable() // ------(1) .bind(to: self.searchButton.rx.isEnabled).disposed(by: self.disposeBag) // ViewModelのoutputs.itemsをコレクションビューにbind self.viewModel.outputs.items.asObservable() .bind(to: self.collectionView.rx.items(cellIdentifier: "ImageItemCell", cellType: ImageItemCell.self)) { (index, element, cell) in cell.configure(URL(string: element.src)!) // ------(4)セル内で画像を表示 }.disposed(by: self.disposeBag) // ViewModelのoutputs.isLoadingとインジゲーターのisAnimatingをbind self.indicatorView.hidesWhenStopped = true // インジゲーターは回っていない時は非表示に self.viewModel.outputs.isLoading // ------(2) .bind(to: self.indicatorView.rx.isAnimating).disposed(by: self.disposeBag) // エラーがあれはアラートで内容を表示 self.viewModel.outputs.error.subscribe(onNext: { [weak self] error in // ------(3) let alert = UIAlertController(title: "エラー", message: error.localizedDescription, preferredStyle: .alert) alert.addAction(UIAlertAction(title: "OK", style: .default, handler: nil)) self?.present(alert, animated: true, completion: nil) })disposed(by: self.disposeBag) }
SearchViewModelクラスの各プロパティは、inputs/outputsのプレフィックスがつくのでわかりやすいです。データバインドを行う際、inputsの場合はViewのUIの後に、outputsの場合はUIより先に、位置します。連載第4回のサンプルと処理がかぶるものは説明を割愛します。
outputs.isSearchButtonEnabledプロパティは検索ボタンのrx.isEnabledプロパティとバインドします(1)。こうすることで、ViewModelの処理からUIの動きを制御できます。同様に、outputs.isLoadingとインジゲーターのisAnimatingプロパティを結合して、Actionクラスの処理中にインジゲーターが回るようにできます(2)。outputs.errorプロパティを監視しておくことで、エラーが発生した場合の処理も他の処理と同様に定義できます(3)。また、Actionクラスを利用することで、処理中/エラー発生時の処理も他のプロパティと同様に行うことができます。
コレクションビューのセルでは、(4)の通りconfigure(_ :)メソッドで画像を表示できるようにします。
import UIKit import AlamofireImage class ImageItemCell: UICollectionViewCell { @IBOutlet weak var imageView: UIImageView! func configure(_ url: URL) { self.imageView.af_setImage(withURL: url) } }
画像を表示する際には、AlamofireImageライブラリのaf_setImage(withURL:)メソッドで行います。UIImageViewの拡張メソッドで、画像のURLを渡すと非同期に画像を取得して表示できます。
サンプルを実行すると次のように動作を確認できます。
まとめ
連載最終回では、RxSwift+MVVMをよりわかりやすく、より短い処理で機能を実装できるライブラリを利用しました。RxSwiftもMVVMモデルも、アプリ開発の効率をあげる点では非常に優秀です。ですが、両方とも導入しただけではまだ不十分なこともあります。RxSwift Communityなどのライブラリ開発コミュニティでは、既存ライブラリの不足する機能を補うための開発が活発に行われています。こういった派生ライブラリを取り入れることで、より良いアプリ開発を行うこともできます。みなさんもぜひ利用してみてください。