Swift @Escaping and Completion Handler

Swift @escaping and Completion Handler

Swift Completion Handler Escaping & Non-Escaping:

As Bob Lee explains in his blog post Completion Handlers in Swift with Bob:

Assume the user is updating an app while using it. You definitely want
to notify the user when it is done. You possibly want to pop up a box
that says, “Congratulations, now, you may fully enjoy!”

So, how do you run a block of code only after the download has been
completed? Further, how do you animate certain objects only after a
view controller has been moved to the next? Well, we are going to find
out how to design one like a boss.

Based on my expansive vocabulary list, completion handlers stand for

Do stuff when things have been done

Bob’s post provides clarity about completion handlers (from a developer point of view it exactly defines what we need to understand).

@escaping closures:

When one passes a closure in function arguments, using it after the function’s body gets executed and returns the compiler back. When the function ends, the scope of the passed closure exist and have existence in memory, till the closure gets executed.

There are several ways to escaping the closure in containing function:

  • Storage: When you need to store the closure in the global variable, property or any other storage that exist in the memory past of the calling function get executed and return the compiler back.

  • Asynchronous execution: When you are executing the closure asynchronously on despatch queue, the queue will hold the closure in memory for you, can be used in future. In this case you have no idea when the closure will get executed.

When you try to use the closure in these scenarios the Swift compiler will show the error:

error screenshot

For more clarity about this topic you can check out this post on Medium.

Adding one more points , which every ios developer needs to understand :

  1. Escaping Closure : An escaping closure is a closure that’s called after the function it was passed to returns. In other words,
    it outlives the function it was passed to.
  2. Non-escaping closure : A closure that’s called within the function it was passed into, i.e. before it returns.

In Swift, What is the difference between (()-()) and @escaping () - Void?

Quick Excerpt


Escaping Closures

A closure is said to escape a function when the closure is passed as an argument to the function, but is called after the function returns. When you declare a function that takes a closure as one of its parameters, you can write @escaping before the parameter’s type to indicate that the closure is allowed to escape.

One way that a closure can escape is by being stored in a variable that is defined outside the function. As an example, many functions that start an asynchronous operation take a closure argument as a completion handler. The function returns after it starts the operation, but the closure isn’t called until the operation is completed—the closure needs to escape, to be called later. For example:

var completionHandlers: [() -> Void] = []
func someFunctionWithEscapingClosure(completionHandler: @escaping () -> Void) {
completionHandlers.append(completionHandler)
}

The someFunctionWithEscapingClosure(_:) function takes a closure as its argument and adds it to an array that’s declared outside the function. If you didn’t mark the parameter of this function with @escaping, you would get a compile-time error.”

“let alsoIncrementByTen = incrementByTen
alsoIncrementByTen()
// returns a value of 50

incrementByTen()
// returns a value of 60

The example above shows that calling alsoIncrementByTen is the same as calling incrementByTen. Because both of them refer to the same closure, they both increment and return the same running total.

Escaping Closures
A closure is said to escape a function when the closure is passed as an argument to the function, but is called after the function returns. When you declare a function that takes a closure as one of its parameters, you can write @escaping before the parameter’s type to indicate that the closure is allowed to escape.

One way that a closure can escape is by being stored in a variable that is defined outside the function. As an example, many functions that start an asynchronous operation take a closure argument as a completion handler. The function returns after it starts the operation, but the closure isn’t called until the operation is completed—the closure needs to escape, to be called later. For example:

var completionHandlers: [() -> Void] = []
func someFunctionWithEscapingClosure(completionHandler: @escaping () -> Void) {
completionHandlers.append(completionHandler)
}

The someFunctionWithEscapingClosure(_:) function takes a closure as its argument and adds it to an array that’s declared[…]”


Excerpt From: Apple Inc. “The Swift Programming Language (Swift 5.2).” Apple Books. https://books.apple.com/us/book/the-swift-programming-language-swift-5-2/id881256329


In other words, @escaping closures require the use of self, if you are trying to reference methods within the current object. i.e. Completion Closures of SKActions are a really good example of this.

And non escaping closure's don't require the use of self. Because they weren't used to reference an object.

Swift optional completion handler

Completion handlers are similar to function return values, but not the same. For example, compare the following functions:

/// 1. With return value
func createList(name: String) -> Response { }
/// 2. With completion handler
func createList(name: String, completion: @escaping (Response) -> Void) { }

In the first function, you'd get the return value instantly.

let response = barback.createList(name: name)
if response.status = 200 {
/// trigger some UI component
}

However, if you try the same for the second function, you'll get the Missing argument for parameter 'completion' in call error. That's because, well, you defined a completion: argument label. However, you didn't supply it, as matt commented.

Think of completion handlers as "passing in" a chunk of code into the function, as a parameter. You need to supply that chunk of code. And from within that chunk of code, you can access your Response.

                                           /// pass in chunk of code here
barback.createList(name: name, completion: { response in
/// access `response` from within block of code
if response.status = 200 {
/// trigger some UI component
}
})

Note how you just say barback.createList, not let result = barback.createList. That's because in the second function, with the completion handler, doesn't have a return value (-> Response).

Swift also has a nice feature called trailing closure syntax, which lets you omit the argument label completion:.

barback.createList(name: name) { response in
/// access `response` from within block of code
if response.status = 200 {
/// trigger some UI component
}
}

You can also refer to response, the closure's first argument, by using $0 (which was what I did in my comment). But whether you use $0 or supply a custom name like response is up to you, sometimes $0 is just easier to type out.

barback.createList(name: name) {
/// access $0 (`response`) from within block of code
if $0.status = 200 {
/// trigger some UI component
}
}

How do you escape a completionBlock in Swift to return to normal application flow?

not sure how to get Swift to say "Ok, the completion block is now done -- come back to the main program."

There's no way and no need to say that - it happens automatically.

static func loadBoard(completionHandler: @escaping (Board?, Error?) -> Void) {
PListFileLoader.getDataFrom(filename: "data.plist") { (data, error) in
// completion handler code here
}

// "back to main"
}

In your example, execution will get "back to main" in one of two ways, depending on whether PListFileLoader.getDataFrom is running asynchronously or not.

If it's running synchronously, the execution order will be:

  • your app calls loadBoard
  • this calls getDataFrom
  • which in turn calls its completion handler parameter
  • the completion handler runs
  • "back to main" runs

OTOH, if getDataFrom is asynchronous (e.g. because it does a network request), the order will be:

  • your app calls loadBoard
  • this calls getDataFrom
  • which starts its asynchronous work and returns
  • "back to main" runs
  • at some later point in time, the work started by getDataFrom is done, and it calls the completion handler parameter
  • the completion handler runs

Either way, you get back to main without special effort.

Completion handler in function

You need to declare your completionHandler to be an escaping closure. E.g.:

func getValueFromAPI(completionHandler: @escaping (Bool) -> Void) {
...
}

Note the @escaping qualifier.

What's a good example of an escape closure in Swift?

An example of an escaping closure would be the completion handler in some asynchronous task, such as initiating a network request:

func performRequest(parameters: [String: String], completionHandler: @escaping (Result<Data, Error>) -> Void) {
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.httpBody = try! JSONEncoder().encode(parameters)

let task = URLSession.shared.dataTask(with: request) { data, _, error in
guard let data = data else {
completionHandler(.failure(error!))
return
}
completionHandler(.success(data))
}
task.resume()
}

And this is called like so:

performRequest(parameters: ["foo" : "bar"]) { result in
switch result {
case .failure(let error):
print(error)

case .success(let data):
// now use data here
}
}

// Note: The `completionHandler` above runs asynchronously, so we
// get here before the closure is called, so don't try to do anything
// here with `data` or `error`. Any processing of those two variables
// must be put _inside_ the closure above.

This completionHandler closure is defined as @escaping because URLSession method dataTask runs asynchronously (i.e. it returns immediately and its own closure will be called later when the request finishes).

Why is Swift @escaping closure not working?

return customerList in searchCustomer happens synchronously when the data (that's obtained asynchronously from getJsonFromAPI) isn't yet available. So, you're assigning and empty [Customer] array to @State var customerList.

In any case, you can't directly assign an asynchronously-obtained value to a property.

Instead, change searchCustomer to also accept a completion handler, and use .onAppear to invoke it and assign the value from within a completion handler (just like you with getJsonFromAPI):

func searchCustomer(criteria: String, 
completionHandler: @escaping ([Customer]) -> Void) -> Void {

//API call full url
let fullURL = FTP_API_URL + criteria
var customerList = [Customer]()

if criteria == "" {
completionHandler(customerList)
}

getJsonFromAPI(url: fullURL, fetchCompletionHandler: {customers, error in

if let jsonCustomers = customers{
customerList = jsonCustomers.customers
completionHandler(customerList)
}
})
}
struct CustomerList: View {

@State var customerList = []

var body: some View {
List(self.customerList, id: \.id){ customer in
CellRow(customer: customer)
}
.onAppear() {
searchCustomer(criteria: "jose") { customers in
customerList = customers
}
}
}
}

How to get rid of waiting for Completion Handler

You said:

As I understand it, this is because the controller is expecting a Completion Handler callback.

If the view controller is not getting deallocated, it is because there is a lingering strong reference to it. In your case it sounds like your closure is keeping that strong reference. To avoid keeping that strong reference, you would use a [weak self] capture list in your closure, e.g.

let task = URLSession.shared.dataTask(with: url) { [weak self] data, response, error in   // note `[weak self]` capture list
guard let self = self else { return } // safely unwrap the optional

... // the rest of this like before
}
task.resume()

If you wanted to take this a step further, you would also cancel the task when you leave, e.g.

private weak var savedTask: URLSessionTask?

deinit {
savedTask?.cancel() // cancel if if it hasn't finished already
}

func foo() {
...

let task = URLSession.shared.dataTask(with: url) { [weak self] data, response, error in // note `[weak self]` capture list
guard let self = self else { return } // safely unwrap the optional

... // the rest of this like before
}
task.resume()

savedTask = task
}

This way, if the sole purpose of the network request is to update the UI, we can free up the network resources when the requested data is no longer required.


All of this assumes that this is the only lingering strong reference you have (i.e., that you are seeing deinit called when the network request finishes). If not, make sure that all asynchronous closures are using weak references and that you do not have any strong reference cycles. Common sources of problems would be repeating timers, observers, etc., or traditional strong reference cycles. (You can use the “debug memory graph” feature to figure out where these strong references are, such as outlined in this answer.)

Swift inferring a completion handler closure to be the default @nonescaping instead of @escaping when completion handler explicitly uses @escaping

The problem has nothing to do with the closure, or static, or private. It has to do with the type parameter. You cannot call this method:

private static func getAndCacheAPIData <CodableClass: Any & Codable>(type:CodableClass.Type, completionHandler: @escaping (String?)->Void)

with a variable of type Codable.Type. The type value you pass must be a concrete type, known at compile-time. If you want to pass a variable, you can't use a generic. It would have to be:

private static func getAndCacheAPIData(type: Codable.Type, completionHandler: @escaping (String?)->Void)

Alternately, you can call this as:

 AppNetwork.getAndCacheAPIData(type: Int.self) {(firstErrorString) in ... }

or some other known-at-compile-time type.

Probably what you really want here is something like:

let completion: (String?) -> Void = {(firstErrorString) in ... }

switch ... {
case ...:
AppNetwork.getAndCacheAPIData(type: Int.self, completion: completion)
case ...:
AppNetwork.getAndCacheAPIData(type: String.self, completion: completion)
...

The basic problem is that protocols do not conform to themselves, so a variable of type Codable.Type does not satisfy the : Codable requirement. This comes down to the same reason you can't just call:

AppNetwork.getAndCacheAPIData(type: Codable.self) {...}

Alternately, you could refactor it this way:

private static func handleAPI<CodableClass: Codable>(type: CodableClass.Type) {
getAndCacheAPIData(type: type.self) { _ in ... the completion handler ..}
}


switch ... {
case ...:
AppNetwork.handleAPI(type: Int.self)
case ...:
AppNetwork.handleAPI(type: String.self)
...

Side note: Any & is meaningless here. You just meant <CodableClass: Codable>.



Related Topics



Leave a reply



Submit