Thanks for stopping by this article. In this article, I’m going to share a bit about my experience while handling
chained API Calls in my Nano Challenge 2 application Colorio. Here I will share my stupid ghetto way in handling
them, and share other alternative that can be used.
- Race Condition
- Completion Handlers
- Recursion
- Semaphores
Have you ever wanted to use an API and load their data to a tableview? Well usually you will do this in your view controller:
Example JSON data fetched from API:
[
{
"id": 1,
"author": "Gregorius Albert",
"content": "This is my first tweet"
},
{
"id": 2,
"author": "Taylor Swift",
"content": "It's August babyyyyy"
},
{
"id": 3,
"author": "Justin Bieber",
"content": "What's about the Baby song thing?"
}
]
Code to fetch data from API and load to tableview:
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 a data in bulk or collection. It’s like doing a SELECT *
in an SQL
Database.
But what if you are using a public API that doesn’t provide bulk data? Some API only allows you to get a single data based on a parameter. Take a look 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 that I gave in the URL which
is hex=E62028
. But I need to get the data for multiple colors at once.
How can I do that?
Well, everyone might think “just loop the API call”. Well you’re technically can loop an API call. Let’s try to loop 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, Blue
for 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 hoping 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 and run the code again, it will return in different order.
💡 This condition is called **race condition.**Race condition is a situation where there are multiple tasks that are executed in the same time. Even though we’ve call
the fetchAPI()
in sequence of the array, but their completion time of each API calls are not the same.
As we know, each called URLSession
is running asynchronously. So if you need the looped call to be in order, you have
to manipulate the API calls.
URLSession will run other task asynchronously after .resume()
is called next to the URLSession close bracket. So in
theory, you need to call fetchAPI()
before the closing bracket of the URLSession.
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)
}
// MARK: The next API call should be here
}.resume()
The easiest way to implement this theory, 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, because it is pure logic and doesn’t utilize Swift features like queues and semaphores. Performance might also be an issue because memory management in recursion isn’t known as the best.
Here’s how I implement the recursion.
- Define how many times we want to execute the recursion
- Set a base condition when to stop the recursion
- We can use simple
if-else
orguard
- We need to add an
index
parameter to mark when to stop
- We can use simple
- Recurse the function before the closing bracket
- Set a base condition when to stop the recursion
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 a lot slower and one by one.
White
Black
Red
Green
Blue
Let’s try to implement the sequenced call using another way, semaphore.
Before we continue to the code implementation, here’s a bit of theory about semaphore I quoted from Roy Kronenfeld at Medium.
A semaphore consists of a threads queue and a counter value (type Int).
The threads queue is used by the semaphore to keep track of waiting threads in FIFO order (The first thread entered into the queue will be the first to get access to the shared resource once it is available).
The counter value is used by the semaphore to decide if a thread should get access to a shared resource or not. The counter value changes when we call signal() or wait() function.
So, when should we call wait() and signal() functions?
- Call
wait()
each time before using the shared resource. We are basically asking the semaphore if the shared resource is available or not. If not, we will wait.- Call
signal()
each time after using the shared resource. We are basically signaling the semaphore that we are done interacting with the shared resource.
- Call
Here are the steps we need to do to initialize the semaphore.
- Declare a variable that contains
DispatchSemaphore(value: Int)
let semaphore = DispatchSemaphore(value: 1)
- Assign a value to the value parameter based on the queue quantity. Here, because we want to do it sequentially, we
need to queue the array one by one. So assign 1 to the parameter.
DispatchSemaphore(value: 1)
- Wrap the API Call into a function. Here named
fetchAPI(hexParam: String)
- Create a loop calling the function
- Assign
semaphore.wait()
to initiate the queue before callingfetchAPI(hexParam: String)
- Assign
semaphore.signal()
to continue the queue before.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 the function for a single API call? How can we remove the semaphore.signal()
in our function
while still able to pass the signal while chaining an API call like before?
We can use completion handlers for this kind of usage.
💡 A completion handler allows you to embed certain functions or lines of code anywhere in the function, not just at the end of the function call.In dealing asynchronous tasks, we need to use @escaping
in the 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 won’t wait for async task to
complete before running the handler.
Because we are putting the finished()
handler inside a URLSession closure that is asynchronous, without
the @escaping
keyword, Xcode will return an error of:
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 the fetchAPI
function before it resumes. So we can create a closure
and put the signal inside the closure that will runs when the code reaches finished().
For a single API call, just
call the function and give it an empty closure.
let hexArr = ["FFFFFF", "000000", "FF0000", "00FF00", "0000FF"]
// White, Black, Red, Green, Blue
let 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, and your API request will be fetched in sequence.
Colorio is a color palette generator that offers daily suggestions for your perfect colory day. We suggest the perfect shades and artful combinations from your base, adding a sense of personal style. Then you can choose what you need - your base, or one of our great selection.
This application is mainly powered by two API provided by:
- colormind.io to generate color palettes
- thecolorapi.com to get individual color data
GitHub - gal-bert/Colorio: Apple Developer Academy - Nano Challenge 2