MVVMモデルの各クラスを作成する
前節で設計したサンプルの各クラスを順番に作成していきます。
Modelの作成
Modelは、Wikipedia APIの検索と検索結果のJSONを解析した結果を格納する役目を果たします。検索結果の格納には、連載第3回のサンプルと同じものを利用します。
// 検索結果を格納する構造体
struct Result {
let title: String
let pageid: Int
}
Jsonの検索結果の1つ1つの項目を格納する役目を果たします。検索を実行する部分は、次項より作成していきます。
HTTPリクエストを行うクラスを作成する
ViewModelを作成する前に、ViewModelの中から呼び出すWikipedia APIへのHTTPリクエストを先に作成します。Wikipwdia APIだけでなく、他のAPIなどにも拡張できるように先にプロトコルから作成します。
protocol APIClient {
var url: String { get }
func getRequest(_ parameters: [String : String]) -> DataRequest
}
APIClientの名前でプトロコルを定義し、その中でAPIのエンドポイントとメソッドの仕様だけ定義しています。先にプロトコルでメソッドの仕様を定義することで、プロトコルを実装したクラス間でメソッドの仕様を共通化することができます。Wikipedia以外のAPIを利用するクラスを作成する場合も、同じプロトコルを実装することで同じ仕様でメソッドを利用できます。並列関係にあるクラスのメソッドをプロトコルを利用して同じ仕様にすることで、クラスの切り替え/継承/バージョンアップ/テストなどの管理をしやすくることができます。
プロトコルを定義した後に、実際にWikipwdia APIへのHTTPリクエストを行うクラスを作成します。HTTPリクエストを行う部分は、非同期のHTTPリクエストを簡単に記述できるAlamofireライブラリを利用します。Alamofireのページに記載されているように、Podでインストールしてください。
HTTPリクエストは、Alamofireのrequest(_:method:parameters:encoding:headers:)メソッドを利用します。request(_:method:parameters:encoding:headers:)メソッドの書式は次の通りです。
request(#URLオブジェクト#, method: #HTTPリクエストメソッド#, parameters:#パラメータ#, encoding:#エンコーディング#, headers:#HTTPヘッダ#)
戻り値はDataRequestオブジェクトです。DataRequestオブジェクトが得られた後の処理は次節で説明します。
Wikipedia APIへのHTTPリクエストを行うクラスは、プロトコルの部分と合わせてWikipediaAPIClientの名前で次のように作成します。
// Wikipedia APIを呼び出すクラス
class WikipediaAPIClient: APIClient {
// Wikipedia APIのエンドポイント
var url = "https://ja.wikipedia.org/w/api.php?format=json&action=query&list=search"
// Wikipedia APIに向けてHTTPリクエストを実行
func getRequest(_ parameters: [String : String]) -> DataRequest
{
return Alamofire.request(URL(string:"https://ja.wikipedia.org/w/api.php?format=json&action=query&list=search")!,
method: .get, parameters: parameters, encoding: URLEncoding.default, headers: nil)
}
}
WikipediaAPIClientクラス自体は、Alamofireのrequest(_:method:parameters:encoding:headers:)メソッドをWikipedia API向けに呼び出すgetRequest(_ parameters:)メソッドとして実装しているだけです。作成したクラスを利用して、WikipediaSearchAPIModelクラスで検索結果をObservableな[Result]として得る処理を作成します。
class WikipediaSearchAPIModel {
let client = WikipediaAPIClient()
// wikipedia 検索実体 Wikipedia APIでの検索結果をObservableで扱えるようにするためのメソッド
private func searchWikipedia(_ parameters:[String:String]) -> Observable<[Result]> {
// [Result]型のObservableオブジェクトを生成
return Observable<[Result]>.create { (observer) -> Disposable in
// Wikipedia APIへHTTPリクエストを送信
let request = self.client.getRequest(parameters).responseJSON{ response in // -------(1)
// 結果にエラーがあればonErrorに渡して処理を終える
if let error = response.error { // -------(2)
observer.onError(error)
}
// 結果をパースして[Result]に変換
let results = self.parseJson(response.result.value as? [String: Any] ?? [:]) // -------(3)
// onNextに渡す
observer.onNext(results)
// 完了
observer.onCompleted()
}
return Disposables.create { request.cancel() }
}
}
// JSON解析メソッド *前回のサンプルと同様
private func parseJson(_ json: Any) -> [Result] {
guard let items = json as? [String: Any] else { return [] }
var results = [Result]()
// JSONの階層を追って検索結果を配列で返す
if let queryItems = items["query"] as? [String:Any] {
if let searchItems = queryItems["search"] as? [[String: Any]] {
searchItems.forEach{
guard let title = $0["title"] as? String,
let pageid = $0["pageid"] as? Int else {
return
}
results.append(Result(title: title, pageid: pageid))
}
}
}
return results
}
}
連載第2回で説明したように、ObservableクラスのcreateメソッドでResult構造体の配列を監視するObservableオブジェクトを生成して返します。createメソッドの中で、前節で作成したWikipediaAPIClientクラスのgetRequest(_ parameters:)メソッドを呼び出します。getRequest(_ parameters:)メソッドの戻り値であるDataRequestオブジェクトのresponseJSONメソッドで、戻り値であるDataResponseをオブジェクトが得られます(1)。得られたDataResponseオブジェクトから次のプロパティでJSONとエラー情報を参照します。
| プロパティ名 | 値 |
|---|---|
| result.value | JSONを変換したDictionaryオブジェクト |
| error | Errorオブジェクト |
DataResponseオブジェクトの他のプロパティや詳細な仕様については、Alamofireのドキュメントを参照してください。
Wikipedia APIでの検索を行った際にエラーがあれば、onErrorのクロージャにエラー情報を渡します(2)。Wikipedia APIから得られるJSONは、[String: Any]型のDictionaryオブジェクトとして得られます。このDictionaryオブジェクトをparseJsonメソッドでResult構造体の配列に変換し、onNextのクロージャに渡します(3)。Wikipedia APIからの戻り値を[String: Any]型であることを明記してparseJsonメソッドに渡します。
その際に、Wikipeaid APIからの戻り値が[String: Any]でない場合も考慮してオプショナルで変換し、値がない場合は空のDictionaryオブジェクトを渡すようにしています。parseJsonメソッド自体は連載第3回のサンプルと同じなのでここでの解説は割愛します。
最後にonCompletedで終了します。これで、Wikipedia API検索をRxSwiftのObservableとして扱うことができます。
最初に戻って見方を変えると、メソッドの戻り値はObservable<[Result]>型の変数ですが、これはonNextに[Result]型の変数を渡すことと同じ意味です。今回のサンプルのように非同期で行う処理でObservableオブジェクトを扱う時には、途中で何を渡すべきか忘れてしまうこともあるので注意してください。
WIkipedia APIへのHTTPリクエストを行った後の処理は、ViewModelで実装します。
ViewModelの作成
ViewModel内でRxSwiftのObserveパターンを利用します。具体的には、Wikipedia API検索時にObserverを定義して、onNext/onErroron/Completeの各イベントを実行するように処理を記述します。
最初にクラスの冒頭で、Viewとバインドするプロパティ/モデルオブジェクト/メモリを解放するDisposeBagオブジェクトを定義します。
class WikipediaSearchAPIViewModel {
var searchWord = Variable<String>("")
var items = Variable<[Result]>([])
private let model = WikipediaSearchAPIModel()
private var disposeBag = DisposeBag()
# 略
searchWordプロパティがViewの検索バーにある入力欄の値、itemsプロパティがWikipedia APIの検索結果としてViewのテーブルのデータとバインドします。View-ViewModelのやり取りは基本的にデータバインドで行いますので、View側に相当するプロパティを忘れずに作成してください。
次にsearchWordプロパティの値からWikipedia APIの検索を実行して、検索結果をitemsプロパティにバインドする処理を記述します。
init() {
// searchWordからWikipedia APIの検索結果を得てitemsにbind
searchWord.asObservable()
.filter { $0.count >= 3 } // -------(1)
.debounce(0.5, scheduler: MainScheduler.instance)
.flatMapLatest { [unowned self] str in
return self.model.searchWikipedia(["srsearch": str]) // -------(2)
}
.bind(to: self.items)
.disposed(by: self.disposeBag)
}
検索キーワードが3文字以上の場合に、処理を実行します(1)。Wikipedia API検索を呼びすぎないように、debounceオペレーターで0.5秒間隔で新たなイベントを制御します。その後に、モデルクラスで作成したWikipedia API検索を実行して(2)、結果として得られるObservable<[Result]>型の変数をitemsプロパティにバインドします。ViewModelを作成する時に最初に作成したプロパティの要件を満たすことができました。
