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
URLSession
object (optionally configured with a URLSessionConfiguration
object).URL
object (optionally using a URLComponents
object to customize the URL).URLRequest
object to further customize the URL request.URLSession
object and the URL
(or URLRequest
) object to create a task.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.
let session = URLSession.shared
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)
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 } }