I have a quick question:
I have a network request that returns Observable<Result<String, RequestError>>, let’s call it requestToken
if this request succeeds, I want to use the String (token) to do another request that returns Observable<Result<NSDictionary, RequestError>>, let’s call it requestData
when that second request comes back, I wanna merge the token into its dictionary
in the end I wanna map from Observable<Result<String, RequestError>> to Observable<Result<NSDictionary, RequestError>>
How can I achieve that without multiple nested levels in my code?
This is what I have today:
requestToken()
.flatMap({ result -> Observable<Result<NSDictionary, RequestError>> in
switch result {
case .success(let token):
return requestData(token: token).map({ $0.map({ $0 + ["token": token] }) })
case .failure(let error):
return Observable.of(.failure(error))
}
})
Updated:
It's a detailed example, hope this may help:
enum RequestError: Error {
case unknown
}
func requestToken() -> Observable<String> {
return Observable.create { observer in
let success = true
if success {
observer.onNext("MyTokenValue")
observer.onCompleted()
} else {
observer.onError(RequestError.unknown)
}
return Disposables.create()
}
}
func requestData(token: String) -> Observable<[String: Any]> {
return Observable<[String: Any]>.create { observer in
let success = false
if success {
observer.onNext(["uid": 007])
observer.onCompleted()
} else {
observer.onError(RequestError.unknown)
}
return Disposables.create()
}
.map { (data: [String: Any]) in
var newData = data
newData["token"] = token
return newData
}
}
requestToken() // () -> Observable<String>
.flatMapLatest(requestData) // Observable<String> -> Observable<[String: Any]>
.materialize() // Observable<[String: Any]> -> Observable<Event<[String: Any]>>
.subscribe(onNext: { event in
switch event {
case .next(let dictionary):
print("onNext:", dictionary)
case .error(let error as RequestError):
print("onRequestError:", error)
case .error(let error):
print("onOtherError:", error)
case .completed:
print("onCompleted")
}
})
.disposed(by: disposeBag)
Original:
I think it's much easier to achieve it using materialize() with less extra work:
func requestToken() -> Observable<String> { return .empty() }
func requestData(token: String) -> Observable<NSDictionary> { return .empty() }
enum RequestError: Error {}
requestToken()
.flatMapLatest(requestData)
.materialize()
.subscribe(onNext: { event in
switch event {
case .next(let dictionary):
print("onNext:", dictionary)
case .error(let error as RequestError):
print("onRequestError:", error)
case .error(let error):
print("onOtherError:", error)
case .completed:
print("onCompleted")
}
})
.disposed(by: disposeBag)
Hope this may help.
If you use the built in error system, you can save yourself from having to manually pass the error along and all the switches that would entail. You can cast the error at the end.
I would do something more like this:
// this is necessary to handle adding the token to the dictionary.
extension Dictionary {
/// An immutable version of update. Returns a new dictionary containing self's values and the key/value passed in.
func updatedValue(_ value: Value, forKey key: Key) -> Dictionary<Key, Value> {
var result = self
result[key] = value
return result
}
}
// function signatures, note that they don't return Results anymore.
func requestToken() -> Observable<String> { /*...*/ }
func requestData(withToken: String) -> Observable<[String: Any]> { /*...*/ }
requestToken().flatMapLatest {
requestData(token: $0)
.map { $0.updatedValue($0, forKey: "token") }
.map { .success($0) }
}.catchError {
Observable.just(.failure($0 as! RequestError))
}
With the above, the end result would be an Observable<Result<[String: Any], RequestError>> just like in your case, but the error handling is much cleaner.
If you can't change the signatures of the two functions you are using then I would do this:
func throwError<T, U: Error>(result: Result<T, U>) throws -> T {
switch result {
case .success(let token):
return token
case .failure(let error):
throw error
}
}
requestToken().map {
try throwError(result: $0)
}.flatMapLatest {
requestData(token: $0)
.map { try throwError(result: $0) }
.map { $0.updatedValue($0, forKey: "token") }
}
.map { .success($0) }
.catchError {
Observable.just(.failure($0 as! RequestError))
}
Related
I am learning iOS and amplify and struggling my way through implementing a custom auth flow. I have run into an issue I can't resolve. Here is my code:
//
// SessionManager.swift
// Mapwork
//
// Created by James Nebeker on 2/27/21.
//
import Foundation
import Combine
import Amplify
import AmplifyPlugins
enum AuthState {
case signUp
case login
case confirmCode(username: String)
case session (user: AuthUser)
case firstTime
case confirmSignIn
}
final class SessionManager: ObservableObject {
#Published var authState: AuthState = .firstTime
func getCurrentAuthUser() {
if let user = Amplify.Auth.getCurrentUser() {
authState = .session(user: user)
} else {
authState = .login
}
}
func showSignUpView()
{
authState = .signUp
}
func showLoginView()
{
authState = .login
}
func showConfirmationSignInView()
{
authState = .confirmSignIn
}
func signUp(username: String, email: String) {
let userAttributes = [AuthUserAttribute(.email, value: email)]
let options = AuthSignUpRequest.Options(userAttributes: userAttributes)
_ = Amplify.Auth.signUp(username: username, password: UUID().uuidString, options: options) { [weak self] result in
switch result {
case .success(let signUpResult):
if case let .confirmUser(deliveryDetails, _) = signUpResult.nextStep {
print("Delivery details \(String(describing: deliveryDetails))")
DispatchQueue.main.async {
self?.authState = .confirmCode(username: username)
}
} else {
print("Signup Complete")
}
case .failure(let error):
print("An error occurred while registering a user \(error)")
}
}
}
func confirmSignUp(for username: String, with confirmationCode: String) {
Amplify.Auth.confirmSignUp(for: username, confirmationCode: confirmationCode) { [weak self] result in
switch result {
case .success:
print("Confirm signUp succeeded")
DispatchQueue.main.async {
self?.showLoginView()
}
case .failure(let error):
print("An error occurred while confirming sign up \(error)")
}
}
}
func signIn(username: String) {
Amplify.Auth.signIn(username: username) { [weak self] result in
switch result {
case .success:
if case .confirmSignInWithCustomChallenge(_) = result.nextStep {
DispatchQueue.main.async {
self?.showConfirmationSignInView()
}
} else {
print("Sign in succeeded")
}
case .failure(let error):
print("Sign in failed \(error)")
}
}
}
func customChallenge(response: String) {
Amplify.Auth.confirmSignIn(challengeResponse: response) {[weak self] result in
switch result {
case .success:
DispatchQueue.main.async {
self?.getCurrentAuthUser()
}
print("Confirm sign in succeeded")
case .failure(let error):
print("Confirm sign in failed \(error)")
}
}
}
}
In the above code, I have copied this function from the AWS Amplify docs exactly:
func signIn(username: String) {
Amplify.Auth.signIn(username: username) { result in
switch result {
case .success:
if case .confirmSignInWithCustomChallenge(_) = result.nextStep {
// Ask the user to enter the custom challenge.
} else {
print("Sign in succeeded")
}
case .failure(let error):
print("Sign in failed \(error)")
}
}
}
But I am receiving this error:
Type of expression is ambiguous without more context
Within the switch statement. I really don't understand this because I have not changed this code, I have copied it directly from the documentation. Any help would be greatly appreciated.
Edit: Specifically, the error is appearing here:
I want to use catchError for getting back my error as custom type.
At first, I want my network layer return Observable and then in ViewModel I subscribed it for .OnNext, .OnError, .OnCompleted events, But I don't know how should I handle Errors such as 4xx, 5xx network status code and then, them return my Custom Error Object!
My Login ViewModel :
func getAccessToken() {
let network = NetworkRequest()
network.logInRequest(tokenType: .guest, token: "cce577f6021608", secondKey: "09128147040", client: "cce577f6021608bc31424d209cbf5120c3683191").subscribe(onNext: { loginData in
self.token.onNext(loginData.access_token)
}, onError: { error in
print("The Error is: \(error.localizedDescription)")
}, onCompleted: {
print("Its Completed")
}).disposed(by: bag)
}
My network layer function:
class NetworkRequest: NSObject {
var rxProvider: MoyaProvider<WebServiceAPIs>
override init() {
rxProvider = MoyaProvider<WebServiceAPIs>( plugins: [ NetworkLoggerPlugin(verbose:true) ])
}
func logInRequest(tokenType: accessTokenTypeEnum, token: String, secondKey: String, client: String) -> Observable<LoginModel> {
return rxProvider.rx
.request(WebServiceAPIs.getAccessToken(tokenType: tokenType.rawValue, token: token, secondKey: secondKey, client: client))
.filterSuccessfulStatusCodes()
.catchError({ error -> Observable<NetworkError> in
return //Observable.just() => I want to return a custom network error as obserable
})
.map(LoginModel.self, atKeyPath: nil, using: JSONDecoder(), failsOnEmptyData: true).asObservable()
}
}
thanks for any help
In my experience, '.materialize()' operator is the perfect solution for handling HTTP errors.
Instead of separate events for success and error you get one single wrapper event with either success or error nested in it.
Moya returns MoyaError enum in error block which you can handle by extracting the error type using switch on MoyaError and then using statusCode to convert to NetworkError enum
func logInRequest(tokenType: accessTokenTypeEnum, token: String, secondKey: String, client: String) -> Observable<LoginModel> {
return sharedProvider.rx
.request(WebServiceAPIs.getAccessToken(tokenType: tokenType.rawValue, token: token, secondKey: secondKey, client: client))
.filterSuccessfulStatusCodes()
.catchError({ [weak self] error -> Observable<NetworkError> in
guard let strongSelf = self else { return Observable.empty() }
if let moyaError = error as? MoyaError {
let networkError = self?.createNetworkError(from: moyaError)
return Observable.error(networkError)
} else {
return Observable.error(NetworkError.somethingWentWrong(error.localizedDescription))
}
})
.map(LoginModel.self, atKeyPath: nil, using: JSONDecoder(), failsOnEmptyData: true).asObservable()
}
func createNetworkError(from moyaError: MoyaError) -> NetowrkError {
switch moyaError {
case .statusCode(let response):
return NetworkError.mapError(statusCode: response.statusCode)
case .underlying(let error, let response):
if let response = response {
return NetworkError.mapError(statusCode: response.statusCode)
} else {
if let nsError = error as? NSError {
return NetworkError.mapError(statusCode: nsError.code)
} else {
return NetworkError.notConnectedToInternet
}
}
default:
return NetworkError.somethingWentWrong("Something went wrong. Please try again.")
}
}
You can create your custom NetworkError enum like below which will map statusCode to custom NetworkError enum value. It will have errorDescription var which will return custom description to show in error view
enum NetworkError: Swift.Error {
case unauthorized
case serviceNotAvailable
case notConnectedToInternet
case somethingWentWrong(String)
static func mapError(statusCode: Int) -> NetworkError {
switch statusCode {
case 401:
return .unauthorized
case 501:
return .serviceNotAvailable
case -1009:
return .notConnectedToInternet
default:
return .somethingWentWrong("Something went wrong. Please try again.")
}
}
var errorDescription: String {
switch self {
case .unauthorized:
return "Unauthorised response from the server"
case .notConnectedToInternet:
return "Not connected to Internet"
case .serviceNotAvailable:
return "Service is not available. Try later"
case .somethingWentWrong(let errorMessage):
return errorMessage
}
}
}
I am trying to implement a function that handles Network & API errors, my problem is how to emit an observable again after filterSuccessfulStatusCodes().
The main issue I have is that I am not sure if this approach is correct, after the first subscribe.
The current error I have in this code is : Extra argument 'onError' in call
func Request<T: Decodable>(_ request: APIManager) ->
Observable<Result<T>> {
provider.rx
.request(request)
.subscribe(onSuccess: { (response) in
try response.filterSuccessfulStatusCodes()
return Observable.just(response)
.subscribe { event in
switch event {
case .success:
.map(RequestResponse<T>.self)
.map { $0.result! }
.asObservable()
.map(Result.success)
.catchError { error in
return .just(.error(withMessage: "Error \(error)"))
}
case .error:
print("error")
}
}
}, onError: { (error) in
print("network request error")
}, onCompleted: {
print("network request on completed")
}).disposed(by: disposeBag)
}
struct RequestResponse<T: Decodable> {
let statusCode: Int
let statusMessage: String
let success: Bool
let result: T?
let errorBag: String?
}
enum Result<T: Decodable> {
case success(T)
case error(withMessage: String)
}
You can try something like, which converts Single to Observable then call filterSuccessfulStatusAndRedirectCodes and you can handle the errors in catchError closure
func Request<T: Decodable>(_ request: APIManager) -> Observable<Result<T>> {
self.sharedProvider.rx
.request(request)
.asObservable()
.filterSuccessfulStatusAndRedirectCodes()
.map(RequestResponse<T>.self)
.map { Result.success }
.catchError { error in
if let moyaError = error as? MoyaError {
return Objservable.error(handleNetworkError(withMoyaError: moyaError))
} else {
return Observable.error(error)
}
}
}
I want to fulfill a promise with nil but I get error message that I cant here is my code
public static func getCurrentDevice() -> Promise<Device>{
if let identity:[String:AnyObject] = auth?.get("identity") as! [String:AnyObject] {
if let uuididentity = identity["uuid"]{
return Promise { fulfill, reject in
Alamofire.request( Router.getDevice(uuididentity as! String) )
.responseObject { (response: Response<Device, NSError>) in
switch response.result{
case .Success(let data):
fulfill(data)
case .Failure(let error):
return reject(error)
}
}
}
}
}
return Promise { fulfill, reject in
fulfill(nil)
}
}
I get compiler error
Cannot invoke initializer for type 'Promise<>' with an argument list of type '((, _) -> _)'
If the promise doesn't return a value you should use () aka Void:
return Promise { fulfill, reject in
fulfill(())
}
If this doesn't work (I didn't test it) you could try annotate it:
return Promise<()> { fulfill, reject in
fulfill(())
}
(Note that () is the only value of type () aka Void)
Here you go. The question mark makes it for you in the return of the method.
public static func getCurrentDevice() -> Promise<Device?> {
//... logic here
let isDeviceEmpty: Bool
if isDeviceEmpty {
fulfill(nil)
} else {
fulfill(device)
}
}
I'm trying to reuse Alamofire's Result type for own API callbacks.
Here is a shortened version of result type I'm using:
public enum Result<Value> {
case Success(Value)
case Failure(NSData?, ErrorType)
}
So for my API calls I'm using it in completion blocks:
func getUserContent(userId: String, completion: (result: Result<User>) -> Void) {
Alamofire.request(UserRouter.GetUser(userId))
.validate()
.responseJSON { (request, response, result) -> Void in
switch result {
case .Failure(_, let error):
completion(result: .Failure(nil, error))
case .Success(let value):
if let responseDict = value as? [String: AnyObject] {
do {
// synchronous call which parses response and
// creates User struct instance or throws exception
let user = try self.processUserResponse(responseDict)
completion(result: .Success(user))
} catch(let error) {
completion(result: .Failure(nil, error))
}
} else {
completion(result: .Failure(nil, MyAPIError.WrongResponseFormat))
}
}
}
}
I think its perfectly fits here but there is one issue. I have some calls with completion blocks which supposed to return either .Success with no value or .Failure.
E.g. deleteUser method should look something like:
func deleteUser(userId: String, completion: (result: Result<nil>) -> Void) {
// ... do some stuff here
}
so when I call it later I can do:
deleteUser(userId) { (result) -> Void in
switch result {
case .Success:
print("success")
case .Failure(nil, let error):
print("Error: \(error)")
}
}
But I can't create "empty" .Success. Result<nil> of course gives me a compile error. But I don't have any type to pass to some of .Success cases. Does anyone has a better solution that defining another Result Type with no type on .Success?
#ogreswamp is right! You can omit the type requirement with Void. Type Void is simply an empty tuple, in effect a tuple with zero elements, which can be written as (). Here is an example:
enum Result<T, E: ErrorType> {
case Success(T)
case Failure(E)
init(value: T) {
self = .Success(value)
}
init(error: E) {
self = .Failure(error)
}
}
Use this like
enum AuthenticationError: ErrorType {
case MissingEmail
case InvalidPassword
}
func signUp(email email: String, password: String) -> Result<Void, AuthenticationError>
You can return the result like
// Success
return Result(value: ())
// Failure
return Result(error: .InvalidPassword)
And finally, check the result
switch result {
case .Success(_):
print("Request SignUp was sent")
case .Failure(let error):
switch error {
case .InvalidEmail:
print("Invalid email")
default:
break
}
}
You will need to define your own Result type. Also note that Alamofire 3.0 uses a much different Result type that may better suit your needs.