CodeZine(コードジン)

特集ページ一覧

RxSwiftの仕組みを利用して、MVVMモデルを導入しよう

RxSwiftを使った一歩進んだiOSアプリ開発 第4回

  • LINEで送る
  • このエントリーをはてなブックマークに追加
2018/11/14 14:00
目次

MVVMモデルの各クラスを作成する

 前節で設計したサンプルの各クラスを順番に作成していきます。

Modelの作成

 Modelは、Wikipedia APIの検索と検索結果のJSONを解析した結果を格納する役目を果たします。検索結果の格納には、連載第3回のサンプルと同じものを利用します。

[リスト1]Result.swift
// 検索結果を格納する構造体
struct Result {
    let title: String
    let pageid: Int
}

 Jsonの検索結果の1つ1つの項目を格納する役目を果たします。検索を実行する部分は、次項より作成していきます。

HTTPリクエストを行うクラスを作成する

 ViewModelを作成する前に、ViewModelの中から呼び出すWikipedia APIへのHTTPリクエストを先に作成します。Wikipwdia APIだけでなく、他のAPIなどにも拡張できるように先にプロトコルから作成します。

[リスト2]WikipediaAPIClient.swift
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:)メソッドの書式は次の通りです。

[リスト3]request(_:method:parameters:encoding:headers:)メソッドの書式
request(#URLオブジェクト#, method: #HTTPリクエストメソッド#, parameters:#パラメータ#, encoding:#エンコーディング#, headers:#HTTPヘッダ#)

 戻り値はDataRequestオブジェクトです。DataRequestオブジェクトが得られた後の処理は次節で説明します。

 Wikipedia APIへのHTTPリクエストを行うクラスは、プロトコルの部分と合わせてWikipediaAPIClientの名前で次のように作成します。

[リスト4]WikipediaAPIClient.swift
// 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]として得る処理を作成します。

[リスト6]WikipediaSearchAPIModel.swift
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とエラー情報を参照します。

DataResponseオブジェクトの主なプロパティ
プロパティ名
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オブジェクトを定義します。

[リスト5]WikipediaSearchAPIViewModel.swift
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プロパティにバインドする処理を記述します。

[リスト6]WikipediaSearchAPIViewModel.swift
     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を作成する時に最初に作成したプロパティの要件を満たすことができました。


  • LINEで送る
  • このエントリーをはてなブックマークに追加

修正履歴

  • 2018/11/19 12:59 サンプル内の各クラスの役割を見直すという理由から、全体的にサンプルコードと解説を見直しました。編集部

バックナンバー

連載:RxSwiftで一歩進んだiOSアプリ開発

著者プロフィール

  • WINGSプロジェクト 片渕 彼富(カタフチ カノトミ)

    <WINGSプロジェクトについて> 有限会社 WINGSプロジェクトが運営する、テクニカル執筆コミュニティ(代表 山田祥寛)。主にWeb開発分野の書籍/記事執筆、翻訳、講演等を幅広く手がける。2018年11月時点での登録メンバは55名で、現在も執筆メンバを募集中。興味のある方は、どしどし応募頂...

  • 山田 祥寛(ヤマダ ヨシヒロ)

    静岡県榛原町生まれ。一橋大学経済学部卒業後、NECにてシステム企画業務に携わるが、2003年4月に念願かなってフリーライターに転身。Microsoft MVP for ASP/ASP.NET。執筆コミュニティ「WINGSプロジェクト」代表。 主な著書に「入門シリーズ(サーバサイドAjax/XM...

あなたにオススメ

All contents copyright © 2005-2021 Shoeisha Co., Ltd. All rights reserved. ver.1.5