I'm starting my adventure with RxSwift, having small experience with React in js already. I think that my problem is common, but I'm not sure how to describe it in concise abstract way, so instead I will describe it on the example.
I'm building iOS app showing some charts. The part of interest consist of ChartAreaController, ChartInfoController, both embedded in ChartController. First controller is the area showing some graph(based on rx chartData property), and the second one among others will have a slider for user to restrict show x-value (rx selectedXRange property) which is restricted to be between some min and max. The min/max value is defined by the current chart data.
Behavior when slider change updates chart is defined in ChartController:
override func viewDidLoad() {
super.viewDidLoad()
(...)
chartInfoController.selectedXRange.asObservable()
.subscribe(onNext: { [unowned self] selectedXRange in
(...)
let chartData = self.filterChartData(from: self.rawChartData, in: selectedXRange)
self.chartAreaController.chartData.accept(chartData)
}).disposed(by: disposeBag)
The filterChartData() method just filters out data that is not in the range, but for the sake of the argument we can assume it is very costly and I don't want it to run twice when it is not necessary.
When user changes the chart he or she wants to show, the new data arrives from server (again ChartController):
private func handleNewData(_ rawChartData: ChartData) {
self.rawChartData = rawChartData
guard let allowedXRange = rawChartData.xRange() else { return }
let selectedXRange = chartInfoController.selectedXRange.value
let newSelectedXRange = calculateSelectedXRange(currentSelectedDays: selectedDaysRange, availableDaysRange: daysRange)
let chartData = filterChartData(from: rawChartData, in: selectedXRange)
self.chartInfoController.allowedXRange = allowedXRange //this line is not crucial
self.chartInfoController.selectedXRange.accept(newSelectedXRange)
self.chartAreaController.chartData.accept(rawChartData)
}
So upon new chart data arrival it may be the case that the currently selected xRange must be trimmed because of the new min/max values of the data. So the side effect of the method will be changing the selectedXRange and in turn running the subscription I pasted earlier. So when new data arrives the chartData is updated twice and I don't want it to happen.
Of course I can comment out last line of the handleNewData() method, but I don't like it very much, since main reason for existence of the handleNewData() is to set chartData, and with the line commented out it's goal would be achieved because of the side effect of the method (which is updating the slider). Not acceptable.
To chartData I added throttle anyways, because fast moving slider will result in many updates and this solves my problem partially(chartData updated only once). But as you may remember the filterChartData() method is costly, and this part will still be running twice.
So the one question is, if my general layout of tackling the problem is OK, or should it be handled way different? At this point I came to conclusion that I'm looking for some way of temporary disabling particular subscription on selectedXRange (without damaging other subscriptions to that variable). Temporary meaning:
(...)
//disable subscription
self.chartInfoController.selectedXRange.accept(newSelectedXRange)
self.chartAreaController.chartData.accept(rawChartData)
//enable subscription
(...)
This seem legit to me, since ChartController as an owner of the subscription and changer of the values may want to disable the subscription whenever it suits him(it?).
Does RxSwift support something like this? If not, then I think I can achieve it myself e.g. via bool property in ChartController, or via adding the subscription to separate disposeBag, which I would dispose and then recreate the subscription. But if it's good thing to do? For example bool solution may be prone to be ill handled when there is some error, and dispose/recreate may be somehow costly, and it may be the case that disposal was not intended to be used like that.
Is there a better practice to handle such situations? As I said I think the problem is common so I hope there is a canonical solution to it :) Thanks for any answer, sorry for the lengthy post.
So the one question is, if my general layout of tackling the problem is OK, or should it be handled way different?
A properly written UI input element observable will only fire when the user makes a change to the UI, not when the program makes a change. For example:
textField.rx.text.orEmpty.subscribe(onNext: { print($0) }) will only print a value when the user types in the textField, not when you call textField.text = "foo" or from a binding .bind(to: textfield.rx.text).
If you wrote the ChartInfoController, I suggest you modify it to work the way the other UI elements do. If you didn't write it, submit an issue to the developer/maintainer.
Does RxSwift support something like [temporarily disabling particular subscription]?
It depends on what you mean by "temporarily disabling". It doesn't support silently unsubscribing and resubscribing but there are plenty of operators that will filter out some events they receive while passing others along. For example filter, throttle, debounce, ignoreElements... There's a lot of them that do that.
Is there a better practice to handle such situations?
Then best solution is mentioned above.
When We have multiple subscriptions to the same Observable, it will re-execute for each subscription.
To stop re-execute for each subscription. RxSwift has several operators for this: share(), replay(), replayAll(), shareReplay(), publish(), and even shareReplayLatestWhileConnected().
read more at (RxSwift: share vs replay vs shareReplay)
Related
I am aware that using Rx's flatmap or flatmapLatest is preferable to having nested subscriptions. However, I can't find a compelling reason why nested subscription calls "should be avoided at all costs" (RxSwift Github Tips), and want to understand why.
Any insight into the specific issue(s) besides "bad code smell"?
Example (source):
Using nested subscription (bad)
textField.rx.text.subscribe(onNext: { text in
performURLRequest(text).subscribe(onNext: { result in
...
})
.disposed(by: disposeBag)
})
.disposed(by: disposeBag)
Using flatmapLatest (good)
textField.rx.text
.flatMapLatest { text in
// Assuming this doesn't fail and returns result on main scheduler,
// otherwise `catchError` and `observeOn(MainScheduler.instance)` can be used to
// correct this.
return performURLRequest(text)
}
...
.disposed(by: disposeBag) // only one top most disposable
In this particular case, the bad example can yield out of date results - there is nothing that prevents an outdated response to performURLRequest being yielded when the textField updates and triggers a new query.
Of course it depends on your use case, but its often a mistake to display a result (such as a search result) based on an outdated value of textField. Worse, it could happen that an earlier URLRequest runs slowly and returns after a later one, leaving an incorrect result displayed indefinitely.
In contrast flatMapLatest ensures that the pending result stream from the previous value is unsubscribed as soon the textField is updated, which prevents outdated results from being processed.
This concurrency issue is one example of the better coordination and efficiency often achieved by using a single stream.
It also makes subscription management clearer and makes it less likely that you will fail to clean up properly.
I think the first thing to notice is that your two examples don't have the same behavior. The first one will make a network request every time the text field changes without canceling the previous request. In the latter case, when the text changes, the previous request (if any) is cancelled and a new request is started.
If the latter example used flatMap instead of flatMapLatest, they would have been more alike but notice that is the only way the url requests can be combined in the former case. In the latter case you can use flatMapLatest to ensure cancelation of the old request (or flatMapFirst to ignore events when a request is in-flight, or concatMap to store events until the previous request completed.) Using flatMap is much more flexible.
Personally, I do nest subscribes in a few select cases. For example when I'm going to ignore the inner subscribe's events (not even capturing its Disposable) or if setting up an asynchronous looping construct.
I’ve seen all around the documentation that Query-based sync is deprecated, so I’m wondering how should I got about my situation:
In my app (using Realm Cloud), I have a list of User objects with some information about each user, like their username. Upon user login (using Firebase), I need to check the whole User database to see if their username is unique. If I make this common realm using Full Sync, then all the users would synchronize and cache the whole database for each change right? How can I prevent that, if I only want the users to get a list of other users’ information at a certain point, without caching or re-synchronizing anything?
I know it's a possible duplicate of this question, but things have probably changed in four years.
The new MongoDB Realm gives you access to server level functions. This feature would allow you to query the list of existing users (for example) for a specific user name and return true if found or false if not (there are other options as well).
Check out the Functions documentation and there are some examples of how to call it from macOS/iOS in the Call a function section
I don't know the use case or what your objects look like but an example function to calculate a sum would like something like this. This sums the first two elements in the array and returns their result;
your_realm_app.functions.sum([1, 2]) { sum, error in
if let err = error {
print(err.localizedDescription)
return
}
if case let .double(x) = result {
print(x)
}
}
We're using Realm (Swift binding currently in version 3.12.0) from the earliest days in our project. In some early versions before 1.0 Realm provided change listeners for Results without actually giving changeSets.
We used this a lot in order to find out if a specific Results list changed.
Later the guys at Realm exchanged this API with changeSet providing methods. We had to switch and are now mistreating this API just in order to find out if anything in a specific List changed (inserts, deletions, modifications).
Together with RxSwift we wrote our own implementation of Results change listening which looks like this:
public var observable: Observable<Base> {
return Observable.create { observer in
let token = self.base.observe { changes in
if case .update = changes {
observer.onNext(self.base)
}
}
observer.onNext(self.base)
return Disposables.create(with: {
observer.onCompleted()
token.invalidate()
})
}
}
When we now want to have consecutive updates on a list we subscribe like so:
someRealm.objects(SomeObject.self).filter(<some filter>).rx.observable
.subscribe(<subscription code that gets called on every update>)
//dispose code missing
We wrote the extension on RealmCollection so that we can subscribe to List type as well.
The concept is equal to RxRealm's approach.
So now in our App we have a lot of filtered lists/results that we are subscribing to.
When data gets more and more we notice significant performance losses when it comes to seeing a change visually after writing something into the DB.
For example:
Let's say we have a Car Realm Object class with some properties and some 1-to-n and some 1-to-1 relationships. One of the properties is a Bool, namely isDriving.
Now we have a lot of cars stored in the DB and bunch of change listeners with different filters listing to changes of the cars collection (collection observers listening for changeSets in order to find out if the list was changed).
If I take one car of some list and set the property of isDriving from false to true (important: we do writes in the background) ideally the change listener fires fast and I have the nearly immediate correct response to my write on the main thread.
Added with edit on 2019-06-19:
Let's make the scenario still a little more real:
Let's change something down the hierarchy, let's say the tires manufacturer's name. Let's say a Car has a List<Tire>, a Tire has a Manufacturer and a Manufacturer has aname.
Now we're still listing toResultscollection changes with some more or less complex filters applied.
Then we're changing the name of aManufacturer` which is connected to one of the tires which are connected to one of the cars which is in that filtered list.
Can this still be fast?
Obviously when the length of results/lists where change listeners are attached to gets longer Realm's internal change listener takes longer to calculate the differences and fires later.
So after a write we see the changes - in worst case - much later.
In our case this is not acceptable. So we are thinking through different scenarios.
One scenario would be to not use .observe on lists/results anymore and switch to Realm.observe which fires every time anything did change in the realm, which is not ideal, but it is fast because the change calculation process is skipped.
My question is: What can I do to solve this whole dilemma and make our app fast again?
The crucial thing is the threading stuff. We're always writing in the background due to our design. So the writes itself should be very fast, but then that stuff needs to synchronize to the other threads where Realms are open.
In my understanding that happens after the change detection for all Results has run through, is that right?
So when I read on another thread, the data is only fresh after the thread sync, which happens after all notifications were sent out. But I am not sure currently if the sync happens before, that would be more awesome, did not test it by now.
I'm new to CloudKit and I was trying to figure out if a Record was created by the current user. I have researched this topic and have come about two methods to do this. I'm not sure which one is right or better and I don't even quite understand how the second method works.
The first way is using the following method to get the current user and then comparing it to the user who created the record:
func fetchUserRecordID(
completionHandler: (recordID: CKRecordID?, error: CKError?) -> Void
)
The second way involves an extension on CKRecord:
extension CKRecord{
var wasCreatedByThisUser: Bool{
return (creatorUserRecordID == nil) || (creatorUserRecordID?.recordName == "__defaultOwner__")
}
}
The first method is making another call to the server to fetch an additional record. The downside is that costs time, it counts against your monthly traffic quotas, and you have yet another async callback function that your code flow will have to account for. If you wind up calling this check a lot, you would generate a lot of unnecessary traffic to the server.
The second method is checking a value, creatorUserRecordID, that came with the record you already fetched. So at the time you check its value, it's all local data, no additional calls to the server and no async processing required.
Per the answer here: creatorUserRecordID.recordName contains "__defaultOwner__" instead of UUID shown in Dashboard, __defaultOwner__ is a synonym for the local user.
The second method looks to be the better choice for most scenarios I can think of.
I'm attempting to have my individual UITableViewCells: I'd love to have the initial values of the model brought over to their UI representation as well as after the user makes a change have the new UI value brought back to the model. Let's just focus on the latter first: UI changes propagating to the model.
For a little more background understanding, I'm running into an issue when adding a new item. This table is driven by an NSFetchedResultsController which sends one didChangeObject with a ChangeInsert and a second didChangeObject for ChangeUpdate. The ChangeInsert triggers an insertRowsAtIndexPaths on the table and the ChangeUpdate does a reloadRowsAtIndexPath.
Because of these two responses my table view asks for cellForRowAtIndexPath twice. This shouldn't be a problem if the same cell is disposed of properly between requests but it doesn't appear to be: I receive an assert that the property in the model is already bound to a RACSignal! I've tried any number of ways to be more explicit such as:
RAC(self.model,value) = [ [RACSignal merge:#[self.valueField.rac_textSignal] ] takeUntil:self.rac_prepareForReuseSignal]
however the reuse signal does not fire in time as it still asserts (aside, is there a recommended way to directly debug a signal like this firing?)
I've tried adding an additional takeUntil:[RACObserve(self, model) to dispose of the signal as soon as the reused cell's model is overwritten (and have it bind to the new model) and understandably this seems to result in the first value disposing. However adding skip:1 to the observe puts me right back where I am.
Please let me know if there is anywhere else I can add clarity or if you have another way to lay these things out. I'm very new to Reactive Cocoa and still learning best practices :)
Thanks!