realm-swift icon indicating copy to clipboard operation
realm-swift copied to clipboard

Results Notification includes modification using old version indicies

Open asjesset opened this issue 9 years ago • 16 comments

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."

asjesset avatar Jul 06 '16 22:07 asjesset

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.

tgoyne avatar Jul 06 '16 22:07 tgoyne

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!

asjesset avatar Jul 06 '16 22:07 asjesset

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.

asjesset avatar Jul 06 '16 23:07 asjesset

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.

TimOliver avatar Jul 15 '16 15:07 TimOliver

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;
}```

asjesset avatar Jul 19 '16 21:07 asjesset

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.

tgoyne avatar Jul 19 '16 21:07 tgoyne

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 avatar Jul 19 '16 22:07 asjesset

@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?

jpsim avatar Jul 20 '16 19:07 jpsim

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!

asjesset avatar Jul 20 '16 21:07 asjesset

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.

Kirow avatar Aug 26 '16 13:08 Kirow

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 avatar Aug 26 '16 14:08 Kirow

@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.

jpsim avatar Aug 26 '16 20:08 jpsim

@asjesset can you share your updated category?

timbroder avatar Nov 17 '16 16:11 timbroder

Any Update on this issues? 2019 now.

RetVal avatar Jan 10 '19 16:01 RetVal

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. */

RetVal avatar Nov 05 '19 08:11 RetVal

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.

It appears the docs were never updated. Still tripping people up.

dwhitla avatar Nov 01 '20 00:11 dwhitla