NSMutableString: Mutating Strings in Place
Understand NSMutableString as the mutable subclass of NSString, learn its core mutation methods, and reason carefully about when mutation helps performance and when a copy property attribute is required to protect your class from being mutated out from under it.
Every NSString this course has used so far has been immutable: once @"Hello" exists, nothing can change the characters inside it, and every method that looks like it modifies a string — stringByAppendingString:, uppercaseString, and so on — actually allocates and returns a brand-new NSString instead. That immutability is a deliberate, valuable design choice, but it has a real cost when you need to build a string incrementally: a loop that appends a fragment on every iteration using stringByAppendingString: allocates a new string object on every single iteration, copying everything accumulated so far into the new object each time. NSMutableString is Foundation's answer to that problem — a subclass of NSString whose characters can be changed in place, appended to, inserted into, and deleted from, all without allocating a new object for every edit.
NSMutableString Is a Subclass of NSString
NSMutableString inherits from NSString, which means every method NSString defines — length, characterAtIndex:, substringToIndex:, isEqualToString:, and every other read-only method covered earlier in this course — works exactly the same way on an NSMutableString instance. What NSMutableString adds is a set of mutation methods that NSString simply does not have: appendString:, appendFormat:, insertString:atIndex:, deleteCharactersInRange:, and replaceCharactersInRange:withString:. Because of this inheritance relationship, an NSMutableString can be passed anywhere an NSString parameter is expected — a method declared as -(void)logMessage:(NSString *)message happily accepts an NSMutableString argument, since is-a NSString is exactly what subclassing means.
The relationship only works in one direction, though: an NSString instance is not secretly an NSMutableString, and there is no safe way to call appendString: on a plain NSString variable even if you cast it — the underlying object genuinely was allocated as immutable, and Foundation's immutable string classes are optimized internally in ways that assume their contents never change once created. If you need a mutable string, you must explicitly create one, typically with [NSMutableString string], [NSMutableString stringWithString:existingString], or [[NSMutableString alloc] initWithCapacity:someEstimate].
The Core Mutation Methods
appendString: is the workhorse of NSMutableString: [buffer appendString:@"more text"] tacks the given string onto the end of buffer's existing characters, modifying buffer in place and returning nothing (it's a void method, unlike NSString's non-mutating methods, which always return a new object). appendFormat: is the mutable, in-place cousin of stringWithFormat: — [buffer appendFormat:@"Score: %d", score] formats the arguments according to the format string and appends the result, which is extremely common when building up a human-readable report or log line piece by piece across many statements.
insertString:atIndex: inserts a given string at a specific character index, shifting everything from that index onward to the right — [buffer insertString:@"Hello, " atIndex:0] pushes new text onto the front of an existing buffer. deleteCharactersInRange: removes a run of characters described by an NSRange (location plus length), collapsing the gap — [buffer deleteCharactersInRange:NSMakeRange(0, 5)] removes the first five characters. replaceCharactersInRange:withString: is the most general of the four: it deletes the characters in the given range and inserts the replacement string in their place in a single operation, which is how you implement something like a targeted find-and-replace at a known location without doing a separate delete followed by a separate insert.
All four of these mutation methods operate on character (technically, UTF-16 code unit) indices and ranges, exactly like the indexing methods you already know from NSString, so the same care about staying within length is required — passing a range that runs past the end of the string's current length raises an NSRangeException at runtime just as it would for substringWithRange: on an immutable NSString.
• appendString: / appendFormat: — add text to the end, in place. • insertString:atIndex: — insert text at a specific position, shifting the remainder right. • deleteCharactersInRange: — remove a run of characters described by an NSRange. • replaceCharactersInRange:withString: — delete a range and insert replacement text in one step.
Why Mutation Matters: The Performance Case
Building a string incrementally with immutable NSString is deceptively expensive. Consider a loop that runs a thousand times, each iteration doing result = [result stringByAppendingString:fragment]: every single iteration allocates a brand-new NSString object sized to hold everything accumulated so far plus the new fragment, and copies all of the previous characters into that new object before the old one becomes garbage. Across a thousand iterations, that is roughly a thousand allocations and a quadratic amount of character-copying overall — the total work grows with the square of the number of appends, because each append re-copies everything that came before it.
An NSMutableString sidesteps this entirely. [buffer appendString:fragment] extends the same underlying storage in place (Foundation's mutable string implementation typically over-allocates capacity so that repeated appends don't need to reallocate on every single call, similar in spirit to how a dynamic array or NSMutableArray grows), so the total work across a thousand appends is close to linear in the total number of characters written, not quadratic. This is exactly the scenario the spec calls out: whenever you are building a string incrementally — inside a loop, while parsing, while constructing a report line by line — NSMutableString is the right tool, and reaching for repeated immutable concatenation instead is one of the most common avoidable performance mistakes in Objective-C code that processes any nontrivial amount of text.
It's worth being precise about scale here: for a handful of concatenations — three or four fragments joined once — the performance difference between immutable concatenation and NSMutableString is negligible, and either approach will run in effectively the same time a human would ever notice. The performance argument for NSMutableString sharpens specifically as the number of incremental edits grows, which is why 'building inside a loop' is the pattern to watch for, not 'ever calling stringByAppendingString: at all.'
Ownership: Why NSString Properties Use copy, Not strong
This is one of the most commonly tested gotchas in Objective-C, and it follows directly from NSMutableString being a subclass of NSString. Consider a Person class with @property (nonatomic, strong) NSString *name;. Because NSMutableString is-a NSString, nothing stops a caller from doing NSMutableString *mutableName = [NSMutableString stringWithString:@"Alice"]; person.name = mutableName; — the assignment compiles and runs without complaint, because strong simply retains whatever object it's handed, and a mutable string is a perfectly valid NSString as far as the type system is concerned.
The danger is what happens next: because strong only retained the mutableName object rather than copying its contents, person.name and mutableName are now the exact same object in memory. If the caller who still holds a reference to mutableName later calls [mutableName appendString:@" Smith"], person.name silently becomes "Alice Smith" too — not because anyone touched person directly, but because the property was secretly aliased to a mutable object the class never controlled. Your Person instance's internal state has just changed out from under it, invisibly, from code the Person class has no knowledge of and no way to prevent.
The fix is to declare NSString-typed properties as @property (nonatomic, copy) NSString *name; instead of strong. copy changes what the synthesized setter does: instead of retaining whatever object it's handed, it sends -copy to that object and retains the result. Sending -copy to an already-immutable NSString is cheap (well-implemented immutable classes in Foundation typically just retain themselves and return self when copied, since an immutable object's copy can safely share the original), but sending -copy to an NSMutableString allocates a brand-new, genuinely immutable NSString snapshot of its current contents at that moment. From that point on, person.name points to that private, frozen snapshot — later mutations to the caller's original mutableName object have no effect on it whatsoever. This is precisely why the standard Objective-C convention is: any property whose declared type is NSString (or NSArray, NSDictionary, NSSet, and their other mutable-subclass-having Foundation cousins) should almost always use copy rather than strong, specifically to guard against exactly this kind of silent external mutation.
When Not to Use NSMutableString
NSMutableString is not a universal upgrade over NSString, and reaching for it by default has real downsides. For a small, fixed string that is never going to change — a constant label, a single formatted message built once with stringWithFormat:, a literal like @"Submit" for a button title — an immutable NSString literal or a single stringWithFormat: call is simpler to write, simpler to reason about (nothing can ever mutate it later, by construction), and just as fast: there is no loop of incremental edits for NSMutableString's in-place growth to help with.
Immutable strings also carry a real safety advantage that's easy to undervalue: because nothing can change an NSString's contents after creation, it can be freely shared, cached, passed around, and stored in dictionary keys (NSDictionary keys are typically copied, and having a key value that could mutate after insertion would break the dictionary's internal hashing) without ever worrying that some other piece of code will alter it unexpectedly. The rule of thumb worth internalizing: default to NSString for any string whose value is fixed at the point of creation, and reach specifically for NSMutableString when you have a concrete case of building or editing a string's contents incrementally over multiple steps — a loop, a parser accumulating output, or a text buffer that a user is actively editing.
NSMutableString Creation and Mutation
stringWithCapacity: is a hint, not a hard limit — the buffer still grows automatically past it if needed. Every mutation method here modifies buffer in place and returns void, unlike NSString's non-mutating equivalents. Ranges and indices are in UTF-16 code units, same as every NSString indexing method.
NSMutableString *buffer = [NSMutableString stringWithCapacity:64];
[buffer appendString:@"Hello"];
[buffer appendFormat:@", %@!", @"World"];
[buffer insertString:@">> " atIndex:0];
[buffer deleteCharactersInRange:NSMakeRange(0, 3)];
[buffer replaceCharactersInRange:NSMakeRange(0, 5) withString:@"Howdy"];Code Examples
Each iteration of the loop calls appendFormat: on the same buffer object rather than creating a new NSString every time, so building a report from N line items does roughly N units of work instead of the quadratic amount immutable concatenation would require. Expected result: Receipt - Apples: 3 - Bread: 1 - Milk: 2 Thank you!
[report appendFormat:@"- %@\n", item];
Because name is declared copy, assigning mutableName to p.name triggers a copy at assignment time, producing an independent immutable NSString. The later appendString: call on mutableName has no effect on p.name, because they are no longer the same object in memory. If name had been declared strong instead, p.name and mutableName would be the same object, and appendString: would silently change p.name too.
@property (nonatomic, copy) NSString *name;
rangeOfString: locates the substring to replace, and replaceCharactersInRange:withString: swaps it out in a single in-place operation rather than deleting and inserting separately, and without allocating a whole new string object for the entire status line. Expected result: Status: COMPLETE
[status replaceCharactersInRange:wordRange withString:@"COMPLETE"];
NSArray<NSString *> *lineItems = @[@"Apples: 3", @"Bread: 1", @"Milk: 2"];
NSMutableString *report = [NSMutableString stringWithString:@"Receipt\n"];
for (NSString *item in lineItems) {
[report appendFormat:@"- %@\n", item];
}
[report appendString:@"Thank you!"];
NSLog(@"%@", report);@interface Person : NSObject
@property (nonatomic, copy) NSString *name; // copy, not strong
@end
@implementation Person
@end
NSMutableString *mutableName = [NSMutableString stringWithString:@"Alice"];
Person *p = [[Person alloc] init];
p.name = mutableName; // setter copies -> p.name is a frozen snapshot
[mutableName appendString:@" Smith"];
NSLog(@"%@", mutableName); // "Alice Smith"
NSLog(@"%@", p.name); // still "Alice" -- unaffected by the mutationNSMutableString *status = [NSMutableString stringWithString:@"Status: PENDING"];
NSRange wordRange = [status rangeOfString:@"PENDING"];
if (wordRange.location != NSNotFound) {
[status replaceCharactersInRange:wordRange withString:@"COMPLETE"];
}
NSLog(@"%@", status);Important Rules
NSMutableString is a subclass of NSString and inherits every read-only NSString method unchanged.
appendString:/appendFormat:/insertString:atIndex:/deleteCharactersInRange:/replaceCharactersInRange:withString: are the core mutation methods; all mutate in place and return void.
Use NSMutableString when building a string incrementally (loops, parsing) to avoid the quadratic cost of repeated immutable concatenation.
Declare NSString-typed properties with copy, not strong, so assigning an NSMutableString captures an immutable snapshot instead of a live, mutable alias.
For small, fixed, one-shot strings, plain NSString literals or a single stringWithFormat: call are simpler and equally fast.
Log and Report Builders
Almost every nontrivial Objective-C codebase has some form of incremental text-building code: a debug logger accumulating a multi-line diagnostic message, a CSV or report exporter appending one row at a time, or a network layer building up a query string parameter by parameter. In every one of these cases, using NSMutableString instead of chained immutable concatenation is the difference between code that stays fast as the amount of data grows and code that quietly becomes an O(n^2) bottleneck once someone's usage of the app scales past the small inputs it was originally tested with.
@property (nonatomic, strong) NSString *name;
@property (nonatomic, copy) NSString *name;
strong just retains whatever object is assigned. If a caller assigns an NSMutableString, the property silently aliases it, and later mutations by the caller change your object's state invisibly. copy takes an immutable snapshot at assignment time instead.
for (int i = 0; i < 1000; i++) { result = [result stringByAppendingString:fragment]; }
NSMutableString *result = [NSMutableString string]; for (...) { [result appendString:fragment]; }
Repeated stringByAppendingString: allocates and copies a whole new string on every iteration, giving quadratic total cost. appendString: on a mutable buffer grows in place, giving roughly linear cost.
[buffer deleteCharactersInRange:NSMakeRange(0, buffer.length + 5)];
[buffer deleteCharactersInRange:NSMakeRange(0, buffer.length)];
Passing a range whose location+length exceeds the mutable string's current length raises an NSRangeException at runtime, exactly as it would for an out-of-bounds NSString range method.
Why is repeatedly calling stringByAppendingString: inside a loop generally slower than using NSMutableString's appendString: for the same task? A. stringByAppendingString: is implemented in Swift, which is slower to call from Objective-C B. Each call allocates a new NSString and copies all previously accumulated characters into it, giving quadratic total cost, while appendString: grows the same buffer in place C. stringByAppendingString: only works on strings shorter than 256 characters D. NSMutableString caches format strings between calls, but NSString does not
B. Each call allocates a new NSString and copies all previously accumulated characters into it, giving quadratic total cost, while appendString: grows the same buffer in place — Immutable concatenation must allocate and copy on every call since NSString can never be modified in place; NSMutableString extends the same underlying storage, so the total cost across many appends is close to linear instead of quadratic.
Why do NSString-typed properties conventionally use the copy attribute instead of strong? A. copy is required by the compiler for any object-typed property B. strong properties cannot hold NSString values at all C. Because NSMutableString is a subclass of NSString, a strong property could be assigned a mutable string and later be silently mutated by code outside the class; copy captures an immutable snapshot at assignment time instead D. copy makes the property thread-safe automatically
C. Because NSMutableString is a subclass of NSString, a strong property could be assigned a mutable string and later be silently mutated by code outside the class; copy captures an immutable snapshot at assignment time instead — Without copy, assigning an NSMutableString to a strong NSString property aliases the same mutable object, and later mutations by other code silently change the property's value.
Which NSMutableString method both removes a range of characters and inserts replacement text in a single operation? A. deleteCharactersInRange: B. insertString:atIndex: C. replaceCharactersInRange:withString: D. appendFormat:
C. replaceCharactersInRange:withString: — replaceCharactersInRange:withString: deletes the characters in the given range and inserts the replacement string in their place in one call, rather than requiring a separate delete followed by a separate insert.
For a small, fixed string built once (such as a static button title), which approach is most appropriate? A. Always use NSMutableString for every string in the codebase, regardless of size B. A plain NSString literal or a single stringWithFormat: call, since there is no incremental building for NSMutableString's in-place growth to optimize C. NSMutableString with a capacity of at least 1000 D. NSValue boxing the string's characters
B. A plain NSString literal or a single stringWithFormat: call, since there is no incremental building for NSMutableString's in-place growth to optimize — NSMutableString's performance advantage comes specifically from avoiding repeated reallocation during incremental edits. A one-shot fixed string has no such loop, so a plain NSString is simpler and equally fast.
What happens if you call [buffer deleteCharactersInRange:NSMakeRange(0, buffer.length + 10)] on an NSMutableString? A. It silently deletes only up to buffer.length characters B. It raises an NSRangeException at runtime because the range exceeds the string's current length C. It pads the string with null characters before deleting D. It is a compile-time error
B. It raises an NSRangeException at runtime because the range exceeds the string's current length — Just like NSString's range-based methods, NSMutableString's mutation methods require the range to fall within the current length, and an out-of-bounds range raises NSRangeException at runtime rather than being silently clamped.
Write a function that takes an NSArray of NSString names and returns a single NSMutableString formatted as a numbered list, e.g. "1. Alice\n2. Bob\n3. Carol", built using an NSMutableString buffer and appendFormat: inside a loop rather than repeated immutable concatenation.
NSMutableString *list = [NSMutableString string]; NSUInteger idx = 1; for (NSString *name in names) { [list appendFormat:@"%lu. %@\n", (unsigned long)idx, name]; idx++; } return list;
Track the current 1-based index yourself (or use enumerateObjectsUsingBlock: which supplies it), and call appendFormat:@"%d. %@\n" on the buffer for each name.