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.

Setting Up the Project in Xcode

Setting Up the Project in Xcode

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 typeContentViewModel`.

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.

Build and run the application in the simulator.

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?

Threading and the Main Actor

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.