Strings, Numbers and Values
|
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. |
Objective-C has no built-in string or collection types — they come from Foundation, as ordinary classes.
That is why a string is NSString * (a pointer to an object) rather than a primitive, why == does not
compare text, and why almost every value type has an immutable base class and a mutable subclass.
NSString
Creating Strings
NSString *literal = @"Hello"; // compile-time constant object
NSString *fmt = [NSString stringWithFormat:@"%@ has %ld items", name, (long)count];
NSString *joined = [@"Hello" stringByAppendingString:@", world"];
NSString *fromC = [NSString stringWithUTF8String:"a C string"];
NSString *repeat = [@"" stringByPaddingToLength:10 withString:@"-" startingAtIndex:0];
NSError *error = nil;
NSString *fromFile = [NSString stringWithContentsOfURL:url
encoding:NSUTF8StringEncoding
error:&error];
NSString is immutable: every "modifying" method returns a new string. That is why they are all named
stringBy….
Format Specifiers
stringWithFormat: and NSLog take C’s printf specifiers plus %@ for objects:
| Specifier | For |
|---|---|
|
Any object — sends it |
|
|
|
|
|
|
|
|
|
A pointer |
|
A C string ( |
|
A literal |
NSUInteger n = [array count];
NSLog(@"%lu items", (unsigned long)n); // cast: NSUInteger's width varies by platform
Mismatching a specifier is undefined behaviour, not merely wrong output. -Wall -Wextra catches most cases;
never pass user-controlled text as the format argument (NSLog(userInput) is a format-string vulnerability — write NSLog(@"%@", userInput)).
Comparison
NSString *a = @"hello";
NSString *b = [NSString stringWithFormat:@"hel%@", @"lo"];
a == b; // NO -- pointer identity, do not use
[a isEqualToString:b]; // YES -- the correct test
[a isEqual:b]; // YES -- generic, slightly slower
[a compare:b]; // NSOrderedSame / Ascending / Descending
[a caseInsensitiveCompare:@"HELLO"]; // NSOrderedSame
[a compare:b options:NSNumericSearch]; // "file9" < "file10"
[a localizedStandardCompare:b]; // Finder-style, locale-aware -- use for user-visible sorting
[a hasPrefix:@"he"]; // YES
[a containsString:@"ell"]; // YES
For anything shown to a user, prefer the localized… variants: string ordering is locale-dependent, and
compare: alone gives a fixed Unicode-codepoint ordering that reads as wrong in many languages.
Ranges and Substrings
NSRange is a plain C struct of location and length:
NSString *s = @"Hello, world";
NSRange r = [s rangeOfString:@"world"];
if (r.location != NSNotFound) { // the mandatory check
NSString *found = [s substringWithRange:r]; // @"world"
}
[s substringToIndex:5]; // @"Hello"
[s substringFromIndex:7]; // @"world"
[s stringByReplacingOccurrencesOfString:@"world" withString:@"there"];
[s componentsSeparatedByString:@", "]; // @[@"Hello", @"world"]
[s stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
rangeOfString: returns {NSNotFound, 0} when there is no match — always test location !=
NSNotFound before using the range; passing NSNotFound to substringWithRange: raises an exception.
An important subtlety: NSString indexes count UTF-16 code units, not user-perceived characters. An emoji
or a combining sequence spans several units, so slicing at an arbitrary index can split a character. For
user-facing text operations, use enumerateSubstringsInRange:options:usingBlock: with
NSStringEnumerationByComposedCharacterSequences:
[s enumerateSubstringsInRange:NSMakeRange(0, s.length)
options:NSStringEnumerationByComposedCharacterSequences
usingBlock:^(NSString *sub, NSRange r, NSRange er, BOOL *stop) {
NSLog(@"character: %@", sub);
}];
NSMutableString
NSMutableString *m = [NSMutableString stringWithString:@"Hello"];
[m appendString:@", world"];
[m appendFormat:@" (%ld)", (long)count];
[m insertString:@">> " atIndex:0];
[m replaceOccurrencesOfString:@"o" withString:@"0"
options:0 range:NSMakeRange(0, m.length)];
[m deleteCharactersInRange:NSMakeRange(0, 3)];
NSString *frozen = [m copy]; // an immutable snapshot
Because a mutable string can be handed to you and then changed behind your back, any property holding one
should be declared copy — see
Properties and Encapsulation.
Encodings
NSData *utf8 = [s dataUsingEncoding:NSUTF8StringEncoding];
NSString *back = [[NSString alloc] initWithData:utf8 encoding:NSUTF8StringEncoding];
const char *cstr = [s UTF8String]; // borrowed, autoreleased -- copy it to keep it
NSUInteger bytes = [s lengthOfBytesUsingEncoding:NSUTF8StringEncoding];
NSUTF8StringEncoding is the default choice. Note that [s length] (UTF-16 units) and
lengthOfBytesUsingEncoding: (bytes) are different numbers for any non-ASCII text.
NSNumber and Boxing
Foundation collections hold objects only, so scalars must be boxed. NSNumber is the wrapper, and the @
literals make it painless:
NSNumber *i = @42; // int
NSNumber *l = @42L; // long
NSNumber *d = @3.14; // double
NSNumber *b = @YES; // BOOL
NSNumber *c = @'x'; // char
NSNumber *e = @(count * 2); // any expression -- the parentheses form
// Unboxing
NSInteger back = [i integerValue];
double dd = [d doubleValue];
BOOL flag = [b boolValue];
NSArray *numbers = @[ @1, @2, @3 ]; // boxing is what makes this work
Comparison follows the same rule as strings — and here the trap is sharper, because small integers are often
tagged pointers for which == accidentally works:
[a isEqualToNumber:b]; // correct value comparison
[a compare:b]; // ordering
// a == b // do NOT rely on this
NSDecimalNumber, a subclass, provides exact base-10 arithmetic and is what currency calculations should use
instead of double.
NSValue
NSValue boxes non-object, non-number values — C structs, pointers, ranges:
NSValue *rangeValue = [NSValue valueWithRange:NSMakeRange(0, 5)];
NSRange r = [rangeValue rangeValue];
NSValue *pointValue = [NSValue valueWithCGPoint:CGPointMake(1, 2)]; // UIKit/AppKit convenience
NSValue *pointer = [NSValue valueWithPointer:somePointer];
// Any struct at all, via @encode:
MyStruct s = { … };
NSValue *any = [NSValue valueWithBytes:&s objCType:@encode(MyStruct)];
MyStruct out;
[any getValue:&out size:sizeof(out)];
NSArray *ranges = @[ rangeValue, pointValue ]; // now storable in a collection
NSNumber is in fact a subclass of NSValue specialised for scalars.
Other Core Value Types
NSData
An immutable byte buffer (NSMutableData for the mutable form):
NSData *data = [s dataUsingEncoding:NSUTF8StringEncoding];
NSUInteger len = data.length;
const void *bytes = data.bytes; // borrowed, valid while data lives
NSData *fromFile = [NSData dataWithContentsOfURL:url options:0 error:&error];
[data writeToURL:url options:NSDataWritingAtomic error:&error];
NSString *base64 = [data base64EncodedStringWithOptions:0];
NSDate
An absolute point in time — no time zone, no calendar, just seconds since a reference date:
NSDate *now = [NSDate date];
NSDate *later = [now dateByAddingTimeInterval:3600]; // NSTimeInterval is a double, in seconds
NSTimeInterval gap = [later timeIntervalSinceDate:now]; // 3600.0
if ([later compare:now] == NSOrderedDescending) { … }
// Formatting for display -- always locale-aware:
NSDateFormatter *df = [[NSDateFormatter alloc] init];
df.dateStyle = NSDateFormatterMediumStyle;
df.timeStyle = NSDateFormatterShortStyle;
NSString *text = [df stringFromDate:now];
Calendar arithmetic ("the first Monday of next month") belongs to NSCalendar and NSDateComponents, never
to adding multiples of 86400 to an NSDate. NSDateFormatter is expensive to create — cache it.
NSNull
The object that means "no value" inside a collection, because collections cannot store nil:
NSArray *withHole = @[ @"a", [NSNull null], @"c" ];
id value = withHole[1];
if (value == [NSNull null]) { // it is a singleton: == is correct here
NSLog(@"no value at this position");
}
JSON parsed by NSJSONSerialization uses NSNull for null, so any JSON-handling code must expect it.
NSURL
The right type for both file paths and network addresses — prefer it to NSString throughout:
NSURL *web = [NSURL URLWithString:@"https://developer.apple.com/documentation"];
NSURL *file = [NSURL fileURLWithPath:@"/tmp/data.json"];
web.scheme; // @"https"
web.host; // @"developer.apple.com"
web.path; // @"/documentation"
file.isFileURL; // YES
file.lastPathComponent;
NSURLComponents *c = [NSURLComponents componentsWithURL:web resolvingAgainstBaseURL:NO];
c.queryItems = @[ [NSURLQueryItem queryItemWithName:@"q" value:@"blocks"] ];
NSURL *withQuery = c.URL; // correct percent-encoding, handled for you
URLWithString: returns nil for a malformed string — check it. Build query strings with
NSURLComponents rather than by string concatenation, which gets percent-encoding wrong.
NSString versus C Strings
NSString * |
char * |
|
|---|---|---|
Nature |
Object, reference-counted |
Raw pointer to NUL-terminated bytes |
Literal |
|
|
Length |
|
|
Unicode |
Native |
Depends entirely on encoding |
Compare |
|
|
Collections |
Storable |
Not without boxing |
const char *c = [s UTF8String]; // NSString -> C (autoreleased, borrowed)
NSString *back = [NSString stringWithUTF8String:c]; // C -> NSString (copies)
UTF8String returns a buffer whose lifetime is tied to the autorelease pool — do not store it. If it
must outlive the statement, strdup it (and free it), or keep the NSString instead. Use NSString
everywhere in Objective-C code and convert at C API boundaries only.
Toll-Free Bridging
Several Foundation classes and Core Foundation types are the same objects at run time and can be cast
between directly — NSString/CFStringRef, NSArray/CFArrayRef, NSDictionary/CFDictionaryRef,
NSData/CFDataRef, NSNumber/CFNumberRef, NSDate/CFDateRef, NSURL/CFURLRef.
Under ARC the cast must state what happens to the ownership, because ARC manages Objective-C objects but not CF ones:
NSString *s = @"Hello";
// __bridge: no ownership transfer. ARC still owns s; do not CFRelease this.
CFStringRef cf = (__bridge CFStringRef)s;
CFIndex len = CFStringGetLength(cf);
// __bridge_retained: ARC hands ownership to you -- you must CFRelease it.
CFStringRef owned = (__bridge_retained CFStringRef)s;
CFRelease(owned);
// __bridge_transfer: you hand ownership to ARC -- do not CFRelease it.
CFStringRef created = CFStringCreateWithCString(NULL, "hi", kCFStringEncodingUTF8);
NSString *managed = (__bridge_transfer NSString *)created;
The rule of thumb: bridge_transfer for anything a Create/Copy function returned (CF’s ownership rules
say you own it), bridge for a borrowed reference you are just reading. The Objective-C spelling
CFBridgingRetain()/CFBridgingRelease() is equivalent and often clearer. Full detail is in
Automatic Reference Counting, and
the C-interop side in
Objective-C++ and C
Interoperability.
Not every pairing is bridged: NSRunLoop/CFRunLoopRef and NSBundle/CFBundleRef are not, despite the
matching names. Consult the documentation rather than assuming.
See Also
-
Collections and Fast Enumeration —
NSArray,NSDictionaryandNSSet, which hold these value types. -
Basic Syntax and Types —
nilversusNSNull, and the literal syntax. -
Automatic Reference Counting — the bridging casts in full.
-
Inheritance and Polymorphism — why
NSStringis a class cluster.