LoginSignup
23
25

More than 5 years have passed since last update.

RxSwiftでGitHubのリポジトリを検索するサンプル

Posted at

RxSwiftでMVVMパターンのアプリを作ろうとしましたが、
実際に動いていて、参考にできるサンプルがあまりなかったので、
手探りで作ったサンプルを残しておきます。

ソースコードはこちらにあります。
akaimo/RxSwift-GitHubRepository-Search

環境

  • Swift 2.2
  • Xcode 7.3

RxSwiftのセットアップ

投稿時の最新バージョンである、2.3.1で動作確認をしております。
RxSwiftはアップデートが頻繁にあるため、これ以降のバージョンだと動かないおそれがあります。
動かしてみるときは、carthage bootstrap --platform iOSでインストールしてください。

サンプルアプリ

このサンプルはテキストフィールドに入力されたテキストで、GitHubからリポジトリを検索するだけの単純なものです。

機能1: バリデーション

ただ検索するだけではつまらないと思ったので、簡単な文字数制限をつけてみました。
文字数が足りないときはエラーメッセージが表示され、検索ボタンが押せなくなります。

バリデーションで制御されるのはこの3つです。

@IBOutlet weak var searchButton: UIButton!
@IBOutlet weak var searchTextField: UITextField!
@IBOutlet weak var validMessage: UILabel!

基本的に処理はviewModelでやっていきたいため、テキストとタップとリターンを送ります。

SearchViewController.swift
        viewModel = SearchViewModel(
            search: searchTextField.rx_text.asObservable(),
            buttonTap: searchButton.rx_tap.asObservable(),
            keyboardReturn: searchTextField.rx_controlEvent(.EditingDidEndOnExit).asObservable()
        )

viewModelでバリデーションの処理をします。

SerchViewModel.swift
        let minimumSize = 3
        validation = search
            .map { $0.characters.count >= minimumSize }
            .shareReplay(1)

バリデーション結果をviewとバインディングすれば完成です。

SearchViewController.swift
        viewModel.validation
            .bindTo(validMessage.rx_hidden)
            .addDisposableTo(disposeBag)

        viewModel.validation
            .bindTo(searchButton.rx_enabled)
            .addDisposableTo(disposeBag)

        searchTextField.rx_text
            .bindTo(viewModel.searchText)
            .addDisposableTo(disposeBag)

ついでに検索で使うテキストもviewModelとバインディングしておきます。
以上でバリデーションは完成です。

機能2: リポジトリ検索

今回のメイン機能です。
APIKitとHimotokiを使ってGitHubのAPIをたたきます。

まず、APIKitをRxSwiftで使えるようにします。

Session+Rx.swift
import RxSwift
import APIKit

extension Session {
    static func rx_response<T: RequestType>(request: T) -> Observable<T.Response> {
        return Observable.create { observer in
            let task = sendRequest(request) { result in
                switch result {
                case .Success(let response):
                    observer.on(.Next(response))
                    observer.on(.Completed)

                case .Failure(let error):
                    observer.onError(error)
                }
            }

            return AnonymousDisposable {
                task?.cancel()
            }
        }
    }
}

次に、検索ボタンのタップ、キーボードのリターン、どちらかのアクションをトリガーにAPIをたたきます。

SearchViewModel.swift
        request = Observable
            .of(buttonTap, keyboardReturn)
            .merge()
            .withLatestFrom(searchText.asObservable())
            .filter { $0.characters.count >= minimumSize }
            .map { SearchRepositoriesRequest(query: $0) }
            .shareReplay(1)

        response = request
            .flatMap { request in
                return Session
                    .rx_response(request)
                    .doOnError { PublishSubject<ErrorType>().onNext($0) }
                    .catchError { _ in Observable.empty() }
            }
            .shareReplay(1)

この部分にはAPIKitが関わってくるので、よくわからない場合はAPIKitのドキュメントを読んでみることをオススメします。

最後にレスポンス結果をviewModelに格納すれば完了です。

SearchViewModel.swift
        response
            .map { $0.repository }
            .bindTo(repositories)
            .addDisposableTo(disposeBag)

        repositories
            .asObservable()
            .subscribeNext { print($0) }
            .addDisposableTo(disposeBag)

このままだと検索結果がみれないので、まずは標準出力にだしておきます。

通信中の処理と画面遷移

もう少しまともなサンプルにするために、検索結果を次の画面に表示させます。
ボタンを押してから画面遷移までに通信のタイムラグがあるので、インジケータを表示させます。

さきほどのrequestresponseに反応して出し入れします。

SearchViewController.swift
        viewModel.request
            .subscribeNext { [weak self] _ in
                MBProgressHUD.showHUDAddedTo(self?.view, animated: true)
            }
            .addDisposableTo(disposeBag)

        viewModel.response
            .subscribeNext { [weak self] response in
                MBProgressHUD.hideHUDForView(self?.view, animated: true)
                self?.view.endEditing(true)

                let vc = self?.storyboard?.instantiateViewControllerWithIdentifier("ResultViewController") as! ResultViewController
                vc.title = self?.searchTextField.text
                vc.viewModel.repository.value = response.repository
                self?.navigationController?.pushViewController(vc, animated: true)
            }
            .addDisposableTo(disposeBag)

インジケータの消滅と同時に次ぎの画面に遷移させ、tableViewに表示させたら今回のサンプルは完成です。

ResultViewController.swift
        viewModel.repository
            .asObservable()
            .bindTo(tableView.rx_itemsWithCellIdentifier("Cell")) {  _, repository, cell in
                cell.textLabel?.text = repository.fullName
            }
            .addDisposableTo(disposeBag)

まとめ

私がRxSwiftへの入門に大変苦労しているため、次に学ぶ方の資料となればと思い書きました。
Rxにまだなれていないので、おかしなところがあると思いますが、優しく教えてもらえると幸いです。

参考

23
25
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
23
25