In the past four weeks we have explored many of the features available to us via ArgumentParser and how to use them. Here’s a recap of everything we learned so far:
- We learned the very basics
- We learned how to validate user input
- We explored a way to separate our tool into subcommands.
- And finally, we learned how we can improve our documentation pages
In this article, we will not explore a feature exposed to us via ArgumentParser. Instead, we will learn how to do something very essential: Creating tools that require asynchronous APIs.
The Need for Asynchronous APIs.
If you have been programming for Apple platforms for a while, you have most likely used asynchronous APIs. URLSession
, for example, is a fully asynchronous API, as network requests are unpredictable, they can take long, and therefore they need to be executed in a different thread.
The problem with this is that command line tools in general are very linear. They have a beginning point of execution, and an end. They don’t really jump around different threads to do their job. In fact, if you ran a command line tool to do anything asynchronous, you’d see that it finishes up instantly without doing anything.
Below I have written an example that makes use of the PokeApi. It is, for the most part, the same code I used when we talked about Modern Background Tasks in iOS 13.
struct Pokemon: Codable {
struct Species: Codable {
let name: String
}
struct Sprites: Codable {
let backDefault: URL?
let backShiny: URL?
let frontDefault: URL?
let frontShiny: URL?
enum CodingKeys: String, CodingKey {
case backDefault = "back_default"
case backShiny = "back_shiny"
case frontDefault = "front_default"
case frontShiny = "front_shiny"
}
}
let species: Species
let sprites: Sprites
}
class PokeManager {
static let urlSession = URLSession(configuration: .default)
static func pokemon(id: Int,
completionHandler: @escaping (_ pokemon: Pokemon) -> Void) {
let pokeUrl = buildPokemonURL(id: id)
let task = urlSession.dataTask(with: pokeUrl) { (data, _, _) in
let pokemon = try! JSONDecoder().decode(Pokemon.self, from: data!)
DispatchQueue.main.async {
completionHandler(pokemon)
}
}
task.resume()
}
private static func buildPokemonURL(id: Int) -> URL {
var urlComponents = URLComponents()
urlComponents.scheme = "https"
urlComponents.host = "pokeapi.co"
urlComponents.path = "/api/v2/pokemon/\(id)"
return urlComponents.url!
}
}
struct Pokedex: ParsableCommand {
static let configuration = CommandConfiguration(
commandName: "pokedex",
abstract: "Allows you to fetch info from a Pokémon with its Pokédex number.",
discussion: "")
@Argument(help: "number") var number: Int
func run() throws {
PokeManager.pokemon(id: number) { (pokemon) in
self.printInfo(for: pokemon)
}
}
func printInfo(for pokemon: Pokemon) {
print("----------------------------------------------------------\n")
print("INFO FOR POKÉMON: \(number)\n")
print("ESPECIES: \(pokemon.species.name)\n")
print("----------------------------------------------------------\n")
}
}
Pokedex.main()
If you try to run this now, you will see the command line tool exits without printing anything at all:
andyibanez@Andys-iMac Debug % ./MyCommandLinetool
Error: Missing expected argument '<number>'
Usage: pokedex <number>
andyibanez@Andys-iMac Debug % ./MyCommandLinetool 1
andyibanez@Andys-iMac Debug %
Making Asynchronous Tasks Behave Synchronously
As of right now, ArgumentParser has no tools to offer for us to be able to run asynchronous tasks such as network requests. So we have to figure out a way to do this ourselves. The concurrent APIs (the Dispatch
APIs) provided to us by Cocoa and Cocoa Touch allow us to force execution of asynchronous tasks in the same process our command line tool is running.
But forcing the entire task to run asynchronously can be overkill. So what I like to do is to pause the execution of the thread that needs the resource, and continue it when another task finishes its execution. There are many ways to do this, but my favorite method is to use a Semaphore so we can force the static pokemon(id:)
method to return the Pokémon with the return
keyword instead of passing it in a completion handler.
Let’s turn our attention to the method in question:
static func pokemon(id: Int,
completionHandler: @escaping (_ pokemon: Pokemon) -> Void) {
let pokeUrl = buildPokemonURL(id: id)
let task = urlSession.dataTask(with: pokeUrl) { (data, _, _) in
let pokemon = try! JSONDecoder().decode(Pokemon.self, from: data!)
DispatchQueue.main.async {
completionHandler(pokemon)
}
}
task.resume()
}
We want this to return
the Pokémon traditionally. No completion handlers or anything like that. URLSession
is fully asynchronous, so it doesn’t have offer a way for us to do this either.
Fun fact: You could still use the old NSURLConnection
APIs and get synchronous behavior for network requests, but I prefer to use URLSession
because it’s more modern and we never know when NSURLConnection
will go away - not to mention, it’s good to know how to do this because not all asynchronous APIs are necessarily network-related.
First change the signature to this:
static func pokemon(id: Int) -> Pokemon
We are about to do the magic that returns the Pokémon.
Remember to deal with errors properly in a real world application. I’m skipping everything to do with error validation here. In a real app, you may want to return a tuple with an optional Pokémon and an Optional error, or handle errors in a different way.
Now replace the entire body of the method with this:
let pokeUrl = buildPokemonURL(id: id)
var pokemon: Pokemon!
let semaphore = DispatchSemaphore(value: 0)
let task = urlSession.dataTask(with: pokeUrl) { (data, _, _) in
pokemon = try! JSONDecoder().decode(Pokemon.self, from: data!)
semaphore.signal()
}
task.resume()
semaphore.wait()
return pokemon
This implementation uses a Semaphore. This is not an article on concurrent programming, so let’s just provide a very quick explanation of what a semaphore is, and how it works.
In concurrent programming, a semaphore controls access to a shared resource, ensuring that only one entity may access it at any given time.
When we call wait
on the semaphore, it will wait until someone else calls signal
on it. So essentially, this code will execute all the way down to semaphore.wait
. The thread will pause until the completion handler is executed and assigns the Pokémon. After assigning the Pokémon, it will call signal
so our previous thread can continue execution. because we will have a Pokémon before the return
statement, we can just return the Pokémon.
Note that we did not switch threads or anything like that: All we did was to pause the thread that pokémon(id:)
is executing in until the thread with the network request’s completion handler is done executing.
You can read more about semaphores here.
Next change the implementation of your run
method:
func run() throws {
let pokemon = PokeManager.pokemon(id: number)
printInfo(for: pokemon)
}
And we are done! Build and run your tool and it will properly fetch content from the network:
andyibanez@Andys-iMac Debug % ./MyCommandLinetool 1
----------------------------------------------------------
INFO FOR POKÉMON: 1
ESPECIES: bulbasaur
----------------------------------------------------------
If your command line tool is fully asynchronous, you can just move the semaphore calls to the tool’s run
body.
Conclusion
Command line tools that require asynchronous operations are very common. Even more so tools that connect to the network and do something with it. Thanks to Foundation’s Dispatch
APIs, we can return
content that would otherwise need a completion handler. It’s very useful to know this, because command line tools that need a network connection are very common.