While I keep working on CombineDataSources I’ve faced some of the differences between RxSwift and Combine and those are slowing me down a bit.

For example: in RxSwift when dealing with UI I’d usually use shareReplayLatestWhileConnected() - this operator shares an observable and emits the latest value to all subscribers downstream. In Combine there is currently a share() operator which does a similar thing but does not “replay” the latest value.

The inner logic of some of the CombineDataSources types is becoming quite complex too so a lot of said sharing is happening between the different components. Here’s a current data flow diagram for the BatchesDataSource type:

So, among others, I had to put together a quick operator to ensure I’m sharing correctly publishers and more specifically that I don’t re-subscribe unneccessarily publishers that fetch stuff from the network, etc.

assertMaxSubscriptions()

assertMaxSubscriptions() keeps track of how many times a publisher is subscribed and asserts that it’s not more than given amount of times.

This one is currently for internal use (e.g. no special thread-safety measures, etc.) but it gives you an idea of how you can work on your own tools for making your code safer.

Let’s start by adding a dictionary to track the current subscriptions (somewhere in the global context of a file):

1
fileprivate var uuids = [String: Int]()

Then add a new Publisher operator:

1
2
3
4
5
6
7
8
9
extension Publisher {
  func assertMaxSubscriptions(
    _ max: Int, 
    file: StaticString = #file, 
    line: UInt = #line) 
    -> AnyPublisher<Output, Failure> {

  }
}

The operator takes in a parameter called max which allows the consumer to set the maximum number of allowed subscriptions.

The file and line parameters are automatically populated with the file name and the source line where the method is called. This is a simple trick to produce a unique identifier for each instance in the source code where a method is called.

Inside the method body I’ll put together an identifier for the current invocation of the method by using the file name and line together:

1
let uuid = "\(file):\(line)"

This way the method knows if it’s been called multiple times from the same spot, let’s say “ViewController.swift:25”, or from different files/locations in the code.

What’s left to do is to count the subscriptions for the current uuid and fail if they are more than the given max amount:

1
2
3
4
5
6
7
8
return handleEvents(receiveSubscription: { _ in
  let count = uuids[uuid] ?? 0
  guard count < max else {
    assert(false, "Publisher subscribed more than \(max) times.")
    return
  }
  uuids[uuid] = count + 1
})

handleEvents() is an operator allowing you to perform side effects for each of the possible subscription events. Above I’m using receiveSubscription to increment the amount of subscriptions for the current uuid and fail should this number exceed max.

And that’s all there is to assertMaxSubscriptions()! I really love creating this little helpers along the way - it’s really exciting to be able to so easily extend Publisher and create operators that instantly plug-and-play with all the rest of my Combine code.

Usage

So let’s see how to use assertMaxSubscriptions()!

Below I have a publisher with two subscribers:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
let publisher = Publisher.MyPublisher {
 ...
}
.eraseToAnyPublisher()

publisher
  .sink { print($0) }
  .store(in: &subscriptions)

publisher
  .sink { print($0) }
  .store(in: &subscriptions)

Each of the subscribers is creating a new subscription to publisher. Now you can add assertMaxSubscriptions() like so:

1
2
3
4
5
let publisher = Publisher.MyPublisher {
 ...
}
.eraseToAnyPublisher()
.assertMaxSubscriptions(1)

When you run the code, once you “over-subscribe” the assertion will crash the app and you can use the stack trace to find where is the offending subscription created:

You can now track down the issue and add a share() to the publisher you’d like to subscribe multiple times, like so:

1
2
3
4
5
6
let publisher = Publisher.MyPublisher {
 ...
}
.eraseToAnyPublisher()
.assertMaxSubscriptions(1)
.share()

This time around the code runs smoothly and the assert passes - the initial publisher subscription is shared amongst the subscribers.

And that’s all for today! assertMaxSubscriptions() is a simple but useful operator :)

Where to go from here?

To learn more about debugging with Combine check out the debugging chapter in Combine: Asynchronous programming with Swift - this is where you can see all updates, discuss in the website forums, and more.