I'm trying to validate user's email and password using two separate function calls.
Both functions return AnyPublisher publishers, and I use combineLatest to collect the returned values (each validate call returns the string it's validating) into a tuple.
Then I'm using flatMap to make a network request to sign the user up using the values returned by combineLatest, however the flatMap operator never gets called.
validator.validate(text: email, with: [.validEmail])
.combineLatest(validator.validate(text: password, with: [.notEmpty]))
.flatMap { credentials in
return self.userSessionRepository.signUp(email: credentials.0, password: credentials.1)
}
.sink(receiveCompletion: { completion in
switch completion {
case .failure(let error):
print(error)
self.indicateErrorSigningIn(error)
case .finished:
self.goToSignInNavigator.navigateToOtp()
}
}, receiveValue: { _ in })
.store(in: &subscriptions)
signUp(email:password:) returns AnyPublisher
Here's the validator function:
public func validate(text: String, with rules: [Rule]) -> AnyPublisher<String, ErrorMessage> {
rules.publisher
.compactMap { $0.check(text) }
.setFailureType(to: ErrorMessage.self)
.flatMap {
Fail<Void, ErrorMessage>(error: ErrorMessage(title: "Error", message: $0.description))
}
.map { text }
.eraseToAnyPublisher()
}
And the signUp function:
public func signUp(email: String, password: String) -> AnyPublisher<Void, ErrorMessage> {
remoteAPI.signUp(email: email, password: password)
.flatMap(dataStore.save)
.mapError { error -> ErrorMessage in
return ErrorMessage(title: "Error", message: error.description)
}
.eraseToAnyPublisher()
}
It calls these two functions:
public func signUp(email: String, password: String) -> AnyPublisher<Confirmation, RemoteError> {
guard email == "john.doe#email.com" else {
return Fail<Confirmation, RemoteError>(error: .invalidCredentials)
.eraseToAnyPublisher()
}
return Just(Confirmation(otp: "", nonce: "abcd"))
.setFailureType(to: RemoteError.self)
.eraseToAnyPublisher()
}
public func save(confirmation: Confirmation) -> AnyPublisher<Void, RemoteError> {
self.nonce = confirmation.nonce
return Empty().eraseToAnyPublisher()
}
I'm not sure what's wrong, though it's likely my not understanding Combine enough, as I've just started learning it recently.
I have figured it out.
The problem was with the validate(text:with:) function.
In the event of an error, the function behaved correctly, but when there was no error the function wasn't emitting any value, and that's why flatMap or any other operator in the pipeline wasn't being invoked.
The reason it wasn't emitting any values boils down to how the check(_:) function, which is called in compactMap, works. It returns an optional string, which is an error message. But if there's no error, there's no string, and thus no value is emitted.
As a result, the call to .map { text } doesn't get evaluated, and the credential doesn't get returned.
I've changed the code to this and now the program behaves correctly:
public func validate(text: String, with rules: [Rule]) -> AnyPublisher<String, ErrorMessage> {
rules.publisher
.setFailureType(to: ErrorMessage.self)
.tryMap { rule -> String in
if let error = rule.check(text) {
throw ErrorMessage(title: "Error", message: error)
}
return text
}
.mapError { error -> ErrorMessage in
return error as! ErrorMessage
}
.eraseToAnyPublisher()
}
Related
I am trying to fetch a value from local database and when not found wants to save it to local database and return it. All of these I am doing in Interactor file and actual saving or fetching is done in seperate file. Following is my code:
public func fetchCode(codeId: String) -> AnyPublisher<String?, Error> {
//Get code from localdb
codeStorageProvider.fetchCode(codeId).flatMap { (code) -> AnyPublisher<String?, Error> in
if let code = code {
return Just(code).mapError{ $0 as Error }.eraseToAnyPublisher()
}
//If not found in db, Get code from server
let code = self.voucherCodeProvider.fetchVoucherCode(codeId: codeId)
return code.flatMap { code in
//save the code to local db
self.codeStorageProvider.saveVoucherCode(code, codeId)
return code
}.eraseToAnyPublisher()
//return code to presenter
}.eraseToAnyPublisher()
}
I am getting following error in flatMap:
Type of expression is ambiguous without more context
Can someone please help me?
If your saveVoucher doesn't return a Publisher and you are not interested in knowing when the operation is completed, there's no need to use flatMap but you can use handleEvents and call the side effect to save the code from there. Something like this:
func fetchLocal(codeId: String) -> AnyPublisher<String?, Error> {
return Empty().eraseToAnyPublisher()
}
func fetchRemote(codeId: String) -> AnyPublisher<String, Error> {
return Empty().eraseToAnyPublisher()
}
func saveLocal(code: String, codeId: String) {
// Save to BD
}
func fetch(codeId: String) -> AnyPublisher<String?, Error> {
return fetchLocal(codeId: codeId)
.flatMap { code -> AnyPublisher<String, Error> in
if let code = code {
return Just(code)
.setFailureType(to: Error.self)
.eraseToAnyPublisher()
} else {
return fetchRemote(codeId: codeId)
.handleEvents(receiveOutput: {
saveLocal(code: $0, codeId: codeId)
})
.eraseToAnyPublisher()
}
}
.map(Optional.some)
.eraseToAnyPublisher()
}
I have a Combine publisher like this:
enum RemoteError: Error {
case networkError(Error)
case parseError(Error)
case emptyResponse
}
func getPublisher(url: URL) -> AnyPublisher<Entiy, RemoteError> {
return URLSession.shared
.dataTaskPublisher(for: url)
.map(\.data)
.decode(type: RemoteResponse.self, decoder: decoder)
.mapError { error -> RemoteError in
switch error {
case is URLError:
return .networkError(error)
default:
return .parseError(error)
}
}
.map { response -> Entiy in
response.enitities.last
}
.eraseToAnyPublisher()
}
struct RemoteResponse: Codable {
let enitities: [Entity]
let numberOfEntries: Int
}
struct Entity {
}
By the above setting, the compiler complains because response.enitities.last can be nil. The question is can I replace nil with Empty publisher and if not can I replace it with error emptyResponse in Combine chain? The first option is preferable.
You have a couple of options here.
If you don't want the publisher to publish anything in case entities is empty, you can use coampactMap instead of map:
.compactMap { response in
response.entities.last
}
If you would rather publish an error in such a case you can use tryMap which allows you to throw an Error. You would need mapError to come after it:
.tryMap { response in
guard let entity = response.entities.last else {
throw RemoteError.emptyResponse
}
return entity
}
.mapError { error -> RemoteError in
switch error {
case is URLError:
return .networkError(error)
case is DecodingError:
return .parseError(error)
default:
return .emptyResponse
}
}
You need a flat map in order to map to another publisher:
.flatMap {
$0.enitities.last.publisher
}
Optional has a convenient publisher property that gives you a publisher that publishes only that value if the value is not nil, and an empty publisher if it is nil. This is only available in iOS 14+. If you are targeting a lower version, you need to do something like:
.flatMap { (response) -> AnyPublisher<Entity, Never> in
if let last = response.entities.last {
return Just(last).eraseToAnyPublisher()
} else {
return Empty(completeImmediately: true).eraseToAnyPublisher()
}
}
Following the answer from #Sweeper, Here edit with below iOS 14.0
.setFailureType(to: NSError.self)
.flatMap { (response) -> AnyPublisher<Entity, Never> in
if let last = response.entities.last {
return Just(last).eraseToAnyPublisher()
} else {
return Empty(completeImmediately: true).eraseToAnyPublisher()
}
NSError -> will be error type which you are returning
Happy coding 🚀
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")
})
Firstly, I'm aware that the implementation has changed in v6 and and I using the seal object as intended, the problem I'm having is that even when following the example to the letter it still gives me the old Cannot convert value of type '(_) -> CustomerLoginResponse' to expected argument type '(_) -> _' error.
Here is my function that returns the promise:
static func makeCustomerLoginRequest(userName: String, password: String) -> Promise<CustomerLoginResponse>
{
return Promise
{ seal in
Alamofire.request(ApiProvider.buildUrl(), method: .post, parameters: ApiObjectFactory.Requests.createCustomerLoginRequest(userName: userName, password: password).toXML(), encoding: XMLEncoding.default, headers: Constants.Header)
.responseXMLObject { (resp: DataResponse<CustomerLoginResponse>) in
if let error = resp.error
{
seal.reject(error)
}
guard let Xml = resp.result.value else {
return seal.reject(ApiError.credentialError)
}
seal.fulfill(Xml)
}
}
}
and here is the function that is consuming it:
static func Login(userName: String, password: String) {
ApiClient.makeCustomerLoginRequest(userName: userName, password: password).then { data -> CustomerLoginResponse in
}
}
You might have to provide more information if you want to chain more than one promises. In v6, you need to use .done if you don't want to continue the promises chain. If you have only one promise with this request then below is the correct implementation.
static func Login(userName: String, password: String) {
ApiClient.makeCustomerLoginRequest(userName: userName, password: password)
.done { loginResponse in
print(loginResponse)
}.catch { error in
print(error)
}
}
Remember, you have to return a promise if you are using .then until you break the chain by using .done. If you want to chain multiple promises then your syntax should look like this,
ApiClient.makeCustomerLoginRequest(userName: userName, password: password)
.then { loginResponse -> Promise<CustomerLoginResponse> in
return .value(loginResponse)
}.then { loginResponse -> Promise<Bool> in
print(loginResponse)
return .value(true)
}.then { bool -> Promise<String> in
print(bool)
return .value("hello world")
}.then { string -> Promise<Int> in
print(string)
return .value(100)
}.done { int in
print(int)
}.catch { error in
print(error)
}
Closed. This question is opinion-based. It is not currently accepting answers.
Want to improve this question? Update the question so it can be answered with facts and citations by editing this post.
Closed 1 year ago.
Improve this question
I’m practicing Swift and have a scenario (and a method) where the result could either be successful or a failure.
It’s a security service class. I have a method where I can authenticate with an email address and password, and want to either return a User instance if the credentials are correct, or throw some form of false value.
I’m a bit confused as my understanding of Swift methods is you need to specify a return type, so I have:
class SecurityService {
static func loginWith(email: String, password: String) -> User {
// Body
}
}
I’ve seen in Go and Node.js methods that return a “double” value where the first represents any errors, and the second is the “success” response. I also know that Swift doesn’t have things like errors or exceptions (but that may have changed since as I was learning an early version of Swift).
What would be the appropriate thing to do in this scenario?
If you want to handle errors that can happen during login process than use the power of Swift error handling:
struct User {
}
enum SecurityError: Error {
case emptyEmail
case emptyPassword
}
class SecurityService {
static func loginWith(email: String, password: String) throws -> User {
if email.isEmpty {
throw SecurityError.emptyEmail
}
if password.isEmpty {
throw SecurityError.emptyPassword
}
return User()
}
}
do {
let user = try SecurityService.loginWith1(email: "", password: "")
} catch SecurityError.emptyEmail {
// email is empty
} catch SecurityError.emptyPassword {
// password is empty
} catch {
print("\(error)")
}
Or convert to optional:
guard let user = try? SecurityService.loginWith(email: "", password: "") else {
// error during login, handle and return
return
}
// successful login, do something with `user`
If you just want to get User or nil:
class SecurityService {
static func loginWith(email: String, password: String) -> User? {
if !email.isEmpty && !password.isEmpty {
return User()
} else {
return nil
}
}
}
if let user = SecurityService.loginWith(email: "", password: "") {
// do something with user
} else {
// error
}
// or
guard let user = SecurityService.loginWith(email: "", password: "") else {
// error
return
}
// do something with user
Besides the standard way to throw errors you can use also an enum with associated types as return type
struct User {}
enum LoginResult {
case success(User)
case failure(String)
}
class SecurityService {
static func loginWith(email: String, password: String) -> LoginResult {
if email.isEmpty { return .failure("Email is empty") }
if password.isEmpty { return .failure("Password is empty") }
return .success(User())
}
}
And call it:
let result = SecurityService.loginWith("Foo", password: "Bar")
switch result {
case .Success(let user) :
print(user)
// do something with the user
case .Failure(let errormessage) :
print(errormessage)
// handle the error
}
Returning a result enum with associated values, throwing exception, and using a callback with optional error and optional user, although valid make an assumption of login failure being an error. However thats not necessarily always the case.
Returning an enum with cases for success and failure containing result and error respectively is almost identical as returning an optional User?. More like writing a custom optional enum, both end up cluttering the caller. In addition, it only works if the login process is synchronous.
Returning result through a callBack, looks better as it allows for the operation to be async. But there is still error handling right in front of callers face.
Throwing is generally preferred than returning an error as long as the scope of the caller is the right place to handle the error, or at least the caller has access to an object/method that can handle this error.
Here is an alternative:
func login(with login: Login, failure: ((LoginError) -> ())?, success: (User) -> ()?) {
if successful {
success?(user)
} else {
failure?(customError)
}
}
// Rename with exactly how this handles the error if you'd have more handlers,
// Document the existence of this handler, so caller can pass it along if they wish to.
func handleLoginError(_ error: LoginError) {
// Error handling
}
Now caller can; simply decide to ignore the error or pass a handler function/closure.
login(with: Login("email", "password"), failure: nil) { user in
// Ignores the error
}
login(with: Login("email", "password"), failure: handleLoginError) { user in
// Lets the error be handled by the "default" handler.
}
PS, Its a good idea to create a data structure for related fields; Login email and password, rather individually setting the properties.
struct Login {
typealias Email = String
typealias Password = String
let email: Email
let password: Password
}
To add an answer to this question (five years later), there’s a dedicated Result type for this exact scenario. It can return the type you want on success, or type an error on failure.
It does mean re-factoring some code to instead accept a completion handler, and then enumerating over the result in that callback:
class SecurityService {
static func loginWith(email: String, password: String, completionHandler: #escaping (Result<User, SecurityError>) -> Void) {
// Body
}
}
Then in a handler:
securityService.loginWith(email: email, password: password) { result in
switch result {
case .success(let user):
// Do something with user
print("Authenticated as \(user.name)")
case .failure(let error):
// Do something with error
print(error.localizedDescription)
}
}
I think that the result of calling loginWith could be derived from a network request, here is code that I could do in the scenario you presented:
Helper classes:
struct User {
var name: String
var email: String
}
class HTTP {
static func request(URL: String, method: String, params: [String: AnyObject], callback: (error: NSError?, result: [String:AnyObject]?) -> Void) -> Void {
// network request
}
}
class SecurityService {
static func loginWith(email: String, password: String, callback: (error: NSError?, user: User?) -> Void) -> Void {
let URL = ".."
let params = [
"email": email,
"password": password
]
HTTP.request(URL, method: "POST", params: params) { (error, result) in
if let error = error {
callback(error: error, user: nil)
} else {
guard let JSON = result else {
let someDomain = "some_domain"
let someCode = 100
let someInfo = [NSLocalizedDescriptionKey: "No results were sent by the server."]
let error = NSError(domain: someDomain, code: someCode, userInfo: someInfo)
callback(error: error, user: nil)
return
}
guard let name = JSON["name"] as? String, email = JSON["email"] as? String else {
let someDomain = "some_domain"
let someCode = 100
let someInfo = [NSLocalizedDescriptionKey: "No user properties were sent by the server."]
let error = NSError(domain: someDomain, code: someCode, userInfo: someInfo)
callback(error: error, user: nil)
return
}
let user = User(name: name, email: email)
callback(error: nil, user: user)
}
}
}
}
Using the SecurityService class:
SecurityService.loginWith("someone#email.com", password: "123456") { (error, user) in
if let error = error {
print(error)
} else {
guard let user = user else {
print("no user found.")
return
}
print(user)
}
}