Data and the File System: Bytes, Files, and Reliability
Learn Foundation's model for raw binary data and file system access — NSData, NSMutableData, NSFileManager, NSURL, and file attributes — and understand the reliability patterns of atomic writes and error-returning APIs that production file I/O code depends on.
Everything you've studied so far — strings, numbers, collections — deals with structured, typed Objective-C objects. But underneath almost every file on disk, every network response, and every piece of persisted app state is just a sequence of raw bytes, and Foundation needs a way to represent that too. This lesson covers NSData and NSMutableData as the object representation of a raw byte buffer, NSFileManager as the primary API for interacting with the file system, NSURL as the modern way to identify files and resources, and the practical realities of reading, writing, and handling errors when your code talks to a file system that can fail in all sorts of legitimate ways a network call or an in-memory collection never has to worry about.
NSData and NSMutableData: bytes as an object
NSData is Foundation's representation of an immutable buffer of raw bytes — conceptually, it's the Objective-C equivalent of a byte array, wrapping a pointer and a length behind an object interface so bytes can be passed around, retained, and compared using the same conventions as any other Foundation object. It doesn't interpret its contents as text, numbers, or any particular structure; it's just bytes, which makes it the natural type for file contents, image data, network payloads, or any other binary blob whose internal structure is defined by something other than NSData itself (a file format, a serialization protocol, and so on).
NSMutableData is its mutable subclass, adding methods like appendData:, appendBytes:length:, and replaceBytesInRange:withBytes: so you can build up or modify a byte buffer incrementally — useful when you're accumulating a payload across multiple chunks, such as data arriving progressively from a network connection, rather than having the complete buffer available all at once.
You can inspect an NSData's raw contents via its bytes property (a const void * pointer to the underlying buffer) and its length property (the byte count), and you can construct one directly from an existing in-memory buffer with dataWithBytes:length:. In practice, though, the most common way an NSData comes into existence is by reading it from somewhere else entirely — a file on disk or a URL — which is where NSData intersects directly with the file system topics in the rest of this lesson.
NSFileManager: the primary file system API
NSFileManager is Foundation's general-purpose interface to the file system: checking whether something exists, creating and removing directories, listing what's inside a directory, and moving or copying files and directories from one location to another. [NSFileManager defaultManager] gives you a shared, thread-safe-for-most-uses instance suitable for the majority of file operations; you only need a separate instance if you're configuring a delegate to observe or intervene in specific operations like copy or move.
fileExistsAtPath: (and its variant fileExistsAtPath:isDirectory: that also tells you whether the item is a directory) is the basic existence check you'll reach for constantly before deciding whether to create, overwrite, or read something. createDirectoryAtPath:withIntermediateDirectories:attributes:error: creates a new directory, and passing YES for withIntermediateDirectories means it will also create any missing parent directories along the path, similar to mkdir -p at the command line, rather than failing if an intermediate directory doesn't already exist.
contentsOfDirectoryAtPath:error: (or its NSURL-based counterpart, contentsOfDirectoryAtURL:includingPropertiesForKeys:options:error:) lists what's directly inside a directory, returning an array of names or URLs for the immediate children — it does not recurse into subdirectories on its own, which is a common point of confusion for anyone expecting a full recursive listing by default; a genuinely recursive walk requires either calling this repeatedly yourself or using NSDirectoryEnumerator via enumeratorAtPath: or enumeratorAtURL:includingPropertiesForKeys:options:errorHandler:, which does walk the whole subtree.
URLs versus paths
Older Objective-C code, and some of NSFileManager's own older methods, represent file locations as plain NSString path strings (like "/Users/alex/Documents/notes.txt"). Modern Foundation code strongly prefers NSURL instead, even for purely local file locations, via file URLs like file:///Users/alex/Documents/notes.txt. NSURL is the more capable and more correct representation for several concrete reasons: it distinguishes a file URL from a remote URL (http://, https://) using the same type, so code that accepts 'a location to read from' can handle both uniformly; it correctly handles percent-encoding of special characters in a way raw path-string concatenation is easy to get wrong; and it carries richer metadata access through resourceValuesForKeys:error: (file size, creation date, whether it's a directory, and more) in a single typed call, rather than requiring a separate NSFileManager call as path-based APIs often do.
Foundation provides direct conversions between the two representations — [NSURL fileURLWithPath:] to go from a path string to a file URL, and a URL's .path property to go the other way — so you're never stuck with only one representation if some particular API you need to call only accepts the other. The general guidance, though, is to prefer constructing and passing NSURL values through your own code wherever you have the choice, converting to a path string only at the boundary of an API that specifically requires one.
Standard directories, briefly
Apps don't write files to arbitrary locations chosen at random; the platform defines a small set of standard directories with specific purposes, and NSFileManager's URLsForDirectory:inDomains: (or the convenience NSSearchPathForDirectoriesInDomains for the older path-string form) is how you locate them rather than hard-coding a path. The Documents directory is meant for user-generated content that should persist and, on some platforms, gets backed up automatically; the Caches directory is for regenerable data that improves performance but can be safely deleted by the system under storage pressure without asking; and a temporary directory (obtained via NSTemporaryDirectory() or, more precisely, via FileManager's url(for: .itemReplacementDirectory, ...) style APIs for scratch space tied to a specific operation) is for short-lived files that don't need to survive an app relaunch.
The deeper details of platform-specific sandboxing — exactly which directories an app can see, how permissions and entitlements interact with them, and how this varies across platforms — belong to a dedicated platform/security discussion rather than this lesson; the point to take away here is simply that you should always ask the system for the right standard directory's location rather than assuming or hard-coding a path, since the actual underlying location can vary between platform versions and sandbox configurations even though the semantic purpose (documents, caches, temporary) stays stable.
File attributes
Beyond a file's contents, NSFileManager exposes its metadata through attributesOfItemAtPath:error:, which returns an NSDictionary keyed by constants like NSFileSize, NSFileModificationDate, NSFilePosixPermissions, and NSFileType, letting you inspect a file's size, when it was last modified, its Unix-style permission bits, and whether it's a regular file, directory, or symbolic link, all without opening or reading the file's actual content. This is the API you reach for when you need to answer questions like 'has this file changed since I last read it,' 'is this file too large to load into memory,' or 'do I have permission to write here,' without paying the cost of reading the whole file just to find out.
setAttributesOfItemAtPath:attributes:error: lets you go the other direction and change certain attributes after the fact, such as adjusting permission bits or modification date. Both the read and write sides of file attributes follow the same error-returning convention as the rest of NSFileManager's API, which is worth internalizing early: nearly every meaningful NSFileManager operation can fail for reasons entirely outside your app's control, and the API design reflects that by requiring you to at least have the opportunity to check for and handle an error, rather than pretending file operations always succeed.
Reading and writing data
The most direct way to pull a file's entire contents into memory as an NSData is exactly the spec's example: NSData *data = [NSData dataWithContentsOfURL:url]; — this reads the file at that URL fully into memory and returns an NSData wrapping its bytes, or nil if the read failed for any reason (file missing, no permission, and so on). The equivalent NSMutableData class methods exist too, and NSData also offers dataWithContentsOfURL:options:error:, a more explicit overload that both accepts read options (like mapping the file into memory instead of copying it, via NSDataReadingMappedIfSafe, which can matter a great deal for very large files) and gives you an actual NSError describing what went wrong rather than just a bare nil.
Writing works symmetrically: writeToURL:atomically: (or the more modern writeToURL:options:error: with an NSDataWritingAtomic option flag) takes an NSData's contents and writes them out to a specified location. The plain dataWithContentsOfURL: / writeToURL:atomically: pair is convenient and fine for smaller files and simpler code paths, but for anything in production code where you actually need to know why a read or write failed — and, as covered in this lesson's final section, you almost always should — the error-returning variants are the better default.
Atomic writes: never leaving a corrupted file behind
Writing a file 'atomically' means the write either fully succeeds, leaving the destination file exactly as expected, or fully fails, leaving whatever file (or absence of a file) was there before the write started — there is no possible outcome where the destination file exists but contains only half of the new data. Foundation implements this, when you pass an atomic write option, by first writing the complete new content to a temporary file elsewhere on the same volume, and only once that write has fully succeeded, renaming that temporary file on top of the real destination path. A file system rename of this kind is itself an atomic operation at the file system level, so the moment of 'swap in the new content' can never be observed half-done, even if the app crashes, the device loses power, or the process is killed at the worst possible instant.
Without an atomic write, the far more dangerous alternative is to open the destination file directly and write new bytes into it in place; if anything interrupts that write partway through — a crash, a killed process, a full disk mid-write — the destination file is left in a corrupted, half-old-half-new (or worse, truncated and unreadable) state, and there's no way to recover the previous good version because it was already being overwritten. This is exactly why atomic writes matter far more than they might seem to at first glance: for any file whose corruption would actually hurt (a user's saved document, a database file, a critical settings file), paying the small extra cost of a temp-file-then-rename write is a cheap insurance policy against an entire category of data-loss bug that's otherwise very hard to reproduce and debug, because it only manifests on the rare unlucky crash-at-exactly-the-wrong-moment.
Security-scoped resources and error handling
On sandboxed platforms, an app generally cannot read arbitrary files outside its own container just because it has a URL pointing to one — a user has to explicitly grant access, typically by picking the file through a system file-picker UI, and the resulting URL is a security-scoped resource. Before actually reading from or writing to such a URL, code must bracket the access with startAccessingSecurityScopedResource (which returns a BOOL indicating whether access was actually granted) and stopAccessingSecurityScopedResource once done, since the elevated permission to reach outside the sandbox is only active between those two calls. This mechanism matters specifically wherever an app needs to work with a user-selected file living outside its own sandboxed container, and is less relevant to files the app created and manages entirely within its own directories, where the app already has unrestricted access.
The spec's explicit callout deserves direct emphasis here: production code should use the error-returning variants of file APIs — the ones ending in error:, taking an NSError ** out-parameter — rather than the simpler variants that just return nil or NO on failure with no explanation why. The reason is that file system operations fail for many entirely legitimate, common reasons that have nothing to do with a bug in your code: the user revoked a permission, the disk is full, another process moved or deleted the file out from under you, the volume was ejected, or the path simply doesn't exist anymore. Code that ignores these possibilities — treating every file operation as though it will always succeed — doesn't just produce a worse error message when something goes wrong; it can silently proceed as though an operation succeeded when it didn't, corrupting application state in ways that are much harder to diagnose than a clear, immediate, reported error would have been. Always check the returned BOOL or nil-ness of a file operation's result and inspect the accompanying NSError when it's offered, and surface something meaningful to the user or your logs rather than silently swallowing the failure.
Code Examples
This is the spec's exact reading example, extended slightly to show the check you must perform: dataWithContentsOfURL: returns nil on any failure (missing file, no permission, and so on) without telling you which. This convenience form is fine for quick reads where a generic failure message is acceptable, but note the following example's error-returning variant for anything where you need to know why the read failed. Expected result: Read 42 bytes (or the failure branch, depending on whether the file exists)
NSData *data = [NSData dataWithContentsOfURL:url]; — reads the entire file into memory as a byte buffer, or returns nil on any failure.
Both operations here use their error-returning variants rather than the bare nil/NO-returning convenience methods, so a real NSError is available describing exactly what went wrong — a missing source file versus a permissions problem versus a full disk are all distinguishable through readError or writeError rather than collapsing into an unexplained failure. The write additionally passes NSDataWritingAtomic, so destinationURL either ends up with the complete new payload or is left exactly as it was before — never a partially-written, corrupted file.
BOOL wroteOK = [payload writeToURL:destinationURL options:NSDataWritingAtomic error:&writeError]; — atomic option guarantees no partial write is ever observable at destinationURL.
Common mistake: Passing &readError and &writeError is required, not optional cosmetics: forgetting to check readError/writeError after a nil/NO result (or passing NULL for the error parameter and ignoring the return value) throws away exactly the information you need to distinguish a recoverable problem (retry, ask the user) from an unrecoverable one.
This locates the standard Caches directory through NSFileManager rather than hard-coding a path, appends a subdirectory, and creates it with withIntermediateDirectories:YES so any missing parent path segments are created automatically. attributesOfItemAtPath:error: then reads back metadata about the newly created directory without needing to read any file contents — every step consistently checks its error-returning result rather than assuming success. Expected result: Scratch dir size entry: (a small integer representing the directory entry's own size, e.g. 64 or 96)
NSURL *cacheDir = [[fm URLsForDirectory:NSCachesDirectory inDomains:NSUserDomainMask] firstObject]; — always ask the system for a standard directory's location rather than hard-coding one.
[data writeToFile:path atomically:NO]; // direct in-place overwrite of an important file
[data writeToURL:url options:NSDataWritingAtomic error:&error];
A non-atomic in-place write leaves the destination file in a corrupted, half-written state if the process is interrupted partway through. An atomic write guarantees the destination either ends up fully updated or entirely untouched, no matter when a crash or interruption happens.
NSData *data = [NSData dataWithContentsOfURL:url]; if (data == nil) { /* just skip silently */ }
NSError *error = nil; NSData *data = [NSData dataWithContentsOfURL:url options:0 error:&error]; if (data == nil) { NSLog(@"Read failed: %@", error); /* handle explicitly */ }
Silently swallowing a nil result throws away the reason the read failed and lets your code proceed as though the operation succeeded (or simply not proceed, with no diagnostic trail). Production code should use the error-returning variant and surface or log the actual NSError rather than treating failure as an unremarkable no-op.
NSString *path = @"/Users/alex/Documents/notes.txt"; // hard-coded absolute path
NSURL *docsURL = [[fm URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask] firstObject]; NSURL *fileURL = [docsURL URLByAppendingPathComponent:@"notes.txt"];
Hard-coding an absolute path assumes a fixed file system layout and a fixed username that won't hold across other users, other devices, or other platform configurations. Always locate standard directories through NSFileManager's directory APIs and build paths relative to that result.
What is the fundamental difference between NSData and NSMutableData? A. NSData stores text while NSMutableData stores binary bytes B. NSData is an immutable buffer of raw bytes; NSMutableData is its mutable subclass supporting in-place appends and modifications C. NSMutableData can only hold data read from the network, never from disk D. There is no real difference; they are interchangeable in every context
B. NSData is an immutable buffer of raw bytes; NSMutableData is its mutable subclass supporting in-place appends and modifications — NSData represents an immutable byte buffer, while NSMutableData adds methods like appendData: and replaceBytesInRange:withBytes: to build up or change the buffer's contents after creation — the same immutable/mutable split used throughout Foundation's collection classes.
Why does modern Objective-C code generally prefer NSURL over raw path strings for file locations? A. NSString cannot represent any file path longer than 255 characters B. NSURL uniformly represents local and remote locations, handles percent-encoding correctly, and exposes richer metadata access than manual path-string handling C. NSURL objects use significantly less memory than NSString objects D. Path strings were removed entirely from recent versions of Foundation
B. NSURL uniformly represents local and remote locations, handles percent-encoding correctly, and exposes richer metadata access than manual path-string handling — NSURL distinguishes file URLs from remote URLs using the same type, correctly handles special-character encoding that manual path concatenation easily gets wrong, and provides typed metadata access via resourceValuesForKeys:error:, making it the more robust and more capable representation.
How does Foundation typically implement an atomic file write? A. By locking the destination file so no other process can read it during the write B. By writing the new content to a temporary file first, then renaming that temp file onto the destination, since a file system rename is itself atomic C. By writing the file twice in a row to double-check correctness D. Atomic writes are not actually supported by Foundation's file APIs
B. By writing the new content to a temporary file first, then renaming that temp file onto the destination, since a file system rename is itself atomic — An atomic write completes the entire new content in a temporary file elsewhere on the same volume, then performs a rename onto the real destination path — since renaming is atomic at the file system level, the destination file can never be observed in a partially-written state.
Why should production code prefer error-returning file API variants (ending in error:) over the plain nil/NO-returning ones? A. Error-returning variants execute measurably faster in every case B. File operations can legitimately fail for many reasons outside the app's control (permissions, full disk, a file moved elsewhere), and the error-returning variant lets you distinguish and respond to the actual cause rather than getting an unexplained failure C. The plain variants have been fully removed from current Foundation and no longer compile D. Error-returning variants automatically retry the operation until it succeeds
B. File operations can legitimately fail for many reasons outside the app's control (permissions, full disk, a file moved elsewhere), and the error-returning variant lets you distinguish and respond to the actual cause rather than getting an unexplained failure — Because file system operations can fail for many legitimate reasons, ignoring the specific cause and just checking for nil/NO makes it impossible to respond appropriately (retry, prompt the user, log diagnostics) — the error-returning variants give you an actual NSError describing what went wrong.
What must bracket access to a security-scoped resource URL before reading or writing it? A. Nothing; security-scoped URLs behave identically to any other file URL B. startAccessingSecurityScopedResource before the access, and stopAccessingSecurityScopedResource once finished C. A call to [NSFileManager defaultManager] must be made exactly once at app launch D. The file must first be copied into the app's own sandbox before it can be touched at all
B. startAccessingSecurityScopedResource before the access, and stopAccessingSecurityScopedResource once finished — A security-scoped resource URL (typically one the user selected via a system file picker, pointing outside the app's own sandboxed container) requires bracketing actual file access between startAccessingSecurityScopedResource and stopAccessingSecurityScopedResource, since the elevated permission to reach that location is only active between those two calls.
NSURL *url = [NSURL fileURLWithPath:@"/Users/alex/Documents/notes.txt"];
NSData *data = [NSData dataWithContentsOfURL:url];
if (data != nil) {
NSLog(@"Read %lu bytes", (unsigned long)data.length);
} else {
NSLog(@"Failed to read file — no error detail available with this API");
}NSURL *sourceURL = [NSURL fileURLWithPath:@"/tmp/incoming.json"];
NSError *readError = nil;
NSData *payload = [NSData dataWithContentsOfURL:sourceURL
options:NSDataReadingMappedIfSafe
error:&readError];
if (payload == nil) {
NSLog(@"Read failed: %@", readError.localizedDescription);
return;
}
NSURL *destinationURL = [NSURL fileURLWithPath:@"/tmp/saved.json"];
NSError *writeError = nil;
BOOL wroteOK = [payload writeToURL:destinationURL
options:NSDataWritingAtomic
error:&writeError];
if (!wroteOK) {
NSLog(@"Write failed: %@", writeError.localizedDescription);
}NSFileManager *fm = [NSFileManager defaultManager];
NSURL *cacheDir = [[fm URLsForDirectory:NSCachesDirectory inDomains:NSUserDomainMask] firstObject];
NSURL *scratchDir = [cacheDir URLByAppendingPathComponent:@"scratch"];
NSError *dirError = nil;
BOOL created = [fm createDirectoryAtURL:scratchDir
withIntermediateDirectories:YES
attributes:nil
error:&dirError];
if (!created && dirError.code != NSFileWriteFileExistsError) {
NSLog(@"Could not create scratch dir: %@", dirError.localizedDescription);
}
NSError *attrError = nil;
NSDictionary *attrs = [fm attributesOfItemAtPath:scratchDir.path error:&attrError];
if (attrs != nil) {
NSLog(@"Scratch dir size entry: %@", attrs[NSFileSize]);
}