RxSwift+Alamofire custom mapper error handling - swift

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.

Related

Cannot map error after flatMap usage (Never result type)

I have RestManager class which is used for fetching data from Internet and is returning AnyPublisher
class RestManager {
func fetchData<T: Decodable>(url: URL) -> AnyPublisher<T, ErrorType> {
URLSession
.shared
.dataTaskPublisher(for: url)
.tryMap({ data, _ in
let value = try JSONDecoder().decode(T.self, from: data)
if let array = value as? Array<Any>, array.isEmpty {
throw ErrorType.empty
}
return value
})
.mapError { error -> ErrorType in
switch error {
case is ErrorType:
return ErrorType.empty
case let urlError as URLError:
switch urlError.code {
case .notConnectedToInternet, .networkConnectionLost, .timedOut:
return .noInternetConnection
case .cannotDecodeRawData, .cannotDecodeContentData:
return .empty
default:
return .general
}
default:
return .general
}
}
.eraseToAnyPublisher()
}
}
Repository has two functions (getWorldwideData and getCountryData returning AnyPublisher<(WorldwideResponse item or CountryResponse item), ErrorType>)
In viewModel, I made these functions.
private func getData() {
$useCaseSelection
.flatMap { value -> AnyPublisher<Covid19StatisticsDomainItem, ErrorType> in
self.loader = true
self.error = nil
switch value {
case let .country(name):
return self.countryPipeline(name: name)
case .worldwide:
return self.worldwidePipeline()
}
}
.mapError { error in
self.error = error
}
.assign(to: &$homeScreenDomainItem)
}
private func worldwidePipeline() -> AnyPublisher<Covid19StatisticsDomainItem, ErrorType> {
repository
.getWorldwideData()
.map { response -> Covid19StatisticsDomainItem in
self.error = nil
self.loader = false
return Covid19StatisticsDomainItem(worldwideResponseItem: response)
}
.eraseToAnyPublisher()
}
private func countryPipeline(name: String) -> AnyPublisher<Covid19StatisticsDomainItem, ErrorType> {
repository
.getCountryData(for: name)
.map { response -> Covid19StatisticsDomainItem in
self.error = nil
self.loader = false
return Covid19StatisticsDomainItem(countryDayOneStatsResponse: response)
}
.eraseToAnyPublisher()
}
I wanted to make clean code, so I split code into two separate function based on useCaseSelection.
useCaseSelection is enum with two types.
error is ErrorType? value wrapped with #Published, in which I want to save error type if there is any error.
homeScreenDomainItem is Covid19StatisticsDomainItem instance wrapped with #Published.
Problem is in getData function where in MapError pipeline I am getting:
Cannot convert value of type () to closure result type Never
I tried to use setFailureType(to: ErrorType.self) but that is not helping.

How do you sequentially chain observables in concise and readable way

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")
})

RxSwift Chaining requests

The problem I faced with is chaining 2 requests and handling errors.
Below an example of my code:
func fbLogin() -> Observable<String> { ... }
func userLogin(request: Request) -> Observable<User> { ... }
let signedWithLogin = loginTaps
.asDriver(onErrorJustReturn: ())
.flatMapLatest { _ in
return fbLogin()
.map({ ReqestState<String>.loaded($0) })
.asDriver(onErrorRecover: { error in
return Driver.just(.error(error))
})
.startWith(.loading)
}
.map({ UserEndpoint.socialLogin(token: $0) })
.flatMapLatest { request in
return userLogin(request: request)
.map({ ReqestState<User>.loaded($0) })
.asDriver(onErrorRecover: { error in
return Driver.just(.error(error))
})
.startWith(.loading)
}
signedWithLogin
.drive(onNext: { response in
print(response)
})
.disposed(by: disposeBag)
Problem is when I cancel the facebook login popup I send observer.onError(FBLoginManagerError.canceled) error. This error catch first .asDriver(onErrorRecover: { error method but does't pass to the .drive(onNext: { responsemethod.
How can I catch all errors in .asDriver(onErrorRecover: { error method?
Mukesh is correct that you probably should avoid Driver until the end. Also, there's little point of having both RequestState types when you only really care about the final one (RequestState<User>)
Here's a simpler version that I think will do what you want:
let signedWithLogin = loginTaps
.flatMapLatest {
fbLogin()
.map { UserEndpoint.socialLogin(token: $0) }
.flatMap { userLogin(request: $0) }
.map { RequestState.loaded($0) }
.catchError { .just(.error($0)) }
.startWith(.loading)
}
.asDriver(onErrorRecover: { fatalError($0.localizedDescription) }) // I'm using `fatalError()` here because if the above emits an error something has gone horribly wrong (like the RxSwift library isn't working anymore.)
signedWithLogin
.drive(onNext: { response in
print(response)
})
.disposed(by: disposeBag)
The above assumes you change your UserEndpoint.socialLogin(token:) function to accept a String instead of a RequestState<String>.
It also assumes that fbLogin() and userLogin(request:) only emit one value each. You might want to consider switching them to Singles.

how to use a completion handler to await the completion of a firestore request

I'm slowly getting my head around completion handlers.
Kind of working backwards if I have a firestore query if I wanted to use a completion handler i'd have to use completion() when the firestore query finishes.
But it's setting up the function that still confuses me.
So if this is a function definition that takes a closure as a parameter:
func doSomethingAsync(completion: () -> ()) {
}
I don't quite get how to go from the above func definition and implementing it for something real like a firestore query and request.
query.getDocuments(){ (querySnapshot, err) in
if let err = err
{
print("Error getting documents: \(err)")
}
else
{
if(querySnapshot?.isEmpty)!
{
print("there's no document")
completion()
}
else
{
for document in querySnapshot!.documents
{
completion()
}
}
}
}
thanks.
update
so for my example could i do something like
func getFirestoreData(userID: String, completion #escaping() -> ()){
//firestore code:
query.getDocuments(){ (querySnapshot, err) in
if let err = err
{
print("executed first")
completion()
}
else
.......
print("executed first")
completion()
}
}
To call the function i'm doing:
getFirestoreData(userID: theUserID) {
print("executes second")
}
print("executes third") after function execution.
What i'd like to happen is the programming awaits the completion() before continuing to execute.
But "executes third" happens first, then "executes first", then "executes second".
Thanks
Here is full example (With API Call)
Note that : status variable is just a key to finger out what is response from server
(0: error from server, 1: success, -1: something wrong in my code)
func logout(handlerBack: #escaping (_ error: Error?, _ status:Int?, _ error_message:String?)->())
{
Alamofire.request(url, method: .get, parameters: nil, encoding: JSONEncoding.default, headers: nil)
.responseJSON { respons in
switch respons.result {
case .failure(let theError):
handlerBack(theError, 0, nil)
case .success(let data):
let json_data = JSON(data)
/// if couldn't fetch data
guard let status = json_data["status"].int else {
handlerBack(nil,-1, "can't find status")
return
}
/// if statuse == 0
guard status == 1 else {
handlerBack (nil, 0 , json_data["errorDetails"].string)
return
}
// that's means everything fine :)
handlerBack(nil, 1 , nil)
}
}
}
And here is the way to implement it :
// call func
self.logout { (error:error, status:Int, error_message:String) in
// if status not 1, figure out the error
guard status == 1 else {
// try to find error from server
guard let error_message = error_message else {
// else error something else
print ("Error at :: \(#function)")
// don't do anything ..just return
return
}
self.showMessageToUser(title: error_message, message: "", ch: {})
return
}
// this part do what ever you want, means every thing allright
}
UPDATE :
You are looking for something wait unit execute "First" and "Second"
in this case use DispatchGroup() here is the example :
var _dispatch_group = DispatchGroup()
getFirestoreData(userID: theUserID) {
_dispatch_group.enter()
print("executes second")
_dispatch_group.leave()
}
_dispatch_group.notify(queue: .main) {
print("executes third")
}
output is :
executes First
executes Second
executes Third

Observable returned from function never sends onNext

I have and observable that never sends onNext if its returned by a function, but if i subscribe to it in the function that returns it, onNext is called.
class InfoViewModel {
func refreshPushToken() {
PushNotificationService.sharedInstance.pushToken!
.flatMapLatest { (pushToken: String) -> Observable<Result<User>> in
return UserService.registerPushToken(pushToken)
}
.subscribe { (event ) in
print(event)
}
.addDisposableTo(disposeBag)
}
}
struct UserService {
....
static func registerPushToken(_ pushToken: String) -> Observable<Result<User>> {
...
return self.postUser(user: user)
}
static fileprivate func postUser(user: User) -> Observable<Result<User>> {
let rxProvider: RxMoyaProvider<Backend> = RxMoyaProvider<Backend>(endpointClosure: Backend.endpointClosure)
return rxProvider.request(Backend.register(user: user))
.mapObject(type: User.self)
.map({ (user: User) -> Result<User> in
LogService.log(level: .debug, action: "postUser", message: "Posted user with success", parameters: ["user": user.deviceId])
return .success(user)
})
.catchError({ error -> Observable<Result<User>> in
LogService.log(level: .error, action: "postUser", message: "Error posting user", parameters: ["user": user.deviceId, "error": error.localizedDescription])
return Observable.just(.failure(error))
})
}
}
But if I do this
rxProvider.request(Backend.register(user: user))
...
.subscribe { (event ) in
print(event)
}
in the UserService, i will get a next event.
I have tried to use debug() on the observable in InfoViewModel, there is a subscription, i just never receive any events.
So i figured it out, I was creating the RxMoyaProvider inside the method, so as soon as i went out of the scope of the method, it was deallocated. Which means that when was subscribing to it, it could no longer create the request. The reason that this wouldn't fail is because of how the observable is created
open func request(_ token: Target) -> Observable<Response> {
// Creates an observable that starts a request each time it's subscribed to.
return Observable.create { [weak self] observer in
let cancellableToken = self?.request(token) { result in
switch result {
case let .success(response):
observer.onNext(response)
observer.onCompleted()
case let .failure(error):
observer.onError(error)
}
}
return Disposables.create {
cancellableToken?.cancel()
}
}
}
As you can see, the request is called upon subscription, but since self had been deallocated the request was never fired. And all i got back was an empty observable.