巩小鹏 巩小鹏 - 6 months ago 160
Swift Question

RxSwift async task

I want to start a login task by a login button tapped, after this finished, fetch user order list, shipping address, wish list, other info.
startTask is a button, user tap it, i will start these tasks, but now if the login task failure, user tap startTask button again, i can't start these tasks again, why?

Sample code

private func test() {

let data = ["fetch order list", "fetch shipping addresses", "fetch wishlist", "fetch other info"]

let fetchInfoTasks = data.map{ asyncTask($0) }.toObservable()
let someTasks = fetchInfoTasks.merge().toArray()
let result = login().flatMapLatest{ _ in someTasks }

startTask
.rx_tap
.flatMapLatest{ result }
.catchError{ error in
.....error......
return Observable.empty()
}
.subscribeNext{ tasks in
.....all completed....
}
.addDisposableTo(disposeBag)
}

private func login()-> Observable<String> {
return Observable.create{ observer in
performClosure(afterDealy: 1, onMainQueue: false) {
if arc4random() % 4 == 0 {
observer.onNext("login finished")
observer.onCompleted()
} else {
observer.onError(NSError(domain: "", code: -1, userInfo: [NSLocalizedDescriptionKey: "some error"]))
}
}
return AnonymousDisposable{}
}
}

private func asyncTask(name: String)-> Observable<String> {
return Observable.create{ observer in
let delay = Double(arc4random() % 6 + 1)
performClosure(afterDealy: delay, onMainQueue: false) {
observer.onNext(name)
observer.onCompleted()
}
return AnonymousDisposable{}
}
}

func performClosure(afterDealy delay: Double, onMainQueue mainQueueOrNot: Bool, action: dispatch_block_t) {
let delayIntervals = Double(NSEC_PER_SEC) * delay
let time = dispatch_time(DISPATCH_TIME_NOW, Int64(delayIntervals))
let queue = mainQueueOrNot ? dispatch_get_main_queue() : dispatch_get_global_queue(QOS_CLASS_UTILITY, 0)
dispatch_after(time, queue, action)
}

Answer

Once an error occurs, the stream terminates. You don't want this to happen at the level of your button, so you must catch errors at a deeper level.

Eg:

startTask
    .rx_tap
    .flatMapLatest{
        result
            .catchError{ error in
                 .....error......
                 return Observable.empty()
            }
    }
    .subscribeNext{ tasks in
        .....all completed....
    }
    .addDisposableTo(disposeBag)

This way you can prevent an actual Rx error from bubbling up through the flatMap.

If you need to do something on error, you will probably want to wrap your result in some kind of result enum (recommended https://github.com/antitypical/Result).

An example of this would be:

startTask
    .rx_tap
    .flatMapLatest{
        result
            .map { Result.Success(result: $0)
            .catchError{ error in return Result.Error(error: $0) }
    }
    .subscribeNext{ result in
        switch(result) {
            case .Success(let result):
                //display happy case
            case .Error(let error):
                //display sad case
    }
    .addDisposableTo(disposeBag)