URLSession in Swift: The Essential Guide [with Examples]

When you need to download or upload data in Swift, the URLSession class is the solution you should reach for.

URLSession handles the most common Internet protocols and allows you to:

  • Group and optimize data transfers.
  • Handle authentication, cookies, and caching.
  • Pause, resume, or cancel a download.
  • Download data in the background when your app is suspended.

FREE GUIDE - SwiftUI App Architecture: Design Patterns and Best Practices

MVC, MVVM, and MV in SwiftUI, plus practical lessons on code encapsulation and data flow.

DOWNLOAD THE FREE GUIDE

Table of contents

Chapter 1

Using URLSession to perform network transfers

In this chapter:

What is URLSession and how to use it in Swift

URLSession is a class from the Foundation framework you use to perform network requests. It allows you to download data from and upload data asynchronously to endpoints identified by URLs.

Fetching data into memory with URLSession is straightforward. You use the shared instance of the URLSession class to create a data task that delivers the result asynchronously.

import Foundation

let url = URL(string: "https://example.com/endpoint")!
let (data, response) = try await URLSession.shared.data(from: url)

The asynchronous function’s return value is a tuple containing the requested resource in a Data instance and a URLResponse containing its associated metadata.

URLSession supports HTTP, FTP, and other Internet protocols

The Internet is defined by a series of layered protocols that describe how data is transferred between computers through connected networks.

iOS Apps Transfer Data over the Internet Protocol Suite

As an iOS developer, you can ignore the lowest layers: the link, the internet, and the transport layers. However, you must pick one of the protocols in the application layer to define how your app will communicate with a server.

Unless otherwise specified, the URLSession class performs network requests using the HTTP protocol.

In that case, the returned URLResponse object is an instance of the HTTPURLResponse subclass and contains the response’s HTTP status code and headers.

URLSession also supports FTP, WebSocket, and custom networking protocols.

Parsing the JSON data returned by a REST API

URLSession is often used to fetch JSON data from a REST API. The returned Data value contains the requested JSON data in such cases.

You parse the returned JSON data in Swift using the Decodable protocol and the JSONDecoder class.

import Foundation

struct Resource: Decodable {
	// ...
}

let url = URL(string: "https://example.com/endpoint")!
let (data, response) = try await URLSession.shared.data(from: url)
let resource = try JSONDecoder().decode(Resource.self, from: data)

Replacing the old URLSession API using completion closures with Swift Concurrency

Before the introduction of Swift Concurrency, URLSession used a callback-based API that delivered its results to a completion handler.

You will still find many examples using this API online and in old projects.

import Foundation

struct Resource: Decodable {
	// ...
}

func getResource(from url: URL, completion: @escaping (Result<Resource, Error>) -> Void) {
	let task = URLSession.shared.dataTask(with: url) { data, response, error in
		if let error = error {
			completion(.failure(error))
			return
		}
		do {
			let resource = try JSONDecoder().decode(Resource.self, from: data!)
			completion(.success(resource))
		} catch {
			completion(.failure(error))
		}
	}
	task.resume()
}

Completion closures create several problems and make mistakes easy. They can create strong reference cycles, and they don’t enjoy the benefits of Swift Concurrency, leaving the developer with the task of preventing data races.

If you are working on a project that still uses these old APIs, it is recommended that you convert it to Swift Concurrency and replace completion closures with async await.

Chapter 2

Creating and Configuring a URLSession for your App

In this chapter:

FREE GUIDE - SwiftUI App Architecture: Design Patterns and Best Practices

MVC, MVVM, and MV in SwiftUI, plus practical lessons on code encapsulation and data flow.

DOWNLOAD THE FREE GUIDE

The URLSession class is the central piece of the URL Loading System on Apple platforms. As such, it handles much more than mere network transfers.

Most network requests in an app are directed towards the same server and often share characteristics like caching, cookies, and authentication.

The best example to understand a session is to look at what happens when you load a web page in your browser.

Grouping the Downloads for a Web Page into a Session

Behind the scenes, the browser loads several files:

  • The HTML file with the page’s content and the URLs of all other resources.
  • The CSS style sheets defining the visual design of the page.
  • Every image on the page.
  • Additional JavaScript files that define some behaviors, like the formatting of code snippets.

Moreover, if the user cancels or restarts a page’s loading, all these connections must be canceled as a group.

The same can happen when working with REST APIs. The initial JSON data might carry links to images and other resources.

URLSession performance with HTTP/2 and HTTP/3

In addition, HTTP/2 introduced server push, which allows a server to speed up the loading of related resources by sending them to a client before the client requests them.

HTTP/3 also changes how sessions are maintained, delivering further speed improvements.

These are the main reasons why the URLSession class exists.

Be aware that the Foundation framework previously offered the NSURLConnection class, which does not have all the benefits of URLSession and is now deprecated.

However, you can still find code examples online using this class, so you should ignore them.

How to configure a URLSession

With such complexity to manage, it’s no surprise that URLSession is the central hub of a constellation of types that allows you to configure every session aspect to group multiple requests.

The URLSession Constellation of Types in the iOS URL Loading System

The URL Loading System consists of many types, and it would be impractical to list them here. However, you will need these principal types when you work with URLSession.

  • A URLSession instance is configured using a URLSessionConfiguration object, except when using the shared singleton. Such configuration object defines the common attributes of all network transfers performed throughout the session, such as caching, HTTP headers, security, etc.
  • A session can have a delegate conforming to the URLSessionDelegate protocol, which handles the session and task lifecycle events.
  • You create instances of URLSessionTask through a session for each network transfer. URLSession also has corresponding async APIs you should use when you don’t need to manage tasks directly.
  • A task takes either a destination URL value or a URLRequest value, defining the parameters of a single request, like the HTTP method or specific headers.
  • When a transfer is finished, a task returns a Data value with the server response’s content and a URLResponse object containing the response’s metadata, such as the HTTP status code and headers.

 

Picking the correct type of session for your app

URLSession instances come in different flavors, and you must choose the right one for your app’s needs.

There are four types of sessions you can use:

  • The shared session works for most requests.
  • A default session allows a more fine-grained configuration.
  • Ephemeral sessions are used for privacy.
  • Background sessions allow you to transfer data when your app is not running.

Use the shared session for standard network requests

The shared session is a singleton, while other sessions are created using a URLSessionConfiguration object during initialization.

According to Apple’s documentation:

[The shared session] is not as customizable as sessions you create, but it serves as a good starting point if you have very limited requirements.

However, it is more versatile than it might appear. It provides standard caching, can store cookies and credentials, and works in many scenarios. While its documentation lists a series of limitations, those usually do not apply when working with REST APIs.

Remember that the shared session is a singleton. While it might be sufficient for your app, you might need to replace it through dependency injection in unit tests.

Use a default session when you need more fine-grained control

The default session lets you configure several aspects that affect all the network requests performed through the session, like HTTP headers, timeouts, and other properties.

Using a default session, you can also change the caching, cookies, and security policies, among many other properties listed in the documentation for the URLSessionConfiguration class.

One common reason for creating a default session is configuring a delegate to handle lifecycle events and authentication challenges.

While the URLSessionDelegate protocol has limited features, it’s essential to note that it has several subprotocols that allow you to control many network transfer events. These can be found on its documentation page.

Use an ephemeral session to preserve the user’s privacy

Ephemeral sessions are used for privacy, e.g., private browsing windows in Safari.

An ephemeral session does not use persistent storage for caches, cookies, or credentials but keeps these only in RAM. When your app invalidates the session, all ephemeral session data is purged automatically.

I have never seen private sessions in iOS apps besides web browsers, so you will rarely need to use an ephemeral session.

Use a background session when you need to download files while your app is not running

Finally, background sessions allow iOS apps to upload or download large files while your app is running in the background. Even if an app is suspended or terminated, it can resume a download when relaunched.

Apps that use background sessions usually download some form of media, e.g., music or podcasts.

Chapter 3

Performing network transfers with session tasks

The URLSessionTask class is the base for data transfer tasks handled by a URLSession instance.

You never create task instances directly. Instead, you use one of the URLSession methods to create a specific subclass of URLSessionTask depending on your goal.

The Subclasses of URLSessionTask

When using Swift Concurrency, you should use the asynchronous methods corresponding to the task type you want to run whenever possible.


In this chapter:

FREE GUIDE - SwiftUI App Architecture: Design Patterns and Best Practices

MVC, MVVM, and MV in SwiftUI, plus practical lessons on code encapsulation and data flow.

DOWNLOAD THE FREE GUIDE

Data tasks perform HTTP requests and return the result in memory

The most common task with URLSession is URLSessionDataTask, which performs a network request and returns the downloaded data directly to the app in memory.

In its simplest form, a data task takes a simple URL and performs an HTTP GET request. URLSession provides an asynchronous API that allows you to request data without explicitly creating a data task.

import Foundation

struct Resource: Decodable {
	// ...
}

func getResource(from url: URL) async throws -> Resource {
	let (data, response) = try await URLSession.shared.data(from: url)
	guard let response = response as? HTTPURLResponse else {
		throw URLError(.badServerResponse)
	}
	let statusCode = response.statusCode
	guard (200...299).contains(statusCode) else {
		throw URLError(.resourceUnavailable)
	}
	guard response.value(forHTTPHeaderField: "content-type") == "application/json" else {
		throw URLError(.cannotDecodeContentData)
	}
	return try JSONDecoder().decode(Resource.self, from: data)
}

You can inspect the response’s status code or HTTP headers by casting it as HTTPURLResponse.

Data tasks can also use other HTTP methods, such as PUT, POST, PATCH, HEAD, or DELETE. In such cases, you must create a URLRequest value using the appropriate HTTP method and headers.

import Foundation

struct Resource: Encodable {
	// ...
}

func post(resource: Resource, to url: URL) async throws {
	var request = URLRequest(url: url)
	request.httpMethod = "POST"
	request.addValue("application/json", forHTTPHeaderField: "content-type")
	request.httpBody = try JSONEncoder().encode(resource)
	let (_, response) = try await URLSession.shared.data(for: request)
	guard let response = response as? HTTPURLResponse else {
		throw URLError(.badServerResponse)
	}
	guard (200...299).contains(response.statusCode) else {
		throw URLError(.userAuthenticationRequired)
	}
}

Download tasks save large files directly to the disk

To download large files, you use a URLSessionDownloadTask instance.

For large amounts of data, download tasks are better than data tasks because they save the downloaded data into a temporary file on disk instead of delivering it to the app in memory.

The URLSession class also has an asynchronous API to create download tasks.

func downloadFile(from url: URL) async throws {
	let (downloadURL, response) = try await URLSession.shared.download(from: url)
	let destinationURL = URL.downloadsDirectory
		.appending(path: "download")
		.appendingPathExtension("mp3")
	try FileManager.default.moveItem(at: downloadURL, to: destinationURL)
}

After the download, you must use FileManager to move the downloaded file from its temporary location to a more appropriate one, such as the app’s downloads or documents directories.

Apps commonly show the progress of files being downloaded to the user. You need to provide a delegate to the download task to receive that information.

@MainActor
class Downloader: NSObject, URLSessionDownloadDelegate {
	var progress: Double = 0

	func downloadFile(from url: URL) async throws {
		let (downloadURL, response) = try await URLSession.shared.download(
			from: url,
			delegate: self
		)
		let destinationURL = URL.downloadsDirectory
			.appending(path: "download")
			.appendingPathExtension("mp3")
		try FileManager.default.moveItem(at: downloadURL, to: destinationURL)
	}

	nonisolated func urlSession(
		_ session: URLSession,
		downloadTask: URLSessionDownloadTask,
		didWriteData bytesWritten: Int64,
		totalBytesWritten: Int64,
		totalBytesExpectedToWrite: Int64
	) {
		Task {
			await MainActor.run {
				progress = Double(totalBytesWritten) / Double(totalBytesExpectedToWrite)
			}
		}
	}
}

Download updates are delivered to the urlSession(_:downloadTask:didWriteData:totalBytesWritten:totalBytesExpectedToWrite:) delegate method, where you can calculate the download progress.

You can pair download tasks with background sessions to download files in the background while your app is inactive.

If you need to download multiple files simultaneously, you can read my article on Downloading files from URLs in Swift

Upload tasks send data to a server, often from a file on disk

Upload tasks are used to send data to a server. It is usually used for HTTP requests with a body, such as PUT, POST, or PATCH.

As seen above, data tasks can also be used for these HTTP methods. One advantage of the URLSessionUploadTask class is that it can read data directly from a file.

func upload(to url: URL) async throws {
	let fileURL = URL.documentsDirectory
		.appending(path: "upload")
		.appendingPathExtension("mp3")
	var request = URLRequest(url: url)
	request.httpMethod = "POST"
	request.addValue("audio/mpeg", forHTTPHeaderField: "content-type")
	try await URLSession.shared.upload(for: request, fromFile: fileURL)
}

WebSocket tasks keep open connections for low-latency apps

Unlike other tasks,  the URLSessionWebSocketTask class does not use the HTTP protocol but the WebSocket protocol.

Unlike HTTP, WebSocket keeps an open connection between a client and a server, allowing the server to send content to the client without being requested.

While WebSocket is primarily used in interactive web applications, it’s also useful for iOS apps that require low latency, such as live chats, collaboration on shared documents, and data broadcasts like stock prices and sports scores.

URLSession does not have an asynchronous API to create a URLSessionWebSocketTask as it happened for the tasks above.

Instead, you must create one explicitly calling the webSocketTask(with:) method on a session and then start it using the task’s resume() method.

let task = URLSession.shared.webSocketTask(with: URL(string: "wss://example.com")!)
task.resume()

func send(message: String) async throws {
	try await task.send(.string(message))
}

func receive() async throws -> String {
	let message = try await task.receive()
	guard case .string(let message) = message else {
		throw URLError(.cannotDecodeContentData)
	}
	return message
}

Once a WebSocket task runs, you can send and receive messages using its asynchronous send(_:) and receive() methods.

While URLSession does not offer async methods for WebSocket tasks, you can transform received messages into an asynchronous sequence using AsyncStream.

Stream tasks provide an interface for naked TCP/IP connections for custom application layer protocols

Finally, the URLSessionStreamTask provides an interface to a naked TCP/IP connection without using an application layer protocol like HTTP.

The methods of URLSessionStreamTask allow you to read raw data from and write it to an open TCP connection, leaving its interpretation to the developer.

However, don’t get confused by the class name. Media streaming is usually built on other application-layer protocols, like HTTP Live Streaming. Apple provides several frameworks that support HTTP Live Streaming, including AVKit, AVFoundation, and WebKit.

SwiftUI App Architecture: Design Patterns and Best Practices

It's easy to make an app by throwing some code together. But without best practices and robust architecture, you soon end up with unmanageable spaghetti code. In this guide I'll show you how to properly structure SwiftUI apps.

GET THE FREE BOOK NOW

2 thoughts on “URLSession in Swift: The Essential Guide [with Examples]”

  1. I really love this approach, it’s clean and elegant. But when testing the process for moving the downloaded file, I realised there’s a bug in the podcast app, I wrote it up on the repo: https://github.com/matteom/Podcast/issues/1

    Basically continuations aren’t suitable because you have to move the downloaded function out from the temporary location before the delegate function returns, or else it’s deleted.

    Would love to know of a solution to, will write back if I find one. For now I added a `var handleCompletedFile: ((URL) -> Void)?` to `Download`.

    Reply

Leave a Comment