RxMarblesQuiz開発秘話


RxSwift勉強会@Sansan で登壇したのですが、その際にRxMarblesのクイズアプリをデモで紹介しました。コードはこちら (★ please)に公開しているので、ストアに出す必要ないかなって思っていたのですがGWで時間が取れたので、せっかくだしストアに公開することにしました!

この記事ではアプリのコードの紹介と、登壇の裏話(ポエム)を書きたいと思います。発表資料はこちらです。

デモ

このように、Rxのオペレーター適用前と後のMarbleが問題として出ますので、正しいオペレーターを4択で選ぶクイズアプリとなっています。できるだけRxを使ってコードを書きましたので、このアプリで遊ぶだけでなくコードを読むことでも勉強ができてお得なアプリになっています。

Rx使用箇所

では、早速コードの紹介です。

ボタンタップイベント

    @IBOutlet weak var answerButton1: UIButton!
    @IBOutlet weak var answerButton2: UIButton!
    @IBOutlet weak var answerButton3: UIButton!
    @IBOutlet weak var answerButton4: UIButton!

    private var buttons: [UIButton]  {
        return [answerButton1, answerButton2, answerButton3, answerButton4]
    }

    buttons.forEach { button in
        button.rx_tap
            .subscribeNext { [unowned self] in
                if button.titleLabel!.text! == self.currentAnswer.value {
                    self.correctAnswerCount.value += 1
                    correctSoundAudioPlayer.play()
                } else {
                    incorrectSoundAudioPlayer.play()
                }
                self.currentQuestionIndex.value += 1
            }
            .addDisposableTo(disposeBag)
    }

4択クイズなので4つのボタンをStoryboardで作り、タップされた時のイベントを実装する必要がありました。button.titleLabel.textに答えが書いてあるので、今の問題の答え(currentAnswer)と一致しているかどうかで判定をしています。IBActionで普通はタップイベントを定義すると思いますが、rx_tapを使うことでコードを分散させずに書くことができています。

問題の管理

    private let currentQuestionIndex = Variable<Int>(0)
    private let questions = Variable<[Question]>([])
    private let currentQuestion = Variable<Question>(Question(beforeImage: "", afterImage: "", answer: ""))
    private let currentAnswer = Variable<String>("")
    struct Question {
      let beforeImage: String
      let afterImage: String
      let answer: String
    }

    currentQuestionIndex.asDriver()
        .filter { [unowned self] in $0 < self.questions.value.endIndex }
        .driveNext { [unowned self] count in
            let question = self.questions.value[count]
            self.currentQuestion.value = question
            self.currentAnswer.value = question.answer
        }
        .addDisposableTo(disposeBag)

今何問目かはcurrentQuesionIndex: Variableで管理することにしました。currentQuestionIndex.valueに値が代入されるたびに処理が走るので、問題配列questionsから問題を取得し、currentQuestionとcurrentAnswerにそれぞれ代入しています。もしRxSwiftを使わずに書くならば、didSetを使うだけで書けると思います。この記事書いている途中で気づいたのですが、UIの更新をしているわけではないのでDriveを使う必要がなく、subscribeNextで書くほうが良いと思いました…

    private let currentQuestion = Variable<Question>(Question(beforeImage: "", afterImage: "", answer: ""))

    currentQuestion.asDriver()
        .driveNext { [unowned self] question in
            self.beforeImageView.image = UIImage(named: question.beforeImage)
            self.afterImageView.image = UIImage(named: question.afterImage)
        }
        .addDisposableTo(disposeBag)

先ほどのコードであったようにcurrentQuestionIndexが更新されるとcurrentQuestion.valueにquestionが代入されるので、currentQuestionでの処理は上のようになります。今回はふんだんにRxSwiftを使ってコードを書いているだけですので、先ほどのcurrentQuestionIndexのところでcurrentQuestionを取得できているのでそこにまとめて書いてしまっても問題ないと思います。

実装の感想

viewDidLoad()内でほとんどの処理を書くことができたのですが、そこまでする意味があるのかどうかはなんとも言えません。笑 コードが分散していないのであとで見直すときに見やすいですが、ViewModelを導入して問題数の管理とかをするほうが良いとは思いました。このアプリ自体がそこまで複雑な処理をしていないですが、今何問目か、今の問題と答えは何か、など管理する変数が多いので普通に書くと条件分岐などが多くなってややこしくなっていたとは思いますが、Variableを使うことで特に迷うこと無く実装できました。

登壇にあたってのポエム

RxSwiftは業務で使ってはいたのですが、発表するほどの知見があるかと言われると難しい状況でした。また、発表者として申し込んだのは良かったのですが抽選で1人が落ちる状況でした。参加申込者の一覧を見ると、そうそうたるメンバーだったのでキャンセルしたほうが聞きにくる人のためかもしれないって正直思っていました。笑 ですが、勇気を出して登壇をして本当に良かったと思っています。勉強会は発表者として参加したほうが良いという記事を見たことがある人はいるとは思いますが、その通りだと思います。下手な発表をしてはいけないというプレッシャーがあり、めちゃくちゃその分野について調べることになるので自分が一番勉強になります。懇親会も色々話しかけてもらえて楽しかったですし、また何か機会があれば発表をしたいと思います。良かったら誘ってください:)

自分も何か発表をしてみようかなと考えている方がいましたら、是非発表をすることをオススメします。 自分なんかが発表できるだろうか、という不安はあるとは思いますが、このテーマについてもっと勉強したいから発表という手段を利用しているんだ、と思えば良いのです。笑 そのテーマについて詳しくない人にとっては発表者の経験や知見を知れるだけで価値があるので、特に何か発表することはないな、と思う必要はないと思いました。

さいごに

RxSwiftは初めはハードルが高いかもしれませんが、書いてて楽しいので一緒に楽しみましょう:D わからないところがありましたら直接@nakailandに聞いてください。繰り返しになりますが、良かったらアプリのインストールとRxQuizプロジェクトのプルリクと★をお願いします:)