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 }
で問題ありません。