Swift Networking Essentials: Using URLSession and URLRequest in iOS Apps

Swift Networking Essentials: Using URLSession and URLRequest in iOS Apps

iOSSwift
Fix bugs faster! Log Collection Made Easy
START NOW!

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.

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:

  1. The instance of URLSessionDataTask is initialized by providing a URL, and then we have a closure to execute completion of the function.
  2. Now we have the URLSessionDataTask, we can use its resume() method to initiate the network request.
  3. 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:

  1. Conform to the URLSessionDelegate protocol
  2. Implement the required functions
  3. Create a session with a delegate
  4. 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?

  1. No internet connection: If a device isn’t connected to the internet it won’t be able to make server requests.
  2. Timeout: Occurs when a server takes too long to respond to a request and the set timeout interval is exceeded.
  3. 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.
  4. Invalid URL: If the provided URL is incorrect or cannot be converted to a valid URL object.
  5. SSL Certificate issues: Results from a problem with the SSL handshake or if the certificate is invalid.
  6. 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.

Expect the Unexpected! Debug Faster with Bugfender
START FOR FREE

Trusted By

/assets/images/svg/customers/cool/airmail.svg/assets/images/svg/customers/projects/vorwerk.svg/assets/images/svg/customers/highprofile/schneider_electric.svg/assets/images/svg/customers/highprofile/gls.svg/assets/images/svg/customers/highprofile/oracle.svg/assets/images/svg/customers/projects/ultrahuman.svg/assets/images/svg/customers/highprofile/adt.svg/assets/images/svg/customers/cool/continental.svg

Already Trusted by Thousands

Bugfender is the best remote logger for mobile and web apps.

Get Started for Free, No Credit Card Required