Intermediate Combine

Apr 13 2021 · Swift 5.3, macOS 11.1, Xcode 12.2

Part 1: Intermediate Combine

01. Networking with Combine

Episode complete

Play next episode

Next
About this episode
Leave a rating/review
See forum comments
Cinema mode Mark complete Download course materials
Next episode: 02. Sharing Resources

Notes: 01. Networking with Combine

Prerequisites: Intermediate Swift knowledge, basic Combine knowledge, knowledge of URLSession.

Transcript: 01. Networking with Combine

URLSession supports a variety of operations such as: data transfer tasks to retrieve contents of a URL; download tasks to retrieve the contents 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.

Only the first operation - data transfer tasks - expose a Combine publisher, taking either a URL or a URLRequest. Let’s look at that in an example.

Start an example block, and make a URL by passing in a URL for a JSON file, and if it fails use a guard statement to return

example(of: "dataTaskPublisher") {

	guard let url = URL(string: "https://jsonplaceholder.typicode.com/todos/1") else { 
 	 return 
	}

Create the Combine pipeline, making sure to keep the resulting subscription; otherwise it will immediately be cancelled and the request will never execute.

On the URLSession.shared instance, call dataTaskPublisher(for:), passing in the url you made earlier.

// 1
URLSession.shared
  // 2
  .dataTaskPublisher(for: url)

Add a sink to the Combine pipeline, and use the more complex sink(receiveCompletion: receiveValue:) version, so both errors and received values can be sent to the console. This is very important because networks are prone to failure.

  .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)")
  })
  .store(in: &subscriptions)

The received value in the sink is a tuple, containing the Data object and the URLResponse from the server. This is very similar to the closure used in normal URLSession use, but with a simpler publisher abstraction from Combine.

Codable is a very powerful protocol in Swift that provides an encoding and decoding mechanism you should definitely be familiar with. If you’re not, be sure to check out the various courses on raywenderlich.com including, but not limited to, “Encoding and Decoding with Swift” by Cosmin Pupaza.

There are several types of encoders and decoders in Foundation, but one pair stands out when it comes to networking - JSONDecoder and JSONEncoder. Let’s continue the URLSession and use JSONDecoder to decode the JSON you downloaded - and then see how we can improve on that with the decode() operation from Combine.

Create the same subscription as you did in the URLSession example, and add the dataTaskPublisher(for:) operator.

example(of: "decode") {

guard let url = URL(string: "https://jsonplaceholder.typicode.com/todos/1") else {
	return
}

URLSession.shared
  .dataTaskPublisher(for: url)

Remember that dataTaskPublisher emits a tuple, so you have to use map to grab only the data part of the tuple. Since JSONDecoder.decode can throw an error, you have to put the try keyword in front of it, which means that you must use tryMap and not just map

  .tryMap { data, _ in
    try JSONDecoder().decode(Todo.self, from: data)
  }

Complete the rest of the Combine pipeline with the same sink as before.

  .sink(receiveCompletion: { completion in
    if case .failure(let err) = completion {
      print("Retrieving data failed with error \(err)")
    }
  }, receiveValue: { object in
    print("Retrieved object \(object)")
  })
  .store(in: &subscriptions)

This works, but Combine introduces some syntactical sugar to help get around that tryMap block. The decode(type: decoder:) operator takes in a decoder instead of a Data object, instead expecting that data from upstream. You still need to extract out the data part of the tuple, so you still need to use map. Replace the tryMap block with the map operator followed by the decode operator.

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