LoginSignup
47
33

More than 5 years have passed since last update.

Observableをnilでフィルタしてアンラップする

Last updated at Posted at 2016-12-01

RxSwiftでOptionalの値を扱うとき、nilでない場合のみ処理をしたいことがあります。
この記事では、nilをフィルタし値をアンラップして使う(ストリームに流す)方法を記載します。

方法1 filter と map
方法2 flatMap
方法3 unwrapを自作する

追記:
コメント@su_k さんから RxOptional を教えていただきました! Optional関連の高機能な拡張ですね。
また追加情報@mono0926 さんからいただきました!

方法1

let observable = PublishSubject<String?>()

observable
    .filter { $0 != nil }
    .map { $0! }

filterでnilを取り除きます。

filterは条件でフィルタするだけで、型はString?のままです。それをmapで強制的にアンラップします。
ビックリ!するのが嫌な感じですが、短く読みやすいです。

方法2

observable
    .flatMap { string -> Observable<String> in
        if let string = string {
            return Observable.just(string)
        } else {
            return Observable.empty()
        }
}

RxSwiftのflatMapを使う方法です。flatMapの引数のクロージャはObservableを返すようにします。

クロージャが返したObservableが空っぽなら値は流れません。つまりフィルタされます。
Observableに値をひとつ入れると、値がひとつ流れていきます。
(ここでは使いませんが、Observableに複数の値を入れると複数の値が流れていきます。)

nilの場合は空Observable.empty()を返すことでフィルタされます。
値がある場合はif letで取り出したStringObservable.just()に入れることで、String?ではなくStringが流れていきます。

OptionalのflatMapも使うと短く書けます。

observable
    .flatMap { $0.flatMap { Observable.just($0) } ?? Observable.empty() }

もしくは

observable
    .flatMap { $0.flatMap(Observable.just) ?? Observable.empty() }

通常はこの書き方がおすすめです。
if letの書き方は戻り値の型を指定しないとコンパイルできず冗長になりがちです。(returnがふたつあると型推論してくれない?🤔)

方法3

protocol Optionable {
    associatedtype Wrapped
    func flatMap<U>(_ transform: (Wrapped) throws -> U?) rethrows -> U?
}

extension Optional: Optionable {}

extension ObservableType {
    func unwrap<R: Optionable>(_ transform: @escaping ((E) -> R)) -> Observable<R.Wrapped> {
        return flatMap { (element: E) -> Observable<R.Wrapped> in
            transform(element).flatMap(Observable.just) ?? Observable.empty()
        }
    }
}

observable
    .unwrap { $0 }

方法1,2では標準の機能を使いました。方法3はObservableを拡張しunwrapを追加します。

使い方はunwrapの引数にOptionalを返すクロージャを渡すだけ。nilを返せばフィルタされ、値があれば流れていきます。上記例では$0String?なので単純に$0を返しています。
nilになるかもしれない式を書き、map + nilのフィルタ + アンラップのようにも使えます

observable
    .unwrap { $0 } // ここを通るとStringになる
    .unwrap { Int($0) } // StringをIntに変換できた場合のみ、Intが流れていく

{ $0 } を書くのもめんどくさい、という場合は以下も組み合わせると幸せになれます😃

func identify<A>(_ id: A) -> A {
    return id
}

extension ObservableType where E: Optionable {
    func unwrap() -> Observable<E.Wrapped> {
        return unwrap(identify)
    }
}

observable
    .unwrap()

nilをフィルタすることが多い場合、unwrapを追加する方法もおすすめです。

補足

RxSwiftのObservableObservableTypeを実装しているのでそれを拡張しています。ObservableTypeの値の型はEと定義されています。

unwrapでやりたいことは以下の通りです。

  1. クロージャを引数で受け取る
  2. 値: E をクロージャへ渡し、クロージャからOptionalの何かを受け取る
  3. nilならフィルタし、値があればアンラップして流す

残念ながら「Optionalの何か」を R? Optional<R> R = Optional のようには書けません。書けたら楽そうなのに😑

unwrapで使いたいOptionalの機能(定義)は以下のふたつです

associatedtype Wrapped
func flatMap<U>(_ transform: (Wrapped) throws -> U?) rethrows -> U?

これをプロトコルOptionableとして定義し、Optionalはそれを実装していると宣言します。

unwrap(_:)はクロージャの戻り値がOptionableであるとして処理を書いています。
unwrap()はEがOptionableであるとして処理を書いています。

identify(_:)を定義しているのは趣味です😃
もちろん{ $0 }で問題ありません。

47
33
4

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
47
33