Debugging in Combine

Ario Liyan
4 min readApr 15, 2024

--

In this small post we read about approaches we can take to help us debugging in combine.

Created by Co-pilot powered by DALL-E

Table of contents

  • Printing events
  • Print with TextOutputStream
  • Side Effects
  • breakpoint operator

Printing events

Debugging a stream of events to find where the bugs nest is not a delightful experience due to asynchronous call backs pile up on each other and the reactive nature of the combine.

The first and easiest way to log the events and make sure everything goes according to the plan is using the print operator.

The print operator is same as the print function we have been using for a quite long time.

let subscription = ["a", "b", "c"].publisher
.print("publisher")
.sink { _ in }

// Console output
//publisher: receive subscription: (["a", "b", "c"])
//publisher: request unlimited
//publisher: receive value: (a)
//publisher: receive value: (b)
//publisher: receive value: (c)
//publisher: receive finished

Print with TextOutputStream

There also is another overload of print function which gets another argument of type “TextOutputStream”.

Through this overload we can redirect our string to a logger, then our logger can add or manipulate the string and then print.

As for the first step let’s implement a Logger that calculates the duration between publishing events and print the duration and emitted value.

class TimeLogger: TextOutputStream {
private var previousEventDate = Date()
private let durationFormatter = NumberFormatter()

init() {
durationFormatter.maximumFractionDigits = 5
durationFormatter.minimumFractionDigits = 5
}

func write(_ string: String) {
let eventString = string.trimmingCharacters(in: .whitespacesAndNewlines)
guard !eventString.isEmpty else { return }

let now = Date()

let duration = now.timeIntervalSince(previousEventDate)
let durationString = durationFormatter.string(for: duration)!

print("It tooks +\(durationString)s: \(string)")

previousEventDate = now
}
}

The first two lines of the write function is the guarantor that our string is not empty after removing new lines and white spaces.

let subscription = ["a", "b", "c"].publisher
.print("publisher", to: TimeLogger()))
.sink { _ in }

// Console output
//It tooks +0.00865s: publisher: receive subscription: (1...3)
//It tooks +0.02768s: publisher: request unlimited
//It tooks +0.02046s: publisher: receive value: (1)
//It tooks +0.00087s: publisher: receive value: (2)
//It tooks +0.00073s: publisher: receive value: (3)
//It tooks +0.00079s: publisher: receive finished

Side Effects

Sometime it’s a good idea to perform actions if some particular events happen, this is not directly a debugging tool but as you’ll see, it can be used as one.

Performing a side effect don’t directly impact the downstream but rather can have external effects.

handleEvents(receiveSubscription:receiveOutput:receiveCompletion:rece iveCancel:receiveRequest:) this is the function that helps us to perform side effect.

As a common scenario let’s say we have performed a network call, but we don’t get any response, and we want to solve the problem first we should identify it. So what is our problem?

  1. Server don’t respond to us
  2. We did not subscribing properly to listen for the results.
let request = URLSession.shared
.dataTaskPublisher(for: URL(string: "https://jsonplaceholder.typicode.com/")!)

request
.sink(receiveCompletion: { completion in
print("Sink received completion: \(completion)")
}) { (data, _) in
print("Sink received data: \(data)")
}

If we run this code, we don’t get anything printed. It’s time to add a handle event operator.

let request = URLSession.shared
.dataTaskPublisher(for: URL(string: "https://jsonplaceholder.typicode.com/")!)

request
.handleEvents(receiveSubscription: { _ in
print("Network request will start")
}, receiveOutput: { _ in
print("Network request data received")
}, receiveCancel: {
print("Network request cancelled")
})
.sink(receiveCompletion: { completion in
print("Sink received completion: \(completion)")
}) { (data, _) in
print("Sink received data: \(data)")
}

//Console output
//Network request will start
//Network request cancelled

Now according to our prints we can find the issue that is we haven’t store our subscription properly.

breakpoint Operator

Another option that we have to debug our combine code is the breakpoint operator that is good for introspecting our state in certain scenarios.

There is also breakpointOnError operator which is as its name implies good for when an error happens.

A more useful and complete varian is breakpoint(receiveSubscription:receiveOutput:receiveCompletion:).

.breakpoint(receiveOutput: { value in
return value == -1
})

In this case for example, we break only if the emitted value is -1.

At last but not least is Timelane package. For more information read this links:

Next

--

--

Ario Liyan

As an iOS developer with a passion for programming concepts. I love sharing my latest discoveries with others and sparking conversations about technology.