Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationEventAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
1
Help us understand the problem. What are the problem?

RxSwiftを理解する[初級編]

概要

今回は、RxSwiftを「RxSwift研究読本I 入門編」を用いて学習したので、その内容をまとめていきます。
この本は、とても内容が分かりやすく、間違いなくオススメの本なのでRxSwiftを学習したい方は是非チェックしてみてください!!

RxSwiftの概要

iOSアプリ開発は、ユーザーによる操作やキーボード表示非表示などのイベント処理をリアルタイムで複数検出する必要があります。
これらを処理する場合は、delegateやKey Value observing、クロージャによるコールバックなどで処理をしていました。しかし、これらの方法は処理が複雑に絡み合い保守性が落ちることは言うまでもありません。
これらの問題を解決してくれるのがRxSwiftです。
RxSwiftは、複雑になりがちなイベント処理コードをObservableというクラスを用いて、処置を統一的に扱うことができます。

Observableを理解する

Observableは、イベントを流す役割を持っています。

次の例では、アプリを使うユーザーがインクメンタル検索を行うことを想定しています。
具体的には、ユーザーが「Olympic」という文字列を入力していくとき、その入力は順次「O」や「Ol」などの情報が送られてきます。

では、実際のコードを見ていきましょう

RxSwift
let observable = Observable.of(
"O",
"Ol",
"Oly",
"Olym",
"Olymp",
"Olympi",
"Olympic"
)

 _ = observable
 .subscribe(onNext: {
print("onNext: ", $0)
}, onCompleted: {
print("終了")
})

// 出力結果
onNext: O
onNext: Ol
onNext: Oly
onNext: Olym
onNext: Olymp
onNext: Olympi
onNext: Olympic
終了

また、次の例ではtextFieldに入力された値に関するObservableの例です。
これを実行すると、textFieldに入力された値が変わるたびに、その値がprintされます。
つまり、「値が更新されるたびに、登録された処理を実行」します。

RxSwift
let observable = textField.rx.text.asObservable()
let subscription = observable
    .subscribe(onNext: { string in
        print(string)
    })

onNext, onCompleted, onErrorを理解する

それぞれ、onNextは「値が更新された」、onCompletedは「処理が完了した」、onErrorは「エラーが発生した」というイベントを表します。

onNextは値を渡すことができますが、onCompletedは値を渡すことができません。
(RxSwift/Event.swift)では、以下のような定義で書かれています。

RxSwift
public enum Event<Element> {
    case next(Element)
    case error(Swift.Error)
    case completed
}

これら3つの組み合わせで色々な動作を表すことができます。
但し、ルールとして一度onCompletedやonErrorが発生するとそれ以降onNext等を呼ぶことはできなくなるので注意が必要です。

filterメソッドを理解する

次の例では、先程のコードに「入力が2文字以上の場合のみ処理を行う」という条件を加えてみます。
結果は以下のように「O」の1文字では検索できないようにフィルタリングされています。

RxSwift
let observable = Observable.of(
"O",
"Ol",
"Oly",
"Olym",
"Olymp",
"Olympi",
"Olympic"
)
 _ = observable
 .filter {$0.count >= 2 } // 2文字以上ならtrue
 .subscribe(onNext: {
print("onNext: ", $0)
})

// 実行結果
onNext: Ol
onNext: Oly
onNext: Olym
onNext: Olymp
onNext: Olympi
onNext: Olympic

mapメソッドを理解する

次の例では、先程のコードに全て小文字にするという条件を付け加えてみましょう。
小文字にするという条件を付け加えたい場合は、mapメソッドを使用します。

RxSwift
let observable = Observable.of(
"O",
"Ol",
"Oly",
"Olym",
"Olymp",
"Olympi",
"Olympic"
)
 _ = observable
 .map { $0.lowercased() } // 小文字にする
 .subscribe(onNext: {
print("onNext: ", $0)
})

// 実行結果
onNext: ol
onNext: oly
onNext: olym
onNext: olymp
onNext: olympi
onNext: olympic

時間的概念を理解する

RxSwiftの特徴として、時間の概念が扱えるというメリットがあります。
「時間の概念が扱える」とは何かというと、時間を軸とした制御が可能になるということです。

少しわかりにくいと思いますので、サンプルコードを見ていきましょう。
経過時間という条件を考慮したコードを書くために、RxSwiftではdebounceというオペレータが用意されています。
次の例では、debounceを使いユーザーの入力自体も1秒ごとであると仮定したコードをご紹介します。

説明のため、コードに対する時間についての表現はRxSwiftのテスト用フレームワークであるRxTestを使用します。

RxSwift
import RxTest

let scheduler = TestScheduler(initialClock: 0)
let observable = scheduler.createHotObservable([
Recorded.next(1, "O"),
Recorded.next(2, "Ol"),
Recorded.next(3, "Oly"),
Recorded.next(4, "Olymp"),
Recorded.next(5, "Olympi"),
Recorded.next(6, "Olympic")
])
_ = observable
.debounce(1, scheduler: scheduler)
.subscribe(onNext: {
print("onNext: ", $0)
})
scheduler.start()

// 実行結果
onNext: Olympic

ここから、さらにわかりやすくdebounceの効果が出力される例を示すために、debounceの条件1秒はそのままに、「Ol」から「Oly」の間が2秒ほど時間がかかるようにしてみます。

次の例では、「Ol」から「Oly」の間が2秒ほどかかっていることによって、debounceの1秒よりも長い時間となります。
よって、文字列「Ol」は経過時間条件を抜けるため、イベントとして伝わります。

RxSwift
import RxTest

let scheduler = TestScheduler(initialClock: 0)
let observable = scheduler.createHotObservable([
Recorded.next(1, "O"),
Recorded.next(2, "Ol"), // ここから
Recorded.next(4, "Oly"), // ここまで2秒時間がかかった
Recorded.next(5, "Olymp"),
Recorded.next(6, "Olympi"),
Recorded.next(7, "Olympic")
])
_ = observable
.debounce(1, scheduler: scheduler)
.subscribe(onNext: {
print("onNext: ", $0)
})
scheduler.start()

// 実行結果
onNext: Ol
onNext: Olympic

Subjectを理解する

RxSwiftで頻繁に利用されるPublishSubjectに代表されるSubjectは、ObservableとObserver両方の機能を有していると表現されています。
具体的にSubjectが柔軟にイベントを発火できる仕組みであるということを理解するために、PublishSubjectを使ったサンプルコードを見ていきましょう。

次の例のSubjectの動作からわかる通り、Subjectは購読され、Subjectのインスタンスはイベントを任意のタイミングで発火できることが分かります。

RxSwift
let subject = PublishSubject<String>()
subject.subscribe(onNext: {
print("onNext: ", $0)
})
subject.onNext("A")
subject.onNext("B")
subject.onNext("C")
subject.onNext("D")
subject.onCompleted()

// 出力結果
onNext: A
onNext: B
onNext: C
onNext: D

dispose・DisposeBagを理解する

dispose

RxSwiftは開発者の任意のタイミングでObservableシーケンスを破棄することができます。
いつまでも、subscribe処理を続けてしまうとメモリリークするので、適切なタイミングでObservableシーケンスを破棄することが重要です。
先程のコードにdispose()メソッドを呼び出してみましょう。

下記の例からわかる通りsubscribeメソッドではdisposeされたことを購読でき、disposeされてからは以降のイベントは購読できなくなります。

RxSwift
let subject = PublishSubject<String>()

let subscription = subject
    .subscribe(onNext: {
        print("onNext: ", $0)
    }, onCompleted: {
        print("終了")
    }, onDisposed: {
        print("破棄")
    })

subject.onNext("A")
subject.onNext("B")
subscription.dispose()
subject.onNext("C")
subject.onNext("D")
subject.onCompleted()

// 出力結果
onNext: A
onNext: B
破棄

しかし、一つ一つにdispose()メソッドを書いていると大変ですよね。
そこで、登場するのがDisposeBagです。
DisposeBagを使うと半自動的に開放してくれるので、ViewControllerのメンバには必ずと言ってよいほど定義して、画面に関わるsubscriptionを登録しておくことが多いです。

DisposeBag

disposeBagは、disposeBagオブジェクト自身が開放されるタイミングで、登録されたsubscriptionをdisposeします。
RxSwiftを利用したコード内で以下の定義がされていることが多いみたいです。

次の例では、subscribeしているところでメソッドチェーンで.disposed(by: disposeBag)という定義がされています。

RxSwift
let disposeBag = DisposeBag()

/*
* 省略
*/

func bind() {
   hogehoge.subscribe(onNext: { [weak self] value in
     // 何らかの処理
   }).disposed(by: disposeBag)
}

disposeBagはまとめてObservableシーケンスを処分するための仕組みです。
disposeBagにObservableシーケンスを保持させ、そのdisposeBagを破棄することで、bservableシーケンスをまとめて破棄することができます。

では、実際にdisposeBagをすると、どのような挙動になるかみていきましょう。
次のサンプルコードでは、didTapStartButtonが押されると、現在のcountが出力されて、その3秒後にcreateObservableLoadのevent.onNextが呼ばれています。

RxSwift
class DetailViewController: UIViewController {

    let disposeBag = DisposeBag()
    var count: Int = 0

    @IBAction func didTapStartButton() {
        print("start \(count)")

        createObservableLoad(count: count).subscribe { [weak self] event in
            guard let self = self else { return }
            switch event {
            case .next(let text):
                print("text desu \(text)")
            case .error(let error):
                print("error \(error)")
            case .completed:
                print("completed!")
            }
        }
        count += 1
    }

    func createObservableLoad(count: Int) -> Observable<String?> {
        return Observable.create { event -> Disposable in
            DispatchQueue.main.asyncAfter(deadline: .now() + 3.0) {
                event.onNext("next dayo")
                event.onNext("next dayo2 count \(count)")
            }
            return Disposables.create {
            }
        }
    }
}

出力の結果は以下の通りです。
スクリーンショット 2021-07-29 14.52.14.png

しかし、ここにdisposeBagがないと、循環参照が起こるのでメモリリークを起こしてしまいます。

スクリーンショット 2021-07-29 14.57.28.png

それでは、disposeBagを書き加えてみましょう。

RxSwift
class DetailViewController: UIViewController {

    let disposeBag = DisposeBag()
    var count: Int = 0

    @IBAction func didTapStartButton() {
        print("start \(count)")

        createObservableLoad(count: count).subscribe { [weak self] event in
            guard let self = self else { return }
            switch event {
            case .next(let text):
                print("text desu \(text)")
            case .error(let error):
                print("error \(error)")
            case .completed:
                print("completed!")
            }
        } .disposed(by: disposeBag) // ここに書く
        count += 1
    }

    func createObservableLoad(count: Int) -> Observable<String?> {
        return Observable.create { event -> Disposable in
            DispatchQueue.main.asyncAfter(deadline: .now() + 3.0) {
                event.onNext("next dayo")
                event.onNext("next dayo2 count \(count)")
            }
            return Disposables.create {}
        }
    }
}

.disposed(by: disposeBag)を書くことによって、didTapStartButtonを押してもメモリリークしなくなります。
スクリーンショット 2021-07-29 15.11.30.png

combineLatestを理解する

RxSwiftは様々なオペレータによって成り立っています。
combineLatestは最新の値へと切り替わるごとに動作し、合成される全てのシーケンスの最新の値を使用します。
早速、簡単なサンプルコードを見ていきましょう。
次の例のイベントは2つのPublishSubjectを使い、次々に文字列イベントを作成し、そのシーケンスを合成した結果を出力しています。

RxSwift
let password = PublishSubject<String>()
let repeatedPassword = PublishSubject<String>()

_ = Observable.combineLatest(password, repeatedPassword) { "\($0), \($1)"}
.subscribe(onNext: { print("onNext: ", $0) })

password.onNext("a")
password.onNext("ab")
repeatedPassword.onNext("A")
repeatedPassword.onNext("AB")
repeatedPassword.onNext("ABC")

// 出力結果
onNext: ab, A
onNext: ab, AB
onNext: ab, ABC

repeatedPassword: PublishSubjectの値が変わっても、常にpassword: PublishSubjectの最新の値abのみを使っています。

zipを理解する

Observableのzipオペレータは、「イベントが揃ったら動作する仕組み」として使われることが多いです。
では、早速サンプルコードを見ていきましょう。

次の例では、入力として実行する1,2,3,4の順序とA,B,C,Dのそれぞれの順序が、zipオペレータを使用することによって、出力時に揃っていることが分かります。

RxSwift
let intSubject = PublishSubject<Int>()
let stringSubject = PublishSubject<String>()

_ = Observable.zip(intSubject, stringSubject) {
        "\($0) \($1)"
    }
    .subscribe(onNext: { print($0) })

intSubject.onNext(1)
intSubject.onNext(2)

stringSubject.onNext("A")
stringSubject.onNext("B")
stringSubject.onNext("C")
stringSubject.onNext("D")

intSubject.onNext(3)
intSubject.onNext(4)

// 出力結果
1 A
2 B
3 C
4 D

まとめ

今回は、「RxSwift研究本I 入門編」を読んだので、理解した内容をまとめました。
最初、RxSwiftのコードを見たときは、何をやっているか全く分かりませんでしたが、何回も読んでいく内に少しずつ理解が深まっていくのを感じました。
また、このようにアウトプットをしていくことによって、基本的なところの理解が深まったと思います。ただ、実際の現場でゴリゴリコードが書けるのかと言ったらそうではないので、より実践的なRxSwiftのコードを見たり、RxSwiftを用いたサンプルアプリを作成したりして、より理解を深めていこうと思いました。

参考文献

「RxSwift研究読本I 入門編」
RxSwiftについてようやく理解できてきたのでまとめることにした(1)
 RxSwiftについて分かりやすくまとめられています。是非、チェックしてみてください!

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
1
Help us understand the problem. What are the problem?