Let’s start at the very beginning, so it’s clear exactly what we’re talking about here – in Swift, networking is the process of sending and receiving data between an iOS application and a remote server or another device over the internet.
Table of Contents
Why is networking is important in app development?
So why is networking an important for a mobile application? Put simply, it powers the dynamic content, enabling users to see the latest data as activity is saved to a remote database using web APIs, from where it can be fetched and viewed by users on other devices.
Networking supports a range of functionality, including:
- Real-time data: Networking enables mobile apps to send and receive the data they rely on in real-time by interacting with external web services, APIs, or cloud platforms.
- Security: Networking supports user authentication, ensuring sensitive data remains secure through server-side authorization checks.
- Communication and messaging: WebSocket and other real-time protocols provide instant data updates enabling communication features such as instant messaging and chat.
- Bug fixing: Networking allows developers to change the configuration of apps and servers so dynamic content related bugs can be resolved without deploying new builds.
- Cross-platform integration: Networking facilitates communication between different platforms and devices, enabling more streamlined user experiences.
- Analytics: Networking is important for sending analytics data back to servers.
Ok great! Now let’s get into it and look at how we implement networking solutions in Swift, starting with URLSession.
What is URLSession?
URLSession is a Swift framework that supports networking by providing a set of classes and methods for configuring and managing network sessions.
It enables apps to fetch data from a server, upload and download media and content files, and handles WebSocket communication.
Components and implementation
URLSession has three components to think about, they are:
- URLSessionDataTask – for making basic HTTP GET requests and fetching data
- URLSessionUploadTask – for uploading the data to a server
- URLSessionDownloadTask – for downloading files and data in the background
That’s the top-line, let’s now look at how we can use these components to build functionality into our app.
Using URLSessionDataTask
to fetch data
URLSessionDataTask
is a URLSession
framework used mainly for making simple GET requests to fetch text data (e.g. post titles and descriptions) from a URL without downloading other media such as images.
Let’s see how this works with an example:
import Foundation
// Define the URL you want to request
let apiUrlStr = "<https://bugfender.request.url>"
// Create a URL object from the string
if let apiUrl = URL(string: apiUrlStr) {
// Create a URLSession instance
let session = URLSession.shared
// Create a data task using URLSessionDataTask
let dataTask = session.dataTask(with: apiUrl) { (data, response, error) in
// Handle the response
// Check for errors
if let error = error {
print("Error: \(error)")
return
}
// Check if data is available
guard let responseData = data else {
print("No data received")
return
}
// Process the received data
do {
if let json = try JSONSerialization.jsonObject(with: responseData, options: []) as? [String: Any] {
print("Response JSON: \(json)")
if let title = json["title"] as? String {
print("Title: \(title)")
}
}
} catch {
print("Error parsing JSON: \(error)")
}
}
dataTask.resume()
} else {
print("URL is not valid!")
}
Let see what the example does:
- The instance of
URLSessionDataTask
is initialized by providing a URL, and then we have a closure to execute completion of the function. - Now we have the
URLSessionDataTask
, we can use itsresume()
method to initiate the network request. - The completion handler is called when the data task completes the execution and includes the received data, the URL response, and any error that may have occurred during the request from the server.
As you can see in the example, we’re using URLSession.shared
singleton, this shared session is ideal for simple tasks that don’t require special configurations.
Upload data using URLSessionUploadTask
So we can fetch data, now let’s look at using URLSessionUploadTask
to upload the data to a specified URL, like this:
import Foundation
// Define the URL for the server endpoint
let apiUrlString = "<https://bugfender.request.url>"
// Create a URL object from the string
if let apiUrl = URL(string: apiUrlString) {
// Create a URLSession instance
let session = URLSession.shared
// Define the data you want to upload
let jsonPayload = ["key": "value"]
// Convert the JSON payload to Data
do {
let jsonData = try JSONSerialization.data(withJSONObject: jsonPayload, options: [])
// Create a URLRequest with the URL and set the HTTP method to POST
var request = URLRequest(url: apiUrl)
request.httpMethod = "POST"
request.httpBody = jsonData
// Create an upload task using URLSessionUploadTask
let uploadTask = session.uploadTask(with: request, from: jsonData) { (data, response, error) in
// Handle the response here
// Check for errors if received from server
if let error = error {
print("Error: \(error)")
return
}
// Check if data is available
if let responseData = data {
// Process the response data as needed
let responseString = String(data: responseData, encoding: .utf8)
print("Response: \(responseString ?? "No response data")")
}
}
// Resume the upload task to initiate the request
uploadTask.resume()
} catch {
print("Error in JSON data: \(error)")
}
} else {
print("URL is invalid")
}
In this example we’re uploading a simple JSON payload to the remote server.
It’s worth noting that the server will need to be able to handle JSON payloads at a specific endpoint.
Download files using URLSessionDownloadTask
After uploading, comes downloading and we can use URLSessionDownloadTask
to download files from a specified URL, like this:
import Foundation
// Define the URL for the file you want to download
let fileUrlString = "<https://bugfender.request.url>"
// Create a URL object from the string
if let fileUrl = URL(string: fileUrlString) {
// Create a URLSession instance
let session = URLSession.shared
// Create a download task using URLSessionDownloadTask
let downloadTask = session.downloadTask(with: fileUrl) { (temporaryUrl, response, error) in
// Handle the response
// Check for errors
if let error = error {
print("Error: \(error)")
return
}
// Check if a temporary file URL is available
if let tempUrl = temporaryUrl {
do {
// Create a destination URL to save the downloaded file
let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
let destinationUrl = documentsDirectory.appendingPathComponent("downloadedFile.pdf")
// Move the temporary file to the destination URL
try FileManager.default.moveItem(at: tempUrl, to: destinationUrl)
print("File downloaded successfully. Saved at: \(destinationUrl)")
} catch {
print("Error moving file: \(error)")
}
}
}
// Resume the download task to initiate the request
downloadTask.resume()
} else {
print("Invalid URL")
}
In this example, we have demonstrated how to download a file and save it to a local directory usingURLSessionDownloadTask
.
With us so far? Good!
Now let’s look at something a little more complex.
Configuring URLSession
to handle tasks with delegates and closures
We can use URLSession
to handle tasks using delegates and closures, both of which help us manage asynchronous requests and responses in networking.
Each is set up in a different way, we’ll look at the delegates approach first and to do this we’ll need to:
- Conform to the
URLSessionDelegate
protocol - Implement the required functions
- Create a session with a delegate
- Associate tasks with that session
import Foundation
class NetworkDelegateClass: NSObject, URLSessionDelegate, URLSessionDataDelegate {
// URLSessionDataDelegate method to handle response data
func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
// Process the received data
print("Received data: \(data)")
}
// URLSessionDataDelegate method to handle completion
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
if let error = error {
// Handle error
print("Task completed with error: \(error)")
} else {
// Task completed successfully
print("Task completed successfully")
}
}
}
// Example of using delegates
let delegateClass = NetworkDelegateClass()
let delegateSession = URLSession(configuration: .default, delegate: delegateClass, delegateQueue: nil)
if let url = URL(string: "<https://bugfender.sample.request/url>") {
let delegateTask = delegateSession.dataTask(with: url)
delegateTask.resume()
}
Alternatively, we can use closures to handle the response, which is more straightforward.
For this we’ll need to provide closures directly when creating the tasks, as below:
import Foundation
// Example of using closures
if let url = URL(string: "<https://bugfender.sample.request/url>") {
let closureSession = URLSession.shared
let closureTask = closureSession.dataTask(with: url) { (data, response, error) in
// Handle the response
// Check for errors
if let error = error {
print("Error: \(error)")
return
}
// Check if data is available
guard let responseData = data else {
print("No data received")
return
}
// Process the received data
print("Received data: \(responseData)")
}
// Resume the task to initiate the request
closureTask.resume()
}
That’s it forURLSession
, now we can get to know URLRequest
.
What is URLRequest
?
URLRequest
is is used for creating the network requests in Swift and contains the details of a request (the URL, HTTP method [GET, POST, etc.], request headers, and other parameters), allowing us to configure and customize a network request before it’s sent to the server for execution.
URLRequest
works with URLSession
for networking tasks in Swift and it allows us to configure the requests based on specific requirements whilst supporting more complex scenarios than a shared URLSession
.
With that in mind, let’s look at how we could setup the URL, HTTP method, headers and cache policy with some examples.
Setting the URL
The URL is the core component of any network request as it’s the location to which the request is sent.
The example below shows how to set the URL when creating URLRequest
object:
import Foundation
// Example of setting the URL in a URLRequest
if let url = URL(string: "<https://api.bugfender.com/data>") {
var request = URLRequest(url: url)
// Additional configurations and parameters can be set for this request
}
Setting the HTTP method
Equally important, the HTTP method defines the type of operation to be performed by a particular network request.
Common methods include GET, POST, PUT and DELETE and we’ll need to specify when creating a URLRequest,
as below:
// Example of specifying the HTTP method in a URLRequest
request.httpMethod = "POST"
Adding headers
Headers can be used to provide additional information (such as authentication tokens, content type, and other custom metadata) about the request to the server.
We can add headers to a URLRequest
using the addValue(_:forHTTPHeaderField:)
, as below:
// Example of adding headers in a URLRequest
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
request.addValue("Bearer YourAccessToken", forHTTPHeaderField: "Authorization")
Configuring the cache policy
The cache policy establishes if a request should use cached data and how to handle caching.
We can configure the cache policy when creating the URLRequest
object, like this:
// Example of configuring the cache policy in a URLRequest
request.cachePolicy = .reloadIgnoringLocalCacheData
Encoding parameters
Query parameters and data for a POST request can be encoded and added to a URLRequest
and this is done as below:
// Example of encoding parameters in a URLRequest
let parameters: [String: Any] = ["key1": "value1", "key2": "value2"]
if let jsonData = try? JSONSerialization.data(withJSONObject: parameters) {
request.httpBody = jsonData
}
These parameters are encoded as JSON and set as the HTTP body of the request, although encoding parameters depend on the specific requirements of the API and various methods (e.g. URL or JSON) can be used.
So far, so good. Now we’ve configured both URLSession
and URLRequest
, it’s time to bring them together.
Running a request with URLRequest
To use URLRequest
with URLSession
to run network requests, we’ll first need to pass it to the dataTask
method as shown in this example:
import Foundation
// Example of setting the URL in a URLRequest
if let url = URL(string: "<https://api.bugfender.com/data>") {
var request = URLRequest(url: url)
// Set the request HTTP method to GET. This is actually the default if not set, but included here for clarity.
request.httpMethod = "GET"
// Add your headers here, for example:
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
// Replace `YourAccessToken` with your actual access token for the API
request.setValue("Bearer YourAccessToken", forHTTPHeaderField: "Authorization")
// URLSession configuration
let config = URLSessionConfiguration.default
let session = URLSession(configuration: config)
// Create the task to fetch the data
let task = session.dataTask(with: request) { (data, response, error) in
//Process the data
//...
}
// Start the task
task.resume()
}
Almost as important as the initial configuration is handling errors, let’s take a look at that next.
Error handling in networking
So, what are the most common errors in networking?
- No internet connection: If a device isn’t connected to the internet it won’t be able to make server requests.
- Timeout: Occurs when a server takes too long to respond to a request and the set timeout interval is exceeded.
- Server-side errors: Issues such as the requested resource not being found or the server is down due to load -will come with a HTTP response code.
- Invalid URL: If the provided URL is incorrect or cannot be converted to a valid URL object.
- SSL Certificate issues: Results from a problem with the SSL handshake or if the certificate is invalid.
- Data parse errors: When the data cannot be parsed correctly, often due to a format mismatch.
Error handling with URLSessionDelegate
We can use URLSessionDelegate
methods and URLSessionTaskDelegate
protocols to handle these errors, like this:
class MyDelegateClass: NSObject, URLSessionDelegate, URLSessionTaskDelegate {
// URLSessionTaskDelegate method to handle completion with error
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
if let error = error {
// Handle the error
print("Task completed with error: \(error)")
} else {
// Task completed successfully
print("Task completed successfully")
}
}
}
Error handling using Result types
From Swift 5, a more effective way of handling errors is the Result type, this is particularly useful when working with the closures, as shown below:
import Foundation
enum NetworkError: Error {
case noInternet
case timeout
case serverError
// ... other specific error cases
}
func fetchData(completion: @escaping (Result<Data, NetworkError>) -> Void) {
guard hasInternetConnection() else {
completion(.failure(.noInternet))
return
}
// Perform URLSession task
let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
if let error = error {
completion(.failure(.serverError))
return
}
guard let responseData = data else {
completion(.failure(.serverError))
return
}
completion(.success(responseData))
}
task.resume()
}
// Usage
fetchData { result in
switch result {
case .success(let data):
// Handle success
print("Data received: \(data)")
case .failure(let error):
// Handle failure
print("Error: \(error)")
}
}
As you can see in the previous example, we use a Swift enum to facilitate error handling.
Now we know how to handle errors, let’s look at some more complex tasks.
Asynchronous URLSession
tasks
Asynchronous programming is where tasks are executed simultaneously, allowing the system to perform other operations while waiting for tasks to complete.
URLSession
tasks such as data tasks, download tasks, and upload tasks, operate asynchronously by default, ensuring the network requests do not block the main thread, and preventing the UI from freezing during data fetching or upload/download operations.
Using closure based completion handlers
URLSession
tasks use closure based completion handlers to manage the asynchronous response.
Closures are mainly executed when the task is completed, providing access to the data, response, and errors, and we would set them up like this:
let url = URL(string: "<https://api.bugfender.com/data>")!
let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
// Handle the asynchronous response
if let error = error {
print("Error: \(error)")
return
}
if let responseData = data {
// Process the received data
print("Received data: \(responseData)")
}
}
// Resume the task to initiate the request
task.resume()
Using dispatch queues to manage background tasks
Dispatch queues allow users to manage concurrent tasks and control the overall execution of code on different threads.
Using the background queues is crucial when handling time consuming tasks without affecting the main thread.
Below we’ve executed a network request on a background queue, and updated the UI on the main queue:
let url = URL(string: "<https://api.bugfender.com/data>")!
DispatchQueue.global(qos: .background).async {
// Perform the network request on a background queue
let data = try? Data(contentsOf: url)
DispatchQueue.main.async {
// Update the UI on the main queue
if let responseData = data {
print("Received data: \(responseData)")
}
}
}
That’s it!
URLSession
best practice
Finally, let’s take a look at some examples of best practice for developers to keep in mind when configuring and using URLSession
for networking.
Managing sessions and tasks
- Session configuration: Choosing an appropriate URLSessionConfiguration based on requirements (e.g. using a background configuration for tasks that continue running in the background) is essential.
- Reuse sessions: Be sure to reuse the
URLSession
object for the same kind of tasks to optimize the use of resources by reusing the connection. - Task Prioritization: Utilizing the task priorities (high, default, low) to manage the order of task execution in a session.
let highPriorityTask = session.dataTask(with: highPriorityRequest)
highPriorityTask.priority = URLSessionTask.highPriority
highPriorityTask.resume()
- Cancellation and invalidation of tasks: If tasks are no longer needed, cancel them to free up resources and invalidate sessions if they are no longer required.
task.cancel()
session.invalidateAndCancel()
Handling background tasks
- Background Configuration: Use a background session configuration for tasks that need to continue running in the background, even if the app is not active.
let backgroundConfig = URLSessionConfiguration.background(withIdentifier: "com.bugfender.backgroundSession")
let backgroundSession = URLSession(configuration: backgroundConfig, delegate: delegate, delegateQueue: nil)
- Background completion handlers: Don’t forget to implement a background completion handler to handle events in the background.
func application(_ application: UIApplication, handleEventsForBackgroundURLSession identifier: String, completionHandler: @escaping () -> Void) {
// Store the completion handler for later use
backgroundCompletionHandler = completionHandler
}
- Handling background session events: Remember to implement the appropriate URLSessionDelegate methods to handle background session events.
func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) {
if let completionHandler = backgroundCompletionHandler {
backgroundCompletionHandler = nil
completionHandler()
}
}
Monitoring network activity
- Activity Indicator: Use a network activity indicator to inform users about ongoing network requests, ensuring they’re aware the app is waiting for a response from the server.
UIApplication.shared.isNetworkActivityIndicatorVisible = true // Show indicator
// Perform network request
UIApplication.shared.isNetworkActivityIndicatorVisible = false // Hide indicator
- Combine multiple requests: Where possible, combine multiple network requests into a single logical operation to reduce the recurrence of the network activity indicator.
- Completion handlers: It’s a good idea to turn off the network activity indicator when data is received in the completion handlers.
UIApplication.shared.isNetworkActivityIndicatorVisible = true // Show indicator
session.dataTask(with: request) { (data, response, error) in
// Handle response
DispatchQueue.main.async {
UIApplication.shared.isNetworkActivityIndicatorVisible = false // Hide indicator
}
}.resume()
And that’s everything for now.
To sum up
- Working with
URLSession
for networking tasks requires a good understanding of the key concepts, and adopting the best practices will ensure reliable and maintainable code for networking. URLRequest
is a Swift struct representing a URL request, HTTP method, headers, and cache policy.- The configuration of
URLRequest
involves setting the correct URL, adding the HTTP method, adding headers, and configuring the cache policy of network request. URLSession
is Swift API for making the network requests.- The best practices for managing sessions and tasks include reusing sessions, prioritizing tasks, and cancelling tasks if they are no longer needed.
URLSession
tasks are asynchronous by default, ensuring the responsiveness in the iOS app.- Closures are used for handling the asynchronous tasks, and the Result type in Swift 5 (and later), enhances error handling.
By following best practices and gaining an understanding of the core fundamental concepts, developers can build robust and scalable networking code using URLSession
. As with anything, use this guide to help you experiment with and better understand.