web-dev-qa-db-ja.com

RxSwift、さまざまなオブザーバブルをチェーンする方法

私はまだリアクティブプログラミングの初心者であり、RxSwift全般です。 2つの異なる操作を連鎖させたい。私の場合、WebサーバーからZipファイルをダウンロードして、ローカルで解凍したいだけです。同時に、ダウンロードしたファイルの進行状況も表示したいと思います。それで私は最初の観察可能なものを作り始めました:

 class func rx_download(req:URLRequestConvertible, testId:String) -> Observable<Float> {

    let destination:Request.DownloadFileDestination = ...

    let obs:Observable<Float> = Observable.create { observer in
       let request =  Alamofire.download(req, destination: destination)
        request.progress { _, totalBytesWritten, totalBytesExpectedToWrite in
            if totalBytesExpectedToWrite > 0 {
                observer.onNext(Float(totalBytesWritten) / Float(totalBytesExpectedToWrite))
            }
            else {
                observer.onNext(0)
            }
        }
        request.response {  _, response, _, error in
            if let responseURL = response {
                if responseURL.statusCode == 200 {
                    observer.onNext(1.0)
                    observer.onCompleted()
                } else  {
                    let error = NSError(domain: "error", code: responseURL.statusCode, userInfo: nil)
                    observer.onError(error)
                }
            } else {
                let error = NSError(domain: "error", code: 500, userInfo: nil)
                observer.onError(error)
            }

            }
            return AnonymousDisposable () {
                request.cancel()
            }
        }
    return obs.retry(3)

}

その後、解凍するための同様の関数を作成します

class func rx_unzip(testId:String) -> Observable<Float> {
    return Observable.create { observer in
        do {
            try Zip.unzipFile(NSURL.archivePath(testId), destination: NSURL.resourceDirectory(testId), overwrite: true, password: nil)
                {progress in
                    observer.onNext(Float(progress))
                }
        } catch let error {
            observer.onError(error)
        }
        observer.onCompleted()
        return NopDisposable.instance
    }
}

今のところ、「モデルレイヤーの表示」にこのロジックがあるので、ダウンロード->完了時にサブスクライブ->解凍します

私が欲しいのは、最初にダウンロードを実行し、完了したらファイルを解凍するために、2つのObservableを1つに結合することです。これを行う方法はありますか?

10
Punty

Concat演算子には同じデータ型が必要です

実際、concat演算子を使用すると、オブザーバブルのシーケンスを適用できますが、concatの使用で発生する可能性のある問題は、concat演算子でObservablesは同じ汎用タイプを持っています。

let numbers     : Observable<Int>    = Observable.from([1,2,3])
let moreNumbers : Observable<Int>    = Observable.from([4,5,6])
let names       : Observable<String> = Observable.from(["Jose Rizal", "Leonor Rivera"])


// This works
numbers.concat(moreNumbers)

// Compile error
numbers.concat(names)

FlatMap演算子を使用すると、Observablesのシーケンスをチェーンできます。

これが例です。

class Tag {
    var tag: String = ""
    init (tag: String) {
        self.tag = tag
    }
}

let getRequestReadHTML : Observable<String> = Observable
                            .just("<HTML><BODY>Hello world</BODY></HTML>")

func getTagsFromHtml(htmlBody: String) -> Observable<Tag> {
    return Observable.create { obx in

        // do parsing on htmlBody as necessary

        obx.onNext(Tag(tag: "<HTML>"))
        obx.onNext(Tag(tag: "<BODY>"))
        obx.onNext(Tag(tag: "</BODY>"))
        obx.onNext(Tag(tag: "</HTML>"))

        obx.onCompleted()

        return Disposables.create()
    }
}

getRequestReadHTML
    .flatMap{ getTagsFromHtml(htmlBody: $0) }
    .subscribe (onNext: { e in
        print(e.tag)
    })

getRequestReadHTMLの型がObservable<String>であるのに対し、関数getTagsFromHtmlの型はObservable<Tag>であることに注意してください。

複数のflatMapを使用すると、放出頻度を増やすことができます

ただし、flatMap演算子は配列([1,2,3]など)またはシーケンス(Observableなど)を取り込んで、すべての要素を放出として放出するため、注意が必要です。これが、1...nの変換を生成することが知られている理由です。

ネットワーク呼び出しなどのオブザーバブルを定義し、エミッションが1つだけであることが確実な場合、その変換は1...1(つまり、1つのオブザーバブルから1つのNSData)であるため、問題は発生しません。すごい!

ただし、Observableに複数の排出量がある場合は、チェーンされたflatMap演算子は排出量が指数関数的に(?)増加することを意味するため、十分に注意してください。

具体的な例は、最初の観測値が3つの排出量を放出する場合、flatMap演算子は1...nを変換します。ここでn = 2は、合計6つの排出量があることを意味します。別のflatMapオペレーターは、再び1...nを変換できます。ここで、n = 2です。これは、合計12のエミッションがあることを意味します。これが予想される動作であるかどうかを再確認してください。

6
dsapalo

concat演算子を使用して、これら2つのObservableをチェーンできます。結果のObservableは、最初の値からnext値を送信し、完了すると、2番目の値から送信します。

注意点があります。rx_downloadから0.0から1.0の範囲の進捗値を取得し、rx_unzipからの進捗は0.0から始まります。単一の進行状況ビューに進行状況を表示する場合、これはユーザーを混乱させる可能性があります。

考えられるアプローチは、進行状況ビューとともに何が起こっているかを説明するラベルを表示することです。進行状況の値と説明テキストを含むタプルに対して各Observableをmapしてから、concatを使用できます。次のようになります。

let mappedDownload = rx_download.map {
    return ("Downloading", $0)
}

let mappedUnzip = rx_download.map {
    return ("Unzipping", $0)
}

mapped1.concat(mapped2)
 .subscribeNext({ (description, progress) in
    //set progress and show description
})

もちろん、考えられる解決策はたくさんありますが、これはコーディングの問題というよりも設計上の問題です。

1
Michał Ciuba