RxSwiftでの実装練習の記録ノート(前編:Observerパターンの例とUITableViewの例)

  • 59
    いいね
  • 0
    コメント

1. はじめに

昨年からなかなか着手ができなかった部分でもあるRxSwiftに今年こそは実際にサンプルを作って、最初の触りの部分や感覚を掴んで見たいという思いから、自分なりの取り組みではありますがRxSwiftに関するサンプルを実装(写経)をした上で、できる限り自分の言葉でのドキュメンテーションと実装時に参考にした資料等をまとめることができればと思い、まとめた次第です。

2. 本サンプルに関して

今回のサンプルに関しては【Warming Up】〜【Chapter3】の現在全4パターンありますが、この記事内では比較的実装コード量が少なくかつ通信が伴わないものに関しての解説を行なっていく形になります。

Githubのリポジトリ:

サンプルのキャプチャ画像:

sample_capture.jpg

今回解説を行うサンプル:

  • 【Warming Up】テキストフィールドやボタンコレクションで挨拶文を作るプラクティス
  • 【Chapter1】ラーメンの一覧をRxDataSourcesを利用してUITableViewに一覧表示をするプラクティス

環境やバージョンについて:

  • Xcode8.2
  • Swift3.0.2
  • MacOS Sierra (Ver10.12.2)

使用ライブラリ:

本サンプルではRxSwiftとRxCocoaを使用していますが、【Chapter1】のサンプルではUITableViewないしはUICollectionViewのDatasource部分の処理をRxSwiftの書き方に置き換えることができる下記のライブラリを使用しています。

今回紹介する2つのサンプルを作成するにあたり必要なライブラリとPodfileの書き方については下記の通りになります。

target 'RxSwiftPracticeNote' do
  use_frameworks!
  # RxSwift使用時に必要なライブラリ
  pod 'RxSwift'
  pod 'RxCocoa'
  # TableViewやCollectionViewのDataSourceをRxSwiftで扱うのに必要なライブラリ
  pod 'RxDataSources'
  ・・・(省略)・・・
end

※1. 実際にお手元で動かす場合には試して見たいサンプルのStoryBoard上のInitialViewControllerの矢印の位置を変更して各サンプルの動きを見て頂ければと思います。

sample_project.png

※2. このサンプルに関しては適宜内容やサンプルケースの追加や修正を行う場合もあります。

※3. 今回のリポジトリにある下記のサンプルに関しては後編にて解説致します。

  • 【Chapter2】GithubのAPIを利用してuser名検索してリポジトリ一覧をUITableViewに表示をするプラクティス
  • 【Chapter3】FoursquareAPIを利用して検索した場所を表示するプラクティス

3. 【Warming Up】テキストフィールドやボタンコレクションで挨拶文を作るプラクティス

私自身もRxSwiftの実装に関しては慣れているわけではありませんでしたので、動画サイトでの解説と紹介サンプルを元に実装(写経)を行った上でサンプル内のコメントの中で、調べた知識や実装の方法や参考資料をまとめていくようなスタイルで取り組んでみました。
(後半で紹介するサンプルに関しても同様になっています)

まずはRxSwiftの中でもObserverパターンを用いた実装が、比較的掴みやすいと感じたテキストフィールドとボタンを用いて挨拶文を作成するサンプルをピックアップしました。

★3-1. 本サンプルで使用するUIパーツの準備に関する部分

まずは下記のように各々のUIパーツを従来通りに配置をしていきます。部品の内訳としては、

  • greetingLabel:挨拶文の冒頭と名前を表示するラベル
  • nameTextField:挨拶文の名前を入力するテキストフィールド
  • freeTextField:挨拶文の冒頭部分を入力するテキストフィールド
  • stateSegmentedControl:挨拶文の冒頭を定型文か自由入力にするかを選択するセグメントコントロール(状態の内訳はenumで管理)
  • greetingButtons:挨拶文の冒頭につく定型文を選ぶボタン群(OutletCollectionを使用)

という形になります。セグメントコントロールが切り替わるとボタン群ないしは、挨拶文の冒頭につく定型文を選ぶボタン群の中で最後に選択されたものを格納する変数を用意しておきます。この部分に関しては、値の変化を観測対象にしたいので、Variable("初期値")という形にして初期化しています。

ViewController.swift
//観測対象のオブジェクトの一括解放用
let disposeBag = DisposeBag()

//SegmentedControlに対応する値の定義
enum State: Int {
    case useButtons
    case useTextField
}

//UIパーツの配置
@IBOutlet var greetingLabel: UILabel!
@IBOutlet weak var stateSegmentedControl: UISegmentedControl!
@IBOutlet weak var freeTextField: UITextField!
@IBOutlet weak var nameTextField: UITextField!

//初期化時の初期値の設定
let lastSelectedGreeting: Variable<String> = Variable("こんにちは")

//(注意)ここはOutletCollectionで紐づける
//(参考)アウトレットコレクションを使う
//http://develop.calmscape.net/dev/220/
@IBOutlet var greetingButtons: [UIButton]!

定型文のボタンに関しては、まとめて取り扱いたかったので今回はアウトレットコレクションで紐付けを行い中の処理に関してはRxSwitで記載する形にしています。

★3-2. 各々の部分でObsevableで役割と処理のポイントに関して

viewDidLoad内の処理ではまずは、Observable(値の変化を観測の対象)とするために、下記の図解のような形で記載を行なっていきます。サンプルの中でそれぞれのUIパーツで受け取れる値の変化を観測対象にする際には、

let 定数名 = Outlet接続をしたUIパーツ.rx.text.asObservable()

という形で記述をしていきます。下記は名前を入力するUITextFieldを.asObservable()メソッドを用い値の変化を観測対象とする場合の処理の記載例になります。

observable_image1.jpg

UIパーツから受け取ることができる値の変化を観測対象とするとともに、UIパーツから受け取った値を元に加工した値に関しても同様に、観測対象としていきます。その中で下記のようなObservableで生成されるメソッドを用いてそれぞれの観測対象にした定数に対して適用する形になります。

  • combineLatest: 受け取った値の直近の最新の値同士の結合
  • map: 受け取った値を別の要素への変換
  • subscribe: イベント発生時にイベントの状態に応じて処理を行う

また今回のサンプル内では、上記のメソッドを利用して各種の受け取った値の処理内容に関してはクロージャー内に記載する形になっています。

最終的にUI要素に表示する部分に関してはbindTo:メソッドを用いて受け取った値とUIパーツへの関連付けを行い、処理が終わって観測対象から外したい場合にはaddDisposableTo:メソッドをお用いて観測対象から除外する処理を追加します。

全体的な処理の流れに関してのおおまかなイメージをまとめると概要は下記のような形になります。

observable_image2.jpg

これまでの解説を踏まえてviewDidLoad内で入力した値及び選択した値の受け取って挨拶文を表示するロジックをまとめると下記のような形になります。

ViewController.swift
//観測対象のオブジェクトの一括解放用
let disposeBag = DisposeBag()

・・・(省略)・・・

override func viewDidLoad() {
    super.viewDidLoad()

    //「お名前:」の入力フィールドにおいて、テキスト入力のイベントを観測対象にする
    let nameObservable: Observable<String?> = nameTextField.rx.text.asObservable()

    //「自由入力:」の入力フィールドにおいて、テキスト入力のイベントを観測対象にする
    let freeObservable: Observable<String?> = freeTextField.rx.text.asObservable()

    //(combineLatest)「お名前:」と「自由入力:」それぞれの直近の最新値同士を結合する
    let freewordWithNameObservable: Observable<String?> = Observable.combineLatest(
        nameObservable,
        freeObservable
    ) { (string1: String?, string2: String?) in
        return string1! + string2!
    }

    //(bindTo)イベントのプロパティ接続をする ※bindToの引数内に表示対象のUIパーツを設定
    //(DisposeBag)購読[監視?]状態からの解放を行う
    freewordWithNameObservable.bindTo(greetingLabel.rx.text).addDisposableTo(disposeBag)

    //セグメントコントロールにおいて、値変化のイベントを観測対象にする
    let segmentedControlObservable: Observable<Int> = stateSegmentedControl.rx.value.asObservable()

    //セグメントコントロールの値変化を検知して、その状態に対応するenumの値を返す
    //(map)別の要素に変換する ※IntからStateへ変換
    let stateObservable: Observable<State> = segmentedControlObservable.map {
        (selectedIndex: Int) -> State in
        return State(rawValue: selectedIndex)!
    }

    //enumの値変化を検知して、テキストフィールドが編集を受け付ける状態かを返す
    //(map)別の要素に変換する ※StateからBoolへ変換
    let greetingTextFieldEnabledObservable: Observable<Bool> = stateObservable.map {
        (state: State) -> Bool in
        return state == .useTextField
    }

    //(bindTo)イベントのプロパティ接続をする ※bindToの引数内に表示対象のUIパーツを設定
    //(DisposeBag)観測状態からの解放を行う
    greetingTextFieldEnabledObservable.bindTo(freeTextField.rx.isEnabled).addDisposableTo(disposeBag)

    //テキストフィールドが編集を受け付ける状態かを検知して、ボタン部分が選択可能かを返す
    //(map)別の要素に変換する ※BoolからBoolへ変換
    let buttonsEnabledObservable: Observable<Bool> = greetingTextFieldEnabledObservable.map {
        (greetingEnabled: Bool) -> Bool in
        return !greetingEnabled
    }

    //アウトレットコレクションで接続したボタンに関する処理
    greetingButtons.forEach { button in

        //(bindTo)イベントのプロパティ接続をする ※bindToの引数内に表示対象のUIパーツを設定
        //(DisposeBag)観測状態からの解放を行う
        buttonsEnabledObservable.bindTo(button.rx.isEnabled).addDisposableTo(disposeBag)

        //メンバ変数:lastSelectedGreetingにボタンのタイトル名を引き渡す
        //(subscribe)イベントが発生した場合にイベントのステータスに応じての処理を行う
        button.rx.tap.subscribe(onNext: { (nothing: Void) in
            self.lastSelectedGreeting.value = button.currentTitle!
        }).addDisposableTo(disposeBag)
    }

    //挨拶の表示ラベルにおいて、テキスト表示のイベントを監視対象にする
    let predefinedGreetingObservable: Observable<String> = lastSelectedGreeting.asObservable()

    //最終的な挨拶文章のイベント
    //(combineLatest)現在入力ないしは選択がされている項目を全て結合する
    let finalGreetingObservable: Observable<String> = Observable.combineLatest(stateObservable, freeObservable, predefinedGreetingObservable, nameObservable) { (state: State, freeword: String?, predefinedGreeting: String, name: String?) -> String in

        switch state {
            case .useTextField: return freeword! + name!
            case .useButtons: return predefinedGreeting + name!
        }
    }

    //最終的な挨拶文章のイベント
    //(bindTo)イベントのプロパティ接続をする ※最終的な挨拶文章を表示する
    //(DisposeBag)購読[監視?]状態からの解放を行う
    finalGreetingObservable.bindTo(greetingLabel.rx.text).addDisposableTo(disposeBag)
}

このようにUITextFieldの編集イベントに合わせて挨拶文の表示ラベルの状態を変更させる処理や、セグメントコントロールの値に応じてテキストフィールドの編集可能状態が変化する場合では、要素が少ない場合ではUITextFieldDelegete@IBActionの処理だけでも実現が可能です。

しかしながら要素が多い場合や他の要素の状態に応じて状態が変化するようなケースでは、予めObserveパターンを活用してそれぞれの要素を観測対象に登録しておき、その状態に応じた変化に対応できるような形にする実装が実現しやすい点はRxSwiftのメリットだと思います。

特に入力値の状態に応じて表示や状態変化を伴うサンプルは実装を掴むを理解する上で良いプラクティスになると感じました。

4. 実装サンプルで参考にした動画リンク GreetingGenerator with RxSwift (iOS 8, Swift 3):

5. Observerパターン実装の際に参考にした記事:

このサンプルの一番ポイントとなる部分は、Observableを生成した際に使用ができるメソッドを活用して、各UIパーツの状態変化を観測して変化に応じて受け取れる値や表示の更新を行う部分になります。上記の参考動画と合わせてそれぞれのObservableで提供されるメソッドの機能や実装例を追う上では下記の記事が参考になりました。

4. 【Chapter1】ラーメンの一覧をRxDataSourcesを利用してUITableViewに一覧表示をするプラクティス

iOSアプリを作成する上で活用する機会の非常に多いUITableViewやUICollectionViewへのデータ一覧表示に関してもRxSwiftの書き方に合わせて処理を記載する例に関してもまとめてみました。

今回のサンプルは前述したRxDataSourceを活用する形になり、記述量がどうしても多くなりがちなUITableViewやUICollectionViewのDataSource部分をRxDataSourcesでまとめてソースの記載を少なくすると共に、表示するデータの定義や処理の流れに関しても「View層 - Presenter層 - Model層」という形にして役割に応じてファイルを分割して管理をするようにしています。

※写真素材に関しては「足成」のフリー素材を利用しています。

★4-1. サンプルの図解と全体的な処理ポイントに関して

このサンプルに関する図解とそれぞれの使用しているファイル及び処理の概要は下記にまとめました。

mvp_pattern.jpg

「View層 - Presenter層 - Model層」での処理のポイントとなる部分でRxDataSourcesのメソッドを活用したつくりにしています。このサンプルは規模の小さなものなので、このように分割するメリットはあまり感じないかもしれませんが、取得したデータが複雑で表示する前に表示整形処理が必要になる場合には、このような形にしておくと見通しが良くなるかと思います。

※私自身もPHPのフレームワーク(FuelPHP等)で同様ないしは非常に似た形のものを実務で経験したことがありますが、規模が大きくなるとより効果があるなと感じることが多かったです。

★4-2. 各々のファイルの役割と処理のポイントに関して

次にそれぞれのファイルに関しての役割と実装ポイントに関して見ていければと思います。このサンプルに関しては後述する動画で紹介されていたサンプルを元に表示等のカスタマイズや修正を加えてアレンジをしたものになります。

※ 動画自体は英語で公開されているものになりますが、ライブコーディングを収録したものなので英語に苦手意識がある方でもキャッチアップはしやすいかと思います。

1. Model層(Ramen.swift)部分の実装に関して:

この部分は取得ないしは設定をするデータに関する定義が記載されています。今回は紹介してはいませんが、API等から取得したデータに対する定義やマッピングに関しての処理もModel側にまとめておくと、どのようなデータが取得できるかが把握しやすくなります。

Ramen.swift
import UIKit
import RxDataSources

//ラーメンデータ定義用の構造体(Model層)
struct Ramen {

    //取得データに関する定義
    let name: String
    let taste: String
    let imageId: String
    var image: UIImage?

    //取得データのイニシャライザ
    init(name: String, taste: String, imageId: String) {
        self.name = name
        self.taste = taste
        self.imageId = imageId
        image = UIImage(named: imageId)
    }
}

//既存の独自型(RxDataSourcesで定義されているIdentifiableType型)を拡張する
extension Ramen: IdentifiableType {
    typealias Identity = String
    var identity: Identity { return imageId }
}

またRxDataSourcesにはIdentifiableTypeがプロトコルとして定義されているので、モデルにおけるIdentifiableTypeを設定することを忘れないように注意してください。この部分では一意のIDとなるようなものを設定する形になります。

extension Ramen: IdentifiableType { ... }の部分に関しては下記のようにプロトコルで定義されているため、RxDataSourcesでデータ定義を行う場合には必ず実装しなければいけない部分になります。

2. Presenter層(RamenPresenter.swift)部分の実装に関して:

この部分は前述のModel層で定義したデータの形式を踏まえた上で、UITableViewのDataSourceへ引き渡すための実データを作成していく部分になります。

表示するためのデータはRamen.swift内での定義に則った形式にはなりますが、加えて今回はラーメンの種類によってのセクション付きで表示したいので、RxDataSources内にあるSectionModelメソッドを用いて整えていく形になります。

RamenPresenter.swift
SectionModel(model: "セクション名", items: [
    //初期化したモデル(Modelファイルで定義した型で値を設定する)
    Ramen(name: "○○○", taste: "△△△", imageId: "xxx")
 ])

セクション付きのデータを設定する実装に関する部分に関しては下記の記事が参考になりました。

そしてこの部分で設定されたデータを観測対象にするために、ある特定の値をを元にObservableを生成するjustメソッドを利用します。下記がPresenter層のクラスを全体になります。

RamenPresenter.swift
//表示データ形式定義に関するPresenterクラス
class RamenPresenter {

    //表示用のデータの具体的な設定をする
    let ramens = Observable.just([

        //引数(model: セクション名, items: [モデル構造体で定義する値の配列])
        SectionModel(model: "醤油", items: [
            Ramen(name: "豚骨醤油ラーメン",taste: "濃いめ", imageId: "sample005"),
            Ramen(name: "喜多方ラーメン", taste: "あっさり", imageId: "sample009"),
            Ramen(name: "チャーシューメン", taste: "あっさり", imageId: "sample010")
        ]),

        SectionModel(model: "塩味", items: [
            Ramen(name: "野菜たっぷりタンメン", taste: "あっさり", imageId: "sample007")
        ]),

        SectionModel(model: "味噌", items: [
            Ramen(name: "8番ラーメン味噌味", taste: "ふつう", imageId: "sample001"),
            Ramen(name: "もやしそば味噌味", taste: "濃いめ", imageId: "sample008")
        ]),

        SectionModel(model: "その他", items: [
            Ramen(name: "台湾風まぜそば", taste: "濃いめ", imageId: "sample002"),
            Ramen(name: "長崎ちゃんぽん", taste: "ふつう", imageId: "sample003"),
            Ramen(name: "酸辣湯麺", taste: "ふつう", imageId: "sample004"),
            Ramen(name: "トマトと野菜のラーメン", taste: "あっさり", imageId: "sample006")
        ])
    ])
}

3. View層(RamenListController.swift)部分の実装に関して:

View層の部分もRxDataSourcesに合わせた実装にしていきます。「Presenter層にあたるクラスから表示するデータの取得」「DataSourceの生成にRxTableViewSectionedReloadDataSourceメソッドを用いる」という2点を準備する必要があります。

RamenListController.swift
//Presenter層から表示するラーメンデータの取得
let ramensData = RamenPresenter()

//データソースの定義
let dataSource = RxTableViewSectionedReloadDataSource<SectionModel<String, Ramen>>()

次に上記で設定したデータの変数を元に、セルの生成と値の表示を行います。概要としては、

  • configureCell内のクロージャーでセル内に配置しているUI要素と受け取るデータの関連付けを行う
  • titleForHeaderInSection内のクロージャーでセクションの設定を行う
  • bindToメソッドでPresenter(整形したデータ)をセルのDataSourceに表示する

という形で記述する流れになります。

今回のRxSwiftを用いた書き方に置き換えるとUITableViewのDataSource部分をひとまとめにすることができ、viewDidLoad内を完結にまとめてしまうことができます。また今回はUITableViewであらかじめ用意されているセルを使用した形になりますが、この部分は独自のセルに対しても適用することができます。下記がView層のクラスを全体になります。

RamenListController.swift
override func viewDidLoad() {
    super.viewDidLoad()

    //データソースを元にしてセルの生成を行う
    dataSource.configureCell = {_, tableView, indexPath, ramens in
        let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
        cell.textLabel?.text = ramens.name
        cell.detailTextLabel?.text = ramens.taste
        cell.imageView?.image = ramens.image
        return cell
    }

    //作成したデータと表示するUITableViewをBindして表示する
    ramensData.ramens.bindTo(ramenTableView.rx.items(dataSource: dataSource)).addDisposableTo(disposeBag)

    //RxSwiftを利用してUITableViewDelegateを適用する
    ramenTableView.rx.setDelegate(self).addDisposableTo(disposeBag)

    //データソースの定義を元にセクションヘッダーを生成する
    dataSource.titleForHeaderInSection = { (ds, section: Int) -> String in
        return ds[section].model
    }
}

//UITableViewCellのセル高さを設定する(UITableViewDelegate)
extension RamenListController: UITableViewDelegate {
    func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
        return CGFloat(65)
    }
}

UITableViewDelegeteに関する設定に関しては、RxReusableのドキュメントを参考に実装しました。またこの設計や実装に関する理解に関しても参考にしたドキュメントに関しても参考資料になったものを下記にピックアップしておきます。

今回のRxDatasourcesを用いたUITableViewへのデータ表示の実装方法に関しては、UITableViewDelegate / UITableViewDataSource部分の記述を少なくできる等のメリットもありますが、ものによってはあまり適さないケースも出てくると思いますので、RxSwiftを活用する場合でも作るプロダクトや処理の局面によって使い分けが必要かもしれませんね。

4. 実装サンプルで参考にした動画リンク:

5. 実装や設計に関する参考:

5. 実装及び概念理解の際に参考にした資料集

私が元々他の言語(主にPHP&Ruby)での開発を経てiOSアプリの開発を始めたという経緯がありましたので他の言語での解説(下記の参考リンクではJavaでの解説)にはなりますが、「Observerパターンとは?」という概念的な部分の理解に関しては下記の資料が参考になりました。

RxSwiftの書き方やサンプルソースを紐解く上では下記の資料を参考にしました。最初はなかなかとっつきにくいと感じてはいましたが、実装(写経)の際に読みながら進めるようにしていくと、徐々にではあるのですが、実装のイメージが掴めるようになるのではと思います。

今回はObservableの生成に関する処理を利用したサンプルになります。またRxSwiftはSwift3になって大幅に変更があったので、今回の実装に関しては適宜置き換えてはいますが、よく使いそうなメソッドがあるかという部分を掴む際に下記の記事を参考にしました。

RxSwiftで実際の業務の中で導入したり、ないしは取り組んでいる方の登壇資料も実際の使われ方のイメージを掴む上で非常に参考になるかと思います。

私自身もまだこの部分に関しては経験はありませんが「こういう設計方針や思想でこのプロダクトは作られているのか」という実例を知る機会はなかなか無いので、もし実際に業務で取り入れるような機会があった場合にも対応できるように私自身もしっかりと予習しておこうと感じた次第です。

6. あとがき

私自身RxSwiftは結構苦手意識が強かったこともあり、今年こそは克服してみたいと感じたので自分なりな学習方法ではありますが、比較的実装の中でもAlamofire等のAPIクライアント経由での通信処理のない部分や平素のサンプル作成等で慣れ親しんだ部分からまずは始めていこうと思い取り組みました。

今回収録したサンプルに関しても、解釈や処理の書き方に関しては「こうするともっと良く書ける」ないしは「このコメントの表現はこのように修正した方が良い」という部分が多々あるかと思いますが、その際にはpull requestやissue等を頂けますと嬉しく思います。

RxSwiftに関しても勉強会への参加の中で、ある程度どのようなものであるかという知識や概念は多少ありましたが、いざ実際に実装(写経)してサンプルの動きを見てみると改めて、RxSwiftを使うと実装が見やすくなるケースがあったりしてなかなか奥深く、実装したい機能によってはすっきりと関係性がわかりやすい書き方ができるケースが意外とありそうと改めて感じました。

※今回の【Warming Up】のように値変化に応じて他のUIパーツの状態変化を伴って起こすようなケースの実装が必要な場合は便利かもしれませんね。

今はそれほど大した実装が私自身できるわけではありませんが、RxSwiftに関しては触れる機会を定期的に持っていければと思っております。

後編はDriverパターンとAPIクライアントとの通信処理・MVVMでの実装に関するサンプルに関してまとめていく予定ですので何卒宜しくお願い致しますm(_ _)m