Combine resource management

Ario Liyan
4 min readApr 17, 2024

--

In a series of article I’m writing around the Combine framework, in this post we mainly focus on resource management and best practices in Combine.

Taken from security magazine website

Table of content

  • Why resource management matters?
  • share operator
  • multicast operator
  • Future publisher
  • Custom share operator with buffering

Why resource management matters?

When it comes to performing heavy processes or network calls, we must be cautious about our resource consumption. The best approach is to ensure that anything resource-intensive is not repeated unnecessarily. The share and multicast operators help us achieve this.

share operator

So far most of publisher we’ve used were structs and used value semantic when passing. This means whenever we pass a publisher to a function or store it in a property, Swift copies it and as result when ever we subscribe to those copies start processing what has been designated to them. But the share operator helps us point to publishers by reference rather than value.

The share operator returns an instance of “Publishers.Share” class in in lieu of using value semantics. As result it shares the underlying publisher.

The share operator shares the upstream publisher, after this operator we only subscribes to this publisher once, and with the first subscription the publisher starts its work.

Noted. If a subscription occurs after the publisher has emitted a value and completed, the subscriber will not receive any value.

import Combine

// Create a publisher
let publisher = (1...3).publisher
.share()

// Create two subscribers
let subscriber1 = publisher
.sink { print("Subscriber1 received \($0)") }

let subscriber2 = publisher
.sink { print("Subscriber2 received \($0)") }

//Console Output
//Subscriber1 received 1
//Subscriber1 received 2
//Subscriber1 received 3

As you can see the second subscribe didn’t get anything.

multicast operator

This operator is somewhat similar to share, actually it is like a wrapper for share, and uses a subject publisher of our choice to to publish values to subscribers. Another difference is that, the multicast operator is of connectable type. Which means it won’t publish values until we connect it. This give us the chance to subscribe all our subscribers first and then connect the publisher.

import Combine

// Create a subject
let subject = PassthroughSubject<Int, Never>()

// Create a publisher
let publisher = (1...3).publisher
.print("Publisher")
.multicast(subject: subject)

// Create two subscribers
let subscriber1 = publisher
.sink { print("Subscriber1 received \($0)") }

let subscriber2 = publisher
.sink { print("Subscriber2 received \($0)") }

// Connect the publisher
publisher.connect()

//Console ourput
//Publisher: receive subscription: (1...3)
//Publisher: request unlimited
//Publisher: receive value: (1)
//Subscriber1 received 1
//Subscriber2 received 1
//Publisher: receive value: (2)
//Subscriber1 received 2
//Subscriber2 received 2
//Publisher: receive value: (3)
//Subscriber1 received 3
//Subscriber2 received 3
//Publisher: receive finished

Note like other connectables it also provides us a autoconnect operator which subscribes to the upstream publisher right away.

Future

First lets talk about the promise concept, prmosie is an object that represents the eventual completion (or failure) of an asynchronous operation and its resulting value.

  • A Promise is returned immediately when an asynchronous call is made, even if the operation hasn’t completed.
  • The Promise object provides methods to handle the eventual success or failure of the operation.
  • When the operation is successful, the Promise is said to be fulfilled.
  • If the operation fails, the Promise is failed.

In Swift Future publishers gives us a closure that receives a Promise argument. We then fulfill the promise whenever the result is available.

It’s easier done than said!!

// Operation 
func performSomeWork() throws -> Int {
print("Performing operations and returning the result ")
return 5
}

// Publisher
let future = Future<Int, Error> { promise in
do {
let result = try performSomeWork()
promise(.success(result))
} catch {
promise(.failure(error))
}
}

// Subscribers
let subscription1 = future
.sink(
receiveCompletion: { _ in print("subscription1ompleted") },
receiveValue: { print("subscription1 received: '\($0)'") }
)

let subscription2 = future
.sink(
receiveCompletion: { _ in print("subscription completed") },
receiveValue: { print("subscription2 received: '\($0)'") }
)

//Performing operations and returning the result
//subscription1 received: '5'
//subscription1ompleted
//subscription2 received: '5'
//subscription completed

The first time we subscribe to the publisher it immediately invoke the promise and return the result and store the result for future subscribers. Future is a class type Publisher.

Note that even if we don’t subscribe to a Future, creating it will call the closure and perform the work. We cannot use defer to delay the execcution because defer is a struct type, and using it causes new Future publisher creation on each subscription.

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.