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 GUIDETable 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.
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 GUIDEThe benefits of grouping HTTP requests in a session
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.
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 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 aURLSessionConfiguration
object, except when using theshared
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 correspondingasync
APIs you should use when you don’t need to manage tasks directly. - A task takes either a destination
URL
value or aURLRequest
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 aURLResponse
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.
When using Swift Concurrency, you should use the asynchronous methods corresponding to the task type you want to run whenever possible.
In this chapter:
- Data tasks perform HTTP requests and return the result in memory
- Download tasks save large files directly to the disk
- Upload tasks send data to a server, often from a file on disk
- WebSocket tasks keep open connections for low-latency apps
- Stream tasks provide an interface for naked TCP/IP connections for custom application layer protocols
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 GUIDEData 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.
Matteo has been developing apps for iOS since 2008. He has been teaching iOS development best practices to hundreds of students since 2015 and he is the developer of Vulcan, a macOS app to generate SwiftUI code. Before that he was a freelance iOS developer for small and big clients, including TomTom, Squla, Siilo, and Layar. Matteo got a master’s degree in computer science and computational logic at the University of Turin. In his spare time he dances and teaches tango.
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`.
Hey Ian, interesting find. Unfortunately, I can’t reproduce the problem.
I added a possible solution to your GitHub issue. Let me know if that works.