Shoeisha Technology Media

CodeZine(コードジン)

特集ページ一覧

MVVMモデルをもっと便利に使ってみよう(後編)~ViewModelの変数と具体的な処理方法

RxSwiftで一歩進んだiOSアプリ開発 第6回

  • LINEで送る
  • このエントリーをはてなブックマークに追加
2019/01/31 11:00

 連載最終回となる今回は、前回に引き続き、MVVMモデルに対してもう少し踏み込んだ使い方について説明します。前編で説明した、ViewModelの入出力の変数とActionクラスを利用した画面上での処理について、サンプルを作成しながら説明します。本連載では、これからRxSwiftを導入する方を対象読者としている関係上、RxSwift導入の初歩的な手順に関して主な説明を行います。そのため、便宜上クラス/メソッドの使い方が必ずしもRxSwiftの詳細な仕様通りでないこともあります。この点をご了承ください。

目次

はじめに

 前回の続きとして、設計したYahoo!画像検索を利用した画像検索アプリの各クラスを作成します。

Yahoo!画像検索を利用した画像検索アプリ
Yahoo!画像検索を利用した画像検索アプリ

 前回は各変数の定義の仕方まで説明しました。今回はそれらの変数を具体的にどう使うか、Actionクラスとどのようにデータバインドして処理と画面を結びつけるかなどを説明します。

サンプルの各クラスを作成する

 作成するサンプルの各クラスは、MVVMモデルに準じて次のように作成します。

表:サンプルの各クラス
クラス/構造体名 概要
Img 検索結果を格納
ViewController  入力欄、検索ボタン、コレクションビューを表示
ImageItemCell コレクションビューのセル
SearchViewModel 入出力変数の管理/Actionによる処理の制御

サンプルでは、Actionライブラリを利用したView-ViewModel間の入出力の変数の管理を目的としています。MVVMモデル内のModelの役割については、簡略化するためにサンプルでは省いています。前回に引き続き、View-ViewModel間の変数のやり取りにはデータバインドを用い、View以外ではUIKitフレームワークをimportしないようにします。

検索結果の格納

 Yahoo!画像検索では、APIは存在しないので、検索結果のHTMLを解析して検索結果を取得します。検索結果はimgタグに入っているので、imgタグに相当する構造体をモデルとして作成します。

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

 imgタグのsrcの値を格納できるようにしています。

ViewModelの作成

 最初に、入力用のプロトコルを次のように定義します。

[リスト2] SearchViewModel.swift
protocol SearchViewModelInputs {
    var searchWord: Variable<String?> { get }       // 検索キーワード
    var searchTrigger: PublishSubject<Void> { get } // 検索トリガ
}

 画面から入力するものは、検索キーワードと、検索ボタンを押した時に発動される検索トリガの2つです。ボタンのようにUIからユーザーの操作を受けるものがあれば、それに相当するトリガを入力の要素として定義します。次に、出力のプロトコルを次のように定義します。

[リスト3] SearchViewModel.swift
protocol SearchViewModelOutputs {
    var items: Observable<[Img]> { get }                // 検索結果
    var isSearchButtonEnabled: Observable<Bool> { get } // 検索ボタンの押下可否
    var isLoading: Observable<Bool> { get }             // 処理中フラグ
    var error: Observable<ActionError> { get }          // エラー情報
}

 画面へ出力するものは、検索結果と検索ボタンの押下可否のオブジェクトです。サンプルのように画面に入力制限がある場合は、入力の可否を判断できるオブジェクトをViewModelから出力するものとして考えます。そのほか、Actionの処理中とエラーに相当するオブジェクトを定義します。

 入出力のプロトコルを定義した後に、2つのプロトコルにアクセスするプロトコルを定義します。

[リスト4] SearchViewModel.swift
protocol SearchViewModelType {
    var inputs: SearchViewModelInputs { get }
    var outputs: SearchViewModelOutputs { get }
}

 ViewModelのクラスの実態であるSearchViewModelクラスでは、この3つのプロトコルを実装します。

[リスト5] SearchViewModel.swift
class SearchViewModel: SearchViewModelType, SearchViewModelInputs, SearchViewModelOutputs {

    // MARK: - Properties
    var inputs: SearchViewModelInputs { return self }
    var outputs: SearchViewModelOutputs { return self }

    // Input Sources
    let searchWord = Variable<String?>(nil)
    let searchTrigger = PublishSubject<Void>()

    // Output Sources
    let items: Observable<[Img]>
    let isSearchButtonEnabled: Observable<Bool>
    let isLoading: Observable<Bool>
    let error: Observable<ActionError>
# 略

 3つのプロトコルを実装して、各プロパティも記述します。入出力のプロパティは初期化処理の後は変更しないので「let」で定義します。また入力のプロパティは、Viewとバインドした際に初期状態があるので、この時点で空のオブジェクトとしてインスタンスを生成しています。出力のプロパティは、initメソッド内で初期化するので、ここでは定義だけにしています。各プロパティをバインドする前に、Actionクラスでの処理も定義しておきます。

[リスト6] SearchViewModel.swift
class SearchViewModel: SearchViewModelType, SearchViewModelInputs, SearchViewModelOutputs {
# 略
    private let action: Action<String, [Img]>  // ------(1)
    private let disposeBag = DisposeBag()

    init() {
        self.action = Action { keyword in  // ------(2)
            let urlStr =  "https://search.yahoo.co.jp/image/search?n=60&p=\(keyword)"  // ------(3)
            let url = URL(string:urlStr.addingPercentEncoding(withAllowedCharacters: NSCharacterSet.urlQueryAllowed)!)!
            var request = URLRequest(url: url)
            request.addValue("×××××@xxxx.com", forHTTPHeaderField: "User-Agent")

            return URLSession.shared.rx.response(request: request)
                .filter{ $0.response.statusCode == 200 }.map{ $0.data }                                                               // ------(4)
                .map{ try! HTML(html: $0 as Data, encoding: .utf8) }
                .map{ $0.css("img").compactMap { $0["src"] }.filter { $0.hasPrefix("https://msp.c.yimg.jp") } }      // ------(5)
                .map{ $0.compactMap{ Img(src:$0) } }.asObservable()      // ------(6)
# 略

 画像検索の処理は、検索キーワードを文字列で入力して、画像検索の結果をImgの配列で返します。その通りにActionクラスを利用することを宣言します(1)。

 初期化処理の中でActionクラスの処理を定義します。クロージャに渡されるのは、Actionクラスを定義した際の第一引数のString型変数なので、keywordで受け取ります(2)。

 受け取ったキーワードからYahoo!画像検索のURLオブジェクトを全角文字列が使えるように生成します。Yahoo!画像検索をプログラムから利用する場合は、ヘッダのUser-Agentに利用者のメールアドレスを追加するのがルールなので、URLRequestオブジェクトを生成する際にヘッダを付加しています(3)。サンプルを実行する際には、メールアドレスを自分のものに変更してください。

 URLSessionクラスのrx拡張プロパティを利用してYahoo!画像検索へのリクエストを実行します。リクエスト結果のステータスコードが200の場合のみ、データストリームにリクエストで得られた値を渡します(4)。

 リクエストで得られた値はData型です。ここでKannaライブラリを利用してHTMLを解析します。Kannaライブラリを利用する際の初期式は次の通りです。

[構文1]KannaライブラリのHTML解析時の書式
let obj  = try! HTML(html: HTML文書 または Dataオブジェクト, encoding: 文字エンコーディング) 
let tags = obj.css(セレクタ)

 URLSessionクラスなどから得られたDataオブジェクトを、文字エンコーディングの指定によってすぐにHTMLをパースしたオブジェクトに変換できます。Kannaライブラリのメソッドにはthrowsが定義されていますので、tryが必要です。

 パース結果は、cssメソッドでセレクタを利用して絞り込めます。ここではimgタグの名前をセレクタとするとともに、画像検索の結果のサムネイルが「https://msp.c.yimg.jp」のURL名で始まることからこれを条件としてimgタグのsrcのみの配列を作成しています(5)。

 srcの配列をImg構造体に変換してObservableオブジェクトとして返却します(6)。ここで返却した値が、Actionクラスのelementsプロパティで得られます。

 定義したActionクラスを使った処理の流れは次の通りです。

Actionクラスを使った処理の流れのイメージ
Actionクラスを使った処理の流れのイメージ

 クラス内の他のプロパティは、initメソッド内で次のように処理を定義します。

[リスト7] SearchViewModel.swift
    // ボタンの押下可否
    self.isSearchButtonEnabled = self.searchWord.asObservable()
        .filterNil().map { $0.count >= 3 }  // ------(1)
        
    // 検索トリガ
    self.searchTrigger.withLatestFrom(self.searchWord.asObservable())  // ------(2)
        .filterNil().bind(to:self.action.inputs).disposed(by: self.disposeBag)

    // 検索結果
    self.items = self.action.elements

    // 処理中か
    self.isLoading = self.action.executing.startWith(false)  // ------(3)

    // エラー
    self.error = self.action.errors

 データストリームが空になる可能性のあるものについては、RxOptionalライブラリの空の場合は処理を行わないfilterNil()メソッドを利用して処理を止めます。

 ボタンの押下可否は検索文字列が3文字以上で有効になるようにします。検索トリガについては、空のPublishSubjectで定義していますので自分自身は値を持ちません。そこでwithLatestFromオペレーターを使って検索キーワードでデータストリームを作成し、これをActionクラスのinputsプロパティにバインドします(2)。こうすることで、ボタンを押した際にActionクラスの入力変数に検索キーワードの値が渡り、Actionクラスで定義した画像検索の処理が走ります。

 Actionクラスを利用する際には、メソッドでの処理を行うのではなく、inputsプロパティの値の変化で処理が行われます。

 Actionクラスの処理結果/処理中/エラーはそれぞれ各プロパティで値を参照できるようにします。エラーに関してのみ、何もない場合はfalseからデータストリームが始まるようにstartWithで初期値falseを指定しています(3)。


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

著者プロフィール

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

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

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

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

バックナンバー

連載:RxSwiftで一歩進んだiOSアプリ開発
All contents copyright © 2005-2019 Shoeisha Co., Ltd. All rights reserved. ver.1.5