In Chapter 26, you used an NSURLConnection to fetch data from a web server. This connection was synchronous – all the data was delivered at one time. It worked fine, but there are two problems with a synchronous connection:
For these reasons, it is more common to use an NSURLConnection asynchronously. In an asynchronous connection, the data comes in chunks rather than all at once. This means that there are connection-related events that you must be ready to respond to. Some examples of connection-related events are a chunk of data arrives, the web server demands credentials, and the connection fails.
To manage this more complex connection, you must give it a helper object. In the helper object, you implement the methods to be executed in response to different connection-related events.
In your Callbacks program, you are going to use an asynchronous NSURLConnection to fetch data from a website. The instance of BNRLogger will serve as the NSURLConnection’s helper object. More specifically, the BNRLogger will be the delegate of the NSURLConnection.
In main.m, create an NSURL and an NSURLRequest like you did in Chapter 26. Then create an NSURLConnection and set the instance of BNRLogger to be its delegate:
#import <Foundation/Foundation.h> #import "BNRLogger.h" int main (int argc, const char * argv[]) { @autoreleasepool { BNRLogger *logger = [[BNRLogger alloc] init]; NSURL *url = [NSURL URLWithString: @"http://www.gutenberg.org/cache/epub/205/pg205.txt"]; NSURLRequest *request = [NSURLRequest requestWithURL:url]; __unused NSURLConnection *fetchConn = [[NSURLConnection alloc] initWithRequest:request delegate:logger startImmediately:YES]; __unused NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:2.0 target:logger selector:@selector(updateLastTime:) userInfo:nil repeats:YES]; [[NSRunLoop currentRunLoop] run]; } return 0; }
Now, in BNRLogger, you need to implement the callback methods – the methods to be executed in response to specific events.
You do not come up with or declare these methods yourself. They have already been declared in a protocol. A protocol is a list of method declarations. You will learn more about protocols in Chapter 29, but for now, think of a protocol as a prearranged set of messages that an object can send its helper object.
In BNRLogger.h, declare that BNRLogger will implement methods from the NSURLConnectionDelegate and NSURLConnectionDataDelegate protocols:
#import <Foundation/Foundation.h> @interface BNRLogger : NSObject <NSURLConnectionDelegate, NSURLConnectionDataDelegate> @property (nonatomic) NSDate *lastTime; - (NSString *)lastTimeString; - (void)updateLastTime:(NSTimer *)t; @end
There are three messages that BNRLogger will need to respond to as the delegate of the NSURLConnection. Two are from the NSURLConnectionDataDelegate protocol:
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data; - (void)connectionDidFinishLoading:(NSURLConnection *)connection;
The other is from the NSURLConnectionDelegate protocol:
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error;
(How do you know what methods a protocol has and which ones you should implement? Go to the developer documentation. Protocols have references, similar to class references, with information about their methods. You will also learn more about protocols and their methods in Chapter 29.)
Before you get to implementing these methods, BNRLogger needs a new instance variable. When you created a synchronous NSURLConnection in Chapter 26, you used an instance of NSData to hold the bytes coming from the server. In an asynchronous connection, you need an instance of NSMutableData. As the chunks of data arrive, you will add them to this object.
In BNRLogger.h, add an NSMutableData instance variable:
#import <Foundation/Foundation.h> @interface BNRLogger : NSObject <NSURLConnectionDelegate, NSURLConnectionDataDelegate> { NSMutableData *_incomingData; } @property (nonatomic) NSDate *lastTime; - (NSString *)lastTimeString; - (void)updateLastTime:(NSTimer *)t; @end
In BNRLogger.m, implement the three protocol methods:
#import "BNRLogger.h" @implementation BNRLogger ... // Called each time a chunk of data arrives - (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data { NSLog(@"received %lu bytes", [data length]); // Create a mutable data if it does not already exist if (!_incomingData) { _incomingData = [[NSMutableData alloc] init]; } [_incomingData appendData:data]; } // Called when the last chunk has been processed - (void)connectionDidFinishLoading:(NSURLConnection *)connection { NSLog(@"Got it all!"); NSString *string = [[NSString alloc] initWithData:_incomingData encoding:NSUTF8StringEncoding]; _incomingData = nil; NSLog(@"string has %lu characters", [string length]); // Uncomment the next line to see the entire fetched file // NSLog(@"The whole string is %@", string); } // Called if the fetch fails - (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error { NSLog(@"connection failed: %@", [error localizedDescription]); _incomingData = nil; } @end
Build and run the program. You will see the data coming from the web server in chunks. Eventually, the BNRLogger will be informed that the fetch is complete.
Here are the rules, so far, for callbacks: When sending one callback to one object, Apple uses target-action. When sending an assortment of callbacks to one object, Apple uses a helper object with a protocol. These helper objects are typically called delegates or data sources.
What if the callback needs to go to multiple objects?