A Brief Intro to Networking With Combine

Ameen Mustafa
7 min readJul 1, 2022

--

Apple’s 2019 WWDC introduced the world to the Combine framework. Combine provides a declarative Swift API for processing values over time. It can be seen as a 1st party alternative framework like RxSwift and ReactiveSwift. I was first introduced to the Combine framework without even knowing it. As have others who have used the @Published, ObservableObject and other Property Wrappers which use Combine behind the scenes. And by the end of this post you’ll be able to consciously use Combine yourself.

Combine is good for Functional Reactive Programming (FRP) to handle asynchronous events and process values over time. This can be used for a variety of asynchronous events like user interface events and network events. If that sounds complicated, don’t worry. This article will all explain that with a basic tutorial. But before we do that, let’s get some background information on publishers, operators and subscribers.

For this tutorial we will be using this free dummy api to query mock data. You will need to create an account and a unique app-id token.

Publishers

Publishers are objects that conform to deliver a sequence of values over time. The protocol has two associated types: Output - the type of value it produces - and Failure - the type of error it could encounter. If you are familiar with RxSwit - publishers are the same as Observables. Every publisher can emit multiple events such as an output value of the Output type, successful event completion or a failure with an error value of the Error type. You will see an example of a publisher in action later in the article.

Publishers are frequently used in the Notification Center. An example below demonstrates.

An example of a publisher used with the Notification Center

So what’s going on here?

  1. Create a new model for new notifications
  2. Create a publisher notification name
  3. Use that name when creating the new publisher

Operators

Operators are special methods that call on publishers to return the same or a different publisher. An operator describes a behavior for changing values, adding values, removing values and many other operations. Multiple operators together to perform complex processing. Another way to think of it - like a river, values come from the upstream publisher and flow to the downstream publisher.

Typical operators used with Combine include map, mapError, filter and other higher order functions. More can be found here.

Building off the previous example and adding an operator

So building off the previous example we add an operator.

  1. Add the map operator to iterate over the data and change it so it outputs a string
  2. If there is no output we simply return an empty string

Subscribers

Subscribers are protocols that listen to published events. Again, if you are familiar with RxSwift then subscribers are the same as Observers. Like the Publisher protocol it has two associated types - Input and Failure. Subscribers receive a stream of values as well as completion or failure events emitted from a publisher.

There are also important rules to remember when using Subscribers.

  1. One subscription per subscriber
  2. Zero or more values can be published
  3. One completion event will be called at most

These rules must be followed throughout the entirety of a subscription’s lifetime.

Now we add Subscribers

So now we are attaching our subscriber to a publisher.

  1. Import UIKit and create a UILabel to display the string sent from the publisher above
  2. Create a subscriber, attach it to the UILabel and assign received elements to the indicated key path
  3. Attach the subscriber to the publisher. Because UILabel requires a type of String we used the map operator to manipulate the input data. This means the UILabel’s text will change whenever the publisher emits new values.

That’s a very basic example.

A note on AnyCancellable:

The AnyCancellable is an important piece of the Combine framework. At a high level it is a cancellable object that executes a provided closure when canceled. Its purpose is to tie the lifecycle of a subscription to an object that retains the cancellable that we receive when subscribed to a publisher. This makes sure the subscription to the publisher is deallocated when the object that retains the AnyCancellable is deallocated. One of the most common ways to do this with the Combine framework is to use the sink method. The AnyCancellable is comparable to the DisposeBag in RxSwift.

To sum it all up: Publishers produce values, operators work with values and subscribers care about values. Okay so now that we got those terms out of the way it’s time to get started on our app! For this tutorial we will be creating a simple app to search for films, actors and television shows. Now to get to the networking.

Networking

Using Publishers

So the first step to networking with Combine is to create a publisher, specifically a URLSession.DataTaskPublisher which takes a URL or URLRequest and returns a tuple with fetched data and a URLResponse or an error if it fails. This has obvious advantages over the dataTask(with:completionHandler:) method provided by the Foundation framework. Mainly, the Combine method unwraps the data so it does not return an optional value. Let’s take a look at the example below.

A simple networking request using Combine publishers

So let’s break down what we have here.

  1. This is where the keys and values for the mandatory app-id toke are mapped to the urlRequest
  2. Creates and launches a URLSessionDataTask
  3. Obtains data if no errors occur
  4. Decodes the model object
  5. Returns a publisher containing either the model object or an error

** You will notice the eraseToAnyPublisher() method exists at the bottom of the dataTaskPublisher chain. This method does not return the publisher but rather allows downstream subscribers to access instances of AnyPublisher. For more info on that read here.

Notice we do not need to unwrap any data, that’s because the dataTaskPublisher handles that for us.

Using Operators

Upon successfully completing a data task, a tuple containing a block of raw data and a URLResponse is delivered. Combine’s operators are functional enough that they can convert this data its own types through a chain of processing operations. Higher order functions like map(_:) can also be used to convert the data to other types. You can also use functions like tryMap(_:) to inspect the response before inspecting the data. Combine also comes with a decode(type:decoder:) operator.

In the above example we see the use of operators immediately after creating the dataTaskPublisher. The first time an operator is used is with the .map(\.data) method which obtains data if no errors occur. In the very next line we see another Combine operator, the .decode(type: T.self, decoder: JSONDecoder()) method. The successful use of operators here allows us to process and utilize the network data from the publisher.

Using Subscribers

Subscribers listen to changes of values from publishers. So upon successful completion of a network request any subscriber can be updated. For example, a notification could be subscribed to a publisher that sends out updates with every successful network request.

Publishers start delivering values when the subscribe(_:) method is called. Once the subscribe method is called a subscription is sent from the publisher to the subscriber. The subscriber now has the ability to make requests from the publisher for values. Now that the publisher is free to start sending values to the subscriber. Finite publishers will eventually return a completion event or an error.

The sink method is one of the most popular methods for using subscriptions. The sink method returns a cancellable instance which can be used when the stream doesn’t fail. Deallocation of the result tears down the subscription stream. Another popular method is the store method which stores a type-erasing cancellable instance in the specified collection. The sink and store methods are demonstrated below.

Using Combine subscribers when making network requests

So let’s break this down:

  1. We create an AnyCancellable for storing a cancellable
  2. We call the getUserData() method and use the Publishers and Operators to request and process the response
  3. We use the sink method to create a subscriber
  4. Returns subscribed values
  5. The store method allows for the storing of a cancellable

Handling Errors

Just like when making network requests with any other framework or any other language, when using Combine you should handle errors. For a variety of reasons you may want to retry a failed data task. Combine comes with many built in methods to retry failed data tasks. There is the retry(_:) operator which can be used with a data task publisher to recreate subscriptions to the upstream publisher a specific number of times. There is also the catch(_:) operator which replaces the error with another publisher rather than letting it reach the subscriber. You can use the catch operator to load data from a fallback URL. Another popular way to handle errors in Combine is with the replaceError(with:) method which replaces the error with an element of the type you provide.

Simple ways to handle errors when networking with Combine. Normally you would use 1 of these 3 methods

So let’s take a look:

  1. This retries the data task publisher’s request one time
  2. This catch method can be used to provide a fallback URL to load data
  3. This replaces the error with an input of the type entered in your decoder. So since our decoder uses input with the type UsersResponse we must substitute data of that same type.

When you are implementing error handling with Combine you probably don’t need to use all three of these chained methods. You can also use the mapError(_:) operator provided by Combine.

And that’s the basics of networking with the Combine framework!

--

--

Ameen Mustafa

iOS/watchOS developer with a passion for coding, climbing, hiking and everything else you can do outdoors