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, instancetype, Clang modules, lightweight generics, nullability and @available — as published at Apple Developer Documentation and the Clang Objective-C specifications, which are the references these pages are written and verified against.

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 description

%d / %u

int / unsigned int

%ld / %lu

long / unsigned long — cast NSInteger/NSUInteger to these

%f, %.2f

double

%zd

ssize_t; also correct for NSInteger on Apple platforms

%p

A pointer

%s

A C string (char *), not an NSString

%%

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

@"text"

"text"

Length

[s length] — UTF-16 units

strlen(s) — bytes

Unicode

Native

Depends entirely on encoding

Compare

isEqualToString:

strcmp

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