[iOS] RxSwiftのmapとflatMapの違い

2019/03/2

こんにちは。きんくまです。

今回はRxSwiftのmapとflatMapの違いについていまいちわかってなかったので、まとめです。

Rxは観測するObserverと、観測されるObservableという大事な概念があります。
で、それと同じくらい大事なのが、流れを作るSequenceと、その中を流れるElementということだと思いました。
これをイメージしておくと、とたんにわかりやすくなりました。

Observableというのは、SequenceでElementを流すやつ。みたいな。

PublishSubject/Single/Observable などは Sequence です。どんなイベントを発行するか決定します。逆にいうと、どのイベントを発行しないかというのもポイントになってきます。

そのイベントにともなって、Elementを流したりします。(completedのときなどelementを流さないイベントもあるのでこんな表現)

通常

こんな感じのasyncのSingleがあったとします。
0.5秒後にStringを返します。

    func loadText() -> Single<String> {
        let single = Single<String>.create { event -> Disposable in
            DispatchQueue.global(qos: .utility).asyncAfter(deadline: .now() + 0.5) {
                event(.success("Hello"))
            }
            return Disposables.create()
        }
        return single
    }

普通に使います。

    func normalExample() {
        loadText().subscribe { event in
            switch event {
            case .success(let text):
                print("success: \(text)") //Hello
            case .error(let error):
                print("error \(error)")
            }
        }.disposed(by: disposeBag)
    }

map

mapを使ってみます。

    func mapExample() {
        loadText().map { $0 + " World" }
            .subscribe { event in
                switch event {
                case .success(let text):
                    // text は String型
                    print("success: \(text)") // Hello World
                case .error(let error):
                    print("error \(error)")
                }
            }.disposed(by: disposeBag)
    }

これは、loadTextで返ってくる値を加工して、subscribeの中に受け入れています。

で、私はJavaScriptのmapが頭にあったので、配列が流れてこないといけないと思っていたので、最初はよくわからなかったです。が、RxのSwiftは流れてくるElementが特に配列じゃなくても大丈夫でした。

なので、Rxのmapは、Elementを加工するためのものと考えてよいと思います。それは型の変換かもしれないし、さきほどのようにデータを直接いじるものかもしれません。実案件などでは、文字列からCodableプロトコルに適応したModelクラスを作るなんてのもできますね。

上と全く同じことをsubscribeの中でやります。

    func mapExample2() {
        loadText().subscribe { event in
            switch event {
            case .success(let text):
                let outputText = text + " World"
                print("success: \(outputText)") // Hello World
            case .error(let error):
                print("error \(error)")
            }
        }.disposed(by: disposeBag)
    }

この方法でも全く問題ないです。ただ、subscribeの中に目的のデータの形に加工済みのものが入ってきた方が実装としてはきれいな気がします。

実際にやってみます。

例えばこんなstructがあるとします。

    struct Message {
        let baseText: String
        
        var greetText: String {
            return baseText + " World"
        }
        
        init(baseText: String) {
            self.baseText = baseText
        }
    }

subscribeにこの型になったデータが入ってくるようにします。

    func mapExampleConvertStruct() {
        loadText().map { Message(baseText: $0) }
            .subscribe { event in
                switch event {
                case .success(let message):
                    // message は Message型
                    print("success: \(message.greetText)") //Hello World
                case .error(let error):
                    print("error \(error)")
                }
            }.disposed(by: disposeBag)
    }

こうすることで、加工済みのデータが subscribe 内で処理できるようになりました!

flatMap

flatMapを使ってみます。

    func flatMapExample() {
        loadText().flatMap { text -> Single<String> in
            let outputText = text + " World"
            return Single<String>.just(outputText)
        }.subscribe { event in
            switch event {
            case .success(let text):
                print("success: \(text)") // Hello World
            case .error(let error):
                print("error \(error)")
            }
        }.disposed(by: disposeBag)
    }

flatMapの中を注目して欲しいのですが、クロージャーの中で返す型が Single<String> になっています。

flatMapのクロージャーは、Sequenceの中からElementを一度取り出して、流れを消してしまいます。
なので、もう一度ElementをSequenceにくるんで流れを作って、返す必要があるみたいです。

まとめ

まとめると

map() は Sequence を操作しないで、Element だけを加工します。
flatMap() は Sequence を消して Element を取り出すので、また Sequenceを作成して返す必要があります。

おまけ:flatMapの実験

flatMap内のクロージャでSequenceを操作できることについて実験してみます。(例がよいのかわかりませんが、、、)
0.5秒後に一連のイベントとともにElementを発行するメソッドがあるとします。

    func loadSplittedTexts() -> PublishSubject<String> {
        let subject = PublishSubject<String>()
        DispatchQueue.global(qos: .utility).asyncAfter(deadline: .now() + 0.5) {
            subject.onNext("hello")
            subject.onNext("world")
            subject.onNext("!")
            subject.onCompleted()
        }
        return subject
    }

受け取り側

        loadSplittedTexts()
            .subscribe { event in
                switch event {
                case .next(let text):
                    print("success: \(text)")
                case .error(let error):
                    print("error \(error)")
                case .completed:
                    print("completed")
                }
            }.disposed(by: disposeBag)

出力

success: hello
success: world
success: !
completed

flatMapでSequenceを操作してみる

flatMapで Sequence をいじってみます。

        loadSplittedTexts()
            .flatMap { text -> Observable<String> in
                if text == "world" {
                    return Observable<String>.empty()
                }
                return Observable<String>.just(text)
            }
            .subscribe { event in
                switch event {
                case .next(let text):
                    print("success: \(text)")
                case .error(let error):
                    print("error \(error)")
                case .completed:
                    print("completed")
                }
            }.disposed(by: disposeBag)

出力

success: hello
success: !
completed

“world” という出力がなくなっていました。
これはflatMap内で Sequenceを操作して、値の流れを止めてしまった(empty()を返した)ことによるからだと思います。

こんな感じにmapとflatMapの使い分けができるみたいです。ではでは。

LINEで送る
Pocket

自作iPhoneアプリ 好評発売中!
フォルメモ - シンプルなフォルダつきメモ帳
ジッピー電卓 - 消費税や割引もサクサク計算!

LINEスタンプ作りました!
毎日使える。とぼけたウサギ。LINEスタンプ販売中! 毎日使える。とぼけたウサギ

ページトップへ戻る