rdelfin
rdelfin

Reputation: 816

Xcode exception: "Collection <__NSArrayM: 0xb1a2970> was mutated while being enumerated" with Mutable Arrays

I have a problem with three clases. The the first class is called Player. This class has an NSMutableArray inside it called units. This array is made up of objects of the class Unit. This class in turn, has an NSMutableArray called bullets. It works like this:

At a certain point the class Player (it could just be the ViewController instead) adds an object to the units. Then, when an instance of Unit is initialized, as a result of the above, it creates an NSTimer that is in charge of creating bullets every second.

The thing is, is crashes somewhere in the middle of this with a SIGABRT that tells me that there was an exception because: Collection <__NSArrayM: 0xb1a2970> was mutated while being enumerated. Also, I took away the line that created bullets and it stops crashing, proving that is the problem. What does that mean!

Here is a bit of executable code that might work:

ViewController.h (instead of player)

@interface ViewController : UIViewController
{
    NSMutableArray *units;
    NSTimer *updateTimer;
}
-(void)Update;

ViewController.m

@implementation ViewController
//methods...

- (void)viewDidLoad
{
    //more default code

    //Initialized array and adds one object with the default constructor for simplicity
    units = [[NSMutableArray alloc] initWithObjects:[[Unit alloc] init], nil]
}

-(void)Update
{
    for(Unit *unit in units)
    {
        [unit Update];
        if(unit.deleteFromList)
            [units removeObject:unit];   
    }
}
//More methods
@end

Unit.h

@interface Unit : NSObject
{
    NSMutableArray *bullets;
    NSTimer *bulletTimer;
    boolean deleteFromList;
}

@property(readonly, assign)deleteFromList;

-(void)Fire;

-(void)Update;

Unit.m

@implementation Unit

@synthesize deleteFromList;

-(id)init
{
    if(self)
    {
        bullets = [[NSMutableArray alloc] init];
        bulletTimer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(fire) userInfo:NULL repeats:true];
        deleteFromList = false;
    }

    return self;
}

-(void)Fire
{
    [bullets addObject:[[Bullet alloc] init]];
}

-(void)Update
{
    for(Bullet *bullet in bullets)
    {
        [bullet Update];
        if(bullet.deleteFromList)
            [bullets removeObject:bullet];
    }

    if(certainCondition)
        deleteFromList = true;
}

The bullet class will be omitted because the contents are irrelevant to what happens. Also, all the classes and constructors were shortened because the rest is useless for this example

EDIT:

Another thing I forgot to add is that the timer is created in an enumeration of the NSMutableArray units in the update method i'm about to add. I'm also adding a variable to Unit and Bullet that orders it to delete. The bullet update changes the position and also changes the deleteFromList variable

Upvotes: 0

Views: 2822

Answers (3)

Nick Forge
Nick Forge

Reputation: 21464

There are two easy approaches - one is to create an array of objects to remove, and the other is to iterate over a copy of the original way. This second technique can be somewhat easier to read.

Method 1

NSMutableArray *toRemove = [NSMutableArray array];
for (Bullet *bullet in bullets)
{
    [bullet Update];
    if (bullet.deleteFromList)
    {
        [toRemove addObject:bullet];
    }
}
[bullets removeObjectsInArray:toRemove];

Method 2

for (Bullet *bullet in [bullets copy])
{
    [bullet Update];
    if (bullet.deleteFromList)
    {
        [bullets removeObject:bullet];
    }
}

The first approach is slightly more verbose, but won't make a copy of the original array. If the original array is very large, and you don't want to make a copy of it for performance/memory reasons, the first approach is best. If it doesn't matter (99% of the time), I prefer the second approach.


As an aside, you shouldn't start a method name with a capital letter in Objective-C unless the method name starts with an abbreviation, e.g. URL, so [bullet Update] should really be renamed to [bullet update].

Upvotes: 4

Elden
Elden

Reputation: 680

Or

NSIndexSet *indexSet = [units indexesOfObjectsPassingTest:^BOOL(Unit *udit, NSUInteger idx, BOOL *stop) {
    [unit Update];
    return unit.deleteFromList;
}];

[units removeObjectsAtIndexes:indexSet];

Upvotes: 0

fannheyward
fannheyward

Reputation: 19307

You can't remove any item in NSMutableArray while for-loop or enumerating it.

Document: It is not safe to modify a mutable collection while enumerating through it. Some enumerators may currently allow enumeration of a collection that is modified, but this behavior is not guaranteed to be supported in the future.

for(Bullet *bullet in bullets)
{
    [bullet Update];
    if(bullet.deleteFromList)
        [bullets removeObject:bullet];
}

to

NSMutableArray *toRemove = [NSMutableArray array];
for(Bullet *bullet in bullets)
{
    [bullet Update];
    if(bullet.deleteFromList)
        [toRemove addObject:bullet];
}
[bullets removeObjectsInArray:toRemove];

Upvotes: 4

Related Questions