Skip to content

Instantly share code, notes, and snippets.

@couchdeveloper
Last active December 18, 2015 09:58
Show Gist options
  • Save couchdeveloper/5764723 to your computer and use it in GitHub Desktop.
Save couchdeveloper/5764723 to your computer and use it in GitHub Desktop.
SimpleGetHTTPRequest This is a simple Objective-C class which wraps a `NSURLConnection` and relevant state information. It's meant to give an idea how one can implement a more "real" and more versatile connection class. It's deliberately kept simple.
//
// 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
@martinr448
Copy link

Note that with ARC, __block does not create a weak reference, you have to use __weak.

@couchdeveloper
Copy link
Author

@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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment