02. Combine (2) ———— Common types (1)

It will take about 10 minutes to finish reading this article.

Overview

In the previous section, we introduced the Publisher and Subscriber types along with some of the types under the Publishers and Subscribers namespaces in the Combine framework. These two types are the core concepts of the Combine framework. This article will continue to introduce some other common types in the Combine framework.

1. AnyPublisher

AnyPublisher is the type-erased version of the Publisher type. It hides the specific underlying publisher type and exposes a generic interface for handling publishers.

Example Code One:

1
2
3
4
5
6
7
8
9
10
11
let specificPublisher = Just("Hello, Combine!")
let anyPublisher = specificPublisher.eraseToAnyPublisher()

anyPublisher
.sink(receiveCompletion: { completion in
// Handle completion state (finished or failure)
}, receiveValue: { value in
// Handle the emitted value
})
.store(in: &cancellables)

You can create an AnyPublisher from any publisher using the eraseToAnyPublisher() method. You can subscribe to it, apply operators, and handle the values and errors it emits just like any other Publisher. It’s especially useful when you want to combine or merge multiple publishers of different types into a single type-erased publisher for further processing.

Example Code Two:

AnyPublisher is particularly useful when you want to combine or merge multiple publishers of different types into a single type-erased publisher for further processing:

1
2
3
let publisher1 = ...
let publisher2 = ...
let combinedPublisher = Publishers.Merge(publisher1, publisher2).eraseToAnyPublisher()

2. Publishered

Published is a property wrapper in SwiftUI based on Combine, used to create observable object properties. It converts property changes into Combine publishers, enabling the interface to update automatically to reflect the changes. This is useful for creating responsive user interfaces.

Example Code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Weather {
@Published var temperature: Double

init(temperature: Double) {
self.temperature = temperature
}
}

let weather = Weather(temperature: 20)
cancellable = weather.$temperature
.sink() {
print("Temperature now: \($0)")
}
weather.temperature = 25

// Prints:
// Temperature now: 20.0
// Temperature now: 25.0

3. Cancellable and AnyCancellable

3.1 Cancellable

Cancellable is a protocol that represents an object that can be canceled for a subscription. It allows you to manually cancel subscriptions when they are no longer needed to avoid unnecessary resource consumption or the continued propagation of data streams.

1
protocol Cancellable

Calling cancel() not only frees allocated resources, it may also have side effects such as stopping timers, network access, or disk I/O.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

import Combine

class MyViewModel: ObservableObject {
private var cancellable: Cancellable?

func fetchData() {
cancellable = somePublisher.sink { data in
// Handle data
}
}

func cancelFetch() {
cancellable?.cancel()
}
}

3.2 AnyCancellable

AnyCancellable is a type-erased object for Cancellable that manages and holds one or more Cancellable objects. An instance of AnyCancellable automatically calls cancel() upon deallocation, ensuring that they are properly canceled when they are no longer needed.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import Combine

class MyViewModel: ObservableObject {
private var cancellables = Set<AnyCancellable>()

func fetchData() {
somePublisher1
.sink { data in
// Handle data
}
.store(in: &cancellables)

somePublisher2
.sink { data in
// Handle data
}
.store(in: &cancellables)
}
}

4. Future

Future is a type of Publisher that represents an asynchronous operation that may produce a result in the future. It can be used to create a Publisher that will produce a value or an error at some point in the future.

1
2
3

final class Future<Output, Failure> where Failure : Error

Example Code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
let futureExample = Future<Int, Error> { promise in
// Perform asynchronous operation
DispatchQueue.global().async {
// Simulate completion of the asynchronous operation
let result = 42
// Use promise to complete the Future
promise(.success(result))
}
}

futureExample
.sink(receiveCompletion: { completion in
// Handle completion state (finished or failure)
}, receiveValue: { value in
print("Got value \(value).")
})
.store(in: &cancellables)

Futures are often used in Combine to handle one-time asynchronous operations, such as network requests or computationally intensive tasks. It allows these operations to be wrapped into Publishers so that they can be combined, transformed and processed with other data streams.

5. Just

Just is a type of Publisher that is used to create a Publisher that emits a single specified value. Unlike Future, Just immediately emits its value as soon as it is created, rather than executing an asynchronous operation at some future point.

Example Code:

1
2
3
4
5
6
7
let justExample = Just("Hello, Combine!")

justExample
.sink(receiveValue: { value in
// Handle the emitted value
})
.store(in: &cancellables)

Just Publisher is useful for creating data flows for testing or serving static data, as it can easily convert a specific value into a Publisher. Note, however, that it is not suitable for representing dynamic or asynchronous data flows. For the latter, you might consider using a Future or other appropriate Publisher type.

6. Deferred

Deferred is a type of Publisher used to delay the creation and subscription of another Publisher. Unlike other Publishers, Deferred executes its closure when it’s subscribed to, which is used to create and return the actual Publisher you want to subscribe to.

Example Code:

1
2
3
let deferredExample = Deferred {
return Just("Deferred Example")
}

Initialize with a closure that will be executed every time you subscribe, and the return value of the closure is the actual publisher you want to subscribe to.

1
2
3
4
5
deferredExample
.sink(receiveValue: { value in
// Handle the emitted value
})
.store(in: &cancellables)

You can subscribe to a Deferred publisher just like other Combine publishers and process the values it emits.

Note: The closure of Deferred will only be executed when subscribing, and it will be executed once for each subscription. This means that the actual publisher is created lazily and can be dynamically generated as needed.

Deferreds are a type of conditional Publisher: you can use Deferreds to create Publishers that are generated based on certain conditions, which allows you to generate different data streams when needed to adapt to different situations. Deferred is a very useful publisher type that allows you to generate publishers on demand and perform related logic when subscribing. This is useful for implementing dynamic, conditional, or context-based data flow in Combine flows.

7. Empty

Empty is also a publisher type, which represents a publisher that does not emit any value and will only be completed immediately. It is usually used to represent an empty data stream or a data stream that contains no data.

1
struct Empty<Output, Failure> where Failure : Error

Example Code:

1
let emptyExample = Empty<Int, Never>()

You can subscribe to the Empty publisher just like other Combine publishers, but since it completes immediately and emits no value, you usually only need to handle the completion status.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
emptyExample
.sink(receiveCompletion: { completion in
switch completion {
case .finished:
// Handle completion status (completed)
case .failure(let error):
// This branch will not be entered because the error type is usually `Never`
break
}
}, receiveValue: { value in
// This branch will not be entered because `Empty` does not emit a value
})
.store(in: &cancellables)

Empty Publisher is commonly used to represent situations where no data is available under certain conditions in the Combine data stream. It can be used to create an empty data stream for combining and processing with other Publishers.

8. Fail

Fail is a type of Publisher that represents a Publisher that will immediately complete with a specified error. Unlike Empty, Fail emits an error without emitting any values. The error type is a subtype of Error.

Example Code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let failExample = Fail<Int, MyError>(error: MyError.someError)

failExample
.sink(receiveCompletion: { completion in
switch completion {
case .finished:
// Won't enter this branch because `Fail` completes immediately
break
case .failure(let error):
// Handle the error
}
}, receiveValue: { value in
// Won't enter this branch because `Fail` doesn't emit values
})
.store(in: &cancellables)

Since it completes immediately and issues an error, you usually only need to handle the error.

Fail is usually used to simulate certain failure or error situations in order to test error handling logic in the Combine data flow. It can also be used to indicate that under certain conditions the data stream is unable to provide valid data and an error occurs.

9. Record

Record is also a type of Publisher that allows recording a series of input and completion events for later playback to each subscriber. You can create a Record Publisher using different initialization methods.

1. Create a Record publisher with the provided output and completion events:

1
2
3
4
let values: [Int] = [1, 2, 3]
let completion: Subscribers.Completion<MyError> = .finished

let recordPublisher = Record(output: values, completion: completion)

2. Use closures to interactively log output and completion events:

1
2
3
4
5
6
let recordPublisher = Record<Int, MyError> { recorder in
recorder.receive(1)
recorder.receive(2)
recorder.receive(3)
recorder.receive(completion: .finished)
}

3. Create a Record publisher using existing records (Recording):

1
2
3
let existingRecording: Record<Int, MyError>.Recording = ...
let recordPublisher = Record(recording: existingRecording)

You can subscribe to Record Publisher like any other Combine Publisher. It will replay the previously recorded values and completion events to the subscriber.

1
2
3
4
5
6
7
8
9
10
11
12
recordPublisher
.sink(receiveCompletion: { completion in
switch completion {
case .finished:
// Handle completion event
case .failure(let error):
// Handle error events
}
}, receiveValue: { value in
// Process the recorded value
})
.store(in: &cancellables)

Record is a powerful tool that can be used to record and playback data streams for testing, simulation, and other situations where data needs to be replayed at different points in time. This can help simplify and improve testing and debugging of code.

10. ConnectablePublisher

ConnectablePublisher is a special type of Publisher that requires manually calling the connect() method to start publishing values. Unlike regular Publishers, a ConnectablePublisher doesn’t immediately start propagating data when subscribed to. Instead, it waits until the connect() method is called.

Example Code:

1
2
3
4
5
6
7
8
9
10
let publisher = [1, 2, 3].publisher.makeConnectable()
// Subscribers can subscribe at any time, but data won't be published immediately

// Manually connect at a specific point to start publishing data
let cancellable = publisher
.sink { value in
// Handle the data
}

publisher.connect() // Manually connect

Once connected, the ConnectablePublisher will continue to publish data until you disconnect. You can stop receiving data by canceling the subscription or automatically connect until there are no subscribers by using the autoconnect() method.

1
2
3
cancelable.cancel() // Stop receiving data
// or
let autoconnectedPublisher = publisher.autoconnect() // Automatically connect

ConnectablePublisher is useful for scenarios where multiple subscribers need to operate on the same data stream. By manually connecting, you can ensure that subscribers start receiving data only when they are ready to receive, meeting specific requirements. This approach can improve the performance and efficiency of your code.