NSArray: Ordered, Immutable Collections
Learn how to create, index, and enumerate NSArray, understand its immutability and the role of lightweight generics, and practice filtering and mapping concepts using NSPredicate and manual iteration.
NSArray is Foundation's ordered collection type: a sequence of objects, indexed from zero, that remembers the order elements were inserted in. Every collection example so far in this course involving multiple values in sequence has quietly been using NSArray, and this lesson makes that formal — covering how to create one, read elements out of it by index or by fast enumeration, why NSArray itself is immutable (with NSMutableArray, its mutable subclass, arriving in the next lesson), how lightweight generics like NSArray<NSString *> communicate element type without changing runtime behavior, and how to filter and map an array's contents using NSPredicate and hand-written iteration.
Creating an NSArray
The most common way to create an NSArray is the array literal syntax, @[...], exactly as shown in the spec's canonical example: NSArray<NSString *> *names = @[@"A", @"B", @"C"];. Each comma-separated expression inside the square brackets becomes one element of the array, in the order written, and the literal syntax works for any object type, not just NSString — an array literal can mix NSNumber, NSString, and custom class instances freely, since NSArray's storage is simply id under the hood, capable of holding any object.
The older, more verbose equivalent is +arrayWithObjects:, a variadic class method that must be terminated with a nil sentinel: NSArray *names = [NSArray arrayWithObjects:@"A", @"B", @"C", nil];. The nil at the end is not optional decoration — it's how arrayWithObjects: knows where the argument list ends, since Objective-C's variadic methods have no other way to know how many arguments were passed. Forgetting the trailing nil is a classic, hard-to-spot bug: the method keeps reading arguments off the stack past the ones you actually passed until it happens to hit something that looks like nil, which can crash or silently include garbage. The @[...] literal syntax has no such footgun — it's counted at compile time — which is exactly why it's the preferred, idiomatic way to write array literals in modern Objective-C, with arrayWithObjects: mostly of value for recognizing it in existing code.
One important detail applies to both creation styles equally: neither @[...] nor arrayWithObjects: accepts nil as an actual element value. Attempting @[name1, nil, name3] where name1 or name3 might be nil raises an exception at runtime rather than silently inserting a null placeholder, because NSArray, like every Foundation collection, cannot store nil — every slot must hold a real object.
Indexed Access
Once an array exists, -objectAtIndex: retrieves a single element by its zero-based position: [names objectAtIndex:0] returns the first element, @"A" in the spec's example. Modern Objective-C also supports the shorter subscript syntax, names[0], which the compiler translates directly into a call to objectAtIndex: — the two are functionally identical, and subscripting is simply the more concise, more commonly used spelling in code written today.
Both forms share the same critical constraint: the index must be within bounds, from 0 up to (but not including) the array's count. Asking for names[5] on a three-element array raises an NSRangeException at runtime — Objective-C does not silently return nil or clamp the index the way some other languages might — so any code that computes an index dynamically (from user input, from a loop bound, from a calculation) needs to be certain that index actually falls within [0, count) before using it. count itself, the array's element count, is read via the count property: names.count returns 3 for the spec's example array.
Enumeration and Fast Enumeration
The idiomatic way to walk every element of an NSArray in order is fast enumeration, using Objective-C's for...in syntax: for (NSString *name in names) { NSLog(@"%@", name); } visits each element of names in sequence, binding it to the loop variable name on each iteration. This is called fast enumeration because, unlike a manual indexed loop that calls objectAtIndex: on every iteration (each such call carries its own small overhead) or the older NSEnumerator-based approach (-objectEnumerator plus repeated -nextObject calls, which allocates and drives a separate enumerator object), the compiler and runtime cooperate to walk the array's internal storage in large batches, retrieving many elements per underlying call rather than one call per element. The practical upshot is that for...in is both the clearest syntax to read and, in most cases, the fastest way to iterate — there is rarely a reason to prefer manual indexing or NSEnumerator for straightforward top-to-bottom traversal.
The older NSEnumerator style still appears in existing code and is worth recognizing: NSEnumerator *e = [names objectEnumerator]; NSString *name; while ((name = [e nextObject])) { ... } achieves the same traversal, one element retrieved per -nextObject call, but with more ceremony and (in the general case) more overhead than fast enumeration. A manual indexed loop, for (NSUInteger i = 0; i < names.count; i++) { NSString *name = names[i]; ... }, is occasionally still useful specifically when you need the index itself (to compare adjacent elements, or to build a numbered list, as in the previous NSMutableString lesson's exercise) — for...in gives you elements but not their positions directly, though enumerateObjectsUsingBlock: (which supplies both the object and its index to a block on each call) is often a cleaner choice than manual indexing when you need both.
Immutability: NSArray Cannot Change After Creation
NSArray is immutable, exactly parallel to NSString's immutability covered earlier in this course: once created, an NSArray's elements, their order, and its count are fixed for the object's entire lifetime. There is no addObject:, no removeObjectAtIndex:, no method on NSArray itself that changes its contents — every one of NSArray's own methods either reads existing state (objectAtIndex:, count, containsObject:) or returns a brand-new array built from the original (arrayByAddingObject: returns a new array with one more element, leaving the receiver untouched).
This matters for exactly the same reasons NSString's immutability mattered: an immutable array can be freely shared, passed around, and stored as a dictionary value or handed to another object without any risk that some other piece of code will silently add, remove, or reorder its elements out from under you. If you do need a collection whose contents change over its lifetime — appending elements in a loop, removing items as they're processed, reordering — that is precisely the job of NSMutableArray, NSArray's mutable subclass, which is the subject of the next lesson and follows the same subclassing relationship NSMutableString has with NSString.
Lightweight Generics: NSArray<ElementType *>
The spec's own example, NSArray<NSString *> *names = @[@"A", @"B", @"C"];, uses Objective-C's lightweight generics syntax: the <NSString *> after NSArray declares that this particular array is meant to hold NSString elements specifically, rather than an unconstrained mix of any object type. This is genuinely useful — with the generic type annotation in place, the compiler can warn you at compile time if you write names[0] and then try to call an NSString-only method on some other type of object by mistake, and Xcode's autocomplete correctly suggests NSString methods for elements pulled out of names, rather than the generic id/NSObject method list it would otherwise offer.
It's essential to understand what 'lightweight' means here, though: these generics are compile-time-only type hints, fully erased at runtime. Unlike Java or C# generics, which are (at least partially) enforced by the runtime type system, an NSArray<NSString *> is, at the actual object level while your program is running, the exact same NSArray class as an ungenerified NSArray *. Nothing at runtime prevents [names addObject:@42] from being attempted on some mutable-typed handle that has lost its generic annotation through a cast, and Foundation's actual insertion code, if reached, performs no NSString type-check at all — the safety lightweight generics provide is entirely at compile time, catching mistakes the compiler can see in your source code, not mistakes made through runtime dynamism or explicit casts.
Despite that limitation, lightweight generics are worth using by default on every array (and dictionary, and set) whose element type you know at the point of declaration, for two concrete reasons: they make your API self-documenting to every other engineer reading the header (and to future you), and — increasingly relevant as codebases interoperate with Swift — a lightweight-generic NSArray<NSString *> bridges into Swift as a properly typed [String], while an ungenerified NSArray bridges into Swift as the much less useful [Any] or [AnyObject], forcing Swift-side code to cast every element it pulls out. Annotating your Objective-C collection types with lightweight generics is, in that sense, an investment in how cleanly your code interoperates with any Swift code that consumes it.
Filtering and Mapping Concepts
NSArray provides a built-in, declarative way to filter: -filteredArrayUsingPredicate:, given an NSPredicate describing a condition, returns a new array containing only the elements that satisfy it. NSPredicate *startsWithA = [NSPredicate predicateWithFormat:@"SELF beginswith[c] %@", @"A"]; NSArray *filtered = [names filteredArrayUsingPredicate:startsWithA]; returns a new array holding only the elements of names that begin with "A" (the [c] modifier makes the comparison case-insensitive). NSPredicate's format-string mini-language supports a surprisingly rich set of conditions — comparisons, string matching, compound AND/OR logic — and filteredArrayUsingPredicate: is the idiomatic, declarative way to express 'give me the subset of this array matching some condition' without hand-writing a loop and a temporary mutable array yourself.
Mapping — transforming every element into some new value, producing a new array of the results — has no single dedicated NSArray method the way filtering has filteredArrayUsingPredicate:, because Objective-C, unlike Swift or many other modern languages, has no built-in functional map operation for its base collection types. The idiomatic way to map in Objective-C is to build the result by hand: allocate an NSMutableArray, iterate the source with fast enumeration, transform each element, and append the transformed value, finally handing back an immutable NSArray copy if you want the result to have the same immutability guarantee as any other NSArray. This is more ceremony than array.map { ... } in a language with built-in functional collection operations, but it is the standard, idiomatic pattern in Objective-C code, and it composes naturally with everything else covered in this lesson — fast enumeration to walk the source, NSMutableArray (from the next lesson) to accumulate results, and the finished array behaving exactly like any other NSArray once built.
• filteredArrayUsingPredicate: — declarative filtering using an NSPredicate condition; returns a new array. • No built-in map — build one by hand: NSMutableArray accumulator + fast enumeration + transform + append. • Both approaches leave the original array untouched, consistent with NSArray's immutability.
NSArray Creation, Access, and Enumeration
The lightweight generic <NSString *> is a compile-time-only hint; it has no effect on the array's actual runtime behavior. names[0] and [names objectAtIndex:0] compile to the same underlying call. Indexing past names.count - 1 raises NSRangeException at runtime; there is no silent nil/clamp fallback.
NSArray<NSString *> *names = @[@"A", @"B", @"C"];
NSString *first = names[0]; // subscripting
NSString *firstAlt = [names objectAtIndex:0]; // equivalent, verbose form
NSUInteger n = names.count;
for (NSString *name in names) { // fast enumeration
NSLog(@"%@", name);
}Code Examples
This is the idiomatic way to create a fixed, immutable, typed array in modern Objective-C: the @[...] literal syntax builds the array in one expression, and the <NSString *> lightweight generic documents that every element is expected to be an NSString.
NSArray<NSString *> *names = @[@"A", @"B", @"C"];
Fast enumeration is the clearer and generally faster choice for plain traversal, since the runtime walks the array's internal storage in batches rather than issuing one objectAtIndex: call per element. The manual indexed loop is reserved for cases where the index itself is needed, not just the element. Expected result: Fast: A Fast: B Fast: C Index 0: A Index 1: B Index 2: C for (NSString *name in names) { filteredArrayUsingPredicate: declaratively filters using an NSPredicate condition and returns a new array, leaving names untouched. Since Objective-C has no built-in map, the uppercasing transform is done by hand: an NSMutableArray accumulator collects each transformed element during fast enumeration, and copy produces an immutable NSArray result at the end. Expected result: ( Alice, Amy) ( ALICE, BOB, AMY, CAROL)
NSArray<NSString *> *aNames = [names filteredArrayUsingPredicate:startsWithA];
NSArray<NSString *> *names = @[@"A", @"B", @"C"];NSArray<NSString *> *names = @[@"A", @"B", @"C"];
// Fast enumeration -- preferred for simple top-to-bottom traversal
for (NSString *name in names) {
NSLog(@"Fast: %@", name);
}
// Manual indexed loop -- useful when you specifically need the index
for (NSUInteger i = 0; i < names.count; i++) {
NSLog(@"Index %lu: %@", (unsigned long)i, names[i]);
}NSArray<NSString *> *names = @[@"Alice", @"Bob", @"Amy", @"Carol"];
// Filtering: names beginning with "A"
NSPredicate *startsWithA = [NSPredicate predicateWithFormat:@"SELF beginswith[c] %@", @"A"];
NSArray<NSString *> *aNames = [names filteredArrayUsingPredicate:startsWithA];
// Mapping: uppercase every name, built by hand since ObjC has no built-in map
NSMutableArray<NSString *> *uppercased = [NSMutableArray arrayWithCapacity:names.count];
for (NSString *name in names) {
[uppercased addObject:name.uppercaseString];
}
NSArray<NSString *> *upperNames = [uppercased copy];
NSLog(@"%@", aNames);
NSLog(@"%@", upperNames);Important Rules
@[...] array literal syntax is the standard way to create an NSArray; arrayWithObjects: (with a required trailing nil) is the older, more error-prone equivalent.
objectAtIndex: and subscripting (names[0]) are equivalent; both raise NSRangeException on an out-of-bounds index rather than returning nil.
for...in fast enumeration is the idiomatic, generally fastest way to traverse an NSArray in order; manual indexing is reserved for when the index itself is needed.
NSArray is immutable -- its own methods never change its contents; NSMutableArray (next lesson) is the mutable subclass for arrays that change over time.
Lightweight generics (NSArray<ElementType *>) are compile-time-only hints, fully erased at runtime -- useful for compiler warnings, autocomplete, and Swift bridging, but not a runtime safety guarantee.
filteredArrayUsingPredicate: provides declarative filtering; mapping has no built-in method and is conventionally hand-built with an NSMutableArray accumulator and fast enumeration.
Table and Collection View Data Sources
NSArray is the backbone of nearly every list-driven UI in Cocoa and UIKit-adjacent code: a table view's data source typically holds its row data in an NSArray (often of a custom model class, annotated with lightweight generics like NSArray<Person *> for compiler and autocomplete support), and NSPredicate-based filtering via filteredArrayUsingPredicate: is exactly how a search bar's live-filtering feature is commonly implemented -- filtering the full data array down to a matching subset on every keystroke without mutating the original source array.
NSString *fourth = names[3]; // names has only 3 elements if (names.count > 3) { NSString *fourth = names[3]; }
Indexing past the last valid index (count - 1) raises an NSRangeException at runtime rather than returning nil or clamping; always check count (or the index's provenance) before indexing with a computed value.
NSArray *names = [NSArray arrayWithObjects:@"A", @"B", @"C"]; // missing trailing nil
NSArray *names = [NSArray arrayWithObjects:@"A", @"B", @"C", nil];
arrayWithObjects: is variadic and relies on a trailing nil sentinel to know where the argument list ends. Omitting it causes the method to keep reading past the arguments you actually passed, which can crash or include garbage.
[names addObject:@"D"]; // names is a plain NSArray NSMutableArray *mutableNames = [names mutableCopy]; [mutableNames addObject:@"D"];
NSArray has no addObject: method at all -- it's immutable. To get a collection you can append to, you need an NSMutableArray, either created directly or via mutableCopy from an existing NSArray.
What is the correct way to write the spec's canonical NSArray of names? A. NSArray<NSString *> *names = @[@"A", @"B", @"C"]; B. NSMutableArray *names = {@"A", @"B", @"C"}; C. NSArray names = ["A", "B", "C"]; D. NSArray<NSString> names = @("A", "B", "C");
A. NSArray<NSString *> *names = @[@"A", @"B", @"C"]; — The @[...] array literal syntax, combined with the NSArray<NSString *> lightweight generic annotation, is the idiomatic modern way to declare a fixed, typed array of strings.
Why is fast enumeration (for...in) generally preferred over a manual indexed loop for straightforward traversal of an NSArray? A. Fast enumeration is the only syntax that compiles for NSArray B. It walks the array's internal storage in batches rather than issuing one call per element, and is clearer to read for simple traversal C. Manual indexed loops cannot access every element D. Fast enumeration automatically sorts the array first
B. It walks the array's internal storage in batches rather than issuing one call per element, and is clearer to read for simple traversal — The runtime and compiler cooperate to retrieve many elements per underlying call during fast enumeration, generally making it both clearer to read and faster than a manual objectAtIndex:-per-iteration loop for plain traversal.
What does the lightweight generic annotation in NSArray<NSString *> actually guarantee at runtime? A. The runtime will reject any attempt to insert a non-NSString object into the array B. Nothing at runtime -- it is a compile-time-only hint, fully erased once the program is running C. It automatically converts non-NSString elements to NSString D. It doubles the array's storage capacity
B. Nothing at runtime -- it is a compile-time-only hint, fully erased once the program is running — Lightweight generics are compile-time-only type hints. At runtime, an NSArray<NSString *> is the identical NSArray class as an ungenerified one, with no enforced type checking on insertion.
How does Objective-C conventionally implement a 'map' operation (transforming every element into a new value) on an NSArray, given that NSArray has no built-in map method? A. By calling the nonexistent mapUsingBlock: method B. By building the result manually: iterate with fast enumeration, transform each element, and append it to an NSMutableArray accumulator C. By casting the NSArray to NSDictionary D. It cannot be done in Objective-C
B. By building the result manually: iterate with fast enumeration, transform each element, and append it to an NSMutableArray accumulator — Objective-C has no built-in functional map for NSArray, so the idiomatic pattern is a hand-written loop: fast-enumerate the source, transform each element, append the result to an NSMutableArray, and optionally copy to an immutable NSArray at the end.
What happens when arrayWithObjects: is called without a trailing nil sentinel? A. The compiler automatically inserts one B. The array is created with zero elements C. The method has no reliable way to know where the argument list ends and may keep reading past the intended arguments, which can crash or include garbage D. It throws a clear, descriptive compile-time error
C. The method has no reliable way to know where the argument list ends and may keep reading past the intended arguments, which can crash or include garbage — arrayWithObjects: is a variadic method that relies on a trailing nil to mark the end of its argument list; omitting it is a classic, hard-to-spot bug because there is no other signal telling the method when to stop reading arguments.
Given NSArray<NSString *> *words = @[@"apple", @"kiwi", @"banana", @"fig", @"cherry"];, use filteredArrayUsingPredicate: to produce an array of only the words with more than 4 characters, and separately, hand-build (map-style) an array of each word's length as an NSNumber.
NSPredicate *longWords = [NSPredicate predicateWithFormat:@"SELF.length > 4"]; NSArray<NSString *> *filtered = [words filteredArrayUsingPredicate:longWords]; NSMutableArray<NSNumber *> *lengths = [NSMutableArray arrayWithCapacity:words.count]; for (NSString *word in words) { [lengths addObject:@(word.length)]; } NSArray<NSNumber *> *wordLengths = [lengths copy];
For filtering, use an NSPredicate format string like @"length > 4" evaluated against SELF. For the map-style transform, iterate with fast enumeration, box each word.length with @(), and accumulate into an NSMutableArray before copying to an immutable result.