-
-
Save couchdeveloper/5764723 to your computer and use it in GitHub Desktop.
// | |
// SimpleGetHTTPRequest.h | |
// | |
#import <Foundation/Foundation.h> | |
typedef void (^completionHandler_t) (id result); | |
@interface SimpleGetHTTPRequest : NSObject | |
/** | |
Initializes the receiver | |
Parameter `url` is the url for the resource which will be loaded. The url’s | |
scheme must be `http` or `https`. | |
*/ | |
- (id)initWithURL:(NSURL*)url; | |
/** | |
Start the asynchronous HTTP request. | |
This can be executed only once, that is if the receiver has already been | |
started, it will have no effect. | |
*/ | |
- (void) start; | |
/** | |
Cancels a running operation at the next cancelation point and returns | |
immediately. | |
`cancel` may be send to the receiver from any thread and multiple times. | |
The receiver's completion block will be called once the receiver will | |
terminate with an error code indicating the cancellation. | |
If the receiver is already cancelled or finished the message has no effect. | |
*/ | |
- (void) cancel; | |
@property (nonatomic, readonly) BOOL isCancelled; | |
@property (nonatomic, readonly) BOOL isExecuting; | |
@property (nonatomic, readonly) BOOL isFinished; | |
/** | |
Set or retrieves the completion handler. | |
The completion handler will be invoked when the connection terminates. If the | |
request was sucessful, the parameter `result` of the block will contain the | |
response body of the GET request, otherwise it will contain a NSError object. | |
The execution context is unspecified. | |
Note: the completion handler is the only means to retrieve the final result of | |
the HTTP request. | |
*/ | |
@property (nonatomic, copy) completionHandler_t completionHandler; | |
@end | |
// | |
// SimpleGetHTTPRequest.m | |
// | |
#import "SimpleGetHTTPRequest.h" | |
@interface SimpleGetHTTPRequest () <NSURLConnectionDelegate, NSURLConnectionDataDelegate> | |
@property (nonatomic, readwrite) BOOL isCancelled; | |
@property (nonatomic, readwrite) BOOL isExecuting; | |
@property (nonatomic, readwrite) BOOL isFinished; | |
@property (nonatomic) NSURL* url; | |
@property (nonatomic) NSMutableURLRequest* request; | |
@property (nonatomic) NSURLConnection* connection; | |
@property (nonatomic) NSMutableData* responseData; | |
@property (nonatomic) NSHTTPURLResponse* lastResponse; | |
@property (nonatomic) NSError* error; | |
@end | |
@implementation SimpleGetHTTPRequest | |
@synthesize isCancelled = _isCancelled; | |
@synthesize isExecuting = _isExecuting; | |
@synthesize isFinished = _isFinished; | |
@synthesize url = _url; | |
@synthesize request = _request; | |
@synthesize connection = _connection; | |
@synthesize responseData = _responseData; | |
@synthesize lastResponse = _lastResponse; | |
@synthesize error = _error; | |
- (id)initWithURL:(NSURL*)url { | |
NSParameterAssert(url); | |
// TODO: url's scheme shall be http or https | |
self = [super init]; | |
if (self) { | |
_url = url; | |
} | |
return self; | |
} | |
- (void) dealloc { | |
} | |
- (void) terminate { | |
NSAssert([NSThread currentThread] == [NSThread mainThread], @"not executing on main thread"); | |
if (_isFinished) | |
return; | |
completionHandler_t onCompletion = self.completionHandler; | |
id result = self.error ? self.error : self.responseData; | |
if (onCompletion) { | |
dispatch_async(dispatch_get_global_queue(0, 0), ^{ | |
onCompletion(result); | |
}); | |
}; | |
self.completionHandler = nil; | |
self.connection = nil; | |
self.isExecuting = NO; | |
self.isFinished = YES; | |
} | |
- (void) start { | |
// ensure the start method is executed on the main thread: | |
if ([NSThread currentThread] != [NSThread mainThread]) { | |
[self performSelectorOnMainThread:@selector(start) withObject:nil waitUntilDone:NO]; | |
return; | |
} | |
// bail out if the receiver has already been started or cancelled: | |
if (_isCancelled || _isExecuting || _isFinished) { | |
return; | |
} | |
self.isExecuting = YES; | |
self.request = [[NSMutableURLRequest alloc] initWithURL:_url]; | |
self.connection = [[NSURLConnection alloc] initWithRequest:self.request delegate:self startImmediately:NO]; | |
if (self.connection == nil) { | |
self.error = [NSError errorWithDomain:@"SimpleGetHTTPRequest" | |
code:-2 | |
userInfo:@{NSLocalizedDescriptionKey:@"Couldn't create NSURLConnection"}]; | |
[self terminate]; | |
return; | |
} | |
[self.connection scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes]; | |
[self.connection start]; | |
} | |
- (void) cancel { | |
NSError* reason = [NSError errorWithDomain:@"SimpleGetHTTPRequest" | |
code:-1 | |
userInfo:@{NSLocalizedDescriptionKey:@"cancelled"}]; | |
[self cancelWithReason:reason sender:nil]; | |
} | |
- (void) cancelWithReason:(id)reason sender:(id)sender { | |
// Accessing ivars must be synchronized! Access also occures in the delegate | |
// methods, which run on the main thread. Thus we simply use the main thread | |
// to synchronize access: | |
dispatch_async(dispatch_get_main_queue(), ^{ | |
if (_isCancelled || _isFinished) { | |
return; | |
} | |
self.error = reason; | |
[self.connection cancel]; | |
[self terminate]; | |
}); | |
} | |
#pragma mark - NSURLConnectionDelegate | |
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error { | |
self.error = error; | |
[self terminate]; | |
} | |
#pragma mark - NSURLConnectionDataDelegate | |
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response { | |
assert([response isKindOfClass:[NSHTTPURLResponse class]]); | |
// A real implementation should check the HTTP status code here and | |
// possibly other response properties like the content-type, and then | |
// branch to corresponding actions. Here, our "action" -- call it | |
// "response handler" -- will just accumulate the incomming data into | |
// the NSMutableData object `responseData`. | |
// | |
// A GET request really only succeeds when the status code is 200 (OK), | |
// except redirection responses and authentication challenges, which | |
// are handled elsewhere. | |
// | |
// Any other response is likely an error. When we didn't get a 200 (OK) | |
// we shouldn't terminated the connection, though. Rather we retrieve | |
// the response data - if any - since this may contain valuable error | |
// information - possibly other MIME type than requested. | |
// Note: usually, status codes in the range 200 to 299 are considered a | |
// succesful HTTP response. However, depending on the client needs, a | |
// successful request may only allow status code 200 (OK). | |
// | |
// Redirect repsonses (3xx) and authentication challenges are handled | |
// by the underlaying NSURLConnection and possibly invoke other corres- | |
// ponding delegate methods and do not show up here. | |
// For a GET request, we are fine just doing this: | |
self.responseData = [[NSMutableData alloc] initWithCapacity:1024]; | |
self.lastResponse = (NSHTTPURLResponse*)response; | |
} | |
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data { | |
// Here, we use the most simplistic approach to handle the received data: | |
// we accumulating the data chunks into a NSMutableData object. | |
// This approach becomes problematic when the size of the data will become | |
// large. Alterntative approaches are for example: | |
// - save the data into a temporary file | |
// - synchronously process and reduce the data chunk immediately | |
// - asynchronously disaptch data processing onto another queue | |
[self.responseData appendData:data]; | |
} | |
- (void)connectionDidFinishLoading:(NSURLConnection*)connection | |
{ | |
// If we consider the request a failure - or at least if it was "not successful" - | |
// we construct a descriptive NSError object and assign it our `error` property. | |
// Note that the connection itself may have succeeded perfectly, but it just returned | |
// a status code which would not match our requirements. | |
// Purposefully, the NSError object will contain the response data in the `userInfo` | |
// dictionary. | |
// Notice, that in case of an error, the server may send a respond in an unexpected | |
// content type and encoding. Thus, we may need to check the Content-Type and possibly | |
// do nneed to convert/decode the response data into a format that's readable/processable | |
// by the client. | |
// | |
// So, in order to test if the request succeded, we MUST confirm that we got what we | |
// expect, e.g. HTTP status code, Content-Type, encoding, etc. | |
// The response data (if any) will be kept separately in property `responseData`. | |
if (self.lastResponse.statusCode != 200) { | |
NSString* desc = [[NSString alloc] initWithFormat:@"connection failed with response %d (%@)", | |
self.lastResponse.statusCode, [NSHTTPURLResponse localizedStringForStatusCode:self.lastResponse.statusCode]]; | |
self.error = [[NSError alloc] initWithDomain:@"SimpleGetHTTPRequest" | |
code:-4 | |
userInfo:@{ | |
NSLocalizedDescriptionKey: desc, | |
NSLocalizedFailureReasonErrorKey:[self.responseData description] | |
}]; | |
} | |
[self terminate]; | |
} | |
- (NSCachedURLResponse *)connection:(NSURLConnection *)connection | |
willCacheResponse:(NSCachedURLResponse *)cachedResponse | |
{ | |
// This will effectively prevent NSURLConnection to cache the response. | |
// That's not always desired, though. | |
return nil; | |
} | |
@end |
@MARTIN448 I din't wont to use a weak reference, but intentionally __block
which creates a strong reference. But, obviously, I didn't set it to nil
, even though I know I wanted to demonstrate exactly this :/
So, I fixed this, through setting blockSelf
to nil
. Using __block
has the effect, that it keeps self
alive until after the handler is called. However, it MUST be guaranteed that eventually blockSelf
will be set to zero, which also implies, that the handler MUST be called, too. Otherwise, there remains a cyclic reference which causes a leak.
There is also the possibility to use a __weak
reference. This has the effect, that when the handler will be called and self
has been deallocated since there are no strong references anymore, the weak reference has been set to nil
. Sending a message to nil
will do nothing. Unlike a variable declared with __block, a weak reference doesn't need to be set to nil
after it has been used. That's probably the more safe version, and preferred.
Which one of the two choices you actually use depends on what you intend to accomplish.
Note that with ARC, __block does not create a weak reference, you have to use __weak.