Collections and Fast Enumeration
|
This section documents modern Objective-C as implemented by Apple Clang in the current Xcode release — Objective-C 2.0 plus ARC, literals and subscripting, This content was generated with the assistance of AI and should be verified against developer.apple.com before being relied on in production. This section’s bibliography lists the reference material consulted while preparing these pages. |
Foundation supplies three collection families — ordered (NSArray), keyed (NSDictionary) and unordered-
unique (NSSet) — each as an immutable class with a mutable subclass. They store objects only, so
scalars must be boxed (see
Strings, Numbers and Values), and they
cannot contain nil.
The Collection Hierarchy
the concrete class you get is private
(__NSArrayI, __NSArrayM, ...).
Use isKindOfClass:, never isMemberOfClass:."
Note the inheritance direction: NSMutableArray is an NSArray. A method declaring an NSArray *
parameter will happily accept a mutable one — which is exactly why properties holding collections are
declared copy.
NSArray
NSArray<NSString *> *fruits = @[ @"apple", @"banana", @"cherry" ]; // literal
fruits.count; // 3
fruits[0]; // @"apple" -- subscripting
[fruits objectAtIndex:0]; // the same message
fruits.firstObject; // @"apple" (nil if empty -- safer than [0])
fruits.lastObject; // @"cherry"
[fruits containsObject:@"banana"]; // YES -- uses isEqual:
[fruits indexOfObject:@"cherry"]; // 2, or NSNotFound
[fruits componentsJoinedByString:@", "]; // @"apple, banana, cherry"
[fruits arrayByAddingObject:@"date"]; // a NEW array; the original is unchanged
[fruits subarrayWithRange:NSMakeRange(0, 2)];
Indexing out of range raises an exception (NSRangeException) rather than returning nil — unlike
firstObject/lastObject, which return nil on an empty array. That asymmetry is deliberate: an
out-of-range index is a programming error.
NSMutableArray
NSMutableArray<NSString *> *m = [NSMutableArray array];
[m addObject:@"apple"];
[m addObjectsFromArray:@[ @"banana", @"cherry" ]];
[m insertObject:@"apricot" atIndex:1];
m[0] = @"APPLE"; // subscript assignment -- setObject:atIndexedSubscript:
[m removeObject:@"banana"]; // removes every equal object
[m removeObjectAtIndex:0];
[m removeAllObjects];
[m sortUsingComparator:^NSComparisonResult(NSString *a, NSString *b) {
return [a compare:b];
}];
Adding nil raises an exception. Guard, or use [NSNull null] as an explicit placeholder.
NSDictionary
NSDictionary<NSString *, NSNumber *> *ages = @{
@"Ada": @36,
@"Alan": @41
};
ages[@"Ada"]; // @36 -- subscripting
[ages objectForKey:@"Ada"]; // the same message
ages[@"Nobody"]; // nil -- a missing key is NOT an error
ages.count;
ages.allKeys; // order is undefined
ages.allValues;
[ages objectsForKeys:@[@"Ada"] notFoundMarker:[NSNull null]];
Two rules about keys: they are copied (so a key type must conform to NSCopying, which is why NSString
is the usual choice), and they must implement isEqual: and hash consistently — see
Inheritance and Polymorphism.
NSMutableDictionary
NSMutableDictionary<NSString *, NSNumber *> *m = [NSMutableDictionary dictionary];
m[@"Ada"] = @36; // setObject:forKeyedSubscript:
[m setObject:@41 forKey:@"Alan"];
[m removeObjectForKey:@"Ada"];
[m addEntriesFromDictionary:other];
m[@"Grace"] = nil; // subscript-assigning nil REMOVES the key
// [m setObject:nil forKey:@"Grace"]; // but this raises an exception
That last asymmetry catches people out regularly. setObject:forKey: rejects nil; the subscript form treats
it as a removal.
NSSet
Unordered, unique membership with O(1) lookup:
NSSet<NSString *> *a = [NSSet setWithArray:@[ @"x", @"y", @"x" ]]; // 2 elements
[a containsObject:@"x"]; // YES -- much faster than an array scan
NSSet *b = [NSSet setWithObjects:@"y", @"z", nil];
[a isSubsetOfSet:b]; // NO
[a intersectsSet:b]; // YES
NSMutableSet *m = [a mutableCopy];
[m unionSet:b]; // x, y, z
[m minusSet:b]; // x
[m intersectSet:b];
NSArray *asArray = a.allObjects; // order undefined
There is no set literal. Use a set whenever you need membership testing or de-duplication and do not care
about order — containsObject: on an NSArray is a linear scan.
Two relatives: NSCountedSet (a bag — tracks how many times each object was added) and NSOrderedSet (unique
and ordered, combining array indexing with set-speed lookup).
Literals and Subscripting
NSArray *a = @[ @"x", @"y" ];
NSDictionary *d = @{ @"k": @"v" };
NSNumber *n = @42;
NSNumber *e = @(x + y);
id item = a[0];
id value = d[@"k"];
mutableArray[0] = @"new";
mutableDictionary[@"k"] = @"new";
Subscripting is not special syntax for Foundation — it maps to four methods that any class may implement:
objectAtIndexedSubscript:, setObject:atIndexedSubscript:, objectForKeyedSubscript: and
setObject:forKeyedSubscript:.
Literal arrays and dictionaries reject nil at run time, which is usually a welcome early failure:
NSString *maybeNil = nil;
// NSArray *bad = @[ @"a", maybeNil ]; // raises NSInvalidArgumentException
NSArray *safe = maybeNil ? @[ @"a", maybeNil ] : @[ @"a" ];
Immutability and Copying
| Sent to | copy returns |
mutableCopy returns |
|---|---|---|
An immutable collection |
The same object, retained (cheap) |
A new mutable collection |
A mutable collection |
A new immutable collection |
A new mutable collection |
This is why a collection property should be copy:
@property (nonatomic, copy) NSArray<Item *> *items;
NSMutableArray *m = [NSMutableArray arrayWithObject:item];
obj.items = m; // stores an immutable copy
[m addObject:other]; // obj.items is unaffected -- exactly what you want
Copies are shallow: the new collection holds the same element objects. If the elements are themselves mutable, they are still shared. For a deep copy, archive and unarchive, or copy each element.
Prefer immutable collections in APIs and as stored state, and reach for a mutable one only while building. An immutable collection is also inherently safe to read from several threads.
Fast Enumeration
for…in is the idiomatic walk. It is implemented by the NSFastEnumeration protocol, which hands out a
buffer of several objects per call rather than one message per element — substantially faster than indexing.
for (NSString *fruit in fruits) {
NSLog(@"%@", fruit);
}
for (NSString *key in ages) { // a dictionary enumerates its KEYS
NSLog(@"%@ = %@", key, ages[key]);
}
for (NSString *item in set) { … }
for (NSString *fruit in [fruits reverseObjectEnumerator]) { … } // reversed
Never mutate a collection while enumerating it — doing so raises
NSGenericException: collection was mutated while being enumerated. Collect the changes and apply them
afterwards, or enumerate a copy:
NSMutableArray *toRemove = [NSMutableArray array];
for (Item *item in self.items) {
if (item.isExpired) { [toRemove addObject:item]; }
}
[self.items removeObjectsInArray:toRemove];
NSEnumerator
The older, external-iterator form. Still useful when you need to interleave two sequences or hold an iterator across calls:
NSEnumerator *e = [fruits objectEnumerator];
NSString *fruit;
while ((fruit = [e nextObject])) {
NSLog(@"%@", fruit);
}
[fruits reverseObjectEnumerator];
[dict keyEnumerator];
[dict objectEnumerator];
NSEnumerator itself conforms to NSFastEnumeration, so it can also be used with for…in.
Block-Based Enumeration
[fruits enumerateObjectsUsingBlock:^(NSString *fruit, NSUInteger idx, BOOL *stop) {
NSLog(@"%lu: %@", (unsigned long)idx, fruit);
if (idx >= 9) { *stop = YES; }
}];
[ages enumerateKeysAndObjectsUsingBlock:^(NSString *key, NSNumber *age, BOOL *stop) {
NSLog(@"%@ is %@", key, age);
}];
// Concurrently, across cores -- the block must be thread-safe.
[fruits enumerateObjectsWithOptions:NSEnumerationConcurrent
usingBlock:^(NSString *f, NSUInteger i, BOOL *stop) { … }];
| Use | When |
|---|---|
|
A straightforward walk. Shortest and fastest. |
Block enumeration |
You need the index, early exit ( |
|
You need an external iterator you can advance yourself. |
Sorting and Filtering
// Sorting with a comparator block
NSArray *sorted = [fruits sortedArrayUsingComparator:^NSComparisonResult(NSString *a, NSString *b) {
return [a localizedStandardCompare:b];
}];
// Sorting by key path -- composable, and what table views usually use
NSSortDescriptor *byName = [NSSortDescriptor sortDescriptorWithKey:@"name" ascending:YES];
NSSortDescriptor *byAge = [NSSortDescriptor sortDescriptorWithKey:@"age" ascending:NO];
NSArray *people = [allPeople sortedArrayUsingDescriptors:@[ byAge, byName ]];
// Filtering with NSPredicate
NSPredicate *adults = [NSPredicate predicateWithFormat:@"age >= %d", 18];
NSArray *grown = [allPeople filteredArrayUsingPredicate:adults];
NSPredicate *named = [NSPredicate predicateWithFormat:@"name BEGINSWITH[cd] %@", @"a"];
NSPredicate *both = [NSCompoundPredicate andPredicateWithSubpredicates:@[ adults, named ]];
// Filtering with a block
NSIndexSet *idx = [fruits indexesOfObjectsPassingTest:^BOOL(NSString *f, NSUInteger i, BOOL *s) {
return [f hasPrefix:@"a"];
}];
NSArray *matching = [fruits objectsAtIndexes:idx];
// Mapping and reducing -- via KVC collection operators
NSArray *names = [allPeople valueForKey:@"name"];
NSNumber *total = [allPeople valueForKeyPath:@"@sum.age"];
NSSortDescriptor and NSPredicate work through key-value coding, so they rely on KVC-compliant accessor
naming — see
Key-Value Coding and Observing.
[cd] in a predicate means case- and diacritic-insensitive. Always use %@/%K substitution rather than
building a predicate by string concatenation, which is a predicate-injection risk with user input.
There is no built-in map. valueForKey: covers the common case; otherwise enumerate into a new array, or
add a category.
NSIndexSet
A compact, sorted set of NSUInteger indexes, used throughout Foundation and UIKit for batch operations:
NSIndexSet *first3 = [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, 3)];
NSArray *chosen = [fruits objectsAtIndexes:first3];
NSMutableIndexSet *set = [NSMutableIndexSet indexSet];
[set addIndex:0];
[set addIndexesInRange:NSMakeRange(5, 3)];
[m removeObjectsAtIndexes:set]; // one batched removal
[set enumerateIndexesUsingBlock:^(NSUInteger i, BOOL *stop) { … }];
It stores ranges rather than individual values, so a contiguous span of a million indexes costs almost nothing.
NSCache
A mutable-dictionary-like cache that Foundation may evict from automatically under memory pressure:
NSCache<NSString *, UIImage *> *cache = [[NSCache alloc] init];
cache.countLimit = 100;
cache.totalCostLimit = 50 * 1024 * 1024;
[cache setObject:image forKey:@"avatar" cost:image.size.width * image.size.height];
UIImage *cached = [cache objectForKey:@"avatar"]; // may be nil -- always re-check
[cache removeObjectForKey:@"avatar"];
Two differences from NSMutableDictionary that make it the right choice for caching: it is thread-safe,
and keys are not copied (so they need not conform to NSCopying). Its counterpart is that entries may
vanish at any time — never use it for data you cannot recompute.
Thread Safety
Foundation’s mutable collections are not thread-safe. Immutable ones are safe to read concurrently, since nothing can change.
// Option 1 -- a serial queue (the idiomatic modern answer)
_queue = dispatch_queue_create("com.example.items", DISPATCH_QUEUE_SERIAL);
- (void)addItem:(Item *)item {
dispatch_async(_queue, ^{ [self->_items addObject:item]; });
}
- (NSArray *)items {
__block NSArray *snapshot;
dispatch_sync(_queue, ^{ snapshot = [self->_items copy]; });
return snapshot;
}
// Option 2 -- a concurrent queue with a barrier: parallel reads, exclusive writes
_queue = dispatch_queue_create("com.example.items", DISPATCH_QUEUE_CONCURRENT);
dispatch_barrier_async(_queue, ^{ [self->_items addObject:item]; }); // write
dispatch_sync(_queue, ^{ snapshot = [self->_items copy]; }); // read
Remember that declaring the property atomic does not help: it protects the pointer, not the collection’s
contents. See Concurrency.
See Also
-
Strings, Numbers and Values — the value types that go into collections, and
NSNull. -
Lightweight Generics and Nullability — the
NSArray<NSString *> *annotations used above. -
Blocks — enumeration, sorting and filtering blocks.
-
Concurrency — making shared collections safe.