Understanding Publishers in SwiftUI and Combine

Mike Pesate
Bumble Tech
Published in
6 min readSep 20, 2023
Understanding Publishers in SwiftUI and Combine

SwiftUI and Combine have been around for a couple of years and are fast becoming more mainstream, as they’re adopted as the main tools to develop new iOS and macOS (and other appleOSs) applications.

Embedded into the heart of these new tools is a very versatile and useful component: Publisher.

But now, you might be wondering:

What’s a publisher?

A publisher is nothing more than an object that emits a stream of values over time. These values can be consumed by one or more subscribers, which can then update the state of the user interface or perform other actions.

In other words: Publishers are the source of data and subscribers are the consumers of that data.

A real life example of a publisher would be an email mailing list. Like the one you never subscribed to, but from whom you keep receiving emails. They’d be a publisher in this scenario and you, and anyone else receiving these emails, would be the subscribers.

How do we identify publishers in code?

A publisher is every entity that conforms to the publisher protocol. It could be ready-made, provided by Apple inside the combine framework, or it could be one that you or someone else defined from scratch.

How do we go about instantiating and using publishers in our apps?

I’m very glad you asked that.

Harnessing the power of publishers

Combine provides us with a few ready-to-use publishers, as mentioned above, there are more, but to keep this short and sweet let’s go over the most frequently used ones:

You might be wondering why these “publishers” are suffixed with the word “Subject”. This is how Apple explains it:

A subject is a publisher that you can use to ”inject” values into a stream.

In practice, this means that a subject allows other entities to assign a value to be distributed amongst all its subscribers. This is accomplished with the exposure of send(_ value:). You’ll see how this works in the examples below.

Using a publisher

Let’s focus on the publishers that you and everyone else will be using the most:

CurrentValueSubject

This publisher emits AND stores the current value of a property. Meaning that when a new value is assigned it will be sent to all the subscribers as expected, but, it will also be stored so it can be requested later on for immediate use or requested by new subscribers attached after the value was emitted.

import Combine

/*
* Let's create a new instance.
* You might notice that publishers need to define Result types
* One for Success, which in our case is Int
* and one for Failure, which should represent an Error type but
* for the purposes of our example we will just say errors Never happen.
*/
let subject = CurrentValueSubject<Int, Never>(0)

// We can consult the value stored right away
print("\(subject.value)") // "0"

// We can subscribe using `sink` and that will also be called right away
// and also when a new value is emitted.
let cancellable = subject.sink { newValue in
print("\(newValue)") // "0" the first time
}

// Let's update the value
subject.send(10)

print("\(subject.value)") // "10"

// Sink will also print "10"

In juxtaposition to the CurrentValueSubject, which can be initialised with an initial value and that value can be requested by old and new subscribers, we have:

PassthroughSubject

This publisher is initialised with no initial value and can emit multiple values over time but won’t store any of them for future references. The subscriber is attached to the publisher and it will receive all values sent to the publisher using the send(_:) method. However, if a subscriber attaches after a value has been emitted, they won’t be notified. Let’s see this in code:

import Combine

/*
* Again, we are defining a new publisher with no error type
* to keep the examples as simple as possible.
* As you can see, this Publisher doesn't get an initial value
*/
let subject = PassthroughSubject<Int, Never>()

/*
* It sends this value to any subscriber,
* but because at this point in time it has none,
* no one will ever know this happened.
*/
subject.send(0)

/*
* Now we listen for new values
* But in contrast to CurrentValueSubject nothing will happen
* at the time of subscribing because there's no value present
*/
let cancellable = subject.sink { newValue in
print("\(newValue)")
}

subject.send(10) // print appears in console "10"

Now, there’s one more publisher that has a similar behaviour to CurrentValueSubject but with a huge caveat.

Just

This publisher immediately sends a single value to the subscriber and then completes never to be heard from again, with new values that is. You can always attach a new subscriber and receive the value again just once.

import Combine

// Create a new Just publisher with an initial value
let subject = Just<Int>(10)

// Attach to listen to that value and process it
let cancellable = subject.sink(
// This is the first time completion is being mentioned.
// More on that after this block
receiveCompletion: { _ in
print("Publisher completed")
}, receiveValue: { value in // This is where the value gets send to
print("\(value)")
}
)

// --- Console Prints
// "10"
// "Publisher completed"

What is receiveCompletion(..)? As with most things in life, publishers have a beginning and may also have an end. Meaning, in the case of Just, that after that initial value nothing else will ever be provided to that subscriber.

However, CurrentValueSubject and PassthroughSubject do not close themselves for business after sending one value or two or infinite. For these, you’d have to tell them to send a completion instead of a value, thus notifying the subscribers that they are closed for business and will no longer provide new values. Let’s see this in action:

import Combine

// Just as on the first example we create a new publisher
let subject = CurrentValueSubject<Int, Never>(0)

// Then we attach a lister to it.
// However, this time it comes with the completion closure
let cancellable = subject.sink(
receiveCompletion: { _ in
print("Publisher completed")
}, receiveValue: { value in
print("\(value)")
}
)

subject.send(completion: .finished)
subject.send(10)

// --- Console output
// "0"
// "Publisher completed"
//

What happened to that subject.send(10)? It gets lost in the ether. The publisher is no longer taking new values so “10” will not get distributed to the subscribers.

But you may ask:

We know that a CurrentValueSubject stores the initial value, what happens if we attach a new subscriber to it?

Good question! Here’s an example code. Run it in your project or a playground and let me know what happens:

import Combine

let subject = CurrentValueSubject<Int, Never>(0)
subject.send(completion: .finished)

let cancellable = subject.sink(
receiveCompletion: { _ in
print("Publisher completed")
}, receiveValue: { value in
print("\(value)")
}
)

// Quiz: What's printed when running this code?

Conclusion

Publishers are a powerful tool. They allow a developer to distribute, react and transform data easily. These were Just (pun intended) the most basic and common examples of publishers. There’s more to them and I encourage you to go and find out more about them. Publishers can be combined with others, they can be created by you for specific use cases.

Getting well acquainted with publishers is going to be a must very very soon, if it isn’t already.

Useful links

Bonus section on Just

When I started to learn about publishers, I could immediately see the uses of them in real world examples, except for Just. It took me a while to figure that one out.

So, here’s an IRL example of how Just might be used.

// Function to retrieve an image from a given URL
func imagePublisher(for url: URL) -> AnyPublisher<UIImage, Error> {
// But first you check to see if you already downloaded that image
if let image = cache.image(for: url) {
// If so, why wait? return that with a Just publisher
return Just<UIImage>(image)
// Even though Just doesn't seem have an Failure type,
// it actually has Never.
// We then need to change this to match the expected
// Failure declared by the function.
.setFailureType(to: Error.self)
// You'll probably want to get the response on the main thread
.receive(on: DispatchQueue.main)
// Then type erase to match the expected return type
.eraseToAnyPublisher()
}
//....
}

I hope this provides more insight into how to apply publishers in your projects.

Have fun with this new information!

If you enjoyed this article or it helped you in any way. Do let me know by clapping, commenting or highlighting the portions that you found the most interesting.

You can also find me on LinkedIn.

--

--

Mike Pesate
Bumble Tech

Senior iOS Developer trying to write more and share more.