Skip to content

Category: Combine

Building a Caching Custom Combine Operator #2: The .cache() Method

In part 1, we started building our custom combine operator by starting with the foundation: building the Combine chain that implemented our caching operator. In this part, we want to make this something that is reusable, and functions like a built-in Combine operator, by adding it to the Publisher type via an extension.

As a refresher, let’s review how we wanted our .cache() method to work for somebody using it. The upstream chain would provide input for the caching operator, which would either calculate the output from a given operation, or if it’s already been calculated previously, simply pass on the previous value. So the signature would look something like this:

upstream.cache(operation: ...)

Now we just need to convert that into a method declaration in a Publisher extension. Easy, right?

Now we recall the image from part 1, flatMap()’s method declaration:

How do we put together this big bundle of Swift generics? We’ll break that down in this part, and adapt our code to fit the new declaration. By the end, we will have a .cache() method that will cache the results of our operation called on the upstream inputs, whatever the input and output types are. Put on your generics caps, and let’s start.

Building a Caching Custom Combine Operator: The Chain

Combine provides a wide variety of Publishers that can be combined to fulfill a wide variety of use cases. But as with any framework, you may find at some point that you need something custom that Combine does not provide out of the box.

You may have your own custom operation or you may want to wrap another Combine chain in a convenient wrapper. But how? If you look at Apple’s documentation for even how to add a method to the Publisher as an extension, the signature looks something like this:

😖

The steps are a little complicated, because Combine heavily uses a couple of features of Swift to perform its magic, but once you’ve followed along with an example, those difficult steps become much easier to apply to your own unique case.

Over the next several articles, we’ll trace through the development of a caching operator, which will take an upstream input and a Combine chain that performs the operation we’d like to cache, running the operation if the upstream value received hasn’t been seen before, or sending the cached result if it has. In part 1, we will build out the Combine publisher chain that implements the cache operation. In part 2, we’ll learn how we can make it so that somebody can use upstream.cache(...) to add our operator to their own Combine chains. Part 3 will cover taking our cache operator and turning it into a full-blown Publisher type. Part 4 will move away from Combine and talk about how to package that code into a Swift package that can be easily downloaded and included in your own, and everybody else’s app.

Lots to cover, so let’s get going!

Save that sink! A simple solution to a common Combine problem

So you’re starting to work with Combine, because Apple is finally jumping on the reactive programming bandwagon (yay!). So you build your first Combine workflow, and it looks something like this:

func retrieveData() {
    URLSession.shared.dataTaskPublisher(for: url)
      .map({$0.data})
      .eraseToAnyPublisher()
      .sink(receiveCompletion: { (status) in
        switch status {
        case .failure(let error):
          print(error.localizedDescription)
        case .finished:
          break
        }
      })
      { (data) in
        let str = String(data: data, encoding: .utf8)
        print(str!)
      }
}

And then you try and use it, and instead of getting the data response you were expecting, you get an error:

2019-10-25 14:59:34.452071-0400 TestApp[2127:98883] Task <663D6D3A-48B8-49E6-9103-AA1D89513D84>.<1> finished with error [-999] Error Domain=NSURLErrorDomain Code=-999 "cancelled" …

Cancelled? Why did my request get cancelled?

The key in the above code is that you missed something important. I made this exact same mistake when I started using Combine, and sadly I don’t think it will be the last time, because it is just that easy to miss. Let’s talk about the solution, and why you get the error when you don’t do it.

4 things you can do *right now* to be ready for SwiftUI and Combine

Many of us were excited about all the new frameworks at WWDC: SwiftUI and Combine being the biggest ones. You were probably all ready to start using them in your app, until you opened up the documentation and saw this:

And you felt at least one of these feelings:

😡 😭 🙄

You knew at that point, that even with the usual new iOS version adoption curve, unless you’re looking at sacrificing a significant portion of your user base, you’re looking at adopting all these new features in Spring 2020, at the earliest. If your app needs to support older OS versions for a longer period, it could be even longer.

Bummer.

You can’t fix your users: they upgrade whenever they do (but thank Apple for new emoji ever year). But you also don’t need to sit on the sidelines and wait until those features are widely available to start implementing them. There are some things you can do while you’re waiting for your users to catch up, so that you’ll be ready to deliver apps based on these great new frameworks. You can even do some of this work now, while those betas are less than stable. Here are a few things to think about.