How to properly combine multiple Drivers with RxSwift? - swift

I'm combining a viewDidAppear and filter Drivers with RxSwift. And they work great. But when I introduce a third Driver, it stops calling flatMapLatest on the latest combine.
In my View Controller, I have these Drivers:
let filter: Driver<String>
let viewDidAppear: Driver<Void>
let refresh: Driver<Void>
And in my view model:
// On viewDidAppear, I download a list of portfolios
let viewDidAppearPortfolios = viewDidAppear
.flatMapLatest({ (_) -> Driver<Result<[PortfolioModel]>> in
return networkService.request(Router.portfolios)!
.responseCollections()
.trackActivity(fetching)
.asDriver(onErrorJustReturn: .failure(NSError()))
})
.flatMapLatest({ (result: Result<[PortfolioModel]>) -> Driver<[PortfolioModel]> in
switch result {
case .success(let value): return Driver.just(value)
case .failure(_): return Driver.just([])
}
})
// Then I combine with a filter from my search bar.
self.portfolios = Driver.combineLatest(viewDidAppearPortfolios, filter)
.flatMapLatest { (portfolios: [PortfolioModel], filter: String) -> Driver<[PortfolioModel]> in
if filter.isEmpty {
return Driver.just(portfolios)
}
return Driver.just(portfolios.filter({ (portfolio) -> Bool in
portfolio.portfolio.localizedCaseInsensitiveContains(filter)
}))
}
The above works!
The network requests a list of portfolios, and I'm able to filter those results as I type, client side.
However, I'd like for the user to pull to refresh, and trigger the network request again! And so, I combine with my refresh driver.
And this:
Driver.combineLatest(viewDidAppearPortfolios, filter)
Becomes this:
Driver.combineLatest(viewDidAppearPortfolios, filter, refresh)
Problem!
After combining with refresh the flatMapLatest is no longer called on viewDidAppear! Only if I manually pullToRefresh.
Driver.combineLatest(viewDidAppearPortfolios, filter, refresh).flatMapLatest { _,_,_ in
// No longer get's called on viewDidAppear after combining with refresh
}
The viewDidAppearPortfolios still executes, so the network request
is getting called!
Only if I manually pull to refresh do I get the
list of portfolios that I previously requested...
Any idea why?
Thank you!

It looks like your refresh didn't emit a single event yet and so the combineLatest is not computed.
I tried this code to test:
let one = Driver.just(1)
let two = Driver.just(2)
let three: Driver<Int> = .just(3)
let result = Driver.combineLatest(one, two, three)
.flatMapLatest {
return Driver.just($0 + $1 + $2)
}
result.drive(onNext: {
print($0)
})
This prints 6 but if you use let three: Driver<Int> = .empty() this is not printing anything. So I guess you need a way to set an initial value to refresh stream.

Related

Swift combine publishers where one hasn't sent a value yet

I have a publisher which would need re-evaluating on day change, but should continue to emit values at any other time.
As such, I thought I could use a NotificationCenter publisher for the UIApplication.significantTimeChangeNotification notification and combine it with my publisher such that the combine emission process would re-run on either on data change or day change and hence re-evaluate the map filter. See a rough outline of that code below.
The problem is that there is no published event by NotificationCenter at the point in time that this is setup and hence, none of the following map etc calls actually evaluate. merge(with:) won't work as the two publishers publish different types, but combineLatest(_:) and zip(_:) both won't emit events until both publishers have emitted a single event.
I can validate that my code operates as expected by adding NotificationCenter.default.post(name: UIApplication.significantTimeChangeNotification, object: nil) after this code, but that is undesirable due to it potentially signalling other areas of the app that an actual time change has occurred when it hasn't
private func todaysDate() -> String {
let formatter = DateFormatter()
formatter.dateFormat = "YYYY-MM-dd"
return formatter.string(from: Date())
}
#Published var entities: [MyEntity]
let dayChangePublisher = NotificationCenter.default
.publisher(for: UIApplication.significantTimeChangeNotification)
$entities.combineLatest(dayChangePublisher)
.map(\.0) // Only pass on the entity for further operations
.map { entities -> MyEntity? in
let today = todaysDate()
return entities?.first(where: { $0.id == today })
}
...remainder of combine code
Can this combination of publishers and evaluation of events occur with the current Swift combine framework? Like the behaviour I'd expect from merge(with:) but where the publishers emit two different types.
edit:
I found one solution where I map the notification publisher to a nil array
let dayChangePublisher = NotificationCenter.default
.publisher(for: UIApplication.significantTimeChangeNotification)
.map { _ ➝ [MyEntity]? in
return nil
}
And then use merge and compactMap to avoid passing any nil values on
let mergedPub = repo.$entities
.merge(with: dayChangePublisher)
.compactMap { entity -> MyEntity? in
let today = todaysDate()
return entities?.first { $0.id == today }
}
.share()
It works, but maybe a bit cumbersome if anyone has a better solution?
If I understood your question, you need a combineLatest that is not blocked by not having an initial value from one of the publishers.
You can achieve that with .prepend(value) operator. In this case, since you don't care about the actual value, map first to Void, then prepend a Void. It would work like so:
let dayChangePublisher = NotificationCenter.default
.publisher(for: UIApplication.significantTimeChangeNotification)
$entities.combineLatest(
dayChangePublisher
.map { _ in }
.prepend(()) // make sure to prepend a () value
)
.map(\.0) // Only pass on the entity for further operations
.map { entities -> MyEntity? in
let today = todaysDate()
return entities?.first(where: { $0.id == today })
}
//...

How to use Combine to assign the number of elements returned from a Core Data fetch request?

I want my app to periodically fetch new records and stores them in Core Data. I have a label on my UI that should display the number of elements for a particular record and I want that number to be updated as more records are added into the database. As an exercise, I want to use Combine to accomplish it.
I'm able to display the number of elements in the database when the app launches, but the number doesn't get updated when new data enters into the database (I verified that new data was being added by implementing a button that would manual refresh the UI).
Here's the code that displays the correct number of elements on launch but doesn't update when new records are added:
let replayRecordFetchRequest: NSFetchRequest<ReplayRecord> = ReplayRecord.fetchRequest()
_ = try? persistentContainer.viewContext.fetch(replayRecordFetchRequest).publisher.count().map { String(format: Constants.Strings.playsText, $0) }.assign(to: \.text, on: self.playsLabel)
Here's a code snippet from the WWDC 2019 Session 230 talk that I adapted but this doesn't work at all (the subscriber is never fired):
let replayRecordFetchRequest: NSFetchRequest<ReplayRecord> = ReplayRecord.fetchRequest()
if let replayRecords = try? replayRecordFetchRequest.execute() {
_ = replayRecords.publisher.count().map { String(format: Constants.Strings.playsText, $0) }.assign(to: \.text, on: self.playsLabel)
}
So, I didn't know this until now, but not all publishers are infinitely alive.
And the problem was that the NSFetchRequest.publisher is not a long-living publisher. It simply provides a way to iterate through the sequence of elements in the fetch request. As a result, the subscriber will cancel after the elements are iterated. In my case, I was counting the elements published until cancellation then assigning that value onto the UI.
Instead, I should be subscribing to changes to the managed object context and assigning that pipeline to my UI. Here's some example code:
extension NotificationCenter.Publisher {
func context<T>(fetchRequest: NSFetchRequest<T>) -> Publishers.CompactMap<NotificationCenter.Publisher, [T]> {
return compactMap { notification -> [T]? in
let context = notification.object as! NSManagedObjectContext
var results: [T]?
context.performAndWait {
results = try? context.fetch(fetchRequest)
}
return results
}
}
}
let playFetchRequest: NSFetchRequest<ReplayRecord> = ReplayRecord.fetchRequest()
let replayVideoFetchRequest: NSFetchRequest<ReplayVideo> = ReplayVideo.fetchRequest()
let playsPublisher = contextDidSavePublisher.context(fetchRequest: playFetchRequest).map(\.count)
let replayVideoPublisher = contextDidSavePublisher.context(fetchRequest: replayVideoFetchRequest).map(\.count)
playsSubscription = playsPublisher.zip(replayVideoPublisher).map {
String(format: Constants.Strings.playsText, $0, $1)
}.receive(on: RunLoop.main).assign(to: \.text, on: self.playsLabel)

RxSwift - Chaining Observables and Singles

I need to call a sequences of function to get all the information I need for a notification. First subscribe which opens up the session, then queryNotification to listen on all the incoming notifications, and once a notification is received, need to call getNotificationAttrs with the notificationId returned in queryNotification, then call getAppAttributes with appIdentifier returned in getNotificationAttrs and I need the combined result of queryNotification, getNotificationAttrs and getAppAttributes. How the functions look like are below:
func subscribeNotification() -> Single<Info>
func queryNotification() -> Observable<Notification>
func getNotificationAttrs(uid: UInt32, attributes: [Attribute]) -> Single<NotificationAttributes>
func getAppAttributes(appIdentifier: String, attributes: [AppAttribute]) -> Single<NotificationAppAttributes>
The tricky part is that queryNotification returns Observable and both getNotificationAttrs and getAppAttributes return Single. What I have in mind of chaining them together is like:
subscribeNotification()
.subscribe(onSuccess: { info in
queryNotification()
.flatMap({ notification in
return getNotificationAttributes(uid: notification.uid, attributes: [.appIdentifier, .content])
})
.flatMap({ notifAttrs
return getAppAttributes(appIdentifier: notifAttrs.appIdentifier, attributes: [.displayName])
})
.subscribe {
// have all the result from last two calls
}
})
Is this doable? Any direction is appreciated! Thanks!
The most obvious and IMHO correct solution is to promote your Single into an Observable. Also, I'm not a fan of the first subscribe where it is. You end up with an indentation pyramid.
I'm following your comments about needing the values from all of queryNotification(), getNotificationAttrs(did:attributes:) and getAppAttributes(appIdentifier:attributes:)...
let query = subscribeNotification()
.asObservable()
.flatMap { _ in queryNotification() }
.share(replay: 1)
let attributes = query
.flatMap { getNotificationAttrs(uid: $0.uid, attributes: [.appIdentifier, .content]) }
.share(replay: 1)
let appAttributes = attributes
.flatMap { getAppAttributes(appIdentifier: $0.appIdentifier, attributes: [.displayName]) }
Observable.zip(query, attributes, appAttributes)
.subscribe(onNext: { (query, attributes, appAttributes) in
})
The above will follow the steps you outlined and the subscribe will get called every time a new notification is emitted.
Also notice how the above reads quite a bit like synchronous code would (just with some extra wrapping.)

Unable to infer complex closure return type; add explicit type to disambiguate in RxSwift

I need to make multiple calls.
1. Delete Document Upload
2. Image 1 & server returns URL
3. Upload Image 2 & server returns URL
4. Create Document API contains both URLs & extra
parameters.
The code which I tried to write is in RxSwift,& MVVM.
let resultOfDocumentUpdateWithDelete =
donepressed
.filter{ $0 }
.withLatestFrom(self.existingDocumentIDChangedProperty)
.flatMapLatest {id in
let deleted_document = apiClient.deleteDocument(id).asObservable().materialize()
let upload_frontImage = deleted_document
.withLatestFrom(self.frontImageNameChangedProperty)
.flatMapLatest {image in
apiClient.uploadImage(image: image!).asObservable().materialize()
}
let upload_backImage = upload_frontImage
.withLatestFrom(self.backImageChangedProperty)
.flatMapLatest {image in
apiClient.uploadImage(image: image!).asObservable().materialize()
}
let upload_document = upload_backImage
.withLatestFrom(self.parametersChangedProperty)
.flatMapLatest {parameters in
apiClient.uploadDocument(parameters: parameters)
}
return upload_document.materialize()
}
.share(replay: 1)
Make sure, two responses of server are input in last API, so all of these will be called in a sequence.
how to do in RxSwift.
This was an interesting one! The take-away here is that when you are in doubt, go ahead and make your own operator. If it turns out that you later figure out how to do the job using the built-in operators, then you can replace yours. The only thing with making your own is that they require a lot more testing.
Note, to use the below, you will have to combineLatest of your observables and then flatMap and pass their values into this function.
// all possible results from this job.
enum ProcessResult {
case success
case deleteFailure(Error)
case imageFailue(Error)
case backImageFailure(Error)
case documentFailure(Error)
}
func uploadContent(apiClient: APIClient, documentID: Int, frontImage: UIImage, backImage: UIImage, parameters: Parameters) -> Single<ProcessResult> {
// instead of trying to deal with all the materializes, I decided to turn it into a single process.
return Single.create { observer in
// each api call happens in turn. Note that there are no roll-back semantics included! You are dealing with a very poorly written server.
let deleted = apiClient.deleteDocument(id: documentID)
.asObservable()
.share()
let imagesUploaded = deleted
.flatMap { _ in Observable.zip(apiClient.uploadImage(image: frontImage).asObservable(), apiClient.uploadImage(image: backImage).asObservable()) }
.share()
let documentUploaded = imagesUploaded
.flatMap { arg -> Single<Void> in
let (frontURL, backURL) = arg
var updatedParams = parameters
// add frontURL and backURL to parameters
return apiClient.uploadDocument(parameters: updatedParams)
}
.share()
let disposable = deleted
.subscribe(onError: { observer(.success(ProcessResult.deleteFailure($0))) })
let disposable1 = imagesUploaded
.subscribe(onError: { observer(.success(ProcessResult.imageFailue($0))) })
let disposable2 = documentUploaded
.subscribe(
onNext: { observer(.success(ProcessResult.success)) },
onError: { observer(.success(ProcessResult.documentFailure($0))) }
)
return Disposables.create([disposable, disposable1, disposable2])
}
}

How to combine two observables in Reactive Extensions in order to paginate results?

I am trying to develop a pagination system in an iOS app using RxSwift. The use case is simple: the user can enter text in a search field and the app performs a request that is paginated. When he changes the value, a new request is performed on the first page (that means the value of the observable must be reseted to 1). If the user clears the search field (or enters a text with less than 2 characters), the list of results is cleared and the current page is reset. The next page is fetched when the user scrolls to the bottom of the list.
This is not a swift or iOS specific case and I suppose it could be written in an identical way using RxKotlin or RxJs or any other reactive extension.
My current attempt is to set up an observable for the text and a observable for the current page and to combine them in order to perform the request with both parameters.
I already succeeded in doing exactly what I am looking for but using global attributes for storing the current query and the current page. I would like to find a way to use only the values emitted by the observables without having to maintain them (I guess the code will be more concise and easy to read and understand).
Here is my current code :
// self.nextPage is a Variable<Int>
let moreObs: Observable<Int> = self.nextPage.asObservable()
.distinctUntilChanged() // Emit only if page has changed.
// self.searchTextObservable is a PublishedSubject<String> that receives the values from the textfield
let searchObs: Observable<String> = self.searchTextObservable
.throttle(0.4, scheduler: MainScheduler.instance) // Wait 400ms when the user stops writing.
.distinctUntilChanged() // Emit only if query has changed.
self.resultsObservable = Observable
.combineLatest(searchObs, moreObs) { query, page in
return ["q": query, "p": "\(page)"]
}
.subscribeOn(MainScheduler.instance) // Emit on main thread.
.observeOn(ConcurrentDispatchQueueScheduler(globalConcurrentQueueQOS: .Background)) // Perform on background thread.
.map { params in
if params["q"]!.characters.count > 2 {
return params
}
return [:]
}
.flatMap { params in
return params.isEmpty ?
Observable.of([]) :
self.search(params)
}
.map { results in
if results.count > 0 {
self.results.appendContentsOf(results)
} else {
self.results = []
}
return self.results
}
So far, the only feature that does not work is the reset operation on the nextPage's value. If I force it to 1 when the searchObs emits:
let searchObs: Observable<String> = self.searchTextObservable
.throttle(0.4, scheduler: MainScheduler.instance) // Wait 400ms when the user stops writing.
.distinctUntilChanged() // Emit only if query has changed.
.map {query in
self.nextPage.value = 1
return query
}
Then I have 2 requests that are performed.
Am I misusing Rx?
I wouldn't use combineLatest. Your page number is dependent on your current search text, so you should chain it with flatMapLatest. That way, you aren't responsible to maintain its state yourself, rather, let operator chaining reset that for you.
let disposeBag = DisposeBag()
let searchText = PublishSubject<String>() // search field text
let newPageNeeded = PublishSubject<Void>() // fires when a new page is needed
struct RequestPage {
let query: String
let page: Int
}
let requestNeeded = searchText.asObservable()
.flatMapLatest { text in
newPageNeeded.asObservable()
.startWith(())
.scan(RequestPage(query: text, page: 0)) { request, _ in
return RequestPage(query: text, page: request.page + 1)
}
}
requestNeeded
.subscribeNext { print($0) }
.addDisposableTo(disposeBag)
searchText.onNext("A")
searchText.onNext("B")
newPageNeeded.onNext(())
searchText.onNext("C")
newPageNeeded.onNext(())
newPageNeeded.onNext(())
This will output:
(RequestPage #1)(query: "A", page: 1)
(RequestPage #1)(query: "B", page: 1)
(RequestPage #1)(query: "B", page: 2)
(RequestPage #1)(query: "C", page: 1)
(RequestPage #1)(query: "C", page: 2)
(RequestPage #1)(query: "C", page: 3)