Sending an HTTP request in iOS app iOS 27.10.2019

In order to query the remote web service, the very first thing you must do is send an HTTP request. This involves several steps such as creating a URL with the correct search parameters, sending the request to the server, getting a response back etc.

You'll take these step-by-step.

Setting up the storyboard

Drag a new Table View into the existing view controller.

Make the Table View as big as the main view and then use the Add New Constraints menu at the bottom to attach the Table View to the edges of the screen

Add the following outlets to HttpViewController.swift:

@IBOutlet weak var tableView: UITableView!

var posts = [Post]()
let url = URL(string: "https://jsonplaceholder.typicode.com/posts/")!

Switch back to the storyboard and connect the Table View to it respective outlet (Control-drag from the view controller to the object that you want to connect).

Add the following new extension to HttpViewController.swift:

extension HttpViewController: UITableViewDelegate, UITableViewDataSource {
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return posts.count
    }

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cellIdentifier = "PostCell"
        var cell:UITableViewCell! = tableView.dequeueReusableCell(withIdentifier: cellIdentifier)
        if cell == nil {
            cell = UITableViewCell(style: .subtitle, reuseIdentifier: cellIdentifier)
        }
        cell.textLabel!.text = posts[indexPath.row].title
        cell.detailTextLabel!.text = posts[indexPath.row].description
        return cell
    }
}

In the storyboard, Control-drag from the Table View to HttpViewController. Connect to dataSource. Repeat to connect to delegate.

Creating the URL for the request

We can create the URL using several approaches.

With parameters.

let encodedText = searchText.addingPercentEncoding(
    withAllowedCharacters: CharacterSet.urlQueryAllowed)!
let urlString = String(format:"https://itunes.apple.com/search?term=%@", encodedText)
let url = URL(string: urlString)!

This calls the addingPercentEncoding(withAllowedCharacters:) method to create a new string where all the special characters are escaped, and you use that string for the search term.

Without parameters.

let url = URL(string: "https://jsonplaceholder.typicode.com/posts/")!

Performing the request

Now that you have a valid URL object, you can do some actual networking!

func performRequest(with url: URL) -> String? {
    do {
        return try String(contentsOf: url, encoding: .utf8)
    } catch {
        print("Download Error: \(error.localizedDescription)")
    return nil
    }
}

The meat of this method is the call to String(contentsOf:encoding:) which returns a new string object with the data it receives from the server pointed to by the URL.

Note that you’re telling the app to interpret the data as UTF-8 text.

You can use result of request like that

if let jsonString = performRequest(with: url) {
    print("Received JSON string '\(jsonString)'")
}

Parsing JSON

All you need to do in order to allow your app to read JSON data directly into the relevant data structures is to set them up to conform to Codable!

The trick to using Codable to parse JSON data is to set up your classes — or structs — to reflect the structure of the data that you'll parse. Let's add a new data model for the results wrapper.

Create Post.swift and replace its contents with the following:

import Foundation

class Post: Codable, CustomStringConvertible {
    var userId: Int = 0
    var id: Int = 0
    var title: String = ""
    var body: String = ""

    var description: String {
        return "id: \(id), title: \(title)"
    }
}

The CustomStringConvertible protocol allows an object to have a custom string representation. Or, to put it another way, the protocol allows objects to have a custom string describing the object, or its contents.

You will be using the JSONDecoder class, appropriately enough, to parse JSON data. Only trouble is, JSONDecoder needs its input to be a Data object. You currently have the JSON response from the server as a String.

You can convert the String to Data pretty easily, but it would be better to get the response from the server as Data in the first place — you got the response from the server as String initially only to ensure that the response was correct.

Modify performRequest(with:) as follows:

func performRequest(with url: URL) -> Data? {  
    do {
        return try Data(contentsOf:url)
    } catch {
        ...
    }
}

Add the following method

func parse(data: Data) -> [Post] {
    do {
        let result = try JSONDecoder().decode([Post].self, from: data)

        //DispatchQueue.main.async { [weak self] in
        //  self?.updateUI(result)
        //}

        return result
    } catch {
        print("JSON Error: \(error)")
        return []
    }
}

Error handling

Let’s add an alert to handle potential errors. It’s inevitable that something goes wrong somewhere and it’s best to be prepared.

Add the following method to ViewController:

func showNetworkError() {
    let alert = UIAlertController(title: "Whoops...",
        message: "There was an error accessing the web service." +
        "Please try again.", preferredStyle: .alert)
    let action = UIAlertAction(title: "OK", style: .default, handler: nil)
    present(alert, animated: true, completion: nil)
    alert.addAction(action)
}

Sorting the search results

It’d be nice to sort the search results alphabetically. That’s actually quite easy. A Swift Array already has a method to sort itself. All you have to do is tell it what to sort on.

In ViewController, right after the call to parse(data:) add the following:

posts.sort { $0.title.localizedStandardCompare($1.title)
                == .orderedAscending }

In order to sort the contents of the posts array, the closure will compare the Post objects with each other and return true if result1 comes before result2. The closure is called repeatedly on different pairs of Post objects until the array is completely sorted.

Swift has a very cool feature called operator overloading. It allows you to take the standard operators such as + or * and apply them to your own objects.

Open Post.swift and add the following code, outside of the class:

func < (lhs: Post, rhs: Post) -> Bool {
  return lhs.title.localizedStandardCompare(rhs.title) == .orderedAscending
}

Back in ViewController.swift, change the sorting code to:

posts.sort { $0 < $1 }

That’s pretty sweet. Using the < operator makes it very clear that you’re sorting the items from the array in ascending order.

But wait, you can write it even shorter:

  
posts.sort(by: <)

Spinner and network activity indicator

Let's add spinner:

private func showSpinner() {
    let spinner = UIActivityIndicatorView(style: .gray)
    spinner.center = CGPoint(x: view.bounds.midX + 0.5, y: view.bounds.midY + 0.5)
    spinner.tag = 1000
    print("spinner")
    view.addSubview(spinner)
    spinner.startAnimating()
}

private func hideSpinner() {
    view.viewWithTag(1000)?.removeFromSuperview()
}

And network activity indicator

UIApplication.shared.isNetworkActivityIndicatorVisible = true

This makes the network activity indicator visible in the app’s status bar.

Full code

class MyVCViewController: UIViewController {
    @IBOutlet weak var tableView: UITableView!

    var posts = [Posts]()
    let url = URL(string: "https://jsonplaceholder.typicode.com/posts/")!

    override func viewDidLoad() {
        super.viewDidLoad()

        showSpinner()
        if let response = loadPosts(with: url) {
            posts = parse(data: response)
            tableView.reloadData()
        }
        hideSpinner()
    }

    func loadPosts(with url: URL) -> Data? {
        UIApplication.shared.isNetworkActivityIndicatorVisible = true
        do {
            let data = try Data(contentsOf: url)
            UIApplication.shared.isNetworkActivityIndicatorVisible = false
            return data
        } catch {
            UIApplication.shared.isNetworkActivityIndicatorVisible = false
            print("Download Error: \(error.localizedDescription)")
            return nil
        }
    }

    func parse(data: Data) -> [Posts] {
        do {
            let result = try JSONDecoder().decode([Posts].self, from: data)
            return result
        } catch {
            print("JSON Error: \(error)")
            return []
        }
    }

    private func showSpinner() {
        let spinner = UIActivityIndicatorView(style: .gray)
        spinner.center = CGPoint(x: view.bounds.midX + 0.5, y: view.bounds.midY + 0.5)
        spinner.tag = 1000
        print("spinner")
        view.addSubview(spinner)
        spinner.startAnimating()
    }

    private func hideSpinner() {
        view.viewWithTag(1000)?.removeFromSuperview()
    }
}

extension MyVCViewController: UITableViewDelegate, UITableViewDataSource {
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return posts.count
    }

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cellIdentifier = "PostCell"
        var cell:UITableViewCell! = tableView.dequeueReusableCell(withIdentifier: cellIdentifier)
        if cell == nil {
            cell = UITableViewCell(style: .subtitle, reuseIdentifier: cellIdentifier)
        }
        cell.textLabel!.text = posts[indexPath.row].title
        cell.detailTextLabel!.text = posts[indexPath.row].description
        return cell
    }
} 

Asynchronous Networking

Did you notice that whenever you performed a search, the app became unresponsive? While the network request happens, you cannot scroll the table view up or down. The app is completely frozen for a few seconds.

To prevent blocking the main thread, any operation that might take a while to complete should be asynchronous. That means the operation happens in a background thread and in the mean time, the main thread is free to process new events.

Rather than making your own threads, iOS has several more convenient ways to start background processes. For this app you’ll be using queues and Grand Central Dispatch, or GCD.

In short, GCD has a number of queues with different priorities. To perform a job in the background, you put the job in a closure and then pass that closure to a queue and forget about it.

To make the web service requests asynchronous, you’re going to put the networking part from loadPosts(with:) into a closure and then place that closure on a medium priority queue.

let queue = DispatchQueue.global()
queue.async {
    if let data = self.loadPosts(with: url) {
        self.posts = self.parse(data: data)
        self.posts.sort(by: <)

        DispatchQueue.main.async {
            self.hideSpinner()
            self.tableView.reloadData()
        }
        return
    } 
}

With DispatchQueue.main.async you can schedule a new closure on the main queue.

When working with GCD queues you will often see this pattern:

let queue = DispatchQueue.global()
queue.async {
    // code that needs to run in the background
    DispatchQueue.main.async {
        // update the user interface
    } 
}

There is also queue.sync, without the "a" which takes the next closure from the queue and performs it in the background, but makes you wait until that closure is done. That can be useful in some cases but most of the time you’ll want to use queue.async.

URLSession

iOS itself comes with a number of different classes for doing networking, from low-level sockets stuff that is only interesting to really hardcore network programmers, to convenient classes such as URLSession.

Use the URLSession class for asynchronouys networking instead of downloading the contents of a URL directly.

URLSession is a closured-based API, meaning that instead of making a delegate, you pass it a closure containing the code that should be performed once the response from the server has been received. URLSession calls this closure the completion handler.

Steps to use URLSession

  1. Create or access a URLSession object (optionally configured with a URLSessionConfiguration object).
  2. Create a URL object (optionally using a URLComponents object to customize the URL).
  3. Optionally create a URLRequest object to further customize the URL request.
  4. Use the URLSession object and the URL (or URLRequest) object to create a task.
  5. Resume (begin) the task.
  6. Receive responses from the web service either in a completion callback or with delegate methods.

Following is an example.

let session = URLSession.shared

let dataTask = session.dataTask(with: url,
    completionHandler: { data, response, error in
        if let error = error as NSError? {
            if error.code == -999 {
                print("Cancelled")
            } else {
                print("Failure! \(error.localizedDescription)")
            }
        } else {
            if let data = data {
                self.posts = self.parse(data: data)
                self.posts.sort(by: <)
                DispatchQueue.main.async {
                    self.hideSpinner()
                    self.tableView.reloadData()
                }
                return
            }
        }
})

dataTask.resume()

We can cancel the on-going request when the user initiates a new request. URLSession makes this easy: data tasks have a cancel() method.

URLSessionConfiguration. The configuration object can be used for tweaking the HTTP parameters or for sending extra headers. These options may come in handy when you are implementing more complex APIs which force you to send special information with every request.

URLSessionConfiguration objects come in three flavors:

  • .default. This one is the default one. It's using the global disk cache, user, and credential storage.
  • .ephemeral. This is the one which stores everything in the memory. After the app is closed, there is no trace left. It should be used when we want to keep the user's information private.
  • .background. This is the option we should use when downloading huge files and in case we want to keep the application doing some action (uploading or downloading) after it's sent to the background.

The following sets up a default session configuration object:

let configuration = URLSessionConfiguration.default

Once you have a standard URLSessionConfiguration object, you can configure it further by modifying properties such as.

  • requestCachePolicy. Determines when requests in this session check for cached data. The following, for example, requests that local caches are ignored:
             
configuration.requestCachePolicy = .reloadIgnoringLocalCacheData
  • timeoutIntervalForRequest. The acceptable waiting time before a request times out. The following, for example, changes the timeout interval from 60 (the default) to 30:
configuration.timeoutIntervalForRequest = 30
  • allowsCellularAccess. Specifies whether this session should connect via cellular networks. The following, for example, prevents your session from connecting via cellular networks:
configuration.allowsCellularAccess = false

URLSession. There are three ways to access a URLSession, which range from basic access to the session to broader access to configure the session and receive session events.

  • Shared session. URLSession contains a type property called shared which contains a reference to a URLSession singleton. This object is supplied and configured by the runtime; it is good for very simple, occasional use, where you don’t need configuration, authentication, dedicated cookie storage, and so forth. You can’t interact with the session while it’s performing a networking task for you, because you have no delegate. All you can do is order some task to be performed and then stand back and wait for it to finish.
let session = URLSession.shared
  • Instantiated with a session configuration object. You’ll hand the session a URLSessionConfiguration object describing the desired environment. This means that you can configure the URLSession, but you can’t interact with it while it’s performing a networking task for you, because you have no delegate.
let session = URLSession(configuration: configuration)
  • Instantiated with a session configuration object, delegate, and queue. Like the preceding initializer, you’ll hand the session a URLSessionConfiguration object. But now you also have a delegate, which can receive various callbacks during the course of a networking task.
let session = URLSession(configuration: configuration, delegate: self,
          delegateQueue: OperationQueue.main)

URL and URLRequest. Create a URLRequest object from the URL object.

let request = URLRequest(url: url)

URLSessionDataTask. We should create specific instances of URLSessionTask using the URLSession methods. Each task belongs to a session. Based on the method which we call, the final task could be one of the following:

  • URLSessionDataTask. A standard request call which is not supported in the background session.
  • URLSessionUploadTask. The upload of data is easier if you are using this task. It's supported in the background session.
  • URLSessionDownloadTask. Download and save the resource straight to a file on disk. This is supported in any session.
  • URLSessionStreamTask. Used to establish a TCP/IP connection from a host and port.

Once you have created a task, then you have to call resume() to start it. Each task is executed asynchronously. This is perfect, because it won't hurt our performance. The UI of the app is always rendered on the main (UI) thread. There is one caveat: the handler is executed on the background thread as well.

With the URL or URLRequest you just created, the URLSession object will create and coordinate one or more tasks for you.

Create a URLSessionDataTask by passing in the URLRequest object to the URLSession. A completion handler will receive the response from the server, which contains data, response, and error optional objects. Because all tasks begin life by default in a suspended state, you must trigger them to start by calling the resume method to activate them.

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

You now have a Data object returned from the web service in the dataTask completion handler. Because Data objects are binary, conversion will be necessary. To convert the data object to text, you could instantiate a String, pass in the data object, and specify the most frequently used character encoding, UTF-8.

let dataAsString = String(data: data, encoding: String.Encoding.utf8)

UIImageView extension for downloading images

Add a new file to the project using the Swift File template, and name it UIImageView+DownloadImage.swift.

Replace the contents of the new file with the following:

import UIKit
extension UIImageView {
    func loadImage(url: URL) -> URLSessionDownloadTask {
        let session = URLSession.shared

        let downloadTask = session.downloadTask(with: url,
            completionHandler: { [weak self] url, response, error in
                if error == nil, let url = url,
                    let data = try? Data(contentsOf: url),
                    let image = UIImage(data: data) {

                    DispatchQueue.main.async {
                        if let weakSelf = self {
                            weakSelf.image = image
                        }
                    } 
                }
            })

        downloadTask.resume()
        return downloadTask
    }
}

Using the image downloader extension

var downloadTask: URLSessionDownloadTask?

avatar.image = UIImage(named: "Placeholder")
if let imgUrl = URL(string: urlImage) {
    downloadTask = avatar.loadImage(url: imgUrl)
}

Example of URLSession

let url = URL(string: "https://url")!

let task = URLSession.shared.dataTask(with: url) { data, response, error in
    guard error == nil else {
        print ("error: \(error!)")
        return
    }

    guard let data = data else {
        print("No data")
        return
    }

    guard let items = try? JSONDecoder().decode([Item].self, from: data) else {
        print("Error: Couldn't decode data into item array")
        return
    }

    for item in items {
        print("item name is \(item.name)")
    }
}

task.resume()

Understanding the Result Type

The Result is an enum with two cases: success and failure, both of which are implemented using generics. Success can be anything, but failure must conform to the Error protocol.

Let’s see the benefits of using Result on a simple network request.

enum RequestError: Error {
    case clientError
    case serverError
    case noData
    case dataDecodingError
}

class NetworkService {
    func makeUrlRequest(_ request: URLRequest, resultHandler: @escaping (Result<String, RequestError>) -> Void) {
        let urlTask = URLSession.shared.dataTask(with: request) { (data, response, error) in
            guard error == nil else {
                resultHandler(.failure(.clientError))
                return
            }

            guard let response = response as? HTTPURLResponse, 200...299 ~= response.statusCode else {
                resultHandler(.failure(.serverError))
                return
            }

            guard let data = data else {
                resultHandler(.failure(.noData))
                return
            }

            guard let decodedData: String = self.decodedData(data) else {
                resultHandler(.failure(.dataDecodingError))
                return
            }

            resultHandler(.success(decodedData))
        }

        urlTask.resume()
    }

    private func decodedData(_ data: Data) -> String? {
        return String(data: data, encoding: .utf8)
    }    
}

As you can see, using Result makes the code easier to read.

let networkService = NetworkService()

networkService.makeUrlRequest(request) { (result) in
    switch result {
    case .success(let successValue):
        // handle success
    case .failure(let error):
        // handle error
    }
}