はじめに
アプリ開発を進めるにあたっては、Observerパターンの監視対象から値を取得した後の処理の方が重要です。連載第2回のサンプルのように、データストリームから得た値を単発の処理のみで利用するケースは実際のアプリ開発ではまれです。具体的には、「データストリームを加工して次の処理に渡す」「その際に複数の処理を組み合わせる」「長くなりがちな処理を分離して管理しやすくする」などの方法が、開発の現場では必要です。それらの基本的な処理について順を追って説明します。
データストリームを加工するオペレーター
以前、データストリームから得られる値は時間軸をベースとした配列で扱えることを説明しました。得られた値が配列として扱える以上、SwiftのArrayクラスの要素を操作するメソッドなども利用できます。本節では、これらの配列の要素を操作するメソッドを利用してデータストリームを加工したり処理をつなげたりする基本的な方法について説明します。
オペレーターを利用する
RxSwiftでは、配列の要素を操作するメソッドをオペレーターと呼びます。オペレーターは、Arrayクラスのメソッドと基本的に同じですが、ArrayクラスではcompactMapに置き換えられたflatMapが利用できるなど若干の違いがあります。RxSwiftの場合は、オペレーターはデータストリームを制御するという意味で使われます。RxSwiftのドキュメントに沿って主なオペレーターを表にまとめます。
名前 | 概要 |
---|---|
map | データストリームを別のデータストリームに変換 |
filter | 条件に合わないデータストリームを排除 |
merge | 複数のデータストリームを統合 |
flatMap | 前のデータストリームを維持して次のデータストリームを処理 |
flatMapLates | 前のデータストリームをキャンセルして次のデータストリームを処理 |
zip | 複数のデータストリームが存在する場合に全ての処理が終わるまで待って統合 |
ドキュメントでは、「データストリーム」という表現ですが、実際にはデータストリームから得られる値をどう扱うかといったイメージの方が強いです。複数のオペレーターをつないで連続して処理を行うメソッドチェーンも利用できます。filterとmapを使った、オペレーターの簡単な利用例は次の通りです。
class TextFieldViewController: UIViewController { @IBOutlet weak var textField: UITextField! @IBOutlet weak var textLabel: UILabel! let disposeBag = DisposeBag() override func viewDidLoad() { super.viewDidLoad() self.textField.rx.text.orEmpty .filter { $0.count >= 3 } // 制限 .map { "入力した文字数は\($0.count)です"} // 加工 .bind(to: self.textLabel.rx.text) // 結合 .disposed(by: disposeBag) } }
最初に、入力欄が空の場合は処理を行わないorEmptyプロパティを利用して、値が空の場合は以降の処理を進めないようにします。その後にfilterで文字数が3文字以上の要素だけ抽出します。つまり、文字数が3文字未満の場合は以降の処理は行いません。ここまでの条件を通過した後に、mapで値を加工してラベルに結合します。
サンプルを実行すると、上記のように入力文字数が3文字以上の場合に、文字数を表示する動きが確認できます。
オペレーターで処理をつなぐ
データストリームはオペレーターで加工するだけでなく、別のクラスやメソッドに渡して処理を行うことも可能です。処理の結果を次のオペレーターで別の処理に渡して、さらに次の処理を行うなどすれば、短いコードで複数の処理を連続して行うことも可能です。この一連の処理について、サンプルを作成しながら説明します。作成するサンプルは下図の通りです。検索欄に入力された検索キーワードを元にWikipediaで検索を行い、検索結果をテーブルに表示するアプリです。
WikipediaのAPIの詳細に関しては公式ページで確認できます。サンプルでは検索結果をJSONで出力する形式でAPIを利用します。
Storyboard上でUISearchBarとUITableViewを配置してIBOutletでつないだ後に次の処理を記述します。サンプルでは、各オペレーターの役割を見やすくするために間に改行を入れています。
class WikipediaSearchViewController: UIViewController { @IBOutlet weak var searchBar: UISearchBar! @IBOutlet weak var tableView: UITableView! let disposeBag = DisposeBag() override func viewDidLoad() { super.viewDidLoad() self.searchBar.rx.text.orEmpty .filter { $0.count >= 3 } // -------(1) .map { let urlStr = "https://ja.wikipedia.org/w/api.php?format=json &action=query&list=search&srsearch=\($0)" return URL(string:urlStr.addingPercentEncoding(withAllowedCharacters: NSCharacterSet.urlQueryAllowed)!)! } // -------(2) .flatMapLatest { URLSession.shared.rx.json(url: $0) } // -------(3) .map { self.parseJson($0) } // -------(4) .bind(to: tableView.rx.items(cellIdentifier: "Cell")) { index, result, cell in cell.textLabel?.text = result.title cell.detailTextLabel?.text = "https://ja.wikipedia.org/w/index.php?curid=\(result.pageid)" } // -------(5) .disposed(by: disposeBag) } # 略
前回のサンプルと同様に、文字数が3文字以上の条件をfilterオペレーターで指定します(1)。検索欄のテキストからそのままデータストリームを流す処理なので、この時点でインクリメンタルサーチも実装できます。
次にmapオペレーターで、渡されたデータストリームの値からWikipediaのAPIを呼び出すURLを生成します(2)。クエリの引数として全角文字を扱うオプションでURLオブジェクトを生成し、次のオペレーターに渡します。
渡されたURLからURLSessionオブジェクトのRx拡張jsonメソッドでAPIの戻り値のJSONを取得します(3)。検索欄の入力値が変わるたびにAPIを新しく呼び出すという意味でflatMapLatestオペレーターを使用しています。
(3)で得られたJSONをパースします(4)。JSONをパースする処理は、同じビューコントローラー内のparseJsonメソッドです。parseJsonメソッドの内容は次の通りです。
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]] { // -------(4-1) searchItems.forEach{ guard let title = $0["title"] as? String, let pageid = $0["pageid"] as? Int else { return } results.append(Result(title: title, pageid: pageid)) // -------(4-2) } } } return results // -------(4-3) } # 中略 // 検索結果を格納する構造体 struct Result { let title: String let pageid: Int }
Wikipedia API の JSON出力フォーマットは次の通りです。
APIの戻り値はJSONオブジェクトで取得済みなので、JSONのキーである「query」から「search」までの階層を追って検索結果を取得します(4-1)。検索結果を格納するためにResult構造体を定義しておきます。構造体の内容は、JSONのキーと値の型のみです。検索結果を個々にResult構造体に格納し(4-2)、配列の形式で次のオペレーターに渡せるように返却します(4-3)。
(4)の処理でJSONをパースした配列をテーブルの内容に結合し、セルに表示します(5)。この時にクロージャにはインデックス番号、結合する個々のデータ、テーブルセルのオブジェクトが渡されます。結合する個々のデータはJSONの解析結果のResult構造体なので、検索のタイトルをセルのtextLabelに、ページIDはWikipediaのリンクに加工してセルのdetailTextLabelに表示します。
(1)から(5)までの処理の流れをまとめると次の図のようになります。
サンプルを実行すると、次のように入力したキーワードに関連するWikipediaの項目が確認できます。
UISearchBarから流れてきたデータストリームがオペレーターを通して複数の処理を経て加工され、最終的にUITableViewのセルに表示されることが実感できます。