How to do it...

To create our issue, we will first retrieve all the public repositories created for this book, then find the relevant repository for this chapter and, finally, create a new issue.

As in the preceding recipe, we will need a URLSession to perform our requests, and we need to tell the playground not to finish executing when it reaches the end of the playground:

import Foundation
import PlaygroundSupport

PlaygroundPage.current.needsIndefiniteExecution = true

let config = URLSessionConfiguration.default
let session = URLSession(configuration: config)

Our first step is to fetch all the public repositories for a given user, so let's create a function to do that:

func fetchRepos(forUsername username: String) {

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

}
task.resume()
}
We know from the GitHub API documentation--https://developer.github.com/v3/--that this response data is in JSON format We need to parse the JSON data to turn it into something that we can use; enter JSONSerialization. JSONSerialization is part of the Foundation framework and provides class methods for turning Swift dictionaries and arrays into JSON data (known as serialization), and back again (known as deserialization).
func fetchRepos(forUsername username: String) {

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
print(error ?? "Network Error")
return
}

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

} catch {
print(error)
}
}
task.resume()
}
fetchRepos(forUsername: "SwiftProgrammingCookbook")
(
{
"archive_url" = "https://<GitHub API URL>/{archive_format}{/ref}";
"assignees_url" = "https://<GitHub API URL>/assignees{/user}";
// ..Truncated..
description = "Code relating to Chapter 5 of Swift 4 Programming
Cookbook";
"downloads_url" = "https://<GitHub API URL>/downloads";
"events_url" = "https://<GitHub API URL>/events";
fork = 0;
forks = 0;
"forks_count" = 0;
"forks_url" = "https://<GitHub API URL>/forks";
"full_name" = "SwiftProgrammingCookbook/BeyondTheStandardLibrary";
// ..Truncated..
"issue_comment_url" = "https://<GitHub API
URL>/issues/comments{/number}";
"issue_events_url" = "https://<GitHub API
URL>/issues/events{/number}";
"issues_url" = "https://<GitHub API URL>/issues{/number}";
"keys_url" = "https://<GitHub API URL>/keys{/key_id}";
"labels_url" = "https://<GitHub API URL>/labels{/name}";
language = Swift;
"languages_url" = "https://<GitHub API URL>/languages";
"merges_url" = "https://<GitHub API URL>/merges";
"milestones_url" = "https://<GitHub API URL>/milestones{/number}";
"mirror_url" = "<null>";
name = BeyondTheStandardLibrary;
"notifications_url" = "https://<GitHub API URL>/notifications{?
since,all,participating}";
"open_issues" = 0;
"open_issues_count" = 0;
owner = {
"avatar_url" =
"https://avatars1.githubusercontent.com/u/28363559?v=4";
"events_url" = "https://<GitHub API URL>/events{/privacy}";
"followers_url" = "https://<GitHub API URL>/followers";
"following_url" = "https://<GitHub API
URL>/following{/other_user}";
"gists_url" = "https://<GitHub API URL>/gists{/gist_id}";
"gravatar_id" = "";
"html_url" = "https://<GitHub URL>";
id = 28363559;
login = SwiftProgrammingCookbook;
"organizations_url" = "https://<GitHub API URL>/orgs";
"received_events_url" = "https://<GitHub API
URL>/received_events";
"repos_url" = "https://<GitHub API URL>/repos";
"site_admin" = 0;
"starred_url" = "https://<GitHub API
URL>/starred{/owner}{/repo}";
"subscriptions_url" = "https://<GitHub API URL>/subscriptions";
type = Organization;
url = "https://<GitHub API URL>";
};
private = 0;
// ..Truncated..
url = "https://<GitHub API URL>";
watchers = 0;
"watchers_count" = 0;
},
// More Repos...
)

JSONSerializer has turned our JSON data into familiar arrays and dictionaries that can be used to retrieve the information we need in the normal way. The JSON data is deserialized with the Any type, as the JSON can have a JSON object or an array at its root.

Since, from the preceding output, we know that the response has an array of JSON objects at its root, we need to turn the value from type Any to an array of dictionaries of the [String: Any] type. This is referred to as casting from one type to another, which we can do using the as keyword and then specifying the new type. This keyword can be used in three different ways:

So, let's cast the deserialized data to an array of dictionaries with string keys, with the [[String: Any]] type:

func fetchRepos(forUsername username: String) {
//...
let task = session.dataTask(with: request) { (data, response, error) in
//...
do {
// Deserialisation can throw an error, so we have to `try` and catch errors
let deserialised = try JSONSerialization.jsonObject(with: jsonData,
options: [])
print(deserialised)
// As `deserialised` has type `Any` we need to cast
guard let repos = deserialised as? [[String: Any]] else {
print("Unexpected Response")
return
}
print(repos)
} catch {
print(error)
}
}
}

Now, we have an array of dictionaries for the repositories in the API response, which we need to provide as an input for this function. A common pattern for providing results for asynchronous work is to provide a completion handler as a parameter. A completion handler is a closure that can be executed once the asynchronous work is completed.

Since the output we want to provide is the array of repository dictionaries, we will define this as an input for the closure if the request was successful, and an error if it wasn't:

func fetchRepos(forUsername username: String,
completionHandler: @escaping ([[String: Any]]?, Error?) -> 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(nil, 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 {
completionHandler(nil, ResponseError.unexpectedResponseStructure)
return
}

completionHandler(repos, nil)

} catch {
completionHandler(nil, error)
}
}
task.resume()
}

Now, whenever an error is generated, we execute the completionHandler, passing in the error and a nil for the results value; also, when we have the repository results, we execute the completion handler, passing in the parsed JSON and nil for the error.

We passed in a few new errors in the preceding code, so let's define those errors:

enum ResponseError: Error {
case requestUnsucessful
case unexpectedResponseStructure
}

This changes how we call this fetchRepos function:

fetchRepos(forUsername: "keefmoon") { (repos, error) in

if let repos = repos {
print(repos)
} else if let error = error {
print(error)
}
}

Now that we have retrieved the details of the public repositories, we will submit an issue to the repository for this chapter. This issue can be any feedback you would like to give on this book, it can be a review, a suggestion for new content, or you can tell me about a Swift project you are currently working on.

This request to the GitHub API will be authenticated against your user account and therefore we will need to include details of the personal access token that we created at the beginning of this recipe. There are a number of ways to authenticate requests to the GitHub API, but the simplest is basic authentication, which involves adding an authorization string to the request header. Let's create a method to format the personal access token correctly for authentication:

func authHeaderValue(for token: String) -> String {
let authorisationValue = Data("\(token):x-oauth-basic".utf8).base64EncodedString()
return "Basic \(authorisationValue)"
}

Next, let's create our function to submit our issue. From the API documentation at https://developer.github.com/v3/issues/#create-an-issue, we can see that unless you have push access, you can only create an issue with the following components:

So, our function will take this information as an input, along with the repository name and username:

func createIssue(inRepo repo: String, 
forUser user: String,
title: String,
body: String?) {

}

Creating an issue is achieved by sending a POST request, and information about the issue is provided as JSON data in the request body. To create our request, we can use JSONSerialization, but we will take our intended JSON structure and serialize it into Data this time:

func createIssue(inRepo repo: String, 
forUser user: String,
title: String,
body: String?) {

// 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"
request.setValue("application/vnd.github.v3+json",
forHTTPHeaderField:
"Accept")
let authorisationValue = authHeaderValue(for: <#your personal access token#>)
request.setValue(authorisationValue, forHTTPHeaderField: "Authorization")
// 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

session.dataTask(with: request) { (data, response, error) in
// TO FINISH
}
}

As with the previous API request, we need a way to provide the result of creating the issue, so let's add a completionHandler:

func createIssue(inRepo repo: String, 
forUser user: String,
title: String,
body: String?,
completionHandler: @escaping ([String: Any]?, Error?) ->
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"
request.setValue("application/vnd.github.v3+json",
forHTTPHeaderField:
"Accept")
let authorisationValue = authHeaderValue(for: <#your personal access
token#>)
request.setValue(authorisationValue, forHTTPHeaderField:
"Authorization")

// 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

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

guard let jsonData = data else {
completionHandler(nil, 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 {
completionHandler(nil, ResponseError.unexpectedResponseStructure)
return
}

completionHandler(createdIssue, nil)

} catch {
completionHandler(nil, error)
}
}
}

The API response to a successfully created issue provides a JSON representation of that issue. Our function will return this representation if it was successful, or an error if it was not.

Now that we have a function to create issues in a repository, it's time to use it to create an issue:

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

if let issue = issue {
print(issue)
} else if let error = error {
print(error)
}
}

I will check these created issues, so provide genuine feedback on this book. How have you found the content? Too detailed? Not detailed enough? Anything I've missed or not fully explained? Any questions that you have? This is your opportunity to let me know.