قالب وردپرس درنا توس
Home / IOS Development / Promises in Swift for beginners

Promises in Swift for beginners



Everything you ever wanted to know about futures and promises. Beginner's Guide to Asynchronous Programming in Swift.


Sync vs. Async Run

Writing asynchronous code is one of the most difficult parts in building an app.

What is the difference between a synchronous and an asynchronous execution? Well, I already explained this in my Dispatch Framework tutorial, but here's a quick review. A synchronous function usually blocks the current thread and returns a little value later. A asynchronous function will immediately return and transfer the result value to a completion manager. You can use the GCD framework to perform tasks synchronized on async on a given queue. Let me show you a quick example:

  func aBlockingFunction () -> String {
sleep (.random (in: 1
... 3)) return "hello world!" } func syncMethod () -> String { return aBlockingFunction () } func asyncMethod (completion block: @escaping ((String) -> Void)) { DispatchQueue.global (qos: .background) .async { block (aBlockingFunction ()) } } print (syncMethod ()) print ("sync method returned") asyncMethod {value in print (value) } print ("async method returned") // "Hello world!" // "sync method returned" // "async method returned" // "Hello world!"

As you can see the async method runs entirely on a background queue, the function will not block the current thread. This is why the async method can return immediately, so you always see the return output before the last hello output. The Async method is completion block stored for later execution, which is why it is possible to call back and return the string value after the original function is returned.

What happens if you don't use another queue? The completion block will be executed on the current queue so that your function will block it. It's going to be a little asynchronous, but in reality you just move the return value to a completion block.

  func syncMethod () -> String {
return "hello world!"
}

func fakeAsyncMethod (completion block: ((String) -> Fied)) {
block ("hello world!")
}

print (syncMethod ())
print ("sync method returned")
fakeAsyncMethod {value in
print (value)
}
print ("false async method returned")

I really don't want to focus on completion blocks in this article, which can be a stand-alone post, but if you still have problems with the simultaneous model or you don't understand how tasks and threads work, you should read the linked articles.


Recall hell and pyramid of judgment

What is the problem of asynchronous code? Or what is the result of writing asynchronous code? The short answer is that you need to use completion blocks (recalls) to handle future results.

The long answer is that handling callbacks sucks. You need to be careful because in a block you can easily create a inventory cycle, so you have to pass your variables as weak or unknown references. Also, if you have to use several asynchronous methods, it will be a pain in the donkey. Example time! Tod

  struct Todo: Codable {
la id: Int
Leave the title: String
let finish: Bool
}

la url = URL (string: "https://jsonplaceholder.typicode.com/todos")!

URLSession.shared.dataTask (with: url) {data, answer, error in
if let error = wrong {
fatalError ("Network Error:" + error.localizedDescription)
}
guard let response = answer like? HTTPURLResponse other {
fatalError ("Not an HTTP response")
}
guard response.statusCode> = 200, response.statusCode <300 else {
fatalError ("Invalid HTTP status code")
}
guard let data = data otherwise {
fatalError ("No HTTP data")
}

do {
la todos = try JSONDecoder (). decode ([Todo]. even from: data)
print (todos)
}
catch {
fatalError ("JSON decoder error:" + error.localizedDescription)
}
}.resume()

The clip above is a simple async HTTP data request. As you can see, there are many optional values ​​involved, plus you need to do some JSON decoding if you want to use your own types. This is just a request, but what if you need to get detailed information from the first item? Let's write a helper! #no 🤫

  func request (_ url: URL completion: @escaping ((Data -> Invalid))) {
URLSession.shared.dataTask (with: url) {data, answer, error in
if let error = wrong {
fatalError ("Network Error:" + error.localizedDescription)
}
guard let response = answer like? HTTPURLResponse other {
fatalError ("Not an HTTP response")
}
guard response.statusCode> = 200, response.statusCode <300 else {
fatalError ("Invalid HTTP status code")
}
guard let data = data otherwise {
fatalError ("No HTTP data")
}
completion (data)
}.resume()
}


la url = URL (string: "https://jsonplaceholder.typicode.com/todos")!
request (url) {data in
do {
la todos = try JSONDecoder (). decode ([Todo]. even from: data)
guard let first = todos.first else {
return
}
la url = URL (string: "https://jsonplaceholder.typicode.com/todos/(first.id)")!
request (url) {data in
do {
la todo = try JSONDecoder (). decode (Todo.self, from: data)
print (todo)
}
catch {
fatalError ("JSON decoder error:" + error.localizedDescription)
}
}
}
catch {
fatalError ("JSON decoder error:" + error.localizedDescription)
}
}

See? My problem is that we slowly move down the rabbit hole. Now what if we have a third request? Absolutely not! Next, you need to deeper again at one level, plus you have to pass the required variables, e.g. a weak or unfamiliar view controller reference because at some point you need to update the entire user interface based on the outcome. There must be a better way to fix this.


Results vs. futures vs promises?

Type result was introduced in Swift 5 and it is extremely good to eliminate the optional factor from the equation. This means that you do not have to handle an optional data and an optional error type, but the result is one of them.

Futures basically represent a value in the future. For example, the underlying value may be a result, and it should have one of the following states:

  • waiting – no value yet, waiting for it …
  • met – success, now the result has a value
  • rejected – failed with an error

Per term, a term should not be written by the end user. This means that developers should not be able to create, fulfill or reject one. But if that's the case, and we follow the rules, how do we make futures?

We promise them. You must create a promise that is basically a wrapper around a future that can be written (fulfilled, rejected) or transformed as you like. You don't write futures, you make promises. But some frames allow you to return to the future value of a promise, but you shouldn't be able to write that future at all.

Enough theory, are you ready to fall in love with promises? 19


Promises 101 – A Beginner's Guide

Let's reflect on the previous example using my lifting frame!

  extension URLSession {

enum HTTPError: LocalizedError {
the case invalidResponse
case invalidStatusCode
case noData
}

func dataTask (URL: URL) -> Promise  {
return Promise  {[unowned self] fulfill, reject in
self.dataTask (with: url) {data, response, error in
if let error = wrong {
reject (wrong)
return
}
guard let response = answer like? HTTPURLResponse other {
reject (HTTPError.invalidResponse)
return
}
guard response.statusCode> = 200, response.statusCode < 300 else {
                    reject(HTTPError.invalidStatusCode)
                    return
                }
                guard let data = data else {
                    reject(HTTPError.noData)
                    return
                }
                fulfill(data)
            }.resume()
        }
    }
}

enum TodoError: LocalizedError {
    case missing
}

let url = URL(string: "https://jsonplaceholder.typicode.com/todos")!
URLSession.shared.dataTask(url: url)
.thenMap { data in
    return try JSONDecoder().decode([Todo].self, from: data)
}
.thenMap { todos -> Todo in
guard let first = todos.first else {
throw TodoError.missing
}
return first
}
.the {first in
la url = URL (string: "https://jsonplaceholder.typicode.com/todos/(first.id)")!
return URLSession.shared.dataTask (url: url)
}
.thenMap {data in
try JSONDecoder (). decode (Todo.self, from: data)
}
.onSuccess {todo in
print (todo)
}
.onFailure (queue: .main) {error in
print (error.localizedDescription)
}

What happened here? Well, I have created a legal version of the data task method that was implemented on the [URL45Session] object as an extension. Of course, you can return the HTTP result or just the status code plus the data if you need more info from the network layer. You can use a new response model or even a tuple. 19

Anyway, the more interesting part is the bottom half of the source. As you can see, I call the brand new dataTask method that returns a Promise object. As I mentioned before, a promise can be transformed. Or should I say: chained?

Chaining promises are the biggest advantage over recalls. The source code doesn't look like a pyramid anymore with crazy indentations and do-test blocks, but more like an action chain. In each step, you can transform your past performance value into something else. If you are familiar with some functional paradigms, it will be very easy to understand the following:

  • thenMap is a simple map on a promise
  • then is basically flatMap on a Promise
  • onSuccess is only called if everything was fine in the chain
  • onFailure is only called if an error occurred in the chain
  • always always runs independent of the outcome

If you want To get the main queue, you can just send it through a queue parameter, as I did with the onFailure method, but it works for each element of the chain. These features above are just the tip of the iceberg. You can also press in a chain, validate the result, put a timeout on it or restore from a failed promise.

] There is also a name area Promises for other useful methods, such as zip capable of zipping together 2, 3 or 4 different kinds of promises. Just as Promises.all method awaits the zip function until each promise is completed, it gives you the result of all the promises in a single block.

  // performs the same promises from the same type, e.g. [Promotion]
Promises.all (promises)
.thenMap {arrayOfResults in
// eg. [Data]
}
// zipping together different kinds of promises, eg. Proimse <[Todos]> Promise 
Promises.zip (lift1, lift2)
.thenMap {result1, result2 in
//e.g [Todos] Todo
}

It is also worth mentioning that there is a [first] delay timeout ] and a retry method under Promise's namespace. Feel free to bring these with you too, sometimes they are extremely useful and powerful as well. 💪


There are only two issues with promises

The first problem is cancellation. You can't just interrupt an ongoing promise. It's feasible, but it requires some advanced or some saying "hacky" techniques. The second is async / awaiting . To learn more about it, read the Chis Lattner Convention Manifesto, but since this is a beginner's guide, let's just say that these two keywords can add some syntactic sugar to your code. You no longer need extra (daMap, onSuccess, onFailure) lines so you can focus on your code. I really hope we get something like that in Swift 6, so I can waste my Promise library for good. Oh, by the way, libraries …


Promise libraries worth checking

My lifting implementation is far from perfect, but it's pretty simple (~ 450 lines of code) and it makes me very good. This blog post by @ khanlou helped me a lot to understand promises better, you should also read it! 19

There are many lifting libraries on github, but if I had to choose between them (instead my own implementation), I would definitely go with one of the following:

  • PromiseKit – The Most Popular [19659051] Promises by Google – feature rich, quite popular also
  • Promise by Khanlou – small but based on JavaScript Promises / A + spec
  • SwiftNIO – not an actual lifting library, but it has a beautifully written arrangement Loop-based lifting implementation under the hood [19659025] Pro tips: Don't try to make your own Promise framework because multi-threading is extremely difficult and you won't mess around with threads and locks.


    Promises is very addictive. When you start using them, you can't just go back and write async code with callbacks anymore. Make a promise today! 19


    External sources




Source link