Talking to Servers
The Client-Server model dictates that the server component is to be designed so as to facilitate communication with one or more clients. This is a widely prevalent architecture that we employ in designing products and services and it’s not alien to iOS clients that we build today. On the iOS platform, we have a variety of frameworks assisting in accomplishing various things without having to rely on third-party frameworks. This includes communicating with the server whenever necessary without adding any overhead to your app or affecting performance.
I would like to discuss how the platform provides ways to talk to the server in this post. If you simply want to look at the code, I have a playground with all what we discuss in this post, feel free to check that out.
Meet the URL Loading System
Resources on the internet are identified using a URL, which simply is an abbreviation for Uniform Resource Locator. A URL is constructed with various components which include the protocol used for communication, the hostname, the TLD, the file path etc.
The URL Loading System built into iOS is responsible for interacting and communicating with servers over the internet using the standard Internet Protocols like https, ftp etc. The loading performed by this system is asynchronous to make sure the app is responsive and errors are handled accordingly.
The URL Loading System uses URLSession instances to manage the communication between the app and the server on a broad level, which further leverages URLSessionTask instances to perform tasks that download data from the server, be it images, files or anything practically using the URLSessionDataTask instance or upload data to the server using URLSessionUploadTask instance.
A URLSession can manage multiple URLSessionTasks:
URLSession
The URLSession API is used to manage data transfers within the application and is essentially the object we employ to talk to the servers. It encompasses many URLSessionTasks which can either download or upload data from a server. This API provides various delegate methods which can be leveraged for functions including background downloads and authentication support.
A URLSession instance is typically instantiated with a URLSessionConfiguration object which specifies the connection behaviour like whether to allow transfer over a cellular network, etc. The shared
instance is a singleton on the URLSession class which is an instance without a session configuration object and can be used for very basic transfers when the requirements are minimal. Otherwise, there are three kinds of configurations we could use to instantiate a URLSession:
default session configuration is very much the basic setup which allows configuration of the session and allows data to be obtained using delegates
ephemeral session configuration restricts data writes to disk like caches, cookies etc, which can simply be regarded as a private session
background session configuration is used to upload and download data in the background when the app isn’t running and it’s one of the powerful features we’ll discuss shortly
Session Tasks
A URLSession uses tasks to manage transfers like we’ve learnt sometime before.
URLSessionTask
is an abstract class which describes a task object, and the different kinds of tasks that the session could manage are:
- Data Tasks (
URLSessionDataTask
) - Download Tasks (
URLSessionDownloadTask
) - Upload Tasks (
URLSessionUploadTask
)
These tasks are used for purposes which are implied quite directly from their names.
Data Tasks are used for data transfers using the Data
objects and represents a stream of data.
Upload Tasks and Download Tasks are used for data transfers in the form of file objects, and both of them support background sessions which could be used to download and upload data in the background when the app isn’t active.
All kinds of tasks can be suspended, cancelled and resumed. In addition to this URLSessionDownloadTask
instances can be paused to allow for the ability to be resumed at a later point.
GETting and POSTing data
Let’s briefly go through how to put URLSession
to use to download an image from a URL using a Data Task.
Building a Configuration Object
1// Default Configuration2let defaultConfiguration = URLSessionConfiguration.default3defaultConfiguration.allowsCellularAccess = true45// Ephemeral Configuration6let ephemeralConfiguration = URLSessionConfiguration.ephemeral78// Background Configuration9let backgroundConfiguration = URLSessionConfiguration.background(withIdentifier: "backgroundConfig")
Let’s use the default configuration to instantiate a session in this example.
Instantiating Session with Delegate
1// Instantiate a Session with Delegate2let session = URLSession(configuration: defaultConfiguration, delegate: self, delegateQueue: nil)
Data Task
Completion Handler
A Completion Handler is a swift block which is passed as an argument when creating a data task, this block is called once the task completes and is an appropriate place to use the data we’ve retrieved using our task.
1// Image Download Task2self.dataTaskCompletionHandler = { (data, response, error) in3 guard error == nil else { return }4 guard let imageData = data else { return }5 let image = UIImage(data: imageData)6 DispatchQueue.main.async {7 self.backgroundImageView.image = image8 self.imageView.image = image9 }10}
Building a Data Task and Starting it
1guard let url = URL(string: ImageURL) else { return }2let imageTask =3session.dataTask(with: url, completionHandler: dataTaskCompletionHandler)4imageTask.resume()
The method dataTask(with:completionHandler:)
is used to create a task that downloads the image and in this case, we also pass a completion handler which is called after the task is completed. In most cases, this approach is sufficient to keep things simple. In times where we could use more control over the download/upload process, there is a delegate we could employ for this purpose: URLSessionDataDelegate which lets us handle task-level events specific to the tasks.
Download Task
Completion Handler
In a download task, we get a reference to the location of the file in the filesystem after the task has executed successfully. This can be an effective way to download resources locally and reference elsewhere.
1self.downloadTaskCompletionHandler = { (url, response, error) in2 guard error == nil else { return }3 guard let url = url else { return }4 guard let response = response else { return }5 print("Download Task Response")6 print("----")7 print("File saved to: \(url)")8 print("Type of resource retrieved: \(response.mimeType)")9 }10}
Building a Download Task and Starting it
1guard let url = URL(string: ImageURL) else { return }2let imageFileDownloadTask = session.downloadTask(with: url, completionHandler: downloadTaskCompletionHandler)3imageFileDownloadTask.resume()
From the completion handler, we see that once the download task is executed the URL of the downloaded file and the type of resource downloaded will be printed. A Completion Handler is a simple way to handle such a use case. But often times we would like more control over the download task such as to monitor the download progress. In such cases, we can make use of URLSessionDownloadDelegate methods to implement progress tracking and such. But, please keep in mind you can only either use completion handler OR delegate methods, but not both.
Progress Tracking
Let’s use a separate session to implement this since we have to assign a delegate when we initialize a session and cannot do it after.
1lazy var downloadsSession: URLSession = {2 let config = URLSessionConfiguration.default3 return URLSession(configuration: config, delegate: self, delegateQueue: nil)4}()
Notice we’re using a lazy
variable here to make sure we’re assigning the self
delegate after the current ViewController is initialised.
The delegate method where we could track progress is
1func urlSession(URLSession, downloadTask: URLSessionDownloadTask, didWriteData: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64)
We can implement it this way:
1extension ViewController: URLSessionDownloadDelegate {2 func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {3 print("Finished downloading to \(location)")4 }56 func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {7 let progress = Double(totalBytesWritten) / Double(totalBytesExpectedToWrite)8 print("Download Progress: \(progress)")9 }10}
This way URLSession
gives us extensive control over the download process. Now that we’ve seen how to download things, let’s try to upload some data which is important because hey — who enjoys one-way communication, let’s talk back to the server.
Upload Tasks
Upload Tasks are similar to the tasks that we’ve seen thus far, except they POST data to a server. The method for creating and executing an Upload Task stays the same as other tasks. There are two ways to deal with completion after Upload Tasks, one is using a completion handler when a session is created or the session delegate tasks similar to the data tasks we discussed before. The Upload Task calls the respective data task delegates and behaves like a data task after the upload phase of the request finishes.
In case we want to track the progress of the upload we could do so in the
1func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64)
method of the URLSessionDelegate similar to how we implemented it for the Download Task.
Completion Handler
1// Data Upload Task Completion Handler2self.uploadTaskCompletionHandler = { (data, response, error) in3 guard error == nil else { return }4 guard let data = data else { return }5 print("Upload Task")6 print("----")7 print("Uploaded \(data) of data successfully.\n")8}
We’re simply printing the data back from the server, in case we like to parse this data it can be done easily by casting it to JSON or however the response of the server is.
Building an Upload Task
1guard let url = URL(string: UploadURL) else { return }2let uploadURLRequest = URLRequest(url: url)34// 15let drivers: [String: String] = [6 "Mercedes": "Lewis Hamilton",7 "Ferrari": "Sebastian Vettel",8 "RedBull": "Daniel Ricciardo"9]1011// 212let driversData = try? NSKeyedArchiver.archivedData(withRootObject: drivers)13guard let data = driversData else { return }1415// 316let uploadTask = session.uploadTask(with: uploadURLRequest,17from: data, completionHandler: uploadTaskCompletionHandler)1819uploadTask.resume()
In this example, we simply build a dictionary (1) and convert it into a Data
object (2). Once we have a data
object we could create an upload task (3) from the data and resume the task. In case we would like more control over the task events, we should avoid passing in the completion handler, and implement uploadTask
delegate methods (detailed here).
Background Sessions
Background Sessions are extremely useful in cases where we like to minimize the use of device resources and download things without keeping the user actively engaged. A background URLSession
can be created simply by using an identifier:
1let backgroundConfiguration = URLSessionConfiguration.background(withIdentifier: "backgroundConfig")
This identifier needs to be stored and could be used again to reassociate this to a session when the app is resumed, terminated or crashed. The App Delegate method where this could be handled is
1application(_:handleEventsForBackgroundURLSession:completionHandler:)
This method is implemented in the app’s AppDelegate.swift
and completion can be handled within here, but note that the completion handler provided which is a part of UIKit needs to be called on the main thread.
I highly encourage you to use URLSession
for your networking on iOS, it’s easy to use and you could very well avoid the overhead of a networking library once you understand how to work with this API. Feel free to leave any feedback.