Results Notification includes modification using old version indicies
Realm 1.0.1 w/ encryption enabled.
I have a tableview that I am updating on a notification to a RLMResults. The code looks something like:
__weak typeof(tableView) weakTable = tableView;
return [self addNotificationBlock:^(RLMResults *results, RLMCollectionChange *change, NSError *error) {
if (error) {
NSLog(@"Received failed realm notification: %@", error);
return;
}
UITableView *tableView = weakTable;
// Initial run of the query will pass nil for the change information
if (!change) {
[tableView reloadData];
return;
}
// Query results have changed, so apply them to the UITableView
[tableView beginUpdates];
[tableView deleteRowsAtIndexPaths:[change deletionsInSection:0]
withRowAnimation:UITableViewRowAnimationAutomatic];
[tableView insertRowsAtIndexPaths:[change insertionsInSection:0]
withRowAnimation:UITableViewRowAnimationAutomatic];
NSArray *changePaths = [change modificationsInSection:0];
if ([delegate respondsToSelector:@selector(configureCellForIndexPath:)]) {
for (NSIndexPath *path in changePaths) {
[delegate configureCellForIndexPath:path]; //update without reloading entire cell
}
} else {
[tableView reloadRowsAtIndexPaths:[change modificationsInSection:0]
withRowAnimation:UITableViewRowAnimationAutomatic];
}
[tableView endUpdates];
}];
This is working rather well. Except when I start deleting rows... Sometimes I get a modification that goes off the end of the new RLMResults.
Some example output for an example with 2 rows, where I am deleting the first row (index 0).
(lldb) po results.count
1
(lldb) po [change deletions]
<__NSArrayM 0x7f93dae6ebb0>(
0
)
(lldb) po [change insertions]
<__NSArrayM 0x7f93dae54440>(
)
(lldb) po [change modifications]
<__NSArrayM 0x7f93dd80de20>(
1
)
Here is some of the stacktrace:
2016-07-06 17:16:57.480 CRM-CT[97472:1646359] *** Terminating app due to uncaught exception 'RLMException', reason: 'Index 1 is out of bounds (must be less than 1)'
*** First throw call stack:
(
0 CoreFoundation 0x000000010d81bd85 __exceptionPreprocess + 165
1 libobjc.A.dylib 0x000000010cc5cdeb objc_exception_throw + 48
2 Realm 0x000000010c407736 _ZL10throwErrorP8NSString + 349
3 Realm 0x000000010c405e9d -[RLMResults objectAtIndex:] + 88
4 CRM-CT 0x000000010b1cd0ae -[MyViewController configureCell:atIndexPath:] + 238
5 CRM-CT 0x000000010b1c78f0 -[MyViewController configureCellForIndexPath:] + 112
6 CRM-CT 0x000000010b1f8558 __80-[RLMResults(TableView) addNotificationForTableView:section:rowOffset:delegate:]_block_invoke + 1496
7 Realm 0x000000010c3873e8 _ZZ23RLMAddNotificationBlockIN5realm7ResultsEEP20RLMNotificationTokenP11objc_objectRT_U13block_pointerFvS5_P19RLMCollectionChangeP7NSErrorEbENUlRKNS0_19CollectionChangeSetESt13exception_ptrE_clESG_SH_ + 350
8 Realm 0x000000010c387261 _ZNSt3__128__invoke_void_return_wrapperIvE6__callIJRZ23RLMAddNotificationBlockIN5realm7ResultsEEP20RLMNotificationTokenP11objc_objectRT_U13block_pointerFvS9_P19RLMCollectionChangeP7NSErrorEbEUlRKNS4_19CollectionChangeSetESt13exception_ptrE_SI_SL_EEEvDpOT_ + 45
9 Realm 0x000000010c35063d _ZN5realm5_impl18CollectionNotifier14call_callbacksEv + 245
10 Realm 0x000000010c367b62 _ZN5realm5_impl16RealmCoordinator23process_available_asyncERNS_5RealmE + 248
11 Realm 0x000000010c422a11 _ZZN5realm5_impl17WeakRealmNotifierC1ERKNSt3__110shared_ptrINS_5RealmEEEbEN3$_08__invokeEPv + 45
How can change.modifications contain a 1? The new results only contains one object, so I would think 0 is the only valid index. The docs appear to say "The indices in the new version of the collection which were modified."
UITableView wants indices in the old results for reloadRowsAtIndexPaths:, so that's what change.modifications reports. This was something of a last minute change since I had assumed that it'd want indices in the new results, so there are probably a few spots in the docs I forgot to update.
Thanks for the info and quick response. I will try to reload the row with no animation. I guess the performance will the slightly worse, but it should work!
Bah.. reloading the rows does not work very well for my use case. I will very often have a row selected that is going to be reloaded. This causes the selection to be reset. I could keep track and reselect them, but this becoming onerous.
Is there a way I can see the modifications in the new verison or access the old result set? Not sure how to use it without reloading the rows via tableview.
At the moment, sadly, there's no other information than what's provided in RLMCollectionChange.
I've had a similar instance in one of my apps before when it was necessary to make a copy of the selected cell indexPaths, reload the cells, and then re-apply the selection. It's certainly not a new pattern when dealing with UITableViews.
Anyway, I'll add this issue as an enhancement since it's a good use case for needing to add more information to the RLMCollectionChange object.
I wrote a category for RLMCollectionChange that converts to the new indicies. Tried to be as efficient as possible. You might find it interesting! It uses the insertions/deletion indexes to build the modifications in the new indicis in a single pass. Not sure if it is 100% correct.
- (nonnull NSArray<NSNumber *> *)modificationsInNew {
NSMutableArray *modsInNewIndex = [NSMutableArray new];
int offset = 0;
int insertionIndex = 0;
int deletionIndex = 0;
for (NSNumber *oldIndex in self.modifications) {
while (insertionIndex < self.insertions.count) {
if (oldIndex.intValue > self.insertions[insertionIndex].intValue) {
offset++;
} else {
break;
}
insertionIndex++;
}
while (deletionIndex < self.deletions.count) {
if (oldIndex.intValue > self.deletions[deletionIndex].intValue) {
offset--;
} else {
break;
}
deletionIndex++;
}
[modsInNewIndex addObject:@(oldIndex.intValue + offset)];
}
return modsInNewIndex;
}```
The properties on RLMCollectionChange create a new NSArray on each access, so you'll want to cache the result of self.insertions and self.deletions.
Ahah! I had actually done that, but "eliminated" the redundancy before submitting it here. Always funny the things that bite you. Thank you for the heads-up.
@asjesset what would you like to see improved in Realm to make your use case easier?
Would it make sense to have "new" variants of deletions, insertions, modifications on RLMCollectionChange? Or to expose a @property (readonly) NSArray<NSNumber *> *modificationsInNew property?
A new property that gave the modifications in the new indices would be very nice.
It's pretty obvious that deletions are in the old indicies and insertions are in new indicies. It's just modifications that are ambiguous. You could even have modificationsInOld and modificationsInNew if you want to be super specific. Even UITableView is fairly ambiguous about what indicies you need to be using. I've gotten confused several times, so perhaps explicit names are worth it?
I'm not picky about the names, but those are my thoughts!
I also have problem with Notifications. When I delete one item from my datasource realm throws incorrect changes to me. (2 deletes and 1 insert)
example:
(realm::CollectionChangeSet) $0 = {
deletions = {
realm::_impl::ChunkedRangeVector = {
m_data = size=1 {
[0] = {
data = size=2 {
[0] = (first = 3, second = 4)
[1] = (first = 8, second = 9)
}
begin = 3
end = 9
count = 2
}
}
}
}
insertions = {
realm::_impl::ChunkedRangeVector = {
m_data = size=1 {
[0] = {
data = size=1 {
[0] = (first = 3, second = 4)
}
begin = 3
end = 4
count = 1
}
}
}
}
modifications = {
realm::_impl::ChunkedRangeVector = {
m_data = size=0 {}
}
}
moves = size=1 {
[0] = (from = 8, to = 3)
}
}
Works fine with small data array.
Sample Code:
@interface MyRLMObject : RLMObject
@property(nonatomic, strong) NSString *text;
@end
...
for (int i = 0; i<3; i++) {
MyRLMObject *obj = [[MyRLMObject alloc]init];
obj.text = [NSString stringWithFormat:@"string #%i",i];
[[RLMRealm defaultRealm] transactionWithBlock:^{
[[RLMRealm defaultRealm] addObject:obj];
}];
}
self.dataSource = [MyRLMObject allObjects];
self.token = [self.dataSource addNotificationBlock:^(RLMResults * _Nullable results, RLMCollectionChange * _Nullable change, NSError * _Nullable error) {
if(change.deletions.count > 0){
NSLog(@"deletions:%@", change.deletions);
}
NSLog(@"count:%d", results.count);
}];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
MyRLMObject *obj = self.dataSource[0];
[[RLMRealm defaultRealm] transactionWithBlock:^{
[[RLMRealm defaultRealm] deleteObject:obj];
}];
});
...
@Kirow since your comments are unrelated to this ticket (tracking adding new functionality to changeset notifications), I've created a new issue where we can discuss the problems you're facing: #4016. Please move the conversation there.
@asjesset can you share your updated category?
Any Update on this issues? 2019 now.
as the notification callback of the result, the modifications is out of bounds of the results.
(lldb) po results.count
10
(lldb) po change.modifications
<__NSArrayM 0x6000018bac70>(
0,
1,
2,
3,
4,
5,
6,
8,
10,
11
)
(lldb) po change.insertions
<__NSArrayM 0x6000019775d0>(
)
(lldb) po change.deletions
<__NSArrayM 0x6000018bbf90>(
7,
9
)
but the documents said that the indices of modifications are the new version of collections. /** The indices in the new version of the collection which were modified.
For RLMResults, this means that one or more of the properties of the object at
that index were modified (or an object linked to by that object was
modified).
For RLMArray, the array itself being modified to contain a
different object at that index will also be reported as a modification.
*/
UITableView wants indices in the old results for
reloadRowsAtIndexPaths:, so that's whatchange.modificationsreports. This was something of a last minute change since I had assumed that it'd want indices in the new results, so there are probably a few spots in the docs I forgot to update.
It appears the docs were never updated. Still tripping people up.