I recently started to study the Combine and ran into a certain problem.
First, I will describe what I am doing.
I trying to use Clean Architecture
Here you can see my Repository
protocol Repository {
func test()
}
class MockRepository: Repository {
func test() {
sleep(3)
}
}
Then I created UseCase
class UseCaseBase<TInput, TOutput> {
var task: TOutput? { return nil }
var repository: Repository
init(_ repository: Repository) {
self.repository = repository
}
func execute(with payload: TInput) -> AnyPublisher<TOutput, Never> {
return AnyPublisher(Future<TOutput, Never> { promise in
promise(.success(self.task!))
})
.eraseToAnyPublisher()
}
}
class MockUseCase: UseCaseBase<String, Int> {
override var task: Int? {
repository.test()
return 1
}
}
And then in a init block ContentView I did something like that
init() {
let useCase = MockUseCase(MockRepository())
var cancellables = Set<AnyCancellable>()
useCase.execute(with: "String")
.sink(receiveValue: { value in
print(value)
})
.store(in: &cancellables)
print("Started")
}
At first, I want to get
"Started"
and then after sleep(3)
value "1"
Now I get
"1" and then "Started"
Your sleep(3) call runs on the main thread, which means that it blocks any other operations, including the code that prints the "Started" text.
I won't be rambling about how bad it is to block the main thread, this is well known information, but this is the reason you see the behaviour you asked about.
I don't see any thread switching code in your question, so if you wish to achieve some kind of asynchronicity, then you can either go with Rob's solution of using dispatch(after:), or do the locomotion (the sleep) on another thread:
func execute(with payload: TInput) -> AnyPublisher<TOutput, Never> {
return AnyPublisher(Future<TOutput, Never> { promise in
DispatchQueue.global().async {
promise(.success(self.task!))
}
})
.eraseToAnyPublisher()
}
The first thing I'll mention is that you need to hold a reference to cancellables or your publisher will automatically be cancelled when you add async processing to the chain. Move it out of your init method and into a property.
You can also get rid of the sleep and simply chain to a Delay publisher on a queue of your choice. I chose main.
struct SomeThing {
var cancellables = Set<AnyCancellable>()
init() {
MockUseCase(MockRepository())
.execute(with: "String")
.delay(for: 3.0, scheduler: DispatchQueue.main)
.sink(receiveValue: { print($0) } )
.store(in: &cancellables)
print("Started")
}
}
class MockRepository: Repository {
func test() {
// sleep(3)
}
}
Another option is to get rid of the delay, get rid of the sleep and fulfill your promise asynchronously:
struct SomeThing {
var cancellables = Set<AnyCancellable>()
init() {
MockUseCase(MockRepository())
.execute(with: "String")
.sink(receiveValue: { print($0) } )
.store(in: &cancellables)
print("Started")
}
}
class MockRepository: Repository {
func test() {
// sleep(3)
}
}
func execute(with payload: TInput) -> AnyPublisher<TOutput, Never> {
return Future<TOutput, Never> { promise in
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
promise(.success(self.task!))
}
}
.eraseToAnyPublisher()
}
Related
I implemented a service class with a function that returns a publisher when some data is loaded:
class Service {
let fileURL: URL // Set somewhere else in the program
func loadModels() -> AnyPublisher<[MyModelClass], Error> {
URLSession.shared.dataTaskPublisher(for: fileURL)
.map( { $0.data } )
.decode(type: [MyModelClass].self, decoder: JSONDecoder())
.receive(on: DispatchQueue.main)
.eraseToAnyPublisher()
}
}
This function is used in my view model class like this:
class ViewModel: ObservableObject {
#Published var models: [MyModelClass]?
var cancellables = Set<AnyCancellable>()
let service: Service
init(service: Service) {
self.service = service
loadCityData()
}
func loadModels() {
service.loadModels()
.sink { _ in
} receiveValue: { [weak self] models in
self?.models = models
}
.store(in: &cancellables)
}
}
I find the view model difficult to unit-test because I don't have the publisher returned from the service available directly in my unit test class, but I have the #Published property instead. So I tried to implement a test like this one:
let expectation = expectation(description: "loadModels")
viewModel.$models
.receive(on: RunLoop.main)
.sink(receiveCompletion: { _ in
finishLoading.fulfill()
}, receiveValue: { _ in
})
.store(in: &cancellables) // class-scoped property
viewModel.loadModels()
wait(for: [expectation], timeout: 10)
The problem is that the receiveComplection callback is never called. If I had the publisher available (the one returned from the Service object), the same code applied to the publisher would run successfully and fulfill the expectation. Instead, the complection is not being called but the receiveValue is being called multiple times. Why?
First, instead of passing the entire service to the view model, just pass in the Publisher itself. In the test, you can pass in a synchronous publisher which makes testing much easier.
final class ExampleTests: XCTestCase {
func test() {
let input = [MyModelClass()]
let modelLoader = Just(input).setFailureType(to: Error.self).eraseToAnyPublisher()
let viewModel = ViewModel(modelLoader: modelLoader)
let cancellable = viewModel.$models
.dropFirst(1)
.sink(receiveValue: { output in
XCTAssertEqual(input, output)
})
viewModel.loadModels()
}
}
class ViewModel: ObservableObject {
#Published var models: [MyModelClass]?
var cancellables = Set<AnyCancellable>()
let modelLoader: AnyPublisher<[MyModelClass], Error>
init(modelLoader: AnyPublisher<[MyModelClass], Error>) {
self.modelLoader = modelLoader
}
func loadModels() {
modelLoader
.sink { _ in
} receiveValue: { [weak self] models in
self?.models = models
}
.store(in: &cancellables)
}
}
Notice that there is no need to setup an expectation and wait for it. This makes for a much faster test.
Even simpler would be to just examine the models property directly:
final class ExampleTests: XCTestCase {
func test() {
let input = [MyModelClass()]
let modelLoader = Just(input).setFailureType(to: Error.self).eraseToAnyPublisher()
let viewModel = ViewModel(modelLoader: modelLoader)
viewModel.loadModels()
XCTAssertEqual(viewModel.models, input)
}
}
For all of these though, what exactly do you think you are testing here? There are no transformations and no logic in this code. You aren't testing to ensure the ViewModel calls into the Service, because in order to do this test at all, you have to mock out the Service. So in reality, the only thing you are doing is testing to see if the test itself mocked out the Service correctly. But what's the point in that? Who cares if the test was set up correctly if it doesn't test production code?
You could use a combination of interface and dependency injection to allow testing.
First you define an interface for service:
protocol ServiceInterface {
func loadModels() -> AnyPublisher<[MyModelClass], Error>
}
Next you make Service conform to this new protocol:
class Service: ServiceInterface {
// ...
}
Now you can inject Service into your ViewModel using the interface defined above:
class ViewModel: ObservableObject {
//...
let service: ServiceInterface
init(service: ServiceInterface = Service()) {
self.service = service
loadModels()
}
//...
}
This means you are able to inject any entity conforming to ServiceInterface into the ViewModel, so let's define one in the test target:
struct MockService: ServiceInterface {
let loadModelsResult: Result<[MyModelClass], Error>
func loadModels() -> AnyPublisher<[MyModelClass], Error> {
loadModelsResult.publisher.eraseToAnyPublisher()
}
}
Lastly let's inject MockService into ViewModel for testing purposes:
func testExample() {
let expectedModels = [MyModelClass()]
let subject = ViewModel(service: MockService(loadModelsResult: .success(expectedModels)))
let expectation = expectation(description: "expect models to get loaded")
subject
.$models
.sink(
receiveCompletion: { _ in },
receiveValue: { actualModels in
// or any other test that is meaningful in your context
if actualModels == expectedModels {
expectation.fulfill()
}
}
)
.store(in: &cancellables)
subject.loadModels()
waitForExpectations(timeout: 0.5)
}
I'm having a hard time figuring out how to create a Swift Combine pipeline that contains a .flatMap which has a reference to self. In order to prevent a retain cycle, this should be a [weak self] reference, but this does not work with a .flatMap.
Here is a simplified example showing my problem:
import Foundation
import Combine
class SomeService {
func someOperation(label: String) -> Future<String, Never> {
Future { promise in
print("Starting work for", label)
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
print("Finished work for", label)
promise(.success(label))
}
}
}
}
class SomeDataSource {
let someService = SomeService()
var cancellables = Set<AnyCancellable>()
deinit {
print("Deinit SomeDataSource")
}
func complexOperation() {
// First part 'defined' is inside the complexOperation method:
someService.someOperation(label: "First part")
// Second part is 'defined' in another method (it is shared with other tasks)
.flatMap { _ in self.partOfComplexOperation(label: "Second part") } // <--- This creates a retain cycle
.sink { label in
print("Received value in sink", label)
}
.store(in: &cancellables)
}
func partOfComplexOperation(label: String) -> Future<String, Never> {
someService.someOperation(label: label)
}
}
var someDataSource: SomeDataSource? = SomeDataSource()
someDataSource?.complexOperation()
someDataSource = nil
Output:
Starting work for First part
Finished work for First part
Starting work for Second part
Finished work for Second part
Received value in sink Second part
Deinit SomeDataSource
The problem here is that I want my SomeDataSource to be deinitialised right after starting the "First part" and not even starting the second part. So the output I'm looking for is:
Starting work for First part
Deinit SomeDataSource
Finished work for First part
I'm looking for some kind of combination of .flatMap and .compactMap. Does this exist? If I first .compactMap { [weak self] _ in self } I get the expected result, but maybe there is a better way?
import Foundation
import Combine
class SomeService {
func someOperation(label: String) -> Future<String, Never> {
Future { promise in
print("Starting work for", label)
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
print("Finished work for", label)
promise(.success(label))
}
}
}
}
class SomeDataSource {
let someService = SomeService()
var cancellables = Set<AnyCancellable>()
deinit {
print("Deinit SomeDataSource")
}
func complexOperation() {
// First part 'defined' is inside the complexOperation method:
someService.someOperation(label: "First part")
.compactMap { [weak self] _ in self }
// Second part is 'defined' in another method (it is shared with other tasks)
.flatMap { dataSource in dataSource.partOfComplexOperation(label: "Second part") }
.sink { label in
print("Received value in sink", label)
}
.store(in: &cancellables)
}
func partOfComplexOperation(label: String) -> Future<String, Never> {
someService.someOperation(label: label)
}
}
var someDataSource: SomeDataSource? = SomeDataSource()
someDataSource?.complexOperation()
someDataSource = nil
Output:
Starting work for First part
Deinit SomeDataSource
Finished work for First part
The solution here is to not retain self. You don't even want self in the flatMap so why retain it...
let label = someService.someOperation(label: "First part")
.flatMap { [someService] _ in
someService.someOperation(label: label)
}
Of course seeing all this work on someService implies that the service is missing some functionality. Seeing that the result of someOperation is being ignored might also be a red flag.
If you were truly in a situation where you had to use self, then the solution would look like this:
let foo = someOperation()
.flatMap { [weak self] in
self?.otherOperation() ?? Empty(completeImmediately: true)
}
Or you might consider something like:
someOperation()
.compactMap { [weak someService] _ in
someService?.otherOperation()
}
.switchToLatest()
Which will cancel otherOperation() if a new event comes through from someOperation().
Is there a way to get the value of a PassthroughSubject publisher in a Unit test?
I want to test that a function returns success and to test this one I want to see when the publisher value is .loaded, then is success.
class HomeViewModel: ObservableObject {
var homeState = PassthroughSubject<StatePublisher, Never>()
func load(item: HomeModel) {
self.homeState.send(.loading)
self.dataSource.load(item: item) { result in
switch result {
case .success:
self.homeState.send(.loaded)
case let .failure(error):
self.homeState.send(.error(message: error.localizedDescription))
}
}
}
}
class HomeViewModelTests: XCTestCase {
var sut: ViewModel!
var subscriptions = Set<AnyCancellable>()
override func setUpWithError() throws {
sut = ViewModel()
}
override func tearDownWithError() throws {
sut = nil
subscriptions = []
}
func testUpdateHomeSuccess() {
let expected = StatePublisher.loaded
var result = StatePublisher.loading
sut.load(item: HomeModel.fixture())
sut.homeState
.sink(receiveValue: { state in
result = state
})
.store(in: &subscriptions)
XCTAssert(
result == expected,
"Home expected to be \(expected) but was \(result)"
)
}
}
I tried a test like this, but sink is never called.
Presumably, dataSource.load operates asynchronously. That means you need to use an XCTestExpectation in your test case. Also, because you're using a PassthroughSubject rather than a CurrentValueSubject, you should create your subscription before you start the load, to be sure you can't miss any published values.
let ex = XCTestExpectation()
sut.homeState
.sink {
if case .loaded = $0 {
ex.fulfill()
}
}
.store(in: &subscriptions)
wait(for: [ex], timeout: 10)
// Test fails if it doesn't call `ex.fulfill()` within 10 seconds.
Read Apple's guide to Testing Asynchronous Operations with Expectations.
I changed my PassthroughSubject to CurrentValueSubject as then the state of the value is present also when executing the XCTestCase.
So this test was always failing: (the PassthroughSubject is declared in viewModel class which is tested)
var someValue: PassthroughSubject<Bool, Never> = .init()
func someTest() throws {
let exp = expectation(description: "Value changed")
viewModel.functionChangingTheValue()
viewModel.someValue.sink { result in
XCTAssertTrue(result)
exp.fulfill()
}
.store(in: &cancellables)
wait(for: [exp], timeout: 0.1)
}
But this one is now successful
var someValue: CurrentValueSubject<Bool, Never> = .init(false)
func someTest() throws {
let exp = expectation(description: "Value changed")
viewModel.functionChangingTheValue()
viewModel.someValue.sink { result in
XCTAssertTrue(result)
exp.fulfill()
}
.store(in: &cancellables)
wait(for: [exp], timeout: 0.1)
}
I have a function that returns a publisher. This publisher gives the results of a background process. I only want to trigger the background process when the publisher would be subscribed, so that no results are lost. The background process can update its results many times, so the variant with Future is not suitable.
private let passthroughSubject = PassthroughSubject<Data, Error>()
// This function will be used outside.
func fetchResults() -> AnyPublisher<Data, Error> {
return passthroughSubject
.eraseToAnyPublisher()
.somehowTriggerTheBackgroundProcess()
}
extension MyModule: MyDelegate {
func didUpdateResult(newResult: Data) {
self.passthroughSubject.send(newResult)
}
}
What have I tried?
Future:
Future<Data, Error> { [weak self] promise in
self?.passthroughSubject
.sink(receiveCompletion: { completion in
// My logic
}, receiveValue: { value in
// My logic
})
.store(in: &self.cancellableSet)
self?.triggerBackgroundProcess()
}.eraseToAnyPublisher()
Works the way I want but the subscriber is called only once (logical).
Deffered:
Deferred<AnyPublisher<Data, Error>>(createPublisher: { [weak self] in
defer {
self?.triggerBackgroundProcess()
}
return passthroughSubject.eraseToAnyPublisher()
}
Debugger shows that everything is correct: first return then trigger but the subscriber is not called for the first time.
receiveSubscription:
passthroughSubject
.handleEvents(receiveSubscription: { [weak self] subscription in
self?.triggerBackgroundProcess()
})
.eraseToAnyPublisher()
The same effect as with Deffered.
Is it even possible what I want to achieve?
Or, it is better to create a public publisher subscribe it and receive results from background process. And the fetchResults() function doesn't return anything?
Thanks in advance for your help.
You can write your own type that conforms to Publisher and wraps a PassthroughSubject. In your implementation, you can start the background process when you get a subscription.
public struct MyPublisher: Publisher {
public typealias Output = Data
public typealias Failure = Error
public func receive<Downstream: Subscriber>(subscriber: Downstream)
where Downstream.Input == Output, Downstream.Failure == Failure
{
let subject = PassthroughSubject<Output, Failure>()
subject.subscribe(subscriber)
startBackgroundProcess(subject: subject)
}
private func startBackgroundProcess(subject: PassthroughSubject<Output, Failure>) {
DispatchQueue.global(qos: .utility).async {
print("background process running")
subject.send(Data())
subject.send(completion: .finished)
}
}
}
Note that this publisher starts a new background process for each subscriber. That is a common implementation. For example URLSession.DataTaskPublisher issues a new request for each subscriber. If you want multiple subscribers to share the output of a single request, you can use the .multicast operator, add multiple subscribers, and then .connect() the multicast publisher to start the background process once:
let pub = MyPublisher().multicast { PassthroughSubject() }
pub.sink(...).store(in: &tickets) // first subscriber
pub.sink(...).store(in: &tickets) // second subscriber
pub.connect().store(in: &tickets) // start the background process
It seems to me that your last bit of code is a perfectly viable solution: don't trigger the background process until you detect the subscription. Example:
let subject = PassthroughSubject<String, Never>()
var storage = Set<AnyCancellable>()
func start() {
self.subject
.handleEvents(receiveSubscription: {_ in
print("subscribed")
DispatchQueue.main.async {
self.doSomethingAsynchronous()
}
})
.sink { print("got", $0) }
.store(in: &storage)
}
func doSomethingAsynchronous() {
DispatchQueue.global(qos: .userInitiated).async {
DispatchQueue.main.async {
self.subject.send("bingo")
}
}
}
Is there a good way to handle an array of AnyCancellable to remove a stored AnyCancellable when it's finished/cancelled?
Say I have this
import Combine
import Foundation
class Foo {
private var cancellables = [AnyCancellable]()
func startSomeTask() -> Future<Void, Never> {
Future<Void, Never> { promise in
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(2)) {
promise(.success(()))
}
}
}
func taskCaller() {
startSomeTask()
.sink { print("Do your stuff") }
.store(in: &cancellables)
}
}
Every time taskCaller is called, a AnyCancellable is created and stored in the array.
I'd like to remove that instance from the array when it finishes in order to avoid memory waste.
I know I can do something like this, instead of the array
var taskCancellable: AnyCancellable?
And store the cancellable by doing:
taskCancellable = startSomeTask().sink { print("Do your stuff") }
But this will end to create several single cancellable and can pollute the code. I don't want a class like
class Bar {
private var task1: AnyCancellable?
private var task2: AnyCancellable?
private var task3: AnyCancellable?
private var task4: AnyCancellable?
private var task5: AnyCancellable?
private var task6: AnyCancellable?
}
I asked myself the same question, while working on an app that generates a large amount of cancellables that end up stored in the same array. And for long-lived apps the array size can become huge.
Even if the memory footprint is small, those are still objects, which consume heap, which can lead to heap fragmentation in time.
The solution I found is to remove the cancellable when the publisher finishes:
func consumePublisher() {
var cancellable: AnyCancellable!
cancellable = makePublisher()
.sink(receiveCompletion: { [weak self] _ in self?.cancellables.remove(cancellable) },
receiveValue: { doSomeWork() })
cancellable.store(in: &cancellables)
}
Indeed, the code is not that pretty, but at least there is no memory waste :)
Some high order functions can be used to make this pattern reusable in other places of the same class:
func cleanupCompletion<T>(_ cancellable: AnyCancellable) -> (Subscribers.Completion<T>) -> Void {
return { [weak self] _ in self?.cancellables.remove(cancellable) }
}
func consumePublisher() {
var cancellable: AnyCancellable!
cancellable = makePublisher()
.sink(receiveCompletion: cleanupCompletion(cancellable),
receiveValue: { doSomeWork() })
cancellable.store(in: &cancellables)
}
Or, if you need support to also do work on completion:
func cleanupCompletion<T>(_ cancellable: AnyCancellable) -> (Subscribers.Completion<T>) -> Void {
return { [weak self] _ in self?.cancellables.remove(cancellable) }
}
func cleanupCompletion<T>(_ cancellable: AnyCancellable, completionWorker: #escaping (Subscribers.Completion<T>) -> Void) -> (Subscribers.Completion<T>) -> Void {
return { [weak self] in
self?.cancellables.remove(cancellable)
completionWorker($0)
}
}
func consumePublisher() {
var cancellable: AnyCancellable!
cancellable = makePublisher()
.sink(receiveCompletion: cleanupCompletion(cancellable) { doCompletionWork() },
receiveValue: { doSomeWork() })
cancellable.store(in: &cancellables)
}
It's a nice idea, but there is really nothing to remove. When the completion (finish or cancel) comes down the pipeline, everything up the pipeline is unsubscribed in good order, all classes (the Subscription objects) are deallocated, and so on. So the only thing that is still meaningfully "alive" after your Future has emitted a value or failure is the Sink at the end of the pipeline, and it is tiny.
To see this, run this code
for _ in 1...100 {
self.taskCaller()
}
and use Instruments to track your allocations. Sure enough, afterwards there are 100 AnyCancellable objects, for a grand total of 3KB. There are no Futures; none of the other objects malloced in startSomeTask still exist, and they are so tiny (48 bytes) that it wouldn't matter if they did.