How to remove elements in NSMutableArray or NSMutableDictionary during enumeration?
Asked Answered
B

3

12

I am using block based enumeration similar to the following code:

[[[rows objectForKey:self.company.coaTypeCode] objectForKey:statementType] 
    enumerateObjectsWithOptions:NSEnumerationConcurrent 
                     usingBlock:^(id coaItem, NSUInteger idx, BOOL *stop) { 
// block code here
}]

I would like to remove some of the objects during the enumeration process depending on the their object values.

How could I do this? I know that manipulating an mutable array or dictionary (NSMutableArray or NSMutableDictionary) during enumeration is usually not possible.

What would be the best way to implement this?

Thank you!

Bialystok answered 25/2, 2012 at 17:30 Comment(0)
A
39

Since you can't remove objects from an array or dictionary during enumeration, you'll have to accumulate the items you want to delete, and then delete them all after the enumeration.

If you're dealing with an array, you can just accumulate the indices.:

NSMutableIndexSet *indexesToDelete = [NSMutableIndexSet indexSet];
NSUInteger currentIndex = 0;

for (id obj in yourArray) {
    //do stuff with obj
    if (shouldBeDeleted(obj)) {
        [indexesToDelete addIndex:currentIndex];
    }
    currentIndex++;
}

[yourArray removeObjectsAtIndexes:indexesToDelete];

Since the order of the keys in an NSDictionary is undefined, for an NSMutableDictionary you'll have to accumulate keys instead:

NSMutableArray *keysToDelete = [NSMutableArray array];

for (id obj in [yourDictionary keyEnumerator]) {
    //do stuff with obj
    if (shouldBeDeleted(obj)) {
        [keysToDelete addObject:obj];
    }
}

[yourDictionary removeObjectsForKeys:keysToDelete];

It's the same thing if you're enumerating with a block. Declare the enumerator in the same scope where you declare the block and it will be retained and just work.

Also worth looking at this question from 3 years ago: Best way to remove from NSMutableArray while iterating?.

Aurora answered 25/2, 2012 at 17:38 Comment(0)
A
8

Whether you build up an index set during enumeration, or modify the array itself during enumeration, you will have to give up NSEnumerationConcurrent, because most Cocoa objects cannot safely be modified simultaneously from multiple threads.

Anyway, the simplest (but maybe not most efficient) approach is to just enumerate a copy of the container.

For an array, you can enumerate a copy in reverse. I assume that as each item is being enumerated, you may decide to remove that item, but not other items previously enumerated or yet to be enumerated.

NSMutableArray *array = [[rows objectForKey:self.company.coaTypeCode] objectForKey:statementType];
[[array copy] enumerateObjectsWithOptions: NSEnumerationReverse 
    usingBlock:^(id coaItem, NSUInteger idx, BOOL *stop) {
    if ([self objectIsTooUglyToExist:coaItem])
        [array removeObjectAtIndex:idx];
}]

You have to enumerate the array in reverse to avoid changing the not-yet-enumerated part of the array.

For a dictionary, you can just enumerate a copy with no special options:

NSMutableDictionary *dictionary = someDictionary;
[[dictionary copy] enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
    if ([self object:obj isTooUglyToExistAtKey:key])
        [dictionary removeObjectForKey:key];
}];
Anstus answered 25/2, 2012 at 18:1 Comment(1)
Thank you for the hint on the "NSEnumerationConcurrent" option, Rob!Bialystok
A
1

Another option, with an array, is to use a conventional for loop, with the array's count as the limit. Then one needs to be cognizant of whether an element is removed from a location <= the index (in which case the index should be decremented) or > than the index (in which case the index is left unmodified other than the for statement's increment).

For a dictionary you can first create an array with allKeys, and then iterate through the array. In this case no fiddling with index values is required.

Adrien answered 25/2, 2012 at 18:0 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.