LoginSignup
19
15

More than 3 years have passed since last update.

RxSwift で Observable<T?> を Observable<T> にフィルターしたい

Last updated at Posted at 2018-02-18

Observable を Observable にフィルターしたい

RxSwift 5.0 で、compactMap が追加されたことでカンタンにフィルターできるようになりました :tada:

let sequence: Observable<Int?> = Observable.of(1, nil, 3)
let compact: Observable<Int> = sequence.compactMap { $0 }

@mtgto コメントありがとうございます!
タイトルの内容を実現するだけなら上記の通りで OK です :thumbsup:

以下の内容をそのまま利用することは無くなりましたが、
制約付きで拡張するテクニック自体は使えるので、記事として残しておきます :bow:


はじめに

『Observableとは何か』という概念を理解した後も、
Rx に慣れるまでは、どのようにコードを書けば簡潔に表現できるのか悩むことも多いと思います。

Rx を使わない Swift で簡単だった操作も、Rx を使った途端に記述が複雑になることがあります。
たとえば、T? から T を取り出したい場合はケースバイケースで下記の方法で値を取り出します。

  • Forced Unwrapping
  • Optional Binding
  • Optional Chaining

Swift によく慣れていれば、この選択に悩むことはあまり無いのではないかと思います。
では、 Observable<T?>Observable<T> にフィルターしたい場合はどうでしょうか。

本記事では、再利用性の高い処理をメソッド化することで、Rx の記述が簡潔になる場合があることを、Observable<T?>Observable<T> にフィルターする操作を例にして示したいと思います。

環境

T? を T のみにフィルターしたい

RxSwift を使っていると Observable のイベント要素 T?T のみにフィルターしたいときがあります。
たとえば以下のように T から Observable<S> への関数 transform があり、
この transform を使って、Observable<T?> から Observable<S> を作りたいときです。

func transform(_ t: T) -> Observable<S>
let observable1: Observable<T> = ...
observable1.flatMap(transform) // OK (transform は T から Observable<S> への変換はできる)

let observable2: Observable<T?> = ...
observable2.flatMap(transform) // コンパイルエラー (transform は T? から Observable<S> への変換はできない)

Observable<T?>Observable<T> にフィルターできれば、transform を使って、Observable<S> を作ることができます。

愚直なコード

まずは愚直にフィルターしてみます。

T? から T へのアンラップが可能な場合に限り、強制アンラップすることで、
Observable<T?> から Observable<T> を作ることができます。

let observable: Observable<T?> = ...

observable.filter { $0 != nil }.map { $0! }.flatMap(transform) 

// OK (Observable<T?> から Observable<T> へ変換されるので、Observable<S> へ変換できる)

あるいは、下記のようなコードでも nil をフィルターすることができます。

let observable: Observable<T?> = ...

observable.flatMap(Observable.from(optional: )).flatMap(transform) 

// OK (Observable.from(optional: ) が `nil` のイベントを潰す )

いずれにしろ T?T のみにフィルターしたいという要求はこれで解決です。

愚直なコードの問題点

しかしながら、毎度毎度 filter { $0 != nil }.map { $0! } (あるいは flatMap(Observable.from(optional: )))と記述するのは少々抵抗があります。

  • 記述が面倒(アンラップしたいだけなのに、都度同じ記述しないといけない)
  • 可読性の低下(処理の本質 transform 以外のコードが都度記述されてしまう)
  • クロージャで色々ごちゃごちゃされるコードが散見するのがイヤ

愚直なコードの問題点の解決策

この問題の解決策として filter { $0 != nil }.map { $0! } を1つにまとめたメソッドを追加します。

Observable<T?>Observable<T> を返すメソッド some() が実装されれば、
下記のように記述することが可能になります。

let observable: Observable<T?> = ...

observable.some().flatMap(transform) 

// OK (Observable<T?> -> Observable<T> -> Observable<S>)

愚直なコードを解決

それでは some() メソッドを実装します。
まず、下記のような制約付き拡張で some() メソッドを追加することを試みます。

extension Observable where Element==Optional { // コンパイルエラー

    public func some() -> Observable<Element.Wrapped> {
        return filter { $0 != nil }.map { $0! }
    }
}

しかし、上述のコードは Optional の型パラメータ Wrapped に具体的な型を指定していないため、
コンパイルエラーになります。

代わりに Optional<Any> のように具体的な型で固定すればコンパイルエラーを回避することは可能ですが、
型パラメータ Wrapped は柔軟に扱うことができなくなります。

この問題は Optional が適合すべきプロトコル OptionalProtocol を定義することで解決します。
プロトコルの宣言と適合方法は下記の通りです。

/// Optional が適合すべき自作のプロトコル
public protocol OptionalProtocol {
    associatedtype Wrapped

    /// `nil` かどうか判定せずに強制アンラップした値を返す(`nil`の場合は停止する)
    @inlinable var unsafelyUnwrapped: Wrapped { get }
    /// `nil` の場合に限り `true` を返す
    var isNone: Bool { get }
}

extension Optional: OptionalProtocol {
    public var isNone: Bool {
        return self == nil
    }
}

上述の OptionalProtocol に対して制約付き拡張をすることで
Optional 型にも拡張が適用されます。

extension ObservableType where Element: OptionalProtocol { // OK

    public func some() -> Observable<Element.Wrapped> {
        return filter { !$0.isNone }.map { $0.unsafelyUnwrapped }
    }
}
let observable: Observable<T?> = ...

observable.some().flatMap(transform) // OK

これで T?T のみにフィルターすることができ、filter.map を都度記述する問題も解決できました。

このようにして、オプショナル型のアンラップ処理など、再利用性の高い処理については、
制約付き拡張によってメソッドを追加することで Rx の記述を簡潔にすることができます。

以下、おまけとしてオプショナル型のアンラップ処理以外の拡張の例を挙げてみます。

[ おまけ ] Observable<T?> を更に便利に扱う

T?T のみにフィルターするだけでなく、
エラー処理を扱いたい等の理由で nil のみにフィルターした Observable が欲しい場合もあるかと思います。
そこで、Tnil を別々にフィルターした Observable のタプルを取得できるようにします。

extension ObservableType where Element: OptionalProtocol {

    /// `Wrapped?` を `Wrapped` のみにフィルターした Observable
    public func some() -> Observable<Element.Wrapped> {
        return filter { !$0.isNone }.map { $0.unsafelyUnwrapped }
    }

    /// `Wrapped?` を `nil` のみにフィルターした Observable
    public func none() -> Observable<Void> {
        return filter { $0.isNone }.map { _ in }
    }

    /// `Observable<Wrapped?>` を `Wrapped` と `nil` のイベントシーケンスに分割したタプル
    public func split(replay: Int = 0, scope: SubjectLifetimeScope = .whileConnected)
        -> (some: Observable<Element.Wrapped>, none: Observable<Void>) {
            let shared = share(replay: replay, scope: scope)
            return (shared.some(), shared.none())
    }
}

これで、下記のように split()Tnil のイベントシーケンスを別々に取得することができるようになります。

let subject = PublishSubject<Int?>()

// `Int` と `nil` のイベントシーケンスを別々に監視してログ出力します
let observable = subject.asObservable().split()
_ = observable.some.debug("💛 some:").subscribe()
_ = observable.none.debug("💜 none:").subscribe()

// 1, nil, 3 の順にイベントシーケンスを発行します
let observer = AnyObserver(subject)
observer.onNext(1)
observer.onNext(nil)
observer.onNext(3)

// デバッグログは下記のように出力されます
// 💛 some: -> Event next(1)
// 💜 none: -> Event next(())
// 💛 some: -> Event next(3)

[ おまけ ] Observable<Result<T, Error>> も便利に扱う

T? 型の代わりに ResultResult<T, Error> 型をイベント要素とした Observable を扱いたいこともあるかと思います。

T? 型では値が nil の場合に、その値の nil になった原因まで情報の伝搬ができないためです。

Optional のときと同様にして、制約付き拡張で TError のイベントシーケンスを別々に取得出来るようにしてみます。
Result には Result<T, Error> 型が適合すべきプロトコル ResultProtocol が定義されているので、
これを利用します。

extension ObservableType where Element: ResultProtocol {

    public func success() -> Observable<Element.Value> {
        return map { $0.result.value }.some()
    }

    public func failure() -> Observable<Element.Error> {
        return map{ $0.result.error }.some()
    }

    public func split(replay: Int = 0, scope: SubjectLifetimeScope = .whileConnected)
        -> (success: Observable<Element.Value>, failure: Observable<Element.Error>) {
            let shared = share(replay: replay, scope: scope)
            return (shared.success(), shared.failure())
    }
}

ここで上述の some() は先程自作した、 Observable<T?> から Observable<T> にフィルターするためのメソッドです。
再利用性がここで発揮されましたね。

[ おまけ ] APIKitを使った例

APIKit はAPIリクエストの結果を Result<T, Error> 型でコールバックします。
そこで今回は APIKit を例に挙げて上述のメソッドを使用してみます。

下記のような制約付き拡張を作成して、コールバックを RxSwift.Single<Result<T, Error>> として扱えるようにします。

extension Session: ReactiveCompatible {}

extension Reactive where Base: Session {

    func send<Request: APIKit.Request>(_ request: Request) 
    -> Single<Result<Request.Response, SessionTaskError>> {

        return Single.create { [weak base] (event) -> Disposable in
            let task = base?.send(request) { response in
                event(.success(response))
            }
            return Disposables.create { task?.cancel() }
        }
    }
}

SessionTaskError の場合に, event(.error(Error)) でイベントシーケンスとしてのエラーで発行していないことに注意してください。

レスポンスの取得成功・失敗を振り分けずに RxSwift.Single のイベント要素として
そのまま event(.success(response)) で発行しておきます。

こうすることで、 SessionTaskError の型情報を失わずにコールバックすることができます。
また、イベントシーケンスがエラーで終了することもなくなります。

これで、以下のように ResponseSessionTaskError を別々のイベントシーケンスとして取得できるようになります。

/// リクエストのイベントストリーム(SomeRequest は APIKit.Request に適合した構造体か何か)
let request: Observable<SomeRequest> = ...

/// リクエストの結果を value と error のイベントストリームに分割
let response = request.flatMap(Session.shared.rx.send).split()

_ = response.success
    .subscribe(onNext: { /* `SomeRequest.Response` だけが流れてくる */ })

_ = response.failure
    .subscribe(onNext: { /* `SessionTaskError` だけが流れてくる */ })

おわりに

ということで、再利用性の高い処理をメソッドとして追加することにより、
何度も同じような処理を記述せずにスッキリ記述できるようになります。

Observable<Result<T, Error>> に追加したメソッドは APIKit に限らず、
Result 型であれば使える処理ですので、なかなか便利なのではないかと思います。
また、 subscribe 時にクロージャ内で Result 型を switch する必要も無くなるので、
クロージャ内の処理もスッキリします。

RxSwift は慣れるまでは、コードがぐちゃぐちゃになりがちですが、
色々と工夫を凝らしてスッキリさせたいですね。

19
15
3

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
19
15