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
@quique123
Copy link

How do I access the result from the calling view controller? Sorry but Im not good with blocks. I know the basics but thats it :)

@couchdeveloper
Copy link
Author

Edit: the sample code below contained a bug, where blockSelf has not been set to nil in the handler after it has been used. This would maintain a circular reference, which results in a leak. This has been fixed.

There are these steps:

  1. Create a SimpleGetHTTPRequest instance with a URL.
  2. Setup the completion block. Ensure you don't create cyclic references, e.g: __block ViewController* blockSelf = self;
  3. Invoke the start method.

A simple example using this class:

    - (void)viewDidLoad
    {
        [super viewDidLoad];
        self.currentRequest = [[SimpleGetHTTPRequest alloc] initWithURL:[NSURL URLWithString:@"http://localhost:3000/v1/users"]];

        __block ViewController* blockSelf = self;  // extra hoop to avoid cyclic references
        self.currentRequest.completionHandler = ^(id result) {
            if ([result isKindOfClass:[NSError class]]) {
                NSLog(@"ERROR: %@", result);
            }
            else {
                blockSelf.users = result;   // note: this will require synchronization if accessed elsewhere! Possibly use main thread.
                dispatch_async(dispatch_get_main_queue(), ^{
                    // invalidate view in order to redraw with new content
                });
            }
            blockSelf.currentRequest = nil; 
            blockSelf = nil;                              // set to nil, to break reference cycle.
        };
        [self.currentRequest start];
    }

A few things to note:

A completion handler may be executed on any thread. So, when you invoke UIKit methods, dispatch code on the main thread!

If you access ivars, that is self.users from the completion handler, be sure not to introduce race conditions. If you or any other code access the ivar elsewhere, you need to synchronize all accesses. You can accomplish this with a dedicated dispatch queue - say "sync_queue" where you execute all accesses. You may choose to use the main thread as the dedicated "queue", too which is handy when UIKit accesses it. Then simply dispatch the assignment and all other accesses to self.users on the main thread.

Introducing cyclic references can be made easily by accident. There are basically two idioms which when applied do avoid this. One is shown in the example. You may find more info in Apple's documentation "Transitioning to ARC Release Notes".

Note: you may want to keep a reference to the HTTPRequest instance in order to be able to cancel the request if this is required. Having multiple requests can become more cumbersome to manage: you would need an array to hold the active requests. Alternatively, inherit SimpleGetHTTPRequest from NSOperation which is straight forward and use a NSOperationQueue.

@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