Combine Framework In SwiftUI: Creating Custom Publishers

Bitsandbytesch
6 min readJun 1, 2024

--

Part 3

In our previous article, we delved into the basics of Combine Publishers, laying the groundwork for understanding how data can be processed and managed over time in a declarative way. Building on that foundation, this article will guide you through creating custom publishers in Combine and integrating them into your SwiftUI applications. Custom publishers allow you to define complex data flows, giving you more control over how your app handles and emits data.

Creating a Custom Publisher

Above is the screen snap for Publisher Class in Combine Framework.

To create a custom publisher, you need to conform to the Publisher protocol. From the above Protocol you may notice that it requires you to define two associated types and one method:

1. Output: The type of values the publisher emits.

2. Failure: The type of error the publisher can emit, conforming to the Error protocol.

3. receive(subscriber:): A method that connects the publisher to a subscriber.

Lets look what Publisher we are making:

import SwiftUI
import Combine

struct ContentView: View {
@State private var numbers: [Int] = []

var body: some View {
VStack {
List(numbers, id: \.self) { number in
Text("\(number)")
}
.onAppear {
let numberPublisher = NumberPublisher(numbers: Array(1...10))
numberPublisher
.sink { number in
numbers.append(number)
}
.store(in: &cancellables)
}
}
}

@State private var cancellables = Set<AnyCancellable>()
}

ContentView displays a list of numbers. When the view appears, it subscribes to the NumberPublisher and updates the numbers state variable as new values are emitted. The sink method is used to handle the emitted values, and the subscription is stored in the cancellables set to manage its lifecycle.

Take a pause..

subscription is stored in the cancellables, means ?

An AnyCancellable is a type provided by Combine that encapsulates the logic for cancelling a subscription.

A Subscription represents the relationship between a publisher and a subscriber.

So, when a subscriber subscribes to a publisher, a subscription is created, the subscription is being stored in a collection of AnyCancellable instances and hence we can cancell the subscription when it’s no longer needed.

struct NumberPublisher: Publisher {
typealias Output = Int
typealias Failure = Never

let numbers: [Int]

func receive<S>(subscriber: S) where S : Subscriber, NumberPublisher.Failure == S.Failure, NumberPublisher.Output == S.Input {
let subscription = NumberSubscription(subscriber: subscriber, numbers: numbers)
subscriber.receive(subscription: subscription)
}
}

Our Custom Publisher is NumberPublisher, it conforms to Publisher Protocol.

What does our Publisher do ?

NumberPublisher generates a sequence of numbers to its subscribers.

The objective is to create a sequence of numbers which looks like as this on UI:

final class NumberSubscription<S: Subscriber>: Subscription where S.Input == Int, S.Failure == Never {
private var subscriber: S?
private let numbers: [Int]

init(subscriber: S, numbers: [Int]) {
self.subscriber = subscriber
self.numbers = numbers
}

func request(_ demand: Subscribers.Demand) {
var currentDemand = demand

for number in numbers {
if currentDemand == .none {
break
}
_ = subscriber?.receive(number)
currentDemand -= 1
}

subscriber?.receive(completion: .finished)
}

func cancel() {
subscriber = nil
}
}

The NumberSubscription class handles the subscription logic, generating values according to the demand requested by the subscriber.

Lets see the sequence:

1: onAppear our Publisher (NumberPublisher) get called.

2: The sink method is called on numberPublisher

The sink operator is used to subscribe to a publisher and receive values by that publisher. When ever .sink recieves a new value, it then executes its closure.

.sink {}

Take a pause..

If NumberPublisher is our Publisher, then who is the Subscriber ?

Answer is .sink

The call to sink creates a subscriber that receives values from Publisher(NumberPublisher).

Lets goto step 3

3:When a subscriber (created by the sink method) subscribes to the publisher, the receive(subscriber:) method is invoked.

Inside receive(subscriber:), a subscription object is created. This subscription object is responsible for managing the flow of data between the publisher and the subscriber.

Getting confused ??

Let me stop here and Explain with Real Life example:

Let’s take Airtel Example.

Here Airtel acts as the publisher.

The user, who subscribes to Airtel’s services, acts as the subscriber.

Breakdown the Process:

1. Subscription Process:

The user decides to subscribe to Airtel’s services.

The user goes to an Airtel store or website and subscribes to a data plan.

2. Publisher’s receive(subscriber:):

• When the user subscribes to a data plan, Airtel processes the subscription request.

• Airtel then provides the user with a SIM card and activates the data plan.

3. Subscription Object:

• The SIM card and the data plan activation act like the subscription object.

• This subscription object (SIM card) manages the flow of data from Airtel to the user.

4. Subscriber Requests Data:

• The user, having received the SIM card, puts it into their phone.

• The user starts requesting data by using their phone to browse the internet, stream videos, etc.

5. Publisher Generates Data:

• Airtel, as the publisher, starts emitting data to the user’s phone.

The user’s phone receives internet data, which can be compared to receiving values from a publisher.

6. Subscriber Receives and Uses Data:

• The user’s phone processes the incoming data, enabling the user to browse websites, use apps, and more.

7. Cancellation:

• If the user decides to stop using Airtel’s services, they can cancel their subscription.

• Cancelling a subscription would mean that the subscriber stops receiving values from the publisher.

So How would the code be like ?

Lets See:

The Publisher below:

// Publisher: Airtel
struct Airtel: Publisher {
typealias Output = String
typealias Failure = Never

func receive<S>(subscriber: S) where S : Subscriber, Airtel.Failure == S.Failure, Airtel.Output == S.Input {
//a subscription object is created
let subscription = DataSubscription(subscriber: subscriber)
subscriber.receive(subscription: subscription)
}
}

The Subscriber below:

// Subscriber: User
struct User {
var cancellables = Set<AnyCancellable>()

mutating func subscribeToAirtel() {
let airtel = Airtel() // Call to Publisher
airtel
.sink { data in //creates subscriber to recieve value from publisher
print("User received data: \(data)")
}
.store(in: &cancellables)
}
}

var user = User()
user.subscribeToAirtel()

What all logic and data, the subscription would contain ? — DataSubscription.

final class DataSubscription<S: Subscriber>: Subscription where S.Input == String, S.Failure == Never {
private var subscriber: S?

init(subscriber: S) {
self.subscriber = subscriber
}

func request(_ demand: Subscribers.Demand) {
var currentDemand = demand

let dataStream = ["Browsing", "Streaming", "Downloading"]
for data in dataStream {
if currentDemand == .none {
break
}
_ = subscriber?.receive(data)
currentDemand -= 1
}

subscriber?.receive(completion: .finished)
}

func cancel() {
subscriber = nil
}
}

Let go back to our original example flow:

  1. Subscriber Subscribes: When you call sink on numberPublisher, a subscriber is created and subscribes to the publisher.

2. receive(subscriber:) is Called: The publisher’s receive(subscriber:) method is called with the subscriber as an argument.

3. Subscription Created: Inside receive(subscriber:), a new NumberSubscription object is created.

4. Subscription Passed to Subscriber: The subscriber receives the subscription, which it can use to request values.

5. Values Emitted: The request(_:) method on the subscription is called by the subscriber, causing the subscription to emit values to the subscriber.

Final Thoughts:

I would recommend you to think and create 2–3 Custom Publishers. Creating custom publishers in Combine allows you to define complex data flows and integrate them seamlessly into your SwiftUI applications.

Please consider showing your support by clapping and following the writer! 👏

Upcoming- In Part 4 we will discuss on various subscribers including the .sink in Combine Framework.

Write a Beautiful Code !!

Github — https://github.com/SaurabhBisht?tab=repositories

Instagram: https://www.instagram.com/bitsandbytesch/

Youtube: https://www.youtube.com/@BitsAndBytesCH/videos

--

--

Bitsandbytesch

Join our channel for a comprehensive exploration of Industry Tips, Experts interviews, code languages and system design concepts. Follow now :)