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で取り出したStringをObservable.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を返せばフィルタされ、値があれば流れていきます。上記例では$0がString?なので単純に$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のObservableはObservableTypeを実装しているのでそれを拡張しています。ObservableTypeの値の型はEと定義されています。
unwrapでやりたいことは以下の通りです。
- クロージャを引数で受け取る
-
値: Eをクロージャへ渡し、クロージャからOptionalの何かを受け取る - 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 }で問題ありません。