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.
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.
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.
I'm trying to write an integration test for a Reactor in an app built with ReactorKit and Realm/RxRealm.
I'm having trouble using TestScheduler to simulate user actions and test the expected emitted states.
In a nutshell, my problem is this: I'm binding an action that will make my Reactor save an item to Realm, my Reactor also observes changes to this object in Realm, and I expect my Reactor to emit the new state of this item observed from Realm.
What I'm seeing is that my test does not get the emission of the newly saved object in time to assert its value, it's emitted after my test assertion runs.
There is a fair amount of code involved, but attempting to whittle it down into a self-contained example of what it all roughly looks like below:
struct MyObject {
var counter: Int = 0
}
class MyReactor: Reactor {
enum Action {
case load
case mutateState
}
enum Mutation {
case setObject(MyObject)
}
struct State {
var object: MyObject?
}
func mutate(action: Action) -> Observable<Mutation> {
switch action {
case .load:
return service.monitorObject().map(Mutation.setObject)
case .mutateState:
guard var myObject = currentState.object else { return .empty() }
myObject.counter += 1
return service.save(myObject).andThen(.empty())
}
}
func reduce(state: State, mutation: Mutation) -> Observable<State> {
var newState = state
switch mutation {
case let .setObject(object):
// Should be called twice in tests, once on load, once after mutateState action
newState.object = object
}
}
}
struct Service {
// There is always at least one default instance of `MyObject` in Realm.
func monitorObject() -> Observable<MyObject> {
return Observable
.collection(from: realm.objects(MyObject.self))
.map { $0.first! }
}
func save(_ object: MyObject) -> Completable {
return Completable.create { emitter in
try! realm.write {
realm.add(object, update: .modified)
}
emitter(.completed)
return Disposables.create()
}
}
}
class MyTest: QuickSpec {
var scheduler: TestScheduler!
var sut: MyReactor!
var disposeBag: DisposeBag!
var service: Service!
var config: Realm.Configuration!
override func spec() {
beforeEach {
config = Realm.Configuration(inMemoryIdentifier: UUID().uuidString)
scheduler = TestScheduler(initialClock: 0)
disposeBag = DisposeBag()
sut = MyReactor()
service = Service(realmConfig: config)
}
describe("when my reactor gets a mutateState action") {
it("should mutate state") {
scheduler.createHotObservable([
.next(1, Action.load),
.next(2, Action.mutateState),
])
.bind(to: sut.action)
.disposed(by: disposeBag)
let response = scheduler.start(created: 0, subscribed: 0, disposed: 1000) {
sut.state.map(\.object)
}
// Counter always equals 0
XCTAssertTrue(response.events.last!.value.element!!.counter == 1)
}
}
}
}
What I'm expecting to happen is my Reactor's state is set for a 2nd time, before the XCTAssertTrue is hit. What is actually happening is the assert is hit with the initially loaded state, and then, my reactor's state is set again.
I thought my problem might be related to schedulers. Something I tried was injecting the test scheduler into my Service and doing observeOn(testScheduler) on my monitorObject function. But I'm still observing the assert get hit before the reactor's state is set for the 2nd time. I'm also not sure if a nuance of RxRealm/Realm change set notifications is the cause - not sure how to verify whether that might be the case.
Hopefully the problem and question is clear. Thanks in advance for any help.
I decided attempting to write an integration test was more trouble than it was worth and probably not going to result in very useful tests anyway.
So you are trying to test to see if Realm works. I don't use Realm, but based on your description, it probably updates the object on an internal thread and then you get the emission on a subsequent cycle.
You can test it by using an XCTestExpectation. Here is documentation from Apple: https://developer.apple.com/documentation/xctest/asynchronous_tests_and_expectations/testing_asynchronous_operations_with_expectations
Note however, that if something goes wrong in Realm and this test fails, there isn't anything you can do about it.
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)")
})
))
}