AFNetworking has always been one of my favorite libraries and Alamofire is just as easy to like. Since the introduction of URLSession
in iOS 7 and macOS Mavericks, I've been more reluctant to include either libraries in my projects. Why is that?
The URLSession
API offers an easy-to-use, modern API for networking. It offers flexibility and several features many developers have been asking for. If a project can do without another dependency, then that's something worth considering.
URLSession
is the successor of NSURLConnection
. For many years, NSURLConnection
was the workhorse for networking on iOS, tvOS, macOS, and watchOS. Most developers used or created a wrapper around NSURLConnection
to hide the less enjoyable aspects of the NSURLConnection
API.
What is URLSession
and how is it different from NSURLConnection
? In addition to being a class, URLSession
is a technology that provides the infrastructure for networking, exposed through a modern and elegant API. In this series, I introduce you to the URLSession
stack. You learn how easy it is to get started with the URLSession
API and you discover that it exposes a flexible API that should meet anyone's networking needs.
Meet the Family
The URLSession
family includes a number of core classes. The URLSession
class is the key component of the URLSession
stack. It is used to create and configure network requests.
Another important class is URLSessionConfiguration
. As the name suggests, a session configuration object is used to configure a URLSession
instance. The session is in charge of managing requests. The session configuration defines the behavior of the session. This is especially interesting for uploading and downloading data.
The workhorses of URLSession
are the (abstract) URLSessionTask
and its concrete subclasses. The subclasses you interact with most are URLSessionDataTask
for fetching data, URLSessionUploadTask
for uploading data, and URLSessionDownloadTask
for downloading data. URLSessionDataTask
and URLSessionDownloadTask
directly inherit from URLSessionTask
. URLSessionUploadTask
is a subclass of URLSessionDataTask
.
A URLSessionTask
object is always associated with a session. You can create a URLSessionTask
object by asking the session for one. The following example illustrates this.
import Foundation
// Create URL
let url = URL(string: "https://cocoacasts.com")!
// Obtain Reference to Shared Session
let session = URLSession.shared
// Create Request
let request = URLRequest(url: url)
// Create Data Task
let dataTask = session.dataTask(with: request)
Tasks and Async APIs
In this episode, we use URLSession
's async
APIs and that means we don't directly interact with URLSession
tasks. That said, URLSession
uses URLSession
tasks under the hood to perform requests.
Fetching Data
Now that we have met the family, it's time to make a request to see how the various pieces fit together. Create a new project in Xcode by choosing the App template from the iOS > Application section. Set Product Name to Networking, Interface to SwiftUI, and Language to Swift.
To illustrate how the URLSession
stack works, we fetch a remote image and display it in an Image
view. This simple example shows you how easy it is to make a request using the URLSession
API.
I'm an avid fan of the Model-View-ViewModel pattern so I always put business logic, including networking, in the view's view model. Don't worry, though. The changes we make are easy to understand.
Add a Swift file and name it ContentViewModel.swift. Replace the import statement for Foundation with an import statement for UIKit and declare a final
class with name ContentViewModel
. We conform the ContentViewModel
class to the ObservableObject
protocol so that the view can observe its Published
properties.
import UIKit
final class ContentViewModel: ObservableObject {
}
Declare a Published
, variable property with name image
of type UIImage?
. The image
property stores the image the view displays. If the image
property has no value, then the view displays a ProgressView
.
import UIKit
final class ContentViewModel: ObservableObject {
// MARK: - Properties
@Published var image: UIImage?
}
Declare a method with name start()
. This is the method the view invokes to notify the view model that it is about to appear. The start()
method is an asynchronous method so we annotate it with the async
keyword. That's it for now. We revisit the ContentViewModel
class later in this episode.
import UIKit
final class ContentViewModel: ObservableObject {
// MARK: - Properties
@Published var image: UIImage?
// MARK: - Public API
func start() async {
}
}
Open ContentView.swift and declare an ObservedObject, variable property with name
viewModelof type
ContentViewModel`.
import SwiftUI
struct ContentView: View {
// MARK: - Properties
@ObservedObject var viewModel: ContentViewModel
...
}
This means we also need to update the static previews
property of the ContentView_Previews
struct and the computed body
property of the NetworkingApp
struct.
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView(viewModel: .init())
}
}
import SwiftUI
@main
struct NetworkingApp: App {
// MARK: - App
var body: some Scene {
WindowGroup {
ContentView(viewModel: .init())
}
}
}
Revisit the ContentView
struct. In the computed body
property, we remove the content of the VStack
. We safely access the value of the view model's image
property using an if-else
statement and optional binding. In the if
clause, the view displays the UIImage
instance provided by the view model in an Image
view. In the else
clause, the view displays a ProgressView
.
var body: some View {
VStack {
if let image = viewModel.image {
Image(uiImage: image)
} else {
ProgressView()
}
}
.padding()
}
The view displays an Image
view if the view model's image
property has a value. It displays a ProgressView
if the the view model's image
property has no value.
Because the view model's start()
method is asynchronous, we create a Task
using the task
view modifier and invoke the view model's start()
method in the body of the Task
.
var body: some View {
VStack {
if let image = viewModel.image {
Image(uiImage: image)
} else {
ProgressView()
}
}
.padding()
.task {
await viewModel.start()
}
}
With the view model in place, we can focus on fetching the data of the remote image. Revisit the start()
method of the ContentViewModel
class. We first create a URL
object. The URL
object stores the URL of the image image.
func start() async {
let url = URL(string: "https://goo.gl/wV9G4I")!
}
We obtain a reference to the URLSession
singleton through the shared
class property of the URLSession
class. To fetch the data of the remote image, the view model performs an HTTP request. It does this by invoking the asynchronous data(from:)
method on the URLSession
singleton, passing in the URL
object. Because the data(from:)
method is throwing, we wrap the method call in a do-catch
statement. In the catch
clause, we print the error to the console.
The return type of the data(from:)
method is a tuple that contains two values, a Data
object and a URLResponse
object. We are only interested in the Data
object for now.
func start() async {
let url = URL(string: "https://goo.gl/wV9G4I")!
do {
let (data, _) = try await URLSession.shared.data(from: url)
} catch {
print("Unable to Download Image \(error)")
}
}
We use the Data
object to create a UIImage
instance and store a reference to the UIImage
instance in the view model's image
property.
func start() async {
let url = URL(string: "https://goo.gl/wV9G4I")!
do {
let (data, _) = try await URLSession.shared.data(from: url)
image = UIImage(data: data)
} catch {
print("Unable to Download Image \(error)")
}
}
Build and run the application in the simulator to see the result. Unless you are experiencing a networking issue, the ContentView
displays the remote image in its Image
view.
Threading and the Main Actor
There is a problem we need to resolve, though. The Image
view of the ContentView
is updated on a background thread and that is a red flag. The user interface should always be updated on the main thread, no exceptions. What is happening and how can we resolve the runtime error Xcode displays in the Issue Navigator on the left?
The URLSession
singleton performs its work on a background thread to make sure it doesn't block the main thread. That approach ensures the user interface remains responsive while the HTTP request is in flight. This is very important.
The problem is that the result of the HTTP request, the UIImage
instance, is assigned to the view model's image
property and this too occurs on a background thread. That is something we need to avoid.
The good news is that the solution is simple. We annotate the declaration of the ContentViewModel
class with the MainActor
attribute to guarantee that the image
property is always updated on the main thread. Because the image
property is updated on the main thread, the user interface is also updated on the main thread.
import UIKit
@MainActor
final class ContentViewModel: ObservableObject {
...
}
Build and run the application one more time. Notice that the behavior of the application hasn't changed and that Xcode no longer reports a runtime error.
What's Next?
Using URLSession
is much easier than NSURLConnection
. It's clear Apple decided it was time to rethink networking on its platforms when they created URLSession
. In this episode, you learned about the very basics of URLSession
. In the next episodes, we take a look at the more advanced options of the URLSession
API.
In this episode, we used Swift concurrency to perform the HTTP request. Note that the URLSession
API also defines equivalent APIs that use delegation and completion handlers. It is up to you to decide which flavor you prefer, but I hope you agree that the async
APIs are elegant and easy to use. I typically default to the async
APIs unless there is a very good reason to opt for the APIs that use delegation or completion handlers.