Chapters

Hide chapters

Combine: Asynchronous Programming with Swift

First Edition · iOS 13 · Swift 5.1 · Xcode 11

Before You Begin

Section 0: 3 chapters
Show chapters Hide chapters

9. Networking
Written by Florent Pillet

As programmers, a lot of what we do revolves around networking. Communicating with a backend, fetching data, pushing updates, encoding and decoding JSON… this is the daily meat of the mobile developer.

Combine offers a few select APIs to help perform common tasks declaratively. These APIs revolve around two key components of modern applications:

  • URLSession.
  • JSON encoding and decoding through the Codable protocol.

URLSession extensions

URLSession is the recommended way to perform network data transfer tasks. It offers a modern asynchronous API with powerful configuration options and fully transparent backgrounding support. It supports a variety of operations such as:

  • Data transfer tasks to retrieve the content of a URL.
  • Download tasks to retrieve the content of a URL and save it to a file.
  • Upload tasks to upload files and data to a URL.
  • Stream tasks to stream data between two parties.
  • Websocket tasks to connect to websockets.

Out of these, only the first one, data transfer tasks, exposes a Combine publisher. Combine handles these tasks using a single API with two variants, taking a URLRequest or just a URL.

Here‘s a look at how you can use this API:

guard let url = URL(string: "https://mysite.com/mydata.json") else { 
  return 
}

// 1
let subscription = URLSession.shared
  // 2
  .dataTaskPublisher(for: url)
  .sink(receiveCompletion: { completion in
    // 3
    if case .failure(let err) = completion {
      print("Retrieving data failed with error \(err)")
    }
  }, receiveValue: { data, response in
    // 4
    print("Retrieved data of size \(data.count), response = \(response)")
  })

Here‘s what‘s happening with this code:

  1. It‘s crucial that you keep the resulting subscription; otherwise, it gets immediately canceled and the request never executes.
  2. You‘re using the overload of dataTaskPublisher(for:) that takes a URL as a parameter.
  3. Make sure you always handle errors! Network connections are prone to failure.
  4. The result is a tuple with both a Data object and a URLResponse.

As you can see, Combine provides a transparent bare-bones publisher abstraction on top of URLSession.dataTask, only exposing a publisher instead of a closure.

Codable support

The Codable protocol is a modern, powerful and Swift-only encoding and decoding mechanism that you absolutely should know about. If you don‘t, please do yourself a favor and learn about it from Apple‘s documentation and tutorials on raywenderlich.com!

Foundation supports encoding to and decoding from JSON through JSONEncoder and JSONDecoder. You can also use PropertyListEncoder and PropertyListDecoder, but these are less useful in the context of network requests.

In the previous example, you downloaded some JSON. Of course, you could decode it with a JSONDecoder:

let subscription = URLSession.shared
  .dataTaskPublisher(for: url)
  .tryMap { data, _ in
    try JSONDecoder().decode(MyType.self, from: data)
  }
  .sink(receiveCompletion: { completion in
    if case .failure(let err) = completion {
      print("Retrieving data failed with error \(err)")
    }
  }, receiveValue: { object in
    print("Retrieved object \(object)")
  })

You decode the JSON inside a tryMap, which works, but Combine provides an operator to help reduce the boilerplate: decode(type:decoder:).

In the example above, replace the tryMap operator with the following lines:

.map(\.data)
.decode(type: MyType.self, decoder: JSONDecoder())

Unfortunately, since dataTaskPublisher(for:) emits a tuple, you can‘t directly use decode(type:decoder:) without first using a map(_:) that only emits the Data part of the result.

The only advantage is that you instantiate the JSONDecoder only once, when setting up the publisher, versus creating it every time in the tryMap(_:) closure.

Publishing network data to multiple subscribers

Every time you subscribe to a publisher, it starts doing work. In the case of network requests, this means sending the same request multiple times if multiple subscribers need the result.

Combine, surprisingly, lacks operators to make this easy, as other frameworks have. You could use the share() operator, but that‘s tricky because you need to subscribe all your subscribers before the result comes back.

Besides using a caching mechanism, one solution is to use the multicast() operator, which creates a ConnectablePublisher that publishes values through a Subject. It allows you to subscribe multiple times to the subject, then call the publisher‘s connect() method when you‘re ready:

let url = URL(string: "https://www.raywenderlich.com")!
let publisher = URLSession.shared
// 1
  .dataTaskPublisher(for: url)
  .map(\.data)
  .multicast { PassthroughSubject<Data, URLError>() }

// 2
let subscription1 = publisher
  .sink(receiveCompletion: { completion in
    if case .failure(let err) = completion {
      print("Sink1 Retrieving data failed with error \(err)")
    }
  }, receiveValue: { object in
    print("Sink1 Retrieved object \(object)")
  })

// 3
let subscription2 = publisher
  .sink(receiveCompletion: { completion in
    if case .failure(let err) = completion {
      print("Sink2 Retrieving data failed with error \(err)")
    }
  }, receiveValue: { object in
    print("Sink2 Retrieved object \(object)")
  })

// 4
let subscription = publisher.connect()

In this code, you:

  1. Create your DataTaskPublisher, map to its data and then multicast it. The closure you pass must return a subject of the appropriate type. Alternately, you can pass an existing subject to multicast(subject:). You‘ll learn more about multicast in Chapter 13, “Resource Management.”
  2. Subscribe a first time to the publisher. Since it‘s a ConnectablePublisher it won‘t start working right away.
  3. Subscribe a second time.
  4. Connect the publisher, when you‘re ready. It will start working and pushing values to all of its subscribers.

With this code, you send the request one time and share the outcome to the two subscribers.

Note: Make sure to store all of your Cancellables; otherwise, they would be deallocated and canceled when leaving the current code scope, which would be immediate in this specific case.

This process remains a bit convoluted, as Combine does not offer operators for this kind of scenario like other reactive frameworks do. In Chapter 18, “Custom Publishers & Handling Backpressure,” you‘ll explore crafting a better solution.

Key points

  • Combine offers a publisher-based abstraction for its dataTask(with:completionHandler:) method called dataTaskPublisher(for:).
  • You can decode Codable-conforming models using the built-in decode operator on a publisher that emits Data values.
  • While there‘s no operator to share a replay of a subscription with multiple subscribers, you can recreate this behavior using a ConnectablePublisher and the multicast operator.

Where to go from here?

Great job on going through this chapter!

If you want to learn more about using Codable, you can check out the following resources:

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2026 Kodeco Inc.