RxSwift - How to call method which returns Driver at regular intervals? - swift

I am a RxSwift beginner and making a app with RxSwift + MVVM.
I have a method which calls API and converts to RxCocoa.Driver in ViewModel class like below.
func fetch() -> Driver<HomeViewEntity> {
apiUseCase.fetch(query: HomeViewQuery())
.map { data in
HomeViewEntity(userName: data.name,
emailAddress: data.email
}
.asDriver(onErrorRecover: { [weak self] error in
if let printableError = error as? PrintableError {
self?.errorMessageRelay.accept(AlertPayload(title: printableError.title, message: printableError.message))
}
return Driver.never()
})
}
Now, I'd like to call this fetchListPlace() method at regular intervals a.k.a polling (e.g. each 5 minutes) at ViewController.
How to do that????
I think interval is suit in this case, but I can't get an implementation image....

Here you go:
func example(_ fetcher: Fetcher) -> Driver<HomeViewEntity> {
Driver<Int>.interval(.seconds(5 * 60))
.flatMap { _ in fetcher.fetch() }
}
Also note,
Returning a Driver.never() from your recovery closure is probably a bad idea. Prefer Driver.empty() instead.
I'm not a fan of putting a side effect in the recovery closure in the first place. I think it would be better to have the fetch() return a Driver<Result<HomeViewEntity, Error>> instead and move the side effect to the end of the chain (in a subscribe or a flatMap.)

Related

Swift how to test async call wrapped in a function

Is there a way to wait for an async call to be finished when this call is wrapped in another method?
class Owner{
let dataManager = MockDataManager()
var data: String? = nil
func refresh() {
Task {
self.data = await dataManager.fetchData()
}
}
}
class MockDataManager {
var testData: String = "test"
func fetchData() async -> String {
testData
}
}
class OwnerTests: SKTestCase {
private var owner = Owner()
func testRefresh() {
owner.refresh()
XCTAssertEqual(owner.data, "test") // fail. the value is still nil
}
}
With callbacks, the tests used to work if everything under the hood was replaced with synchronous calls but here it looks like i am missing an operation to wait for a change to owner.data
Late to this party, but I agree with #Cristik regarding not changing the signature of a function just to accommodate testing. In the chat room conversation, #Cristik also pointed out a valid reason why a function can be set up to invoke an async function but yet not define its signature as async:
the Owner class may be in the nature of a view model (in an MVVM pattern) that exposes read-only observable/bindable (say #Published let) properties, that are bindable from (a) view(s), and the refresh function allows the view to request data update following a user event;
the refresh function isn't expected to return any data to the view when invoked, rather the view model (Owner) object will update the observable properties with the data returned while the views bound to (i.e. observing) the properties will be automatically updated.
In this case there's absolutely no need to mark the Owner.refresh() function as async and, thus forcing the view(s) to wrap their invocation of the refresh function in an async or Task (or .task modifier in SwiftUI) construct.
That said, I had similar situation and here's how I implemented the unit (not integration) test:
func testRefreshFunctionFetchesDataAndPopulatesFields() {
let expectation = XCTestExpectation(
description: "Owner fetches data and updates properties."
)
// `Owner` is the "subject under test", so use protocol-driven development
// and dependency injection to enable focusing on testing just the SUT
// unencumbered by peculiarities of the dependency
let owner = Owner(mockDataManager: DataManagerProtocol())
// Verify initial state
XCTAssertNil(owner.data)
owner.refresh()
let asyncWaitDuration = 0.5 // <= could be even less than 0.5 seconds even
DispatchQueue.main.asyncAfter(deadline: .now() + asyncWaitDuration) {
expectation.fulfill()
// Verify state after
XCTAssertEqual(owner.data, "someString")
}
wait(for: [expectation], timeout: asyncWaitDuration)
}
Hope this helps.
The fact that refresh detaches some async code to do its job, is an implementation detail, and your tests should not care about the implementation details.
Instead, focus on the behaviour of the unit. For example, in the scenario you posted, the expected behaviour is that sometime after refresh is called, owner.data should become "test". This is what you should assert against.
Your current test code follows the above good practice, only that, as you observed, it fails because it doesn't wait until the property ends up being set. So, try to fix this, but without caring how the async part is implemented. This will make your tests more robust, and your code easier to refactor.
One possible approach for validating the async update is to use a custom XCTestExpectation:
final class PropertyExpectation<T: AnyObject, V: Equatable>: XCTNSPredicateExpectation {
init(object: T, keyPath: KeyPath<T, V>, expectedValue: V) {
let predicate = NSPredicate(block: { _, _ in
return object[keyPath: keyPath] == expectedValue
})
super.init(predicate: predicate, object: nil)
}
}
func testRefresh() {
let exp = PropertyExpectation(object: owner, keyPath: \.data, expectedValue: "test")
owner.refresh()
wait(for: [exp], timeout: 5)
}
Alternatively, you can use a 3rd party library that comes with support for async assertions, like Nimble:
func testRefresh() {
owner.refresh()
expect(self.owner.data).toEventually(equal("test"))
}
As a side note, since your code is multithreaded, strongly recommending to add some synchronization in place, in order to avoid data races. The idiomatic way in regards to the structured concurrency is to convert your class into an actor, however, depending on how you're consuming the class from other parts of the code, it might not be a trivial task. Regardless, you should fix the data races conditions sooner rather than later.
I would like to contribute a solution for a more restricted situation where XCTestExpectation doesn't work, and that's when a view model is bound to the #MainActor, and you can't make every function call async (relying on property didSet). Waiting for expectations will also block the task in question, even a detached task won't help, the task will always execute after the test function. Storing and later awaiting the task solves the problem:
#MainActor
class ViewModel {
var task : Task<Void, Never>?
#Published var value1: Int = 0 {
didSet {
task = Task {
await update2()
}
}
}
#Published var value2: Int = 0
func update2() async {
value2 = value1 + 1
}
}
And then in the test:
func testExample() {
viewModel.value1 = 1
let _ = await viewModel.task?.result
XCTAssertEqual(viewModel.value2, 2)
}
func refresh() async {
self.data = await dataManager.fetchData()
}
then in the test await owner.refresh()
If you really need to wait synchronously for the async task, you can see this question Swift await/async - how to wait synchronously for an async task to complete?

Swift Combine publishers vs completion handler and when to cancel

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)")
})
))
}

Mapping Swift Combine Future to another Future

I have a method that returns a Future:
func getItem(id: String) -> Future<MediaItem, Error> {
return Future { promise in
// alamofire async operation
}
}
I want to use it in another method and covert MediaItem to NSImage, which is a synchronous operation. I was hoping to simply do a map or flatMap on the original Future but it creates a long Publisher that I cannot erased to Future<NSImage, Error>.
func getImage(id: String) -> Future<NSImage, Error> {
return getItem(id).map { mediaItem in
// some sync operation to convert mediaItem to NSImage
return convertToNSImage(mediaItem) // this returns NSImage
}
}
I get the following error:
Cannot convert return expression of type 'Publishers.Map<Future<MediaItem, Error>, NSImage>' to return type 'Future<NSImage, Error>'
I tried using flatMap but with a similar error. I can eraseToAnyPublisher but I think that hides the fact that getImage(id: String returns a Future.
I suppose I can wrap the body of getImage in a future but that doesn't seem as clean as chaining and mapping. Any suggestions would be welcome.
You can't use dribs and drabs and bits and pieces from the Combine framework like that. You have to make a pipeline — a publisher, some operators, and a subscriber (which you store so that the pipeline will have a chance to run).
Publisher
|
V
Operator
|
V
Operator
|
V
Subscriber (and store it)
So, here, getItem is a function that produces your Publisher, a Future. So you can say
getItem (...)
.map {...}
( maybe other operators )
.sink {...} (or .assign(...))
.store (...)
Now the future (and the whole pipeline) will run asynchronously and the result will pop out the end of the pipeline and you can do something with it.
Now, of course you can put the Future and the Map together and then stop, vending them so someone else can attach other operators and a subscriber to them. You have now assembled the start of a pipeline and no more. But then its type is not going to be Future; it will be an AnyPublisher<NSImage,Error>. And there's nothing wrong with that!
You can always wrap one future in another. Rather than mapping it as a Publisher, subscribe to its result in the future you want to return.
func mapping(futureToWrap: Future<MediaItem, Error>) -> Future<NSImage, Error> {
var cancellable: AnyCancellable?
return Future<String, Error> { promise in
// cancellable is captured to assure the completion of the wrapped future
cancellable = futureToWrap
.sink { completion in
if case .failure(let error) = completion {
promise(.failure(error))
}
} receiveValue: { value in
promise(.success(convertToNSImage(mediaItem)))
}
}
}
This could always be generalized to
extension Publisher {
func asFuture() -> Future<Output, Failure> {
var cancellable: AnyCancellable?
return Future<Output, Failure> { promise in
// cancellable is captured to assure the completion of the wrapped future
cancellable = self.sink { completion in
if case .failure(let error) = completion {
promise(.failure(error))
}
} receiveValue: { value in
promise(.success(value))
}
}
}
}
Note above that if the publisher in question is a class, it will get retained for the lifespan of the closure in the Future returned. Also, as a future, you will only ever get the first value published, after which the future will complete.
Finally, simply erasing to AnyPublisher is just fine. If you want to assure you only get the first value (similar to getting a future's only value), you could just do the following:
getItem(id)
.map(convertToNSImage)
.eraseToAnyPublisher()
.first()
The resulting type, Publishers.First<AnyPublisher<Output, Failure>> is expressive enough to convey that only a single result will ever be received, similar to a Future. You could even define a typealias to that end (though it's probably overkill at that point):
typealias AnyFirst<Output, Failure> = Publishers.First<AnyPublisher<Output, Failure>>

RxSwift state changes trigger "Warning: Recursive call or synchronization error!"

I've inherited some Swift 3 code which uses RxSwift to manage a store. The basic layout of the class is:
class UserActivityStore {
fileprivate lazy var internalDataCache: Variable<Set<NSUserActivity>?> = Variable(nil)
func addAction(_ actionToAdd: NSUserActivity) {
var content = internalDataCache.value ?? Set<NSUserActivity>()
content.insert(actionToAdd)
internalDataCache.value = content
}
func resolveAction(_ action: NSUserActivity) {
var content = internalDataCache.value
_ = content?.remove(action)
internalDataCache.value = content
}
func expectActions(_ filter: #escaping ((NSUserActivity) -> Bool)) -> Observable<NSUserActivity> {
let observable = Observable<NSUserActivity>.create { (observer) -> Disposable in
return self.internalDataCache.asObservable().subscribeNext { (newActions) in
guard let newActions = newActions else { return }
for eachAction in newActions where filter(eachAction) {
observer.onNext(eachAction)
self.resolveAction(eachAction)
}
}
}
return observable
}
}
When an action is added to this, it adds the item to the set correctly. However, the observer (in expectActions) catches that change and resolves it. Since this is all in a single thread, the error "Warning: Recursive call or synchronization error!" is triggered.
I think this is a perfectly legitimate error and that RxSwift is probably correct in its handling. However, I can't help thinking that this is a bad model. The code is essentially handling a queue of NSUserActivity objects itself.
Is this genuinely a modelling error / abuse of RxSwift or is my limited understanding of RxSwift misunderstanding this? As a hacky solution, I've tried replacing the resolveAction function with a single line internalDataCache.value?.remove(action) but that still triggers the observable and hence the bug.
Changing the observable to use a different queue (Serial or Concurrent dispatch) fixes the problem but I'm not convinced its the correct fix.

generically injecting a side effect in a SignalProducer

My app has a status area at the top that shows progress information (similar to Xcode and iTunes). I want to update it by injecting side effects into an event stream, using a closure that converts the stream's value into the ProgressUpdate value. I'm using an extension on SignalProducer so any signal producer in my app can update the app's status area (there is a lot more involved to allow for multiple signals at once, but that doesn't affect this problem).
I'm basing it on SignalProducer's on(starting:, started:, ...). It requires the latest swift 3.1 beta to allow the constraint on the error type, but this is straight from a playground.
import ReactiveSwift
struct Rc2Error: Error {
}
struct ProgressUpdate {
let message: String
let value: Double = -1
}
class MacAppStatus {
fileprivate func process(event: (Event<ProgressUpdate, Rc2Error>) -> Void)
{
//update UI based on the event
}
}
extension SignalProducer where Error == Rc2Error {
func updateProgress<Value>(status: MacAppStatus, converter: #escaping (Value) -> ProgressUpdate) -> SignalProducer<Value, Error>
{
return SignalProducer<Value, Error> { observer, compositeDisposable in
self.startWithSignal { signal, disposable in
compositeDisposable += disposable
compositeDisposable += signal
.on(event: { (orignal) in
switch original {
case .completed:
status.process(Event<ProgressUpdate, Rc2Error>.completed)
case .interrupted:
status.process(Event<ProgressUpdate, Rc2Error>.interrupted)
case .failed(let err):
status.process(Event<ProgressUpdate, Rc2Error>.failed(err))
case .value(let val):
status.process(Event<ProgressUpdate, Rc2Error>.value(converter(val)))
}
})
.observe(observer)
}
}
}
}
```
The last line of .observe(observer) produces an error:
error: cannot convert value of type 'Observer<Value, Rc2Error>' to expected argument type 'Observer<_, Rc2Error>'
Any ideas why this conversion fails? Suggestions on a different way to accomplish this?
It looks like it was just bad error reporting from the compiler. The actual problem was that process() should take an Event, not a closure that takes an event. It also needed an empty external parameter name.
Changing the signature to
fileprivate func process(_ event: Event<ProgressUpdate, Rc2Error>)
and fixing the original typo Mike Taverne pointed out fixed it.