I have an observable (request from network) and dont want it to be disposed when I got an error
My custom error
enum MyError: Error {
case notFound
case unknown
}
My network request using Moya
let registerRequest = didTapJoinButton.withLatestFrom(text.asObservable())
.flatMapLatest { text in
provider.rx.request(API.register(text: text))
}
.flatMapLatest({ (response) -> Observable<Response> in
let statusCode = response.statusCode
if statusCode.isSuccessStatus() {
return Observable.just(response)
} else if statusCode.isNotFoundStatus() {
return Observable.error(MyError.notFound)
} else {
return Observable.error(MyError.unknown)
}
})
.materialize()
.share(replay: 1)
Looks great. I use materialize() to prevent observable being disposed on error
Subscribe: (If status code 200)
Everything works just fine, I got response and the stream is not disposed
registerEventRequest.subscribe(onNext: { (next) in
print("NEXT: \(next)")
}, onError: { (error) in
print("ERRRRROR ME: \(error)")
}, onCompleted: {
print("Completed")
}) {
print("Disposed")
}
BUT if status code is something like 404. I got the error as I expected. However, hey look at the console log
NEXT: error(notFound)
Completed
Disposed
It jumps to NEXT which I expected. But why it throw complete and dispose my sequence.
My question is Why did it dispose my sequence and how I can prevent this?
.materialize() does not prevent an observable from being disposed on error. When an Observable emits an error, it is finished and materialize merely converts that error into a next event.
You need to put the materialize inside the first flatMapLatest to prevent the error from escaping the flatMap closure.
This video might help (note selectMany is the same as flatMap) https://channel9.msdn.com/Blogs/J.Van.Gogh/Reactive-Extensions-API-in-depth-SelectMany?term=select%20many&lang-en=true
Here's another way to compose the Observable:
let registerRequest = didTapJoinButton.withLatestFrom(text.asObservable())
.flatMapLatest { text in
provider.rx.request(API.register(text: text))
.materialize()
}
.map { (event) -> Event<Response> in
switch event {
case .next(let response) where response.statusCode.isNotFoundStatus():
return Event.error(MyError.notFound)
case .next(let response) where response.statusCode.isSuccessStatus() == false:
return Event.error(MyError.unknown)
default:
return event
}
}
.share(replay: 1)
I moved materialize() where it belongs so errors won't break the chain. I also swapped out the second flatMapLatest for a simple map since the extra work wasn't necessary.
The switch statement could have also been written like this:
switch event {
case .next(let response):
let statusCode = response.statusCode
if statusCode.isNotFoundStatus() {
return Event.error(MyError.notFound)
}
else if statusCode.isSuccessStatus() {
return event
}
else {
return Event.error(MyError.unknown)
}
default:
return event
}
But I think the way I did it is cleaner because it reduces the cyclomatic complexity of the closure.
Here is code to deal with the concern brought up in the comments:
extension ObservableType {
func flatMapLatestT<T, U>(_ selector: #escaping (T) -> Observable<U>) -> Observable<Event<U>>
where Self.E == Event<T>
{
return self.flatMapLatest { (event) -> Observable<Event<U>> in
switch event {
case .next(let element):
return selector(element).materialize()
case .completed:
return .just(Event<U>.completed)
case .error(let error):
return .just(Event<U>.error(error))
}
}
}
}
This gist contains a whole suite of operators for dealing with Events. https://gist.github.com/dtartaglia/d7b8d5c63cf0c91b85b629a419b98d7e
Related
RxSwift one more question about error handling:
I'm using Alamofire+RxAlamofire this way:
SessionManager.default.rx.responseJSON(.post, url, parameters:params)
example:
func login() -> Observable<Int> {
let urlString = ...
let params = ...
return SessionManager.default.rx.responseJSON(.post, url, parameters:params)
.rxJsonDefaultResponse()
.map({ (data) in
data["clientId"] as! Int
})
}
....
extension ObservableType where Element == (HTTPURLResponse, Any) {
func rxJsonDefaultResponse() -> Observable<Dictionary<String, Any>> {
return self.asObservable().map { data -> Dictionary<String, Any> in
if... //error chechings
throw NSError(domain: ..,
code: ...,
userInfo: ...)
}
...
return json
}
}
}
using:
loginBtn.rx.tap
.flatMap{ _ in
provider.login()
}.subscribe(onNext: { id in
...
}, onError: { (er) in
ErrorPresentationHelper.showErrorAlert(for: er)
})
.disposed(by: bag)
So if error occurred everything works as intended: error alert shows and 'loginBtn.rx.tap' disposed, but I need it to be still alive, what's my strategy here if I want to use onError block?
You can use materialize function in rxSwift. It will convert any Observable into an Observable of its events. So that you will be listening to Observable<Event<Int>> than Observable<Int>. Any error thrown from the flatmap would be captured as error event in your subscription block's onNext and can be handled there. And your subscription would still be alive. Sample code would be as follows.
button.rx.tap.flatMap { _ in
return Observable.just(0)
.flatMap { _ -> Observable<Int> in
provider.login()
}.materialize()
}.subscribe(onNext: { event in
switch event {
case .next:
if let value = event.element {
print(value) //You will be getting your value here
}
case .error:
if let error = event.error {
print(error.localizedDescription) //You will be getting your captured error here
}
case .completed:
print("Subscription completed")
}
}) {
print("Subscription disposed")
}.disposed(by: disposeBag)
Hope it helps. You can checkout the materialize extension here.
In the process of reading the RXAlamofire source code, there is a place that I don't understand very well.
Since this method is an observable object for creating a DataRequest, why call the responseWith method?
func request<R: RxAlamofireRequest>(_ createRequest: #escaping (SessionManager) throws -> R) -> Observable<R> {
return Observable.create { observer -> Disposable in
let request: R
do {
request = try createRequest(self.base)
observer.on(.next(request))
request.responseWith(completionHandler: { response in
if let error = response.error {
observer.on(.error(error))
} else {
observer.on(.completed)
}
})
if !self.base.startRequestsImmediately {
request.resume()
}
return Disposables.create {
request.cancel()
}
} catch {
observer.on(.error(error))
return Disposables.create()
}
}
}
I believe the authors of RXAlamofire use this as their convention. If you look at there request implementation All of the request methods return the result of a method responseXYZ. The response methods typically execute the request and respond with something (JSON, String, etc.) Sounds a bit confusing but its kind of like this request some data respond with something.
Im new to RXSwift and I've begun investigating how I can perform Promise like function chaining.
I think I'm on the right track by using flatmap but my implementation is very difficult to read so I suspect theres a better way to accomplish it.
What I have here seems to work but I'm getting a headache thinking about what It might looks like if I added another 3 or functions to the chain.
Here Is where I declare my 'promise chain'(hard to read)
LOGIN().flatMap{ (stuff) -> Observable<Int> in
return API(webSiteData: stuff).flatMap
{ (username) -> Observable<ProfileResult> in
return accessProfile(userDisplayName: username) }
}.subscribe(onNext: { event in
print("The Chain Completed")
print(event)
}, onError:{ error in
print("An error in the chain occurred")
})
These are the 3 sample functions I'm chaining
struct apicreds
{
let websocket:String
let token:String
}
typealias APIResult = String
typealias ProfileResult = Int
// FUNCTION 1
func LOGIN() -> Observable<apicreds> {
return Observable.create { observer in
print("IN LOGIn")
observer.onNext(apicreds(websocket: "the web socket", token: "the token"))
observer.on(.completed)
return Disposables.create()
}
}
// FUNCTION 2
func API(webSiteData: apicreds) -> Observable<APIResult> {
return Observable.create { observer in
print("IN API")
print (webSiteData)
// observer.onError(myerror.anError)
observer.on(.next("This is the user name")) // assiging "1" just as an example, you may ignore
observer.on(.completed)
return Disposables.create()
}
}
//FUNCTION 3
func accessProfile(userDisplayName:String) -> Observable<ProfileResult>
{
return Observable.create { observer in
// Place your second server access code
print("IN Profile")
print (userDisplayName)
observer.on(.next(200)) // 200 response from profile call
observer.on(.completed)
return Disposables.create()
}
}
This is a very common problem we run into while chaining operations. As a beginner I had written similar code using RxSwift in my projects as well. And there are two areas of improvement -
1. Refactor the code to remove nested flatMaps
2. Format it differently to make the sequence easier to follow
LOGIN()
.flatMap{ (stuff) -> Observable<APIResult> in
return API(webSiteData: stuff)
}.flatMap{ (username) -> Observable<ProfileResult> in
return accessProfile(userDisplayName: username)
}.subscribe(onNext: { event in
print("The Chain Completed")
print(event)
}, onError:{ error in
print("An error in the chain occurred")
})
In addition to nested flatMap and code formatting, you could omit return and explicit return types:
LOGIN()
.flatMap { webSiteData in API(webSiteData: webSiteData) }
parameter names
LOGIN()
.flatMap { API(webSiteData: $0) }
or even remove parameters at all where appropriate:
LOGIN()
.flatMap(API)
.flatMap(accessProfile)
.subscribe(
onNext: { event in
print(event)
}, onError:{ error in
print(error)
}
)
FYI there is Observable.just method which would be convenient here:
struct ApiCredentials {
let websocket: String
let token: String
}
func observeCredentials() -> Observable<ApiCredentials> {
let credentials = ApiCredentials(websocket: "the web socket", token: "the token")
return Observable.just(credentials)
}
Try to follow official Swift API Guidelines to make your code more readable.
You can also use the point-free style and just pass function references to flatMap:
LOGIN()
.flatMap(API)
.flatMap(accessProfile)
.subscribe(onNext: { event in
print("The Chain Completed")
print(event)
}, onError:{ error in
print("An error in the chain occurred")
})
I don't want to write a separate function to return a Promise in my firstly call. I just want to write this:
firstly
{
return Promise<Bool>
{ inSeal in
var isOrderHistory = false
let importTester = CSVImporter<String>(url: url)
importTester?.startImportingRecords(structure:
{ (inFieldNames) in
if inFieldNames[2] == "Payment Instrument Type"
{
isOrderHistory = true
}
}, recordMapper: { (inRecords) -> String in
return "" // Don't care
}).onFinish
{ (inItems) in
inSeal.resolve(isOrderHistory)
}
}
}
.then
{ inIsOrderHistory in
if inIsOrderHistory -> Void
{
}
else
{
...
But I'm getting something wrong. ImportMainWindowController.swift:51:5: Ambiguous reference to member 'firstly(execute:)'
None of the example code or docs seems to cover this (what I thought was a) basic use case. In the code above, the CSVImporter operates on a background queue and calls the methods asynchronously (although in order).
I can't figure out what the full type specification should be for Promise or firstly, or what.
According to my understanding, since you are using then in the promise chain, it is also meant to return a promise and hence you are getting this error. If you intend not to return promise from your next step, you can directly use done after firstly.
Use below chain if you want to return Promise from then
firstly {
Promise<Bool> { seal in
print("hello")
seal.fulfill(true)
}
}.then { (response) in
Promise<Bool> { seal in
print(response)
seal.fulfill(true)
}
}.done { _ in
print("done")
}.catch { (error) in
print(error)
}
If you do not want to return Promise from then, you can use chain like below.
firstly {
Promise<Bool> { seal in
print("hello")
seal.fulfill(true)
}
}.done { _ in
print("done")
}.catch { (error) in
print(error)
}
I hope it helped.
Updated:
In case you do not want to return anything and then mandates to return a Promise, you can return Promise<Void> like below.
firstly {
Promise<Bool> { seal in
print("hello")
seal.fulfill(true)
}
}.then { (response) -> Promise<Void> in
print(response)
return Promise()
}.done { _ in
print("done")
}.catch { (error) in
print(error)
}
This is my non-reactive code that works just fine.
func getLatestHtml2 () {
Alamofire.request("https://www.everfest.com/fest300").responseString { response in
print("\(response.result.isSuccess)")
if let html = response.result.value {
self.parseHTML(html: html)
}
}
}
However when I make it reactive using this code.
func getLatestHtml1() -> Observable<String> {
return Observable<String>.create { (observer) -> Disposable in
let request = Alamofire
.request("https://www.everfest.com/fest300")
.responseString { response in
print(response.result.value)
observer.onNext(response.result.value!)
observer.onCompleted()
}
return Disposables.create { request.cancel() }
}
}
I get no data in the print statement. I even used RxAlamofire, which I feel is the right way with this code and it has error checking:
func getLatestHtml() -> Observable<String?> {
return RxAlamofire
.requestData(.get,"https://web.archive.org/web/20170429080421/https://www.everfest.com/fest300" )
.debug()
.catchError { error in
print(error)
return Observable.never()
}
.map { (response, value) in
print(response.statusCode)
guard response.statusCode == 200 else { return nil }
print(value)
return String(data: value, encoding: String.Encoding.utf8)
}
.asObservable()
}
which produced no data or errors anywhere. I need to know if my syntax is wrong or my thinking regarding reactive programming is wrong.
I cam calling it as .getLatestHTMLX(). Thanks !
Observable's are lazy, they don't do any work unless they are being watched (and will generally stop working as soon as nobody is watching.) This means you have to subscribe to an observable in order for it to start emitting values.
Also, unless you explicitly share the observable, it will start a new request for every subscriber.