Streams
Learn Foundation's stream-based I/O model -- NSInputStream and NSOutputStream, how they're scheduled on a run loop and driven asynchronously via NSStreamDelegate's stream:handleEvent: callback, how to read and write through a fixed buffer, how file-backed and network-backed streams differ, and how to clean streams up correctly, including avoiding a dangling delegate callback after the owning object is gone.
The Data and File System lesson covered reading and writing whole files at once -- load a file into an NSData, or write an NSData out in a single call. Streams are Foundation's answer to the case that approach doesn't handle well: data that's too large to hold entirely in memory, or data that isn't all available yet because it's arriving incrementally over a network connection. NSInputStream and NSOutputStream model I/O as a sequence of discrete events -- bytes becoming available to read, space becoming available to write, the end of the data being reached, an error occurring -- rather than a single blocking call that hands you everything at once. Because those events happen asynchronously and unpredictably, streams are driven by exactly the same run-loop machinery the sibling Run Loops lesson covers, and this lesson leans on that directly: a stream is scheduled on a run loop, and a delegate object is notified, event by event, as the run loop processes it.
The Stream Model: Producers, Buffers, and Consumers
The mental model behind Foundation's streams is a pipeline with a fixed-size buffer sitting between a producer of bytes and a consumer of them. A file on disk, or a socket receiving data over a network, is the producer; your code, reading a chunk at a time into your own buffer and doing something with each chunk, is the consumer. The stream object itself is what sits in between, managing the underlying source or destination and telling you, via events, when there's something for you to do.
This is deliberately different from the whole-file NSData approach from the Data and File System lesson in one crucial respect: a stream never requires the entire producer's worth of data to exist in memory at once. You read (or write) a bounded chunk, process it, and read (or write) the next chunk, which is exactly what makes streams the right tool for files too large to comfortably load whole, or for network data that literally doesn't exist yet in its entirety at the moment you start reading.
NSInputStream models the reading half of this pipeline (bytes flowing from a producer into your code) and NSOutputStream models the writing half (bytes flowing from your code out to a destination) -- they are separate classes with separate delegate event sets, and a single logical connection (like a network socket) is commonly represented by a paired input stream and output stream working together.
• Streams model I/O as a pipeline: a producer, a fixed-size buffer, and a consumer -- not a single all-at-once read or write. • Unlike loading a whole file into NSData, a stream never needs the producer's entire data set to exist in memory at once. • NSInputStream is the read half (producer -> your code); NSOutputStream is the write half (your code -> destination). • A single network connection is commonly represented by one input stream and one output stream working as a pair.
Opening a File-Backed NSInputStream
The simplest concrete stream to reason about is a file-backed one: [NSInputStream inputStreamWithFileAtPath:] (or the NSURL-based equivalent) creates an input stream that reads from a file already on disk. Creating the stream object does not, by itself, start reading anything -- exactly like NSTimer from the sibling Timers lesson, a stream object describes a source and has to be both opened and, for its events to actually be delivered, scheduled on a run loop before anything happens.
-open must be called before a stream will do anything; calling it transitions the stream from its initial 'not open' status into an active status where it can begin producing events. For a file-backed stream this is a lightweight, effectively synchronous transition (opening a local file descriptor), but the same call is used uniformly for every kind of stream, including ones -- network streams, covered next -- where the underlying connection genuinely takes real, unpredictable time to establish.
Uniformity is the point: code that reads from a stream via its delegate doesn't need to know or care whether the bytes it's being handed came from a local file or a live network socket -- the event-driven read loop covered in the next two sections is identical either way, which is exactly why streams are a good abstraction to reach for when the same reading code might need to work against either kind of source.
• inputStreamWithFileAtPath: (or the NSURL equivalent) creates a file-backed NSInputStream -- creating it does not start reading. • -open transitions a stream from 'not open' into an active status where it can begin producing events; it must be called before anything happens. • The same open/schedule/read pattern applies uniformly whether the stream is file-backed or network-backed. • This uniformity is deliberate: delegate-driven reading code doesn't need to know or care what kind of source is behind the stream.
Scheduling a Stream on a Run Loop
Opening a stream is not enough by itself to receive events from it -- exactly as with NSTimer, a stream has to be scheduled on a run loop for that run loop to notice and deliver its events, via [stream scheduleInRunLoop:forMode:]. The overwhelmingly common call is [inputStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode], scheduling the stream on whichever thread's run loop is issuing the call, in the default mode.
This is the same run-loop-and-mode machinery the sibling Run Loops and Timers lessons both cover, applied to a third kind of run-loop client: a stream is, structurally, just another source a run loop can wait on and notify you about, alongside timers and other input sources. A stream scheduled on a background thread's run loop requires that thread to actually be running its run loop (via [[NSRunLoop currentRunLoop] run] or equivalent) for events to ever arrive, the identical requirement a background-scheduled NSTimer has.
Once scheduled and opened, the stream's delegate (set via the stream's -delegate property, and discussed in full in the next section) begins receiving stream:handleEvent: callbacks on that run loop's thread as bytes become available, as errors occur, or as the stream reaches its end -- the entire rest of a stream-driven read (or write) loop happens inside that delegate callback, not in a single linear block of code the way a whole-file read from the Data and File System lesson would.
• [stream scheduleInRunLoop:forMode:] is required for a run loop to deliver the stream's events -- opening alone is not enough. • The common pattern schedules on [NSRunLoop currentRunLoop] in NSDefaultRunLoopMode, on whichever thread issues the call. • A stream is, structurally, just another kind of source a run loop notifies about, alongside timers -- the same background-thread-needs-a-running-run-loop requirement from the Timers lesson applies here too. • Once scheduled, all further stream activity is reported through delegate callbacks on that run loop's thread, not through a single synchronous read call.
NSStreamDelegate and Asynchronous Stream Events
NSStreamDelegate defines exactly one required method, -stream:handleEvent:, called by the run loop whenever something notable happens to a scheduled, open stream. The event argument is an NSStreamEvent bitmask-style value, and the delegate's job is to branch on it and react appropriately -- there is no separate callback method per event; everything funnels through this single method.
NSStreamEventHasBytesAvailable is the event an NSInputStream delegate cares about most: it means at least one byte can currently be read without blocking, and the delegate should call -read:maxLength: to actually pull bytes into its own buffer (the read/buffer mechanics are covered in the next section). NSStreamEventEndEncountered means the producer has no more data -- for a file stream, the end of the file; for a network stream, the remote side closing its end of the connection -- and no more HasBytesAvailable events will follow; this is the delegate's cue to finish up and begin cleanup. NSStreamEventErrorOccurred means something went wrong (a file I/O error, a dropped network connection), and the delegate should consult the stream's own -streamError property for the underlying NSError and handle it, again followed by cleanup.
Two further events round out the full set: NSStreamEventOpenCompleted, fired once a stream (particularly a network stream, where opening genuinely takes time) has finished establishing its connection, and NSStreamEventHasSpaceAvailable, the NSOutputStream-side counterpart to HasBytesAvailable, meaning the destination is ready to accept more written bytes without blocking. A single stream:handleEvent: implementation commonly switches on all of the events relevant to whichever stream (input or output) it's attached to.
• stream:handleEvent: is NSStreamDelegate's one required method -- every event for a stream funnels through it, distinguished by the NSStreamEvent argument. • NSStreamEventHasBytesAvailable (input streams): bytes are ready to read without blocking -- call -read:maxLength: in response. • NSStreamEventEndEncountered: no more data will ever arrive -- the cue to finish and clean up; no further HasBytesAvailable events follow. • NSStreamEventErrorOccurred: something failed -- check -streamError for the NSError, then clean up. • NSStreamEventOpenCompleted (connection established) and NSStreamEventHasSpaceAvailable (output streams: ready to accept more writes) round out the full event set.
Reading Into a Buffer: -read:maxLength:
When NSStreamEventHasBytesAvailable fires, the delegate reads by calling [inputStream read:buffer maxLength:bufferSize], where buffer is a raw uint8_t * you own (commonly a fixed-size stack or heap array) and bufferSize is its capacity in bytes. The call reads as many bytes as are currently available, up to bufferSize, and returns the actual number of bytes read -- which can legitimately be less than bufferSize even when more data is still coming, since the whole point of the event-driven model is that you only ever get 'what's available right now,' not 'everything, guaranteed, in one call.'
The returned length has three meaningfully different cases to handle: a positive number means that many bytes were read successfully and should be processed (appended to an accumulating buffer, written out via a paired output stream, parsed incrementally, or whatever the use case calls for); zero means the stream has reached its actual end (functionally the same signal as an NSStreamEventEndEncountered event, and both commonly get checked); and a negative number means a read error occurred, at which point -streamError again holds the underlying NSError describing what went wrong.
Because HasBytesAvailable can fire repeatedly, with each fire only guaranteeing SOME bytes are ready rather than all of them, correct stream-reading code is inherently a loop across multiple delegate callbacks, accumulating data across calls, rather than a single read that's expected to get everything. This is the fundamental shape difference from the whole-file APIs in the Data and File System lesson: there, one call returns everything; here, the same logical read is spread across as many HasBytesAvailable events as it takes.
• -read:maxLength: fills a caller-owned buffer with as many bytes as are currently available, up to the buffer's size, and returns the byte count actually read. • A positive return means that many bytes were read and should be processed; zero means the stream has ended; a negative return means an error occurred (check -streamError). • Because each HasBytesAvailable event only guarantees SOME data is ready, reading correctly means accumulating across multiple delegate callbacks, not expecting one call to get everything. • This event-by-event accumulation is the core structural difference from the Data and File System lesson's whole-file NSData reads.
The Output Side: NSOutputStream and -write:maxLength:
NSOutputStream mirrors NSInputStream's shape exactly, with the direction reversed: it's created (for instance via [NSOutputStream outputStreamToFileAtPath:append:] for a file destination), opened, scheduled on a run loop the same way, and given a delegate that implements the same stream:handleEvent: method -- except the event it reacts to is NSStreamEventHasSpaceAvailable rather than HasBytesAvailable, meaning the destination is ready to accept more bytes without blocking.
Writing itself uses [outputStream write:buffer maxLength:bufferSize], where buffer holds the bytes you want written and bufferSize is how many of them to attempt, and it returns the number of bytes actually accepted -- which, exactly like reading, can be less than the full amount you offered, meaning the rest has to wait for the next HasSpaceAvailable event rather than being written in one shot. The same three-way return-value interpretation applies: positive means that many bytes were accepted, zero/negative signal end or error respectively (with -streamError again the source of the underlying NSError on failure).
A common real pattern pairs an input stream and output stream directly, transferring data from one to the other a chunk at a time -- reading whatever's available from the input stream's HasBytesAvailable event, buffering it, and writing it out to the output stream as its own HasSpaceAvailable events allow, which is exactly the shape a file-copy or a network-relay implementation built on raw streams takes.
• NSOutputStream mirrors NSInputStream in every structural respect, reacting to NSStreamEventHasSpaceAvailable instead of HasBytesAvailable. • -write:maxLength: attempts to write up to bufferSize bytes and returns how many were actually accepted -- which can be less than offered. • The same positive/zero/negative return-value meanings apply: bytes written, stream ended, or error (check -streamError). • A common pattern pairs an input and output stream directly, relaying data chunk by chunk as each side's own events allow.
File Streams vs. Network Streams
A file-backed stream's events are, in practice, close to immediate and predictable: opening a local file rarely stalls meaningfully, and HasBytesAvailable/HasSpaceAvailable fire in quick, steady succession as the local disk (or, more accurately, the OS's file-system cache) supplies or accepts data. Errors on a file stream are typically permission or missing-file issues, surfacing quickly at open time or shortly after.
A network-backed stream -- created, for a TCP connection, via [NSStream getStreamsToHostWithName:port:inputStream:outputStream:] or built from an NSURL/NSURLSession-driven pathway covered from a higher level in the URL and Networking lesson -- behaves very differently in practice even though it goes through the identical NSInputStream/NSOutputStream API. NSStreamEventOpenCompleted can take a genuinely unpredictable amount of time to arrive, since it depends on an actual network round trip; HasBytesAvailable events can arrive in irregular bursts as network packets land rather than in the smooth, predictable cadence a file stream shows; and NSStreamEventErrorOccurred is a real, expected possibility in ordinary operation (a dropped connection, a timeout, a DNS failure) rather than the edge case it mostly is for a local file.
The identical delegate-driven API covering both cases is precisely the abstraction's value: code written against stream:handleEvent: doesn't need a different code path for 'reading a file' versus 'reading from the network' -- the events mean the same thing either way, even though the timing characteristics behind them are wildly different. This is also exactly why the URL and Networking lesson's higher-level NSURLSession, for most application networking, is generally preferred over raw network streams: NSURLSession already handles the connection-establishment, retry, and error-surface complexity that raw stream-based networking code has to handle by hand.
• File streams: fast, predictable open and event timing; errors are mostly permission/missing-file issues surfacing quickly. • Network streams (NSStream getStreamsToHostWithName:port:inputStream:outputStream:): open can take real, unpredictable time; bytes arrive in irregular bursts; errors (dropped connections, timeouts) are an expected normal case, not an edge case. • The SAME NSInputStream/NSOutputStream + NSStreamDelegate API covers both -- code doesn't need a separate path per source type, only a mind for how differently the timing behaves. • For most application-level networking, the higher-level NSURLSession (URL and Networking lesson) is preferred over raw streams, which handles much of this connection complexity for you.
Cleanup: Closing, Unscheduling, and the Dangling-Delegate Trap
Proper stream cleanup, once you're done (whether from reaching NSStreamEventEndEncountered, handling an NSStreamEventErrorOccurred, or simply being finished early), is a three-step sequence: call -close on the stream, remove it from the run loop with -removeFromRunLoop:forMode: (the mirror image of the scheduling call from earlier in this lesson), and set the stream's -delegate property to nil. Skipping any one of these leaves something in an inconsistent state -- an unclosed file descriptor or socket, a run loop still holding a reference to a stream nobody's using, or, most dangerously, a delegate reference that outlives the object it points to.
The dangling-delegate case deserves its own emphasis because it's the stream-specific version of a mistake this course has flagged before for other delegate patterns: NSStreamDelegate references are traditionally weak/unretained by convention in older code and assign in newer code, meaning the stream does not keep its delegate alive. If the object acting as a stream's delegate is deallocated WITHOUT first setting the stream's -delegate to nil (and without closing/unscheduling the stream), the stream can still be scheduled and open, and a subsequent event can attempt to call stream:handleEvent: on an object that no longer exists -- a crash from calling a method on a deallocated object, showing up at a point in the code that looks completely unrelated to where the actual mistake (skipped cleanup) was made.
The safe pattern is therefore to treat stream cleanup as mandatory teardown code, structurally similar to the NSTimer -invalidate discipline from the sibling Timers lesson: whenever the object acting as a stream's delegate is about to go away, or whenever the stream itself is done being used, close it, unschedule it, and nil its delegate, in that order, every time -- not only on the success path, but on the error and early-cancellation paths as well.
• Proper cleanup is three steps: -close the stream, -removeFromRunLoop:forMode: to unschedule it, and set -delegate to nil. • NSStreamDelegate references are conventionally weak/unretained -- the stream does NOT keep its delegate alive. • If the delegate object deallocates without the stream's -delegate being nilled first, a later event can call stream:handleEvent: on a deallocated object -- a crash far from the actual root cause. • Treat close/unschedule/nil-delegate as mandatory teardown on every code path (success, error, and early cancellation alike), not just the happy path.
Code Examples
Creating the stream, setting its delegate, scheduling it on the current run loop, and opening it are all separate steps, and every one of them is required before events will actually arrive -- skip the scheduling call and -open alone will not deliver a single stream:handleEvent: callback, since nothing is pumping the run loop for this particular source.
[inputStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
Every stream event, of any kind, arrives through this one method -- the delegate's job is entirely to branch on eventCode. Note that both the end-of-stream and error cases funnel into the same -cleanUpStream: helper, since both are terminal states that require the same close/unschedule/nil-delegate teardown covered later in this lesson.
case NSStreamEventHasBytesAvailable: [self readAvailableBytesFrom:(NSInputStream *)stream]; break;
buffer is a fixed 4KB stack array reused on every call -- it does NOT grow to hold the whole file; instead, each chunk read is appended onto self.accumulatedData (an NSMutableData) so the full contents build up incrementally across as many HasBytesAvailable events as it takes. This loop-across-events structure is the defining difference from a one-shot whole-file NSData read.
NSInteger bytesRead = [inputStream read:buffer maxLength:sizeof(buffer)];
-write:maxLength: can accept fewer bytes than offered, exactly mirroring how -read:maxLength: can return fewer bytes than requested -- -markChunkWritten: is responsible for tracking how much of the current chunk actually went through, so the remainder is retried on the next HasSpaceAvailable event rather than being silently dropped.
NSInteger bytesWritten = [outputStream write:chunk.bytes maxLength:chunk.length];
All three steps matter: -close releases the underlying file descriptor or socket, -removeFromRunLoop:forMode: stops the run loop from holding and servicing this stream, and nilling -delegate is what prevents a dangling reference from being called into after the delegate object itself is later deallocated -- omitting the delegate-nilling step is exactly the mistake covered next.
stream.delegate = nil;
NSInputStream *inputStream =
[NSInputStream inputStreamWithFileAtPath:@"/path/to/large-file.dat"];
inputStream.delegate = self;
[inputStream scheduleInRunLoop:[NSRunLoop currentRunLoop]
forMode:NSDefaultRunLoopMode];
[inputStream open];- (void)stream:(NSStream *)stream handleEvent:(NSStreamEvent)eventCode {
switch (eventCode) {
case NSStreamEventOpenCompleted:
NSLog(@"Stream opened");
break;
case NSStreamEventHasBytesAvailable:
[self readAvailableBytesFrom:(NSInputStream *)stream];
break;
case NSStreamEventEndEncountered:
NSLog(@"Stream reached end -- cleaning up");
[self cleanUpStream:stream];
break;
case NSStreamEventErrorOccurred:
NSLog(@"Stream error: %@", stream.streamError);
[self cleanUpStream:stream];
break;
default:
break;
}
}@property (nonatomic, strong) NSMutableData *accumulatedData;
- (void)readAvailableBytesFrom:(NSInputStream *)inputStream {
uint8_t buffer[4096];
NSInteger bytesRead = [inputStream read:buffer maxLength:sizeof(buffer)];
if (bytesRead > 0) {
[self.accumulatedData appendBytes:buffer length:bytesRead];
} else if (bytesRead < 0) {
NSLog(@"Read error: %@", inputStream.streamError);
[self cleanUpStream:inputStream];
}
// bytesRead == 0 means end-of-stream, which NSStreamEventEndEncountered
// will also report separately -- no special handling needed here.
}- (void)stream:(NSStream *)stream handleEvent:(NSStreamEvent)eventCode {
if (eventCode == NSStreamEventHasSpaceAvailable) {
NSOutputStream *outputStream = (NSOutputStream *)stream;
NSData *chunk = [self nextChunkToWrite];
if (chunk.length > 0) {
NSInteger bytesWritten =
[outputStream write:chunk.bytes maxLength:chunk.length];
if (bytesWritten < 0) {
NSLog(@"Write error: %@", outputStream.streamError);
[self cleanUpStream:outputStream];
} else {
[self markChunkWritten:bytesWritten];
}
}
}
}- (void)cleanUpStream:(NSStream *)stream {
[stream close];
[stream removeFromRunLoop:[NSRunLoop currentRunLoop]
forMode:NSDefaultRunLoopMode];
stream.delegate = nil;
}The Stream Pipeline
A stream sits between a producer (a file or a network connection) and a consumer (your code), moving data through a fixed-size buffer a chunk at a time rather than requiring the whole data set to exist in memory at once.
Producer
↓
Input Stream
↓
Buffer
↓
ConsumerWhere Raw Streams Are Actually Used Today
Most everyday file reading uses the whole-file APIs from the Data and File System lesson, and most everyday networking uses the higher-level NSURLSession from the URL and Networking lesson -- both deliberately hide the raw stream machinery this lesson covers behind a simpler, higher-level interface for the common case. Raw NSInputStream/NSOutputStream still shows up directly when data genuinely must be processed incrementally without ever holding it all in memory (very large file transfers or transformations), when building lower-level networking primitives that NSURLSession itself doesn't expose (custom protocol handling over a raw TCP socket via getStreamsToHostWithName:port:inputStream:outputStream:), and inside the implementation of higher-level frameworks -- understanding streams directly is what makes clear why those higher-level APIs exist and what tradeoffs they're smoothing over on your behalf.
• (void)dealloc { // No stream cleanup at all -- self was set as the stream's delegate // earlier and is simply being deallocated now. } - (void)dealloc { [self.inputStream close]; [self.inputStream removeFromRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode]; self.inputStream.delegate = nil; }
NSStreamDelegate references are conventionally weak/unretained, so the stream does not keep self alive -- but if self deallocates while the stream is still open and scheduled, without nilling the stream's delegate first, a later event can call stream:handleEvent: on memory that no longer holds a valid object, crashing at a point in the code far removed from the actual missing-cleanup root cause.
NSInteger bytesRead = [inputStream read:buffer maxLength:sizeof(buffer)]; [self.accumulatedData appendBytes:buffer length:sizeof(buffer)]; // wrong length!
NSInteger bytesRead = [inputStream read:buffer maxLength:sizeof(buffer)]; if (bytesRead > 0) { [self.accumulatedData appendBytes:buffer length:bytesRead]; } -read:maxLength: returns the ACTUAL number of bytes read, which is commonly less than the buffer's full capacity -- appending sizeof(buffer) bytes regardless appends garbage/stale data left over from a previous read past whatever was actually just filled in, corrupting the accumulated result.
What must happen before an opened, scheduled NSInputStream will actually deliver stream:handleEvent: callbacks to its delegate? A. Nothing further -- opening alone is sufficient B. The stream must be scheduled on a run loop with scheduleInRunLoop:forMode:, and that run loop must actually be running C. The delegate must call -read:maxLength: first, before any event can fire D. The stream must be converted to an NSData first
B. The stream must be scheduled on a run loop with scheduleInRunLoop:forMode:, and that run loop must actually be running — A stream, like an NSTimer, only delivers events while it's scheduled on a run loop that's actively running and processing the mode it was scheduled in -- opening it is necessary but not sufficient on its own.
What does a positive return value from -read:maxLength: mean? A. An error occurred, and the value is an error code B. That many bytes were successfully read into the buffer and should be processed C. The stream has reached its end D. The stream needs to be reopened
B. That many bytes were successfully read into the buffer and should be processed — A positive return is the count of bytes actually read into the buffer (which can be less than the buffer's full capacity). Zero means end-of-stream; a negative value means an error occurred, retrievable via -streamError.
Why is correct stream-reading code structured as accumulation across multiple NSStreamEventHasBytesAvailable events, rather than one single read? A. Because -read:maxLength: always returns exactly the full requested length, so multiple reads are only for style B. Because each HasBytesAvailable event only guarantees SOME data is currently available, not the entire remaining data -- correctness requires accumulating across as many events as it takes C. Because NSInputStream limits every stream to a fixed total number of bytes D. Because file streams require multiple reads but network streams never do
B. Because each HasBytesAvailable event only guarantees SOME data is currently available, not the entire remaining data -- correctness requires accumulating across as many events as it takes — The event-driven model deliberately reports 'some data is ready now,' not 'all data is ready' -- a HasBytesAvailable event can fire many times before the stream ends, and each firing may only supply part of the total, so a correct reader accumulates results across calls.
Why is nilling a stream's -delegate property, in addition to closing and unscheduling it, an important part of cleanup? A. It's purely stylistic and has no functional effect B. NSStreamDelegate references are conventionally weak/unretained, so the stream doesn't keep the delegate alive; if the delegate deallocates without being nilled first, a later stream event can call a method on a deallocated object and crash C. Nilling the delegate is what actually closes the underlying file descriptor or socket D. It prevents the stream object itself from ever being deallocated
B. NSStreamDelegate references are conventionally weak/unretained, so the stream doesn't keep the delegate alive; if the delegate deallocates without being nilled first, a later stream event can call a method on a deallocated object and crash — Because the stream does not retain its delegate, a delegate object that deallocates without first being nilled out of the stream's -delegate property leaves a dangling reference -- a subsequent event delivery calls stream:handleEvent: on memory that no longer holds a valid object.
What is the key practical difference between a file-backed stream and a network-backed stream, given that both use the identical NSInputStream/NSOutputStream API? A. Network streams cannot use NSStreamDelegate at all B. File streams require a different read method than network streams C. Timing and reliability: file-stream events are fast and predictable, while network-stream open times and byte arrival are unpredictable, and NSStreamEventErrorOccurred is a normal, expected possibility rather than a rare edge case D. Network streams never report NSStreamEventEndEncountered
C. Timing and reliability: file-stream events are fast and predictable, while network-stream open times and byte arrival are unpredictable, and NSStreamEventErrorOccurred is a normal, expected possibility rather than a rare edge case — The delegate-driven API is identical for both, but their real-world timing characteristics differ substantially: a file stream's events are fast and predictable, while a network stream's connection establishment and byte delivery depend on genuinely unpredictable network conditions, and errors (dropped connections, timeouts) are an expected, ordinary occurrence.
Sketch the stream:handleEvent: implementation for an object that copies a large file from one path to another using a paired NSInputStream and NSOutputStream: read a chunk when the input stream reports NSStreamEventHasBytesAvailable, buffer it, and write it out when the output stream reports NSStreamEventHasSpaceAvailable, cleaning up both streams correctly when either reaches its end or errors.
• (void)stream:(NSStream *)stream handleEvent:(NSStreamEvent)eventCode {\n if (stream == self.inputStream && eventCode == NSStreamEventHasBytesAvailable) {\n uint8_t buf[4096];\n NSInteger n = [self.inputStream read:buf maxLength:sizeof(buf)];\n if (n > 0) { [self.pendingData appendBytes:buf length:n]; }\n } else if (stream == self.outputStream && eventCode == NSStreamEventHasSpaceAvailable) {\n if (self.pendingData.length > 0) {\n NSInteger written = [self.outputStream write:self.pendingData.bytes maxLength:self.pendingData.length];\n if (written > 0) { [self.pendingData replaceBytesInRange:NSMakeRange(0, written) withBytes:NULL length:0]; }\n }\n } else if (eventCode == NSStreamEventEndEncountered || eventCode == NSStreamEventErrorOccurred) {\n [self cleanUpStream:self.inputStream];\n [self cleanUpStream:self.outputStream];\n }\n}\n\nBoth streams get closed/unscheduled/delegate-nilled together on any terminal event for either one, since a copy operation is only meaningfully finished (successfully or not) as a pair.
One delegate method typically has to distinguish which stream (input or output) is calling it, since both streams share the same stream:handleEvent: signature but need different handling. Cleanup for the whole copy operation should close/unschedule/nil-delegate BOTH streams together, not just whichever one triggered the terminal event.