Posting and Storing Data

Interfacing with REST APIs using URL Session

Service communications are done in a variety of ways, but modern guidelines are leaning towards a RESTful approach. REST is the acronym for Representational State Transfer which is the foundation of stateless representation of data.

In this section we are going to explore the use of Foundation’s URL Session to build a minimal RESTful API for the Bookmarks scene on the Design+Code app.

Decoding Data

Decoding data is done using Foundation’s Codable, especially because both Realm and Firebase are both capable of parsing dictionaries. As for Cloud Kit, it uses its dictionary-like interface that needs manual binding to communicate to Realm for local storage.

This section assumes that you do understand the Codable protocol syntax but will stick to its native implementation. If you want to brush up on those, we suggest you visit its section, titled JSON Parsing.

Persisting Data

The persistence features of the app are mostly backed by Cloud Kit, Firebase, and Realm. Should a user be a logged in subscriber, their data would be backed into Firebase. Otherwise Cloud Kit kicks in, to maintain data sync when lacking cross-device user identification.

In either case, Realm is used for storing the content locally in a database. Whereas the associated assets for video and image are kept in local directories and their URLs stored in the database. If you would like more detail about how this works, we suggest you look into the Realm titled section.

URL Session

This class has come a long way in the last couple of years. There has been a day where it was fundamental to import third-party libraries to do simple networking on iOS. No more. The current interface provided by URL Session is now widely accepted in the community and regarded as sufficient for most of the needs.

To use a session, you may choose to use the URLSession.shared to get its shared instance. If the use case demands detail about how the tasks you send to a session is doing, you may choose to create a session for a configuration

let configuration = URLSessionConfiguration.background(withIdentifier: "io.designcode.downloadVideos")
let session = URLSession(configuration: configuration, delegate: self, delegateQueue: nil)

URL Session Task

In this section, we are mostly going to use the Data Task as we are going to load data about a user’s bookmarks. For downloading and uploading large chunks of media, it is advised to use the Download or Upload Tasks that come with their signature interfaces and tailor-made implementations.

URLSession.shared.dataTask(with: dataRequest, completionHandler: dataCompletion)
URLSession.shared.downloadTask(with: downloadRequest, completionHandler: downlaodCompletion)
URLSession.shared.downloadTask(with: uploadRequest, fromFile: fileURL, completionHandler: uploadCompletion)

URL Session Delegate

This protocol defines the signatures that make an object able to receive events from a URL Session about a given task. It is especially useful when you need fine-grained information about how a download task is doing.

If the app were download multiple videos from a given chapter at once and needed to inform the user of how these download task are doing. In the case of a URL Session Download Delegate, it would be able to:

// Get progress information about the task and relay it to the user interface
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64)
// Save the cached data in a definitive manner
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL)
// Inform the user about the error and take appropriate action
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?)

The underlying interfaces of a generic URL Session Delegate offer a more profound look at what is happening in the URL Session.

Downloads for Posting and Storing Data

In this section, we are going to untangle some of the concepts exposed by showing a possible implementation of the bookmarks scene in a RESTful manner. To do that, download the example database.json file and follow along.

To accompany this material, there is a Posting and Storing Data.playground with all the classes and definitions. Although it does not use the background thread for calls, you should use it on any production app.

JSON Server

Setting up a JSON server is simple using the NPM’s (Node Package Manager) json-server. If you don’t have npm installed on your computer, there is a three-step process to get it. Otherwise, jump to the Install json-server section.

Install Homebrew

First, you need to launch your Terminal.app and run:

/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"

This command will install Homebrew to your machine. As the tagline states, it is the catch-all interface for downloading and building software on your machine. Just like CocoaPods, instead of downloading a closed package, Homebrew builds and installs on site. Just like Ruby Gems, it is widely to install software such as compilers and command-line tools.

Install Node

Once Homebrew is installed, it may be used to install packages such as NPM. Just like CocoaPods, it takes a command to install. Using the same terminal window, do:

brew install node

This process might take a while; you may grab a cup of coffee. If you are not familiar with Node, it is Javascript interpreter responsible for bringing this language to desktops and servers alike. A lot of the innovation we currently enjoy in desktop and web development comes from it. NodeJS comes packaged with its manager of choice, the NPM.

Install json-server

Finally, install json-server globally on your machine by running:

npm install --global json-server

This command will publish json-server publicly as a command in the terminal. This package lets you publish a simple REST API with zero coding required (mainly for testing purposes). Open the folder containing the database.json file on Terminal using the cd command and run:

json-server --watch database.json

HTTP Methods

The main reason we chose to use the json-server dependency in this tutorial is that it consumes the three main methods of a RESTful API realistically:

POST is used to save a record to the server, which will be sent as JSON data. In this server implementation, a unique identifier is created for each record.

GET is used to get data from a select route and may receive parameters for filtering, sorting, and paginating. It may also be used to get a select record by its unique identifier.

DELETE is used to remove a given record from the dataset using its unique identifier.

Swift is a language that enforces type-safety to ensure stability from development. Let’s ensure that by declaring those methods:

enum HTTPMethod : String {
case get = "GET"
case post = "POST"
case delete = "DELETE"
}

Get All Bookmarks Request

Before getting all the bookmarks in a service, it is required to fashion a request object. To do that we need to:

1 . know the URL of the server

2 . append to it the Path/Route of the resource

3 . make a Request with it that composite url

4 . give it additional information about method, data body and content type

Some of these options are not strictly required for GET by are placed for generalization further along. In Swift it would look a lot like:

let baseUrl : URL! = URL(string: "http://localhost:3000/") // 1
let url = baseUrl.appendingPathComponent("bookmarks") // 2
var request = URLRequest(url: url) // 3
request.httpMethod = "GET" // 4
request.httpBody = nil
request.setValue("application/json", forHTTPHeaderField: "Content-Type")

Data Task for Bookmarks

The just created request sufficiently explains how to get information from a server. Now it suffices to make a task that consumes and acts on this information. To do that we need to:

1 . get a Session

2 . ask the session to make a Data Task for the given request

3 . Specify a Completion for when the data finishes loading

4 . ask the task to resume on a background thread

In Swift, this could look like:

let session = URLSession.shared // 1
let dataTask = session.dataTask(with: request) { // 2
(data, response, error) in // 3
// Do something with the data
// This will be called in the background thread
}

DispatchQueue.global(qos: .background).async {
// Call on the background thread
dataTask.resume() // 4
}

All Bookmarks Endpoint

Most of what is specified in a data call can be generalized into the definition of an Endpoint. An endpoint is an interface to a service layer. In this example, we are going to materialize it as a structure with all the necessary means to fashion a data task:

1 . Path, to the resource

2 . HTTP Method, to act on the resource

3 . Data Body, should it need to send data

A broad definition would be:

protocol Endpoint {
var path : String { get }
var httpMethod : HTTPMethod { get }
var body : Data? { get }
}

A specific implementation complying with this protocol would be:

enum BookmarksEndpoint : Endpoint {
case all

var path: String {
return "bookmarks"
}
var httpMethod : HTTPMethod {
return .get
}
var body : Data? {
return nil
}

var baseUrl : URL! { return URL(string: "http://localhost:3000/") }
}

Data Task for Endpoint

To get a data task for this endpoint, let’s create a method inside the Bookmarks Endpoint enum that receives the completion handler and returns the task to be performed. As we are in the enum scope, we can implicitly call self to get information about provided by the endpoint.

func dataTask(completion:  @escaping (Data?) -> Void) -> URLSessionTask {
let url = baseUrl.appendingPathComponent(path)
var request = URLRequest(url: url)
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.httpMethod = httpMethod.rawValue
request.httpBody = body

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

This definition does not seem very broad at first, but it provides the necessary means to extend the Bookmarks Endpoint with new cases while using Swift’s type system for disambiguation and validation.

Decodable Bookmark

The server endpoint returns a data that is encoded as JSON. So make sense of it, it would be natural to define a data structure capable of receiving that information that complies with the Decodable protocol:

struct BookmarkDecodable : Decodable {
var id : Int
var contentId : Int
var sectionId : Int
var userId : Int
var createdDate : Date
}

By using JSON as the data storage on the server, it would be wise to comply with its naming conventions. Those conventions state that, differently from Swift’s camel casing, keys should be named using underscore spacing. Inside this structure should be an exhaustive Coding Keys enum specifying this translation:

enum CodingKeys : String, CodingKey {
case id
case contentId = "content_id"
case sectionId = "section_id"
case userId = "user_id"
case createdDate = "created_date"
}

Decoding the Data

The Bookmarks Endpoint for all is able to create a data task that consumes a completion handler that takes the data for all the bookmarks.

The appropriate place to parse or call something to parse the data into Swift types would be in the completion. Should the data be available, it would decode using the standard JSON Decoder provided with a date decoding strategy of iso8601, the industry standard used on the example data.

let task = BookmarksEndpoint.all.dataTask {
data in
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601
if let bookmarks = try? decoder.decode(Array<BookmarkDecodable>.self, from: data!) {
DispatchQueue.main.sync {
tableView.reloadData()
}
}
}
DispatchQueue.global(qos: .background).async {
task.resume()
}

It is important to remember that the resume call should be sent on the background thread. Likewise, the completion is going to resurface in this same thread. If some interface were to be called directly or indirectly, it would be appropriate to switch to the main thread by using  DispatchQueue.main.sync.

Remove Bookmark Request

A request for removing a piece of data has a lot in common with the task for loading. Its fields are the same but it has different values. Only with direct access to the Bookmarks Endpoint it is possible to extend its functionality. To do so, create a new case on it with an associated value for the id of the record:

case remove(id: Int)

A remove request is different from a all request because it will be targeted to a record unique identifier and will inform its intention to remove by using the HTTP method DELETE. Because the Bookmarks Endpoint uses computed variables, it is possible to disambiguate which endpoint is being used inside each variable like this:

var path: String {
switch self {
case .all: return "bookmarks"
case .remove(let id): return "bookmarks/(id)"
}
}

Should the endpoint be for removal, the path should lead to the record by its unique identifier.

var httpMethod : HTTPMethod {
switch self {
case .all: return .get
case .remove(_): return .delete
}
}

In the same manner, the method to use in this case should be DELETE.

Calling it should be done in the background thread with:

DispatchQueue.global(qos: .background).async {
BookmarksEndpoint.remove(id: 0).dataTask { _ in }
}

Create Bookmark

Similarly to the delete request, creating a new record in the server is going to take creating a new case for the Bookmarks Endpoint. In this case, we could use explicit associated values for contentId, sectionId, userId, and createdDate. Nonetheless, this would bind us to writing a dictionary instantiation.

Alternatively, it would be swifty to create a symmetrical structure for the Bookmark Encodable with coding keys:

struct BookmarkEncodable : Decodable {
var contentId : Int
var sectionId : Int
var userId : Int
var createdDate : Date

enum CodingKeys : String, CodingKey {
case contentId = "content_id"
case sectionId = "section_id"
case userId = "user_id"
case createdDate = "created_date"
}
}

Different from its sibling, it lacks the unique identifier. That is because it relies on the server to provide this identifier on the database.

Create Bookmark Request

Now it suffices to declare a new case for create that has an associated value of Bookmark Encodable and exhaust the switch cases for this possibility:

case create(bookmark: BookmarksEncodable)

The Path value should be the same as the all case, which would be ”bookmarks”:

var path: String {
switch self {
case .all, .create: return "bookmarks"

The HTTP Method for this request would be a POST:

case .create: return .post

And the body would be the encoding of the associated value:

var body : Data? {
switch self {
case .create(let bookmark):
let encoder = JSONEncoder()
encoder.dateEncodingStrategy = .iso8601
return try? encoder.encode(bookmark)
default: return nil
}
}

Sending a Create Bookmark Request

Creating a new bookmark takes a similar for as any other request.

let newBookmark = BookmarksEncodable(contentId: 1, sectionId: 1, userId: 1, createdDate: Date())
let createEndpoint = BookmarksEndpoint.create(bookmark: newBookmark)
let createTask = createEndpoint.dataTask {
_ in
print("One was created")
// Update the user interface on the main thread
}

Like all structs in Swift, Bookmarks Encodable is equipped with a default initializer that would be used to create a bookmark with reference to its target content, section, and user while also providing a date of creation for sorting purposes.

DispatchQueue.global(qos: .background).async {
createTask.resume()
}

And, like all the previous ones, it takes one call to make it work.

Conclusion

You may have noticed that once we created a generic declaration for the endpoint, we stopped interfacing with URL Session. Should you need to create a communications layer by hand, it would be wise to create a solid and generic foundation that would ensure that you cover most cases with the least code.

The implementation presented might not be the smallest, the most resilient, not the most flexible. Like most tasks in software development, it takes studying the specific problem to find a suitable solution.

Tweet "Posting and Storing Data - Interfacing with REST APIs using URL Session by @MengTo"Tweet

Chapter 4: 22 Sections