This question already has answers here:
How To UnitTest Combine Cancellables?
(2 answers)
Closed 1 year ago.
I want to test that some of my PassthroughSubjects are subscribed to on init of my class
my class init is simple:
private let viewType = PassthroughSubject<ViewType, Never>()
init() {
setupObservers()
}
func setupObservers() {
viewType
.sink(
receiveValue: { [weak self] viewType in
guard let self = self else {
return
}
self.currentViewType = viewType
self.titleText.send(viewType.title)
self.hideXButton.send(viewType.shouldHideXButton)
self.reloadTableView.send()
})
.store(in: &publishers)
}
How can I test that viewType has been subscribed to?
Add a "print" operator into the middle of your sequence:
func setupObservers() {
viewType
.print() /* <<< -- Print debugging information*/
.sink(
receiveValue: { [weak self] viewType in
/* Your other code here */
It will print debugging information about the values going through the sequence including when a subscriber subscribes to it.
Obviously you should only use this at development time.
In terms of an XCTest you ask how to test if your publisher was subscribed to. The .sink operator is inline in your code and would do the subscription so it's not clear what you are trying to verify with an XCTest. You could capture the cancellation return in a variable, store it in your publishers set and validate that the cancellation is in the set as a means of ensuring your code was called. But since your subscription is intrinsic to the code it should always be completed so long as the code runs.
Related
Is there a way to have the publisher emit a value only to the latest subscriber/observer?
An example for that would be; a manager class that can be subscribed to by multiple observers. When an event occurs, I would like only the latest subscriber to be observed. As far as I know, there is no way for the publisher to keep track of its subscribers but my knowledge regarding Combine and reactive programming is limited so I am unsure if this is possible in the first place.
You are right. Unfortunately, there is no way to list/track subscribers of a publisher. To solve your problem, you have to implement a custom publisher. There are two possibilities here. Either you implement a custom publisher with the Publisher protocol, but Apple advises against this (see here), or you create a custom publisher with already existing types, as Apple recommends. I have prepared an example for the second option.
The logic is very simple. We create a publisher with a PassthroughSubject inside (it can also be a CurrentValueSubject). Then we implement the methods typical of a PassthroughSubject and use them to overwrite the same methods of the PassthroughSubject, which is inside our class. In the sink method we store all returning subscriptions BUT before we add a new subscription to the Set, we go through all the already cached subscriptions and cancel them. This way we achieve the goal that only the last subscription works.
// The subscriptions will be cached in the publisher.
// To avoid strong references, I use the WeakBox recommendation from the Swift forum.
struct WeakBox<T: AnyObject & Hashable>: Hashable {
weak var item: T?
func hash(into hasher: inout Hasher) {
hasher.combine(item)
}
}
class MyPublisher<T, E: Error> {
private let subject = PassthroughSubject<T, E>()
private var subscriptions = Set<WeakBox<AnyCancellable>>()
deinit {
subscriptions.removeAll()
}
public func send(_ input: T) {
subject.send(input)
}
public func send(completion: Subscribers.Completion<E>) {
subject.send(completion: completion)
}
public func sink(receiveCompletion receivedCompletion: #escaping (Subscribers.Completion<E>) -> Void, receiveValue receivedValue: #escaping (T) -> Void) -> AnyCancellable {
let subscription = subject
.sink(receiveCompletion: { completion in
receivedCompletion(completion)
}, receiveValue: { value in
receivedValue(value)
})
// Cancel previous subscriptions.
subscriptions.forEach { $0.item?.cancel() }
// Add new subscription.
subscriptions.insert(WeakBox(item: subscription))
return subscription
}
}
I tested the class in Playground as follows.
let publisher = MyPublisher<Int, Never>()
let firstSubscription = publisher
.sink(receiveCompletion: { completion in
print("1st subscription completion \(completion)")
}, receiveValue: { value in
print("1st subscription value \(value)")
})
let secondSubscription = publisher
.sink(receiveCompletion: { completion in
print("2st subscription completion \(completion)")
}, receiveValue: { value in
print("2st subscription value \(value)")
})
let thirdSubscription = publisher
.sink(receiveCompletion: { completion in
print("3st subscription completion \(completion)")
}, receiveValue: { value in
print("3st subscription value \(value)")
})
publisher.send(123)
Console output:
3st subscription value 123
If you comment out the line subscriptions.forEach { $0.cancel() }, then you get:
3st subscription value 123
1st subscription value 123
2st subscription value 123
Hopefully I could help you.
I have this function
import Foundation
import Combine
import Amplify
func fetchCurrentAuthSession() -> AnyCancellable {
Amplify.Auth.fetchAuthSession().resultPublisher
.sink {
if case let .failure(authError) = $0 {
print("Fetch session failed with error \(authError)")
}
}
receiveValue: { session in
print("Is user signed in - \(session.isSignedIn)")
}
}
and I am calling it like this
Button(action: {
print("button pressed")
fetchCurrentAuthSession()
}) {
Text("Authenticated Test")
}
I get the Xcode warning Result of call to 'fetchCurrentAuthSession()' is unused
its not clear to me what the "result of the call" is, or how I should be "using it"
Be aware that sink is just another operator and that neither the receivedValue nor the completion is the result of the pipeline.
You have to store the actual result, the AnyCancellable object, into a property.
It's similar to other asynchronous patterns. You need a strong reference to the object to keep it alive until the job is done.
var cancellable : AnyCancellable?
func fetchCurrentAuthSession() {
cancellable = Amplify.Auth.fetchAuthSession().resultPublisher
.sink....
As the name implies a huge benefit is that you are able to cancel the pipeline.
I was writing a unit test for some code I was working on in an iOS App and ran into very strange behavior.
The PassthroughSubject sink closure in my code wasn't running and thus the test was failing. When i uncomment the print line in the below code it passes. It also passes with no problems in a playground. I tried the whole quite Xcode delete derived data trick and that didn't work. I tried to move the class and unit test to a completely different project and it was still failing. To me this looks like an Apple bug but wanted to ask here to see if anyone has any insights. see the below code. Note i've simplified the code from my original project a bit to make is easier to read. This makes me feel like RxSwift is more polished than Combine at this point. Never saw anything like this happen with RxSwift.
class BadPassThroughSendClass {
let publisher = PassthroughSubject<Any, Never>()
var anyCancellable: AnyCancellable?
var sinkClosureBlock: (()->())?
init() {
createSubscription()
}
func createSubscription() {
let subscription = publisher
.subscribe(on: DispatchQueue.global())
.receive(on: DispatchQueue.global())
.sink { [unowned self] event in
sinkClosureBlock?()
}
anyCancellable = AnyCancellable(subscription)
}
}
class UnitTests: XCTestCase {
func testBadPassThroughClass() {
let badClass = BadPassThroughSendClass()
let sinkExpectation = expectation(description: "Sink closure should fire")
badClass.sinkClosureBlock = {
sinkExpectation.fulfill()
}
// print("pass the test with this line")
badClass.publisher.send("my test message")
waitForExpectations(timeout: 0.5)
}
}
I've been working with RxSwift for a few years now, and am starting to explore Combine with SwiftUI and am having some trouble trying to replicate some functionality from RxSwift in Combine.
On the RxSwift GitHub there is an example in a file called ActivityIndicator.swift.
Basic usage is as follows:
class Foo {
let activityIndicator = ActivityIndicator()
lazy var activity = activityIndicator.asDriver()
var disposeBag = DisposeBag()
func doSomething() {
Observable
.just("this is something")
.trackActivity(activityIndicator)
.subscribe()
.disposed(by: disposeBag)
}
}
What this does is allow you to then drive off of the activity driver and it will emit boolean values every time something subscribes or a subscription completes.
You can then directly drive something like a UIActivityIndicatorView's isAnimating property using RxCocoa.
I've been trying to figure out how to create something similar to this in Combine but am not having any luck.
Say I have a viewModel that looks like this:
class ViewModel: ObservableObject {
#Published var isActive = false
func doSomething() -> AnyPublisher<Void, Never> {
Just(())
.delay(for: 2.0, scheduler: RunLoop.main)
.eraseToAnyPublisher()
}
}
What I would like to do is create an operator for a Publisher that will function similarly to how the Rx operator worked where I can forward the events from the subscription through the chain, but change the isActive value every time something subscribes/completes/cancels.
In the SwiftUI View I would initiate the doSomething function and sink to it, while also being able to use the published isActive property to show/hide a ProgressView
Something similar to this:
struct SomeView: View {
let viewModel = ViewModel()
var body: some View {
var cancelBag = Set<AnyCancellable>()
VStack {
Text("This is text")
if viewModel.isActive {
ProgressView()
}
}
.onAppear(perform: {
viewModel
.doSomething()
.sink()
.store(in: &cancelBag)
})
}
}
Is there something that works like this already that I am just completely missing?
If not, how can I go about replicating the RxSwift functionality in Combine?
Thank you in advance for the help.
Looks like someone created a Combine version. I don't know if it has the same issue as discussed by #Daniel T. but it looks promising.
https://github.com/duyquang91/ActivityIndicator
Hmm... The key to the ActivityIndicator class is the Observable.using(_:observableFactory:) operator. Unfortunately, I don't believe there is an equivalent operator in Combine.
The using operator creates a resource when the Observable is subscribed to, and then disposes the resource when the Observable sends a stop event (complete or error.) This insures the resource's lifetime. In this particular case, the resource just increments an Int value on creation and decrements it on disposal.
I think you could kind of mimic the behavior with something like this:
extension Publisher {
func trackActivity(_ activityIndicator: CombineActivityIndicator) -> some Publisher {
return activityIndicator.trackActivity(of: self)
}
}
final class CombineActivityIndicator {
var counter = CurrentValueSubject<Int, Never>(0)
var cancelables = Set<AnyCancellable>()
func trackActivity<Source: Publisher>(of source: Source) -> some Publisher {
let sharedSource = source.share()
counter.value += 1
sharedSource
.sink(
receiveCompletion: { [unowned self] _ in
self.counter.value -= 1
},
receiveValue: { _ in }
)
.store(in: &cancelables)
return sharedSource
}
var asPublisher: AnyPublisher<Bool, Never> {
counter
.map { $0 > 0 }
.eraseToAnyPublisher()
}
}
However, the above class will heat up the Publisher and you might miss emitted values because of it. Use at your own risk, I do not recommend the above unless you are desperate.
Maybe someone has written a using operator for Publisher and will be willing to share.
I know in general a publisher is more powerful than a closure, however I want to ask and discuss a specific example:
func getNotificationSettingsPublisher() -> AnyPublisher<UNNotificationSettings, Never> {
let notificationSettingsFuture = Future<UNNotificationSettings, Never> { (promise) in
UNUserNotificationCenter.current().getNotificationSettings { (settings) in
promise(.success(settings))
}
}
return notificationSettingsFuture.eraseToAnyPublisher()
}
I think this is a valid example of a Future publisher and it could be used here instead of using a completion handler. Let's do something with it:
func test() {
getNotificationSettingsPublisher().sink { (notificationSettings) in
// Do something here
}
}
This works, however it will tell me that the result of sink (AnyCancellable) is unused. So whenever I try to get a value, I need to either store the cancellable or assign it until I get a value.
Is there something like sinkOnce or an auto destroy of cancellables? Sometimes I don't need tasks to the cancelled. I could however do this:
func test() {
self.cancellable = getNotificationSettingsPublisher().sink { [weak self] (notificationSettings) in
self?.cancellable?.cancel()
self?.cancellable = nil
}
}
So once I receive a value, I cancel the subscription. (I could do the same in the completion closure of sink I guess).
What's the correct way of doing so? Because if I use a closure, it will be called as many times as the function is called, and if it is called only once, then I don't need to cancel anything.
Would you say normal completion handlers could be replaced by Combine and if so, how would you handle receiving one value and then cancelling?
Last but not least, the completion is called, do I still need to cancel the subscription? I at least need to update the cancellable and set it to nil right? I assume storing subscriptions in a set is for long running subscriptions, but what about single value subscriptions?
Thanks
Instead of using the .sink operator, you can use the Sink subscriber directly. That way you don't receive an AnyCancellable that you need to save. When the publisher completes the subscription, Combine cleans everything up.
func test() {
getNotificationSettingsPublisher()
.subscribe(Subscribers.Sink(
receiveCompletion: { _ in },
receiveValue: ({
print("value: \($0)")
})
))
}