How to use swift-4 Promises then, done, catch and other blocks - ios

I would like to learn about promises in swift-4. How to use multiple then statements and done, catch blocks.
Here I am trying to get the value from the promise. But I'm getting errors. Could someone help me to understand promises?
Here is my code.
import UIKit
import PromiseKit
struct User {
var firstname : String?
var lastname : String?
}
struct APIError {
var message : String?
}
class ViewController : UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let userPromise : Promise = self.getUserDetails()
userPromise.then { user -> Void in
//print(user.f)
}
}
func getUserDetails()->Promise<User> {
return Promise<User> { resolve in
let user = User(firstname: "Scot", lastname: "dem")
if ((user.firstname?.count) != nil) {
resolve.fulfill(user)
} else {
let error = APIError(message: "User not valid")
resolve.reject(error as! Error)
}
}
}
}
Once I get the user details I want to make a full name, uppercase promises which are dependent on userPromise.
I would like to use multiple then, done, finally blocks. Just want to understand usage.
Why I'm getting an error here when we use userPromise.then { user -> Void in
what should I give inside the block

In PromiseKit 6, then can no longer return Void. This is mainly due to the tuplegate issue in Swift 4.
Quote from PromieKit 6 Release News
With PromiseKit our then did multiple things, and we relied on Swift
to infer the correct then from context. However with multiple line
thens it would fail to do this, and instead of telling you that the
situation was ambiguous it would invent some other error. Often the
dreaded cannot convert T to AnyPromise. We have a troubleshooting
guide to combat this but I believe in tools that just work, and when
you spend 4 years waiting for Swift to fix the issue and Swift doesn’t
fix the issue, what do you do? We chose to find a solution at the
higher level.
So we split then into then, done and map.
then is fed the previous promise value and requires you return a promise.
done is fed the previous promise value and returns a Void promise (which is 80% of chain usage)
map is fed the previous promise value and requires you return a non-promise, ie. a value.
Hence .then { (user) -> Void in is no longer valid and that's why you're getting an error.
It's now designed to return a Thenable like so:
userPromise.then { user -> Promise<User> in
//return yet another promise
}
The .then that used to return Void is now it's own .done.
i.e:
userPromise.done { (user) in
print(user)
}
So when you mix 'em up:
We get:
userPromise
.then { (user) -> Promise<User> in
//return another Promise
return self.uppercasePromise(on: user)
}
.done { (user) in
/*
Depending on your sequence, no more promises are left
and you should have a matured user object by now
*/
print(user)
}
.catch { (error) in
print(error)
}
.finally {
print("finally")
}
func uppercasePromise(on user: User) -> Promise<User> {
/*
Didn't understand your statement but do whatever you meant when you said:
"Once I get the user details I want to make a full name, "
uppercase promises which are dependent on userPromise."
*/
return Promise { seal in
seal.fulfill(user)
}
}

Related

Using Swift Task inside a flatmap for async/await

I'm trying to receive an async function return value inside a flatMap and wrap it under a Task to allow for async functionality but I'm having this error when I'm trying to access the Task value:
'async' property access in a function that does not support concurrency
How do I go about returning the value?
Playground
import UIKit
public func isEvenNumber(num:(Int)) async -> Result<Int, Error> {
if num%2 == 0 {
print("EVEN")
return .success(1)
}
print("ODD")
return .success(0)
}
func profileAsyncFunc() async -> Result<Bool, Error> {
return await isEvenNumber(num: 3)
.flatMap{ _ -> Result<Bool,Error> in
Task {
return await testAsyncFunc()
}.value
}
}
func testAsyncFunc() async -> Result<Bool, Error> {
let basicTask = Task { () -> Result<Bool, Error> in
.success(true)
}
return await basicTask.value
}
Task {
await profileAsyncFunc()
}
A few observations:
Swift concurrency simplifies this quite a bit. No flatMap is needed.
The isEvenNumber is not really asynchronous unless you await something. Adding async qualifier does not make it asynchronous. It only means that you could add some asynchronous code in the function, but only if you await something within the function.
In Swift concurrency, the Result type is no longer needed and quickly becomes syntactic noise. In an async function, you simply either return a value, or throw an error.
Let us assume that isEvenNumber is just a placeholder for something sufficiently complicated that you did need to make it asynchronous to avoid blocking the current actor. Furthermore, while you do not throw errors, let us assume that this is a proxy for some method that would.
If all of that were the case, you would need to make sure that you get it off the current actor with a detached task, and then it would be asynchronous, as you would await its value. And I am arbitrarily defining isEven to throw an error if the value is negative. (Clearly, this is not a reasonable reason to throw an error, but included it for illustrative purposes.)
Thus:
enum NumberError: Error {
case negative
}
// This is not sufficiently computationally intensive to warrant making it run
// asynchronously, but let us assume that it was. You would do something like:
func isEven(_ num: Int) async throws -> Bool {
if num < 0 { throw NumberError.negative }
return await Task.detached {
num.isMultiple(of: 2)
}.value
}
func profileAsyncFunc() async throws -> Bool {
try await isEven(3)
}
Then you could do:
Task {
let isEven = try await profileAsyncFunc()
print(isEven)
}
You said:
I'm trying to receive an async function return value inside a flatMap …
Why? You do not need flatMap in Swift concurrency, as you might use in Combine.
If you are simply trying to adopt async-await, you can just excise this from your code. If there is some other problem that you are trying to solve by introducing flatMap, please edit the question providing a more complete example, and explain the intended rationale for flatMap.
As it stands, the example is sufficiently oversimplified that it makes it hard for us to understand the problem you are really trying to solve.

Why this produces compiler error in Combine?

just trying to implement SwiftUI and Combine in my new project.
But stuck in this:
func task() -> AnyPublisher<Int, Error> {
return AnyPublisher { subscriber in
subscriber.receive(Int(arc4random()))
subscriber.receive(completion: .finished)
}
}
This produces the following compiler error:
Type '(_) -> ()' does not conform to protocol 'Publisher'
Why?
Update
Actually Random here is just as an example. The real code will look like this:
func task() -> AnyPublisher<SomeCodableModel, Error> {
return AnyPublisher { subscriber in
BackendCall.MakeApiCallWithCompletionHandler { response, error in
if let error == error {
subscriber.receive(.failure(error))
} else {
subscriber.receive(.success(response.data.filter))
subscriber.receive(.finished)
}
}
}
}
Unfortunately, I don't have access to BackendCall API since it is private.
It's kind of pseudocode but, it pretty close to the real one.
You cannot initialise an AnyPublisher with a closure accepting a Subscriber. You can only initialise an AnyPublisher from a Publisher. If you want to create a custom Publisher that emits a single random Int as soon as it receives a subscriber and then completes, you can create a custom type conforming to Publisher and in the required method, receive(subscriber:), do exactly what you were doing in your closure.
struct RandomNumberPublisher: Publisher {
typealias Output = Int
typealias Failure = Never
func receive<S>(subscriber: S) where S : Subscriber, Failure == S.Failure, Output == S.Input {
subscriber.receive(Int.random(in: 0...Int.max))
subscriber.receive(completion: .finished)
}
}
Then in your task method, you simply need to create a RandomNumberPublisher and then type erase it.
func task() -> AnyPublisher<Int, Never> {
return RandomNumberPublisher().eraseToAnyPublisher()
}
If all you want is a single random value, use Just
fun task() -> AnyPublisher<Int, Never> {
return Just(Int.random(in: 0...Int.max)).eraseToAnyPublisher()
}
Sidenote: don't use Int(arc4random()) anymore.
You're likely better off wrapping this in a Future publisher, possibly also wrapped with Deferred if you want it to response when subscriptions come in. Future is an excellent way to wrap external async API calls, especially ones that you can't fully control or otherwise easily adapt.
There's an example in Using Combine for "wrapping an async call with a Future to create a one-shot publisher" that looks like it might map quite closely to what you're trying to do.
If you want it to return more than a single value, then you may want to compose something out of PassthoughSubject or CurrentValueSubject that gives you an interface of -> AnyPublisher<YourType, Error> (or whatever you're looking for).

Run 3 observables sequentially, using result from first in the last one

CONTEXT
I would like to run 3 different operations sequentially using RxSwift:
Fetch products
When products fetching is done, delete cache
When cache delete is done, save new cache with products from step 1
These are the function definitions in my services:
struct MyService {
static func fetchProducts() -> Observable<[Product]> {...}
static func deleteCache() -> Observable<Void> {...}
static func saveCache(_ products: [Product]) -> Observable<Void> {...}
}
I implement that behavior usually with flatMapLatest.
However, I will lose the result of the 1st observable ([Product]) with that approach, because the operation in the middle (deleteCache) doesn't receive arguments and returns Void when completed.
struct CacheViewModel {
static func refreshCache() -> Observable<Void> {
return MyService.fetchProducts()
.flatMapLatest { lostProducts in MyService.deleteCache() }
.flatMapLatest { MyService.saveCache($0) } // Compile error*
}
// * Cannot convert value of type 'Void' to expected argument type '[Product]'
}
The compile error is absolutely fair, since the operation in the middle 'breaks' the passing chain for the first result.
QUESTION
What mechanism is out there to achieve this serial execution with RxSwift, accumulating results of previous operations?
service
.fetchProducts()
.flatMap { products in
return service
.deleteCache()
.flatMap {
return service
.saveCache(products)
}
}
The easiest solution would be, just to return a new Observable of type Observable<Products> using the static method in the Rx framework just within the second flatMap(), passing in the lostProducts you captured in the flatmap-closure, i.e.:
static func refreshCache() -> Observable<Void> {
return MyService.fetchProducts()
.flatMapLatest { lostProducts -> Observable<[Product]> in
MyService.deleteCache()
return Observable.just(lostProducts)
}
.flatMapLatest { MyService.saveCache($0) } // No compile error
}
That way you are not losing the result of the first call in the flatMap, but just pass it through after having cleared the cache.
you can use do(onNext:) for deleting the cache data and then in flatMapLatest you can save the products. Optionally SaveCache and DeleteCache should return Completable so that you can handle error if the save or delete operation failed.
struct CacheViewModel {
static func refreshCache() -> Observable<Void> {
return MyService.fetchProducts()
.do(onNext: { _ in
MyService.deleteCache()
}).flatMap { products in
MyService.saveCache(products)
}
}
}

Return a completable in RxSwift without using a create block

I have a Completable being returned from a simple function.
This is not an async call, so I just need to return a succcessful completion or error depending on a conditional (using Rx here so I can tie into other Rx usages):
func exampleFunc() -> Completable {
if successful {
return Completable.just() // What to do here???
} else {
return Completable.error(SomeErrorType.someError)
}
}
The error case works pretty easily, but am having a block on how to just return a successful completable (without needing to .create() it).
I was thinking I just need to use Completable's .just() or .never(), but just is requiring a parameter, and never doesn't seem to trigger the completion event.
.empty() is the operator I was looking for!
Turns out, I had mixed up the implementations of .never() and .empty() in my head!
.never() emits no items and does NOT terminate
.empty() emits no items but does terminates normally
So, the example code above works like this:
func exampleFunc() -> Completable {
if successful {
return Completable.empty()
} else {
return Completable.error(SomeErrorType.someError)
}
}
Here is the documentation on empty/throw/never operators.
I would be more inclined to do the following:
func example() throws {
// do something
if !successful {
throw SomeErrorType.someError
}
}
Then in order to tie it into other Rx code, I would just use map as in:
myObservable.map { try example() }
But then, mapping over a Completable doesn't work because map's closure only gets called on next events. :-(
I tend to avoid Completable for this very reason, it doesn't seem to play well with other observables. I prefer to use Observable<Void> and send an empty event before the completed...
Something like this:
let chain = Observable<Void>.just()
let foo = chain.map { try example() }
foo.subscribe { event in print(event) }

Simplest way to throw an error/exception with a custom message in Swift?

I want to do something in Swift that I'm used to doing in multiple other languages: throw a runtime exception with a custom message. For example (in Java):
throw new RuntimeException("A custom message here")
I understand that I can throw enum types that conform to the ErrorType protocol, but I don't want to have to define enums for every type of error I throw. Ideally, I'd like to be able mimic the example above as closely as possible. I looked into creating a custom class that implements the ErrorType protocol, but I can't even figure out that what that protocol requires. Ideas?
The simplest approach is probably to define one custom enum with just one case that has a String attached to it:
enum MyError: ErrorType {
case runtimeError(String)
}
Or, as of Swift 4:
enum MyError: Error {
case runtimeError(String)
}
Example usage would be something like:
func someFunction() throws {
throw MyError.runtimeError("some message")
}
do {
try someFunction()
} catch MyError.runtimeError(let errorMessage) {
print(errorMessage)
}
If you wish to use existing Error types, the most general one would be an NSError, and you could make a factory method to create and throw one with a custom message.
The simplest way is to make String conform to Error:
extension String: Error {}
Then you can just throw a string:
throw "Some Error"
To make the string itself be the localizedString of the error you can instead extend LocalizedError:
extension String: LocalizedError {
public var errorDescription: String? { return self }
}
#nick-keets's solution is most elegant, but it did break down for me in test target with the following compile time error:
Redundant conformance of 'String' to protocol 'Error'
Here's another approach:
struct RuntimeError: Error {
let message: String
init(_ message: String) {
self.message = message
}
public var localizedDescription: String {
return message
}
}
And to use:
throw RuntimeError("Error message.")
Swift 4:
As per:
https://developer.apple.com/documentation/foundation/nserror
if you don't want to define a custom exception, you could use a standard NSError object as follows:
import Foundation
do {
throw NSError(domain: "my error domain", code: 42, userInfo: ["ui1":12, "ui2":"val2"] )
}
catch let error as NSError {
print("Caught NSError: \(error.localizedDescription), \(error.domain), \(error.code)")
let uis = error.userInfo
print("\tUser info:")
for (key,value) in uis {
print("\t\tkey=\(key), value=\(value)")
}
}
Prints:
Caught NSError: The operation could not be completed, my error domain, 42
User info:
key=ui1, value=12
key=ui2, value=val2
This allows you to provide a custom string (the error domain), plus a numeric code and a dictionary with all the additional data you need, of any type.
N.B.: this was tested on OS=Linux (Ubuntu 16.04 LTS).
Check this cool version out. The idea is to implement both String and ErrorType protocols and use the error's rawValue.
enum UserValidationError: String, Error {
case noFirstNameProvided = "Please insert your first name."
case noLastNameProvided = "Please insert your last name."
case noAgeProvided = "Please insert your age."
case noEmailProvided = "Please insert your email."
}
Usage:
do {
try User.define(firstName,
lastName: lastName,
age: age,
email: email,
gender: gender,
location: location,
phone: phone)
}
catch let error as User.UserValidationError {
print(error.rawValue)
return
}
Simplest solution without extra extensions, enums, classes and etc.:
NSException(name:NSExceptionName(rawValue: "name"), reason:"reason", userInfo:nil).raise()
In case you don't need to catch the error and you want to immediately stop the application you can use a fatalError:
fatalError ("Custom message here")
Based on #Nick keets answer, here is a more complete example:
extension String: Error {} // Enables you to throw a string
extension String: LocalizedError { // Adds error.localizedDescription to Error instances
public var errorDescription: String? { return self }
}
func test(color: NSColor) throws{
if color == .red {
throw "I don't like red"
}else if color == .green {
throw "I'm not into green"
}else {
throw "I like all other colors"
}
}
do {
try test(color: .green)
} catch let error where error.localizedDescription == "I don't like red"{
Swift.print ("Error: \(error)") // "I don't like red"
}catch let error {
Swift.print ("Other cases: Error: \(error.localizedDescription)") // I like all other colors
}
Originally published on my swift blog: http://eon.codes/blog/2017/09/01/throwing-simple-errors/
First, let's see few usage examples, then how to make those samples work (Definition).
Usage
do {
throw MyError.Failure
} catch {
print(error.localizedDescription)
}
Or more specific style:
do {
try somethingThatThrows()
} catch MyError.Failure {
// Handle special case here.
} catch MyError.Rejected {
// Another special case...
} catch {
print(error.localizedDescription)
}
Also, categorization is possible:
do {
// ...
} catch is MyOtherErrorEnum {
// If you handle entire category equally.
} catch let error as MyError {
// Or handle few cases equally (without string-compare).
switch error {
case .Failure:
fallthrough;
case .Rejected:
myShowErrorDialog(error);
default:
break
}
}
Definition
public enum MyError: String, LocalizedError {
case Failure = "Connection fail - double check internet access."
case Rejected = "Invalid credentials, try again."
case Unknown = "Unexpected REST-API error."
public var errorDescription: String? { self.rawValue }
}
Pros and Cons
Swift defines error variable automatically, and a handler only needs to read localizedDescription property.
But that is vague, and we should use "catch MyError.Failure {}" style instead (to be clear about what-case we handle), although, categorization is possible as shown in usage example.
Teodor-Ciuraru's answer (which's almost equal) still needs a long manual cast (like "catch let error as User.UserValidationError { ... }").
The accepted categorization-enum approach's disadvantages:
Is too vague as he comments himself, so that catchers may need to compare String message!? (just to know exact error).
For throwing same more than once, needs copy/pasting message!!
Also, needs a long phrase as well, like "catch MyError.runtimeError(let errorMessage) { ... }".
The NSException approach has same disadvantages of categorization-enum approach (except maybe shorter catching paragraph), also, even if put in a factory method to create and throw, is quite complicated.
Conclusion
This completes other existing solutions, by simply using LocalizedError instead of Error, and hopefully saves someone from reading all other posts like me.
(My laziness sometimes causes me a lot of work.)
Testing
import Foundation
import XCTest
#testable import MyApp
class MyErrorTest: XCTestCase {
func testErrorDescription_beSameAfterThrow() {
let obj = MyError.Rejected;
let msg = "Invalid credentials, try again."
XCTAssertEqual(obj.rawValue, msg);
XCTAssertEqual(obj.localizedDescription, msg);
do {
throw obj;
} catch {
XCTAssertEqual(error.localizedDescription, msg);
}
}
func testThrow_triggersCorrectCatch() {
// Specific.
var caught = "None"
do {
throw MyError.Rejected;
} catch MyError.Failure {
caught = "Failure"
} catch MyError.Rejected {
caught = "Successful reject"
} catch {
caught = "Default"
}
XCTAssertEqual(caught, "Successful reject");
}
}
Other tools:
#1 If implementing errorDescription for each enum is a pain, then implement it once for all, like:
extension RawRepresentable where RawValue == String, Self: LocalizedError {
public var errorDescription: String? {
return self.rawValue;
}
}
Above only adds logic to enums that already extend LocalizedError (but one could remove "Self: LocalizedError" part, to make it apply to any string-enum).
#2 What if we need additional context, like FileNotFound with file-path associated? see my other post for that:
https://stackoverflow.com/a/70448052/8740349
Basically, copy and add LocalizedErrorEnum from above link into your project once, and reuse as many times as required with associative-enums.
I like #Alexander-Borisenko's answer, but the localized description was not returned when caught as an Error. It seems that you need to use LocalizedError instead:
struct RuntimeError: LocalizedError
{
let message: String
init(_ message: String)
{
self.message = message
}
public var errorDescription: String?
{
return message
}
}
See this answer for more details.
First, let's see LocalizedErrorEnum's usage examples, then how to make those samples work (in Sorce-code section).
Usage
do {
let path = "/my/path/to/file.txt";
throw MyErrorCategory.FileNotFound(
atPath: path
);
} catch {
print(error.localizedDescription);
}
Output:
Failed to find file. {
atPath: /my/path/to/file.txt
}
Definition:
public enum MyErrorCategory: LocalizedErrorEnum {
case FileNotFound(String = "Failed to find file.", atPath: String)
case Connection(String = "Connection fail - double check internet access.")
}
The first argument is treated as message (in LocalizedErrorEnum enum).
Features required (background)
#1 Firstly, I want messages without copy/pasting, and that with ability to catch a group of different error-cases, without listing each-case (solution, enum is pretty unique without copy/paste need, and each enum can be considered another group).
#2 Secondly, some errors like "FileNotFound" need to have variable context/details, like for file-path (but Raw-Value enum does not support instance-variables, and unlike #1, the built-in enum is not the solution).
#3 Lastly, I want to be able to catch each case separately, NOT catching entire struct and/or class then doing switch inside the catch, and want to avoid forgeting the rethrow of cases we don't handle.
Source-code (solution meeting requirements)
Simply, copy and add LocalizedErrorEnum from below into your project once, and reuse as many times as required with associative-enums.
public protocol LocalizedErrorEnum: LocalizedError {
var errorDescription: String? { get }
}
extension LocalizedErrorEnum {
public var errorDescription: String? {
if let current = Mirror(reflecting: self).children.first {
let mirror = Mirror(reflecting: current.value);
// Initial error description.
let message = mirror.children.first?.value as? String
?? current.label ?? "Unknown-case";
var context = "";
// Iterate additional context.
var i = 0;
for associated in mirror.children {
if i >= 1 {
if let text = associated.value as? String {
context += "\n ";
if let label: String = associated.label {
context += "\(label): "
}
context += text;
}
}
i += 1;
}
return context.isEmpty ? message : (
message + " {" + context + "\n}"
);
}
return "\(self)";
}
}
Note that as mentioned on my profile, using above code under Apache 2.0 license is allowed as well (without attribution need).
See also my other answer if you don't need additional context-variable with error (or for a comparison with other approaches).
Throwing code should make clear whether the error message is appropriate for display to end users or is only intended for developer debugging. To indicate a description is displayable to the user, I use a struct DisplayableError that implements the LocalizedError protocol.
struct DisplayableError: Error, LocalizedError {
let errorDescription: String?
init(_ description: String) {
errorDescription = description
}
}
Usage for throwing:
throw DisplayableError("Out of pixie dust.")
Usage for display:
let messageToDisplay = error.localizedDescription
I would like to suggest a variation of some of the proposed solutions:
public enum MyError: Error {
var localizedDescription: String {
get {
switch(self) {
case .network(let message, let code):
return "\(message) (\(code))"
case .invalidInput(message: let message):
return message
}
}
}
case network(message: String, code: Int)
case invalidInput(message: String)
}
It's a little more work to create but it provides the best of all worlds:
It's an enum so it can be used in a switch statement.
All the errors must be created with a message that can be a different one even for the same types of error (unlike enums that extend String)
It provides the message under the localizedDescription that every developer is expecting.
To reiterate #pj-finnegan's answer, several people's comments, and the footnote of the accepted answer…
I prefer several other answers provided here (if i'm looking for best practices). But if I'm answering the question as asked, the simplest way to do this (IFF you are are in iOS/macOS/…) is to used the bridged type NSError.
func myFunction(meNoLikey:Bool) throws {
guard meNoLikey == false else {
throw NSError(domain: "SubsystemOfMyApp", code: 99, userInfo: [NSLocalizedDescriptionKey: "My Message!"] )
}
// safe to carry on…
}
You can decide to have meaningful domains or codes, or not. The userInfo key NSLocalizedDescriptionKey is the only thing needed to deliver the message you request.
Look up NSError.UserInfoKey for any additional details you want to provide in the userInfo. You can also add anything you want, if you would like to deliver details to anyone catching the error.

Resources