Reactive Programming in iOS with Combine

Feb 4 2021 · Swift 5.3, macOS 11.0, Xcode 12.2

Part 1: Getting Started

03. Subscriber Operators and Subjects

Episode complete

Play next episode

Next
About this episode

Leave a rating/review

See forum comments
Cinema mode Mark complete Download course materials
Previous episode: 02. Publishers and Subscribers Next episode: 04. Challenge: Create a Blackjack Dealer

Get immediate access to this and 4,000+ other videos and books.

Take your career further with a Kodeco Personal Plan. With unlimited access to over 40+ books and 4,000+ professional videos in a single subscription, it's simply the best investment you can make in your development career.

Learn more Already a subscriber? Sign in.

Transcript: 03. Subscriber Operators and Subjects

Although you ignored them in the previous example, the sink operator actually provides two closures: one to handle receiving a completion event, and one to handle receiving values. Let’s jump back into some code to see that in action.

Add this new example of using a Just publisher, which lets you create a publisher from any value. Using sink again, except this time implement both closures to handle receiving a completion event and a value and just print them out.

example(of: "Just") {
  let just = Just("Hello world!")

  just
    .sink(
      receiveCompletion: {
        print("Received completion", $0)
    },
      receiveValue: {
        print("Received value", $0)
    })
    .store(in: &subscriptions)
}

sink is one of two built-in operators you can use to subscribe to a publisher. The other is assign. The assign operator lets you set a received value to a KVO-compliant property of an object. Let’s create a new example to check this out.

Start by defining a SomeObject type with a single value String property, along with a didSet property observer to print out a new value when it’s assigned. Then create an instance of SomeObject.

example(of: "assign(to:on:)") {
  class SomeObject {
    var value: String = "" {
      didSet {
        print(value)
      }
    }
  }

  let object = SomeObject()
}

Now call publisher on an array of strings to create a publisher that will send its values one by one, and then use assign to set each value as it’s received to the value property of the object.

["Hello", "world!"].publisher
  .assign(to: \.value, on: object)
  .store(in: &subscriptions)

Run the playground and, sure enough, your property is set to each new value received, and printed in the didSet observer.

——— Example of: assign(to:on:) ———
Hello
world!

So far you’ve learned about publishers and subscribers and how to work with them. It turns out there’s another category of publishers that act as a go-between the imperative world and Combine — called Subjects.

Subjects enable non-Combine imperative code to send values to Combine subscribers — and there’s two kinds of them:

PassthroughSubjects enable you to publish new values on demand. They’ll happily pass along those values and a completion event.

And then there’s CurrentValueSubject, which works just like PassthroughSubject, except it hangs on to the most recent value. And you can get that value at any time via its value property.

Now let’s see how these subjects work in code, starting with PassthroughSubject. First, create an instance of a PassthroughSubject of String and Never, and then a subscription to the subject that just prints out received values.

example(of: "PassthroughSubject") {
  let subject = PassthroughSubject<String, Never>()

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

This passthrough subject will emit string values, and never emit an error. Remember that all publishers must declare the type of values and errors it can emit in advance, and subscribers must match those types to its input and failure types in order to subscribe to it.

With the previous publishers you’ve created, these types were inferred. However, a passthrough subject is not initialized with starting value, so you have to explicitly declare the types.

Continuing in this example, add two calls the the subject’s send operator, which allows you to send values on that subject to subscribers.

subject.send("Hello")
subject.send("World")

Now run the playground and you’ll see those strings printed out.

——— Example of: PassthroughSubject ———
Hello
World

Next, send a completion event on a subject using it’s send(completion:), followed by sending another value.

subject.send(completion: .finished)
subject.send("Still there?")

Will Still there? actually be printed out though? Run the playground, and…

——— Example of: PassthroughSubject ———
Hello
World

Nope. Once a subject completes, it’s done. You can also send an error on a subject, wrapped in a failure completion event. You’ll learn all about doing that and handling those errors later on in this course.

So that’s PassthroughSubject. As I mentioned, CurrentValueSubject not only lets you send values to subscribers, it also lets you access the current value imperatively.

Create a new example to see how this works, very similar to the previous example except you’re using CurrentValueSubject this time, and it requires an initial value to be specified. An Int with value 0 in this case.

example(of: "CurrentValueSubject") {
let subject = CurrentValueSubject<Int, Never>(0)

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

The inital value’s type must match the type you specified for the current value subject of course.

Continue this example, and first print out the subject’s current value, then send a couple new values and print the current value again, and finally, send a completion event.

print(subject.value)

subject.send(1)
subject.send(2)

print(subject.value)

subject.send(completion: .finished)

Run the playground and you’ll see the values printed, as they’re received in the subscription and also when you explicitly printed the values.

——— Example of: CurrentValueSubject ———
0
0
1
2
2

You can’t tell which values are being received in the subscription and which are being imperatively printed, though. You could distinguish them in the print statement, but there’s actually a better way to see what’s going on.

Combine has a print operator that will log all publishing events. Insert the print operator right before sink

.print()

…and then run the playground again.

——— Example of: CurrentValueSubject ———
receive subscription: (CurrentValueSubject)
request unlimited
receive value: (0)
0
0
receive value: (1)
1
receive value: (2)
2
2
receive finished

Now it’s much easier to see which values are being received vs. imperatively being printed. Keep that print operator in your hip pocket — it’s gonna come in handy a lot.

In practice, you will often want to let subscribers subscribe to receive events from a publisher without being able to access additional details about that publisher. To accomplish that you can erase the type of the publisher.

This is easiest to understand with an example, so add this new one to your playground. Create a passthrough subject, and then create an erased version of that subject by calling eraseToAnyPublisher.

example(of: "Type erasure") {
  let subject = PassthroughSubject<Int, Never>()

  let publisher = subject.eraseToAnyPublisher()
}

Option-click on publisher and you’ll see that it is of type AnyPublisher<Int, Never>. This type-erased version does not allow you to send values through it.

publisher.send(0)

Delete or comment out that line…

//  publisher.send(0)

…and then finish off this example by creating a subscription and printing out received values — same as before — and then send a new value through the subject.

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

subject.send(0)

Fantastic job! You’ve learned a lot in this chapter, and you’ll put these new skills to work throughout the rest of this course and beyond.

But not so fast! It’s time to challenge yourself. So continue to the next episode, where you’ll create a Blackjack card dealer using what you just learned.