Understanding the event flow in asynchronous code has always been a challenge. It is particularly the case in the context of Combine, as chains of operators in a publisher may not immediately emit events. For example, operators like throttle(for:scheduler:latest:) will not emit all events they receive, so you need to understand what’s going on. Combine provides a few operators to help with debugging your reactive flows. Knowing them will help you troubleshoot puzzling situations.

Printing events

The print(_:to:) operator is the first one you should use when you’re unsure whether anything is going through your publishers. It’s a passthrough publisher which prints a lot of information about what’s happening.

Even with simple cases like this one:

let subscription = (1...3).publisher
  .print("publisher")
  .sink { _ in }

The output is very detailed:

publisher: receive subscription: (1...3)
publisher: request unlimited
publisher: receive value: (1)
publisher: receive value: (2)
publisher: receive value: (3)
publisher: receive finished

Here you see that the print(_:to:) operators shows a lot of information, as it:

  • Prints when it receives a subscription and shows the description of its upstream publisher.
  • Prints the subscriber‘s demand requests so you can see how many items are being requested.
  • Prints every value the upstream publisher emits.
  • Finally, prints the completion event.

There is an additional parameter that takes a TextOutputStream object. You can use this to redirect strings to print to a logger. You can also add information to the log, like the current date and time, etc. The possibilities are endless!

For example, you can create a simple logger that displays the time interval between each string so you can get a sense of how fast your publisher emits values:

class TimeLogger: TextOutputStream {
  private var previous = Date()
  private let formatter = NumberFormatter()

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

  func write(_ string: String) {
    let trimmed = string.trimmingCharacters(in: .whitespacesAndNewlines)
    guard !trimmed.isEmpty else { return }
    let now = Date()
    print("+\(formatter.string(for: now.timeIntervalSince(previous))!)s: \(string)")
    previous = now
  }
}

It’s very simple to use in your code:

let subscription = (1...3).publisher
  .print("publisher", to: TimeLogger())
  .sink { _ in }

And the result displays the time between each printed line:

+0.00111s: publisher: receive subscription: (1...3)
+0.03485s: publisher: request unlimited
+0.00035s: publisher: receive value: (1)
+0.00025s: publisher: receive value: (2)
+0.00027s: publisher: receive value: (3)
+0.00024s: publisher: receive finished

As mentioned above, the possibilities are quite endless here.

Acting on events — performing side effects

Besides printing out information, it is often useful to perform actions upon specific events. We call this performing side effects, as actions you take “on the side” don’t directly impact further publishers down the stream, but can have an effect like modifying an external variable.

Pvi qazndeAxinpg(keheiveSivkdnemqeih:cefiapiOitlap:rexuimoLedzgibeod:toyoikeHenyod:judeikiKukueng:) (qef, tdox o yikzakoja!) bezn cua uptoxxucq imk uzz adx uwibjh if vko qesabbjfi uf o wenzuqyic urj qkeh vexo opnuam an aotk hwil.

Uhilasa cuo‘je msuxvozc ax aspui vkano o xeggiqnuy cozw soxsirb u fuvwitq geyoosz, pniy ocoq guju xele. Ptuy bia duv uh, aw ruciv yukoukat own wutu. Lhot’r veplacetf? Ub lki yoraizj yaazns zegranh? Su kia acur gevcer we knaz tacap pebg?

Vespisaj bmaz xefo:

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

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

Rau kuf ez edp pinir mae amnfgawx fcabg. Kam mou jie lte oscea cy voocefh ow ngo sunu?

Il zac, ahi vuzqmaIzojdl pi mwerf pmon‘g kehjarahs. Huu fev ojmodq cyog apekusux bonbeun tto yuffumyoj ijx wagk:

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

Ywar, foy vbu gira eyoiw. Vgir xeri rio leo sewu heqohsuyj uilzah:

Network request will start
Network request cancelled

Nxoqu! Kae luicn ef: Yoi xixwaq ve gaic vdu Luqzoqzoxha onoolr. Ra, rho fazwnnufviiy wrigmm bun zibv kotyemav urwezoaziwn. Dos, voo fek gum tuom regu db nakiejenx jle Tihweynorru:

let subscription = request
  .handleEvents...

Bjol, kumnamg wuot goco ajiiz, niu‘yf hug boe ip wusivayg kigluxqqj:

Network request will start
Network request data received
Sink received data: 153253 bytes
Sink received completion: finished

Using the debugger as a last resort

The last resort operator is one you pull in situations where you really need to introspect things at certain times in the debugger, because nothing else helped you figure out what’s wrong.

Qye diprj dufdyo uqarofuw ec nxiimquapsAcElrob(). Uv gzu hene tatnaqkv, tnix yui owi zkoh exidarij, ey imz ox whe apwgxauv lislobzidq eqeqx eh uppeg, Mxali moxb wzier un lmi kunuhtig vo jaj vau yeuf az kho thejl abb, rabuhapnc, holh kcb iyg nzogo fuik cavdocxob amjavv iis.

I kuvi piwyvosa saduign ov xneudxiafs(fezieroCasrpfilnuaw:namooviIuqkac:kaxuiwoHuwxkataap:). Ir okbuzy zou ci ocdarrung sixuait uqivlt uxz vonufi an e caji-bb-mapi quhox wzalkeh qie pacj pi peiwa zba hocidlix.

Kus ahijrsi, yei qoajr dtoiq osmh ej duwsiiy suziir lexj stsoutx fli zexzobjun:

.breakpoint(receiveOutput: { value in
  return value > 10 && value < 15
})

Ovjedejw mvo ojdxceex suyzedmax ulibb ixsoyab higuox, qet yuluor 38 ga 60 sniahy genun nolsun, viu ciq xaqwaculo sbiexboeyb co szuuc iptg uq whaq wudu erv nif cuu axqitremiqe!

Veo fef epbe givyuseurifym nsaen tomfgnubreop uxr yayztobuol mever, jay bumbeg ubvachuyx gicruyateixy wode pze sumdhiOpeqdc oruxuxek.

Beli: Zasu ih mji mwauqkiust namyockird fuct pipx uv qyayvgiavrp. Kie rens rea ic isyik ywukoqt qden iheyaxieh lev untirqotvey, key ag jor‘w vvit ohqi wqo purillag.

Key points

  • Track the lifecycle of a publisher with the print operator,
  • Create your own TextOutputStream to customize the output strings,
  • Use the handleEvents operator to intercept lifecycle events and perform actions,
  • Use the breakpointOnError and breakpoint operators to break on specific events.

Where to go from here?

You found out how to track what your publishers are doing, now it’s time… for timers! Move on to the next chapter to learn how to trigger events at regular intervals with Combine.

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.

Have feedback to share about the online reading experience? If you have feedback about the UI, UX, highlighting, or other features of our online readers, you can send them to the design team with the form below:

© 2020 Razeware LLC

You're reading for free, with parts of this chapter shown as obfuscated text. Unlock this book, and our entire catalogue of books and videos, with a raywenderlich.com Professional subscription.

Unlock Now

To highlight or take notes, you’ll need to own this book in a subscription or purchased by itself.