Completion handlers, recursion and semaphores explained using examples
In this article, I am going to share some of my experience while handling chained API calls in my Nano Challenge 2 application Colorio. Here I’ll talk about my goto ways of handling them, and also the alternatives that can be used.
- closing handler
- repatriation
- semaphores
Have you ever wanted to use an API and load their data into a table view? Typically you would do this in your view controller:
Example JSON data received from API:
[
"id": 1,
"author": "Gregorius Albert",
"content": "This is my first tweet"
,
"id": 2,
"author": "Taylor Swift",
"content": "It's August baby"
,
"id": 3,
"author": "Justin Bieber",
"content": "What's about the Baby song thing?"
]
Here is the code for fetching data from API and loading it on table view:
let url = URL(string: Helper.BASE_URL)!
var request = URLRequest(url: url)
request.httpMethod = "GET"URLSession.shared.dataTask(with: request) (data, response, error) in
let json = try! JSONSerialization.jsonObject(with: data!) as! [[String:Any]]
for result in json
let author = result["author"] as! String
let content = result["content"] as! String
let tweet = Tweet(author: author, content: content)
self.tweets.append(tweet)
DispatchQueue.main.async
self.tableView.reloadData()
.resume()
This solution works if the API can return data in bulk or in collections. it’s like doing a SELECT *
in a SQL database.
But what if you’re using a public API that doesn’t provide bulk data? Some APIs allow you to get only a single data based on a parameter. See below for an example.
API URL: https://www.thecolorapi.com/id?hex=E62028
Returned Result:
"hex":
"value": "#E62028",
"clean": "E62028"
,
"rgb":
"fraction":
"r": 0.9019607843137255,
"g": 0.12549019607843137,
"b": 0.1568627450980392
,
"r": 230,
"g": 32,
"b": 40,
"value": "rgb(230, 32, 40)"
,
"name":
"value": "Alizarin Crimson",
"closest_named_hex": "#E32636",
"exact_match_name": false,
"distance": 349
// Some value from the API have been deleted to shorten this article
Here I get the value from a parameter which I have given in URL which is E62028
, But I need to get the data for multiple colors at once. how can i do that?
Well, many people may think “just loop the api call”, Well, you technically can run API calls that way. Let’s try to loop through the 5 hex codes and get the color name from each hex code in the array.
let hexArr = ["FFFFFF", "000000", "FF0000", "00FF00", "0000FF"]
// White, Black, Red, Green, Bluefor hex in hexArr
fetchAPI(hexParam: hex)
func fetchAPI(hexParam: String) -> Void
let url = URL(string: "https://www.thecolorapi.com/id?hex=\(hexParam)")!
var request = URLRequest(url: url)
request.httpMethod = "GET"
URLSession.shared.dataTask(with: request) data, response, error in
let json = try! JSONSerialization.jsonObject(with: data!) as! [String:Any]
let name = json["name"] as! [String:Any]
let nameValue = name["value"] as! String
DispatchQueue.main.async
print(nameValue)
.resume()
We are expecting to get the result of:
White
Black
Red
Green
Blue
But instead, it returned:
Black
White
Green
Red
Blue
We get all the colors right. But not in order…. If you try to run the code again, it will come back in a different order.
💡 This condition is called race condition.
A race condition is a situation where multiple tasks are executed at the same time. Although we have called fetchAPI()
In sequence to the array, but the completion time of each API call is not the same.
As we know, each called URLSession
running asynchronously. So if you need looped calls, you have to manipulate the API calls.
Here, URLSession
will run other tasks asynchronously after .resume()
is called near URLSession
closing parenthesis. So in theory, you need to call fetchAPI()
before the closing bracket of URLSession.
URLSession.shared.dataTask(with: request) data, response, error inlet json = try! JSONSerialization.jsonObject(with: data!) as! [String:Any]
let name = json["name"] as! [String:Any]
let nameValue = name["value"] as! String
DispatchQueue.main.async
print(nameValue)
// MARK: The next API call should be here
.resume()
The easiest way to implement this, in principle, is to use recursion. Because we can call the function again in the specified line.
I personally use this method in my Nano Challenge 2 app Colorio. This method is kinda ghetto though as it is pure logic and doesn’t use Swift features like queues and semaphores. Performance can also be an issue as memory management in recursion is not known to be the best.
Here’s how I implement the recursion:
- Set base position to prevent recursion
- we can use simple
if-else
eitherguard
- we need to add a
index
parameter to mark when to stop - Recurse the function before the closing bracket
💡 Here the function will stop running after 5 function calls, that is counting the array contents
let hexArr = ["FFFFFF", "000000", "FF0000", "00FF00", "0000FF"]
// White, Black, Red, Green, Blue// Defining when the recursion needs to stop based on the array count
let arrayCount = hexArr.count
// Calling the function
fetchAPI(index: 0)
func fetchAPI(index: Int) -> Void
// Guarding the function to stop after it reaches the array count
guard index < arrayCount else return
let url = URL(string: "https://www.thecolorapi.com/id?hex=\(hexArr[index])")!
var request = URLRequest(url: url)
request.httpMethod = "GET"
URLSession.shared.dataTask(with: request) data, response, error in
let json = try! JSONSerialization.jsonObject(with: data!) as! [String:Any]
let name = json["name"] as! [String:Any]
let nameValue = name["value"] as! String
DispatchQueue.main.async
print(nameValue)
// Calling the recursion
fetchAPI(index: index+1)
.resume()
As you can see, the result will always be consistent. But they will load very slowly and one by one.
White
Black
Red
Green
Blue
Let’s try to implement sequenced calls in another way, using semaphores.
Before we continue with the code implementation, here’s a little bit of theory about semaphores piece by roy kronenfeld To speed you up:
A semaphore consists of a thread queue and a counter value (of type int).
The threads queue is used by semaphores to keep track of waiting threads in FIFO order ( The first thread entered into the queue will be the first thread to access the shared resource once it becomes available.,
The counter value is used by the semaphore to decide whether a thread should have access to a shared resource. The counter value changes when we call the signal() or wait() function.
So, when should we call wait() and signal() functions?
– Source: Roy Kronenfeld
To initialize the semaphore we need to do these steps:
let semaphore = DispatchSemaphore(value: 1)
- Assign a value to the value parameter based on the queue quantity. Here, since we want to do it sequentially, we need to queue up the arrays one by one. So assign 1 to the parameter.
DispatchSemaphore(value: 1)
- Wrap the API call in a function. named here
fetchAPI(hexParam: String)
- create a loop that calls the function
- assign
semaphore.wait()
to initialize the queue before callingfetchAPI(hexParam: String)
- assign
semaphore.signal()
to continue the first queue.resume()
let hexArr = ["FFFFFF", "000000", "FF0000", "00FF00", "0000FF"]
// White, Black, Red, Green, Blue// Assign the semaphore variable
let semaphore = DispatchSemaphore(value: 1)
for hex in hexArr
semaphore.wait() // Initiate the queue
fetchAPI(hexParam: hex)
func fetchAPI(hexParam: String) -> Void
let url = URL(string: "https://www.thecolorapi.com/id?hex=\(hexParam)")!
var request = URLRequest(url: url)
request.httpMethod = "GET"
URLSession.shared.dataTask(with: request) data, response, error in
let json = try! JSONSerialization.jsonObject(with: data!) as! [String:Any]
let name = json["name"] as! [String:Any]
let nameValue = name["value"] as! String
DispatchQueue.main.async
print(nameValue)
semaphore.signal() // Continue the queue
.resume()
What if we want to reuse a function for a single API call? how can we remove semaphore.signal()
Is our function still able to pass signals while chasing API calls as before?
We can use completion operators for such usage.
💡 A completion handler allows you to embed certain functions or lines of code anywhere in a function, not just at the end of a function call.
In dealing with asynchronous tasks, we need to use @escaping
in parameter to wait for the async task to complete, then run our completion handler. if we don’t use@escaping
The handler will run immediately and will not wait for the async task to complete before running the handler.
because we are putting finished()
handler inside URLSession closure which is asynchronous, without@escaping
keyword, Xcode will return the error:
expression failed to parse:
error: MyPlayground.playground:21:47: error: escaping closure captures non-escaping parameter 'finished'
URLSession.shared.dataTask(with: request) { data, response, error in
^MyPlayground.playground:15:47: note: parameter 'finished' is implicitly non-escaping
func fetchAPIUsingSemaphore(hexParam: String, finished: () -> Void) -> Void {
^
MyPlayground.playground:32:9: note: captured here
finished()
^
So, here is the implementation example:
myFunction()
// Code to inject to the function
func myFunction(finished: @escaping() -> Void) -> Void
URLSession.shared.dataTask(with: request) data, response, error in
// Any process handling the data
finished() // Put where you want any code injected to the function
.resume()
here we want to inject semaphore.signal()
To fetchAPI
before resuming work.
So we can create a closure and put a signal inside the closure which will run when the code is reached finished().
For a single API call, just call the function and close it empty.
let hexArr = ["FFFFFF", "000000", "FF0000", "00FF00", "0000FF"]
// White, Black, Red, Green, Bluelet semaphore = DispatchSemaphore(value: 1)
// Looping and chaining the API Call
for hex in hexArr
semaphore.wait()
fetchAPI(hexParam: hex)
// Injecting semaphore.signal to the function
semaphore.signal()
// Single API Call. Just give an empty closure
fetchAPI(hexParam: "FAFAFA")
func fetchAPI(hexParam: String, finished: @escaping() -> Void) -> Void
let url = URL(string: "https://www.thecolorapi.com/id?hex=\(hexParam)")!
var request = URLRequest(url: url)
request.httpMethod = "GET"
URLSession.shared.dataTask(with: request) data, response, error in
let json = try! JSONSerialization.jsonObject(with: data!) as! [String:Any]
let name = json["name"] as! [String:Any]
let nameValue = name["value"] as! String
DispatchQueue.main.async
print(nameValue)
finished()
.resume()
Voila, the race condition is dealt with, and your API request will be received in order.
Thank you for reading.