There's more...

When we created our completion handlers, we gave them two inputs: the successful result (either the repository information or the created issue), or an error if there is a failure. Both these values are optional; one or the other will be nil, and the other has a value. However, this convention is not enforced by the language, and a user of this function will have to consider the possibility that it may not be the case. What should the user of this function do if the fetchRepos function fires the completion handler with non-nil values for both the repository and the error? What if both are nil?

The user of this function, without viewing the function's internal code, can't be sure that this won't happen, which means they may need to write functionality and tests to account for this possibility, even though it may never happen.

It would be better if we can more accurately represent the intended behavior of our function, providing the user with a clear indication of the possible outcomes and leaving no room for ambiguity. We know that there are two possible outcomes from calling the function: it will either succeed and return the relevant value, or it will fail and return an error to indicate the reason for the failure.

Instead of optional values, we can use an enum to represent these possibilities. Let's create an enum to be used as a response to the fetchRepos function:

enum FetchReposResult {
case success([[String: Any]])
case failure(Error)
}

We can now define the success and failure states, and use associated values to hold the value that is relevant for each state, which is the repository information for the success state and the error for the failure state.

Now, let's amend our fetchRepos function to provide a FetchReposResult in the completionHandler:

func fetchRepos(forUsername username: String,
completionHandler: @escaping (FetchReposResult) -> Void) {

let urlString = "https://api.github.com/users/\(username)/repos"
let url = URL(string: urlString)!
var request = URLRequest(url: url)
request.setValue("application/vnd.github.v3+json",
forHTTPHeaderField: "Accept")

let task = session.dataTask(with: request) { (data, response, error) in

// Once we have handled this response, the Playground
// can finish executing.
defer {
PlaygroundPage.current.finishExecution()
}

// First unwrap the optional data
guard let jsonData = data else {
// If it is nil, there was probably a network error
completionHandler(.failure(ResponseError.requestUnsucessful))
return
}

do {
// Deserialisation can throw an error,
// so we have to `try` and catch errors
let deserialised = try JSONSerialization.jsonObject(with:
jsonData, options: [] )
// As `deserialised` has type `Any` we need to cast
guard let repos = deserialised as? [[String: Any]] else {
let error = ResponseError.unexpectedResponseStructure
completionHandler(.failure(error))
return
}

completionHandler(.success(repos))

} catch {
completionHandler(.failure(error))
}
}
task.resume()
}

We need to update how we call the fetchRepos function:

fetchRepos(forUsername: "SwiftProgrammingCookbook", completionHandler:{ result in
switch result {
case .success(let repos):
print(repos)

case .failure(let error):
print(error)
}
})

We now use a switch statement instead of if/else, and we get the added benefit that the compiler will ensure that we have covered all possible outcomes.

Having made this improvement to the fetchRepos function, we can similarly improve the createIssue function. We can create another enum to represent the result of this function; however, we can, instead, use generics to make this a common pattern that we can use for all tasks that attempt to retrieve information with either a success or failure:

enum Result<SuccessType> {
case success(SuccessType)
case failure(Error)
}

Now, by just specifying the generic type, this enum can be used in any scenario; situations with multiple returned values can also be handled by specifying a tuple as the generic type:

let result = Result<(Data, HTTPURLResponse)>.success((someData, someHTTPURLResponse))

We can use this new Result in our fetchRepos function by just changing the definition of the completion handler:

func fetchRepos(forUsername username: String,
completionHandler: @escaping (Result<[[String: Any]]>) -> Void) {
//...
}

Lastly, let's rewrite the createIssue function, and how we call it, to also use our Result enum:

func createIssue(inRepo repo: String, 
forUser user: String,
title: String,
body: String?,
completionHandler: @escaping (Result<[String: Any]>) -> Void) {

// Create the URL and Request

let urlString = "https://api.github.com/repos/\(user)/\(repo)/issues"
let url = URL(string: urlString)!
var request = URLRequest(url: url)
request.httpMethod = "POST"
let authorisationValue = authHeaderValue(for: <#your personal access token#>)
request.setValue(authorisationValue, forHTTPHeaderField: "Authorization")
request.setValue("application/vnd.github.v3+json", forHTTPHeaderField: "Accept")

// Put the issue information into the JSON structure required
var json = ["title": title]

if let body = body {
json["body"] = body
}

// Serialise the json into Data. We can use try! as we know it is valid JSON,
// the try will fail if the provided JSON object contains elements that
// aren't valid JSON.
let jsonData = try! JSONSerialization.data(withJSONObject: json,
options: .prettyPrinted)
request.httpBody = jsonData

let task = session.dataTask(with: request) { (data, response, error) in

guard let jsonData = data else {
completionHandler(.failure(ResponseError.requestUnsucessful))
return
}

do {
// Deserialisation can throw an error,
// so we have to `try` and catch errors
let deserialised = try JSONSerialization.jsonObject(with: jsonData,
options: [])

// As `deserialised` has type `Any` we need to cast
guard let createdIssue = deserialised as? [String: Any] else {
let error = ResponseError.unexpectedResponseStructure
completionHandler(.failure(error))
return
}

completionHandler(.success(createdIssue))

} catch {
completionHandler(.failure(error))
}
}
task.resume()
}

createIssue(inRepo: "BeyondTheStandardLibrary",
forUser: "SwiftProgrammingCookbook",
title: <#The title of your feedback#>,
body: <#Extra detail#>) { result in

switch result {
case .success(let issue):
print(issue)

case .failure(let error):
print(error)
}
}