1

I'm trying to put together a static class for my iOS app that will encapsulate my HTTP communication logic. I know how to make static methods in Objective-C via the + sign, but I want to have my class be able to accept a block and invoke it when it's tasks are complete. The class I've got so far compiles fine, but when I invoke it's functions it only get's to a certain point and fails. Here's my current code:

#import <UIKit/UIKit.h>

#pragma mark - Globals
#import "Globals.h"

@interface HTTPRequest : NSObject <NSURLConnectionDelegate>
+ (void)GET:(NSMutableURLRequest *)request completionBlock:(void (^)(void))block;
+ (void)POST:(NSMutableURLRequest *)request andBody:(NSString *)requestString completionBlock:(void (^)(void))block;
@end

#import "HTTPRequest.h"

@interface HTTPRequest () {
    NSInteger statusCode;
}
@end

static void (^completionBlock)(void);

@implementation HTTPRequest
+ (void)GET:(NSMutableURLRequest *)request completionBlock:(void (^)(void))block {
    [[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:YES];

    [request setHTTPMethod:@"GET"];

    NSURLConnection *connection = [[NSURLConnection alloc] initWithRequest:request
                                                                  delegate:self];

    if (connection) {
        responseData = [[NSMutableData alloc] init];
    } else {
        NSLog(@"NSURLConnection INSTANCE FAILED");
    };

    completionBlock = block;
}

+ (void)POST:(NSMutableURLRequest *)request andBody:(NSString *)requestString completionBlock:(void (^)(void))block {
    [[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:YES];

    NSData *requestData = [NSData dataWithBytes:[requestString UTF8String]
                                         length:[requestString length]];

    [request setHTTPMethod:@"POST"];
    [request setValue:@"application/x-www-form-urlencoded" forHTTPHeaderField:@"content-type"];
    [request setHTTPBody:requestData];

    NSURLConnection *connection = [[NSURLConnection alloc] initWithRequest:request
                                                                  delegate:self];

    if (connection) {
        responseData = [[NSMutableData alloc] init];
    } else {
        NSLog(@"NSURLConnection INSTANCE FAILED");
    };

    completionBlock = block;
}

#pragma mark - NSURLConnectionDelegate Methods
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response {
    [responseData setLength:0];

    statusCode = [((NSHTTPURLResponse *)response) statusCode];
}

- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
    [responseData appendData:data];
}

- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
    NSLog(@"CONNECTION ERROR: %@ %@", [error localizedDescription], [[error userInfo] objectForKey:NSURLErrorFailingURLStringErrorKey]);
}

- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
    NSLog(@"RESPONSE STRING: %@", [[NSString alloc] initWithData:responseData encoding:NSUTF8StringEncoding]);

    [[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:NO];

    if (statusCode == 200) {
        completionBlock();
    } else {
        NSLog(@"RESPONSE ERROR: %i", statusCode);
    };
}
@end

I know that GET and POST functions get called and that they create the NSURLConnection object. Thereafter, none of the NSURLConnectionDelegate functions get called.

Can anyone spot the issue, or alternatively, if I'm going about this completely wrong, please let me know. Thanks in advance!

UPDATE

I should point out that the application is ARC based. I've read other questions on here about the NSURLConnectionDelegate functions not being called, but most of them seem to be because the developers set the objects as autorelease. Since I have no control over that, I'm assuming it shouldn't be a problem.

UPDATE 2 (REWRITE?)

I made some changes where from the static functions I create an instance of the class and use it's instance functions. Now the NSURLConnectionDelegate functions get called and I know that my onSuccessBlock blocks are being called too, so the entire class is executing all the way through. Quite frankly, I'm not sure if this is even legal code, but the compiler isn't complaining and I'm seeing the log messages I'm expecting so I'll go with it. Here's the current implementation:

#import <UIKit/UIKit.h>

@interface HTTPRequest : NSObject <NSURLConnectionDelegate>
+ (void)requestAsGET:(NSMutableURLRequest *)request onSuccess:(void (^)(void))successBlock onFail:(void (^)(void))failBlock;
- (void)requestAsGET:(NSMutableURLRequest *)request onSuccess:(void (^)(void))successBlock onFail:(void (^)(void))failBlock;
+ (void)requestAsPOST:(NSMutableURLRequest *)request body:(NSString *)body onSuccess:(void (^)(void))successBlock onFail:(void (^)(void))failBlock;
- (void)requestAsPOST:(NSMutableURLRequest *)request body:(NSString *)body onSuccess:(void (^)(void))successBlock onFail:(void (^)(void))failBlock;
@end

#import "HTTPRequest.h"

@interface HTTPRequest () {
    NSMutableData *responseData;
    NSInteger statusCode;

    void (^onSuccessBlock)(void);
    void (^onFailBlock)(void);
}
@end

@implementation HTTPRequest
- (id)init {
    self = [super init];

    if (self) {
    };

    return self;
}

+ (void)requestAsGET:(NSMutableURLRequest *)request onSuccess:(void (^)(void))successBlock onFail:(void (^)(void))failBlock {
    [[[HTTPRequest alloc] init] requestAsGET:request
                                   onSuccess:successBlock
                                      onFail:failBlock];
}

- (void)requestAsGET:(NSMutableURLRequest *)request onSuccess:(void (^)(void))successBlock onFail:(void (^)(void))failBlock {
    [[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:YES];

    [request setHTTPMethod:@"GET"];

    NSURLConnection *connection = [[NSURLConnection alloc] initWithRequest:request
                                                                  delegate:self];

    if (connection) {
        onSuccessBlock = successBlock;
        onFailBlock = failBlock;
    } else {
        NSLog(@"NSURLConnection INSTANCE FAILED");
    };
}

+ (void)requestAsPOST:(NSMutableURLRequest *)request body:(NSString *)body onSuccess:(void (^)(void))successBlock onFail:(void (^)(void))failBlock {
    [[[HTTPRequest alloc] init] requestAsPOST:request
                                         body:body
                                    onSuccess:successBlock
                                       onFail:failBlock];
}

- (void)requestAsPOST:(NSMutableURLRequest *)request body:(NSString *)body onSuccess:(void (^)(void))successBlock onFail:(void (^)(void))failBlock {
    [[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:YES];

    NSData *requestData = [NSData dataWithBytes:[body UTF8String]
                                         length:[body length]];

    [request setHTTPMethod:@"POST"];
    [request setValue:@"application/x-www-form-urlencoded" forHTTPHeaderField:@"content-type"];
    [request setHTTPBody:requestData];

    NSURLConnection *connection = [[NSURLConnection alloc] initWithRequest:request
                                                                  delegate:self];

    if (connection) {
        onSuccessBlock = successBlock;
        onFailBlock = failBlock;
    } else {
        NSLog(@"NSURLConnection INSTANCE FAILED");
    };
}

#pragma mark - NSURLConnectionDelegate Methods
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response {
    [responseData setLength:0];

    statusCode = [((NSHTTPURLResponse *)response) statusCode];
}

- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
    [responseData appendData:data];
}

- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
    NSLog(@"CONNECTION ERROR: %@ %@", [error localizedDescription], [[error userInfo] objectForKey:NSURLErrorFailingURLStringErrorKey]);
}

- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
    NSLog(@"RESPONSE STRING: %@", [[NSString alloc] initWithData:responseData encoding:NSUTF8StringEncoding]);

    [[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:NO];

    if (statusCode == 200) {
        //  Pass responseData into the block... How?

        onSuccessBlock();
    } else {
        //  Pass statusCode into the block?

        onFailBlock();
    };
}
@end
Gup3rSuR4c
  • 9,145
  • 10
  • 68
  • 126
  • what about making a `[connection start];` call after creating them? Furthermore, use `completionBlock = [block retain];` in the static methods and `[completionBlock release];` after you've used it in the delegate. –  Jul 20 '12 at 04:39
  • Didn't know there was a `start` function. I'm moving these functions out of my controllers and they've always worked fine without the `start` call. Also this is an ARC app, so I'm not sure I need to keep retaining the block. Either way, it didn't work. It's still not calling any of the `NSURLConnectionDelegate` functions. – Gup3rSuR4c Jul 20 '12 at 05:28

2 Answers2

1

I would use the Singleton Pattern instead of a static class. Then you won't have to worry about another request overwriting data from a previous one.

Take for instance your NSMutableData responseData. You're going to need an instance variable to keep track of that data through the connection life cycle. I didn't see one, but if you had one you would need to make sure that no other connection could update it. This means you can't make it a static variable which might be your initial reaction.

Either way you go, you should probably tuck your responseData variable in a dictionary with the NSUrlConnection as the key. That way, on each delegate method implementation, you can look up the data from the connection and do whatever you need.

So in this scenario you could do something like:

NSURLConnection *connection = [[NSURLConnection alloc] initWithRequest:request
                                                              delegate:self];

if (connection) {
    responseData = [[NSMutableData alloc] init];

    // So this would be a NSMutableDictionary you would store your connection in 
    [requests setValue:responseData forKey:connection];

} else {
    NSLog(@"NSURLConnection INSTANCE FAILED");
};

Then you could do this in your delegate methods

- (void) connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
NSMutableData *responseData = [requests valueForKey:connection];

[responseData appendData:data];
}

For my personal implementation I opted for using CFMutableDictionaryRef instead of NSMutableDictionary because I believe NSMutableDictionary doesn't retain objects added to it. My code was written pre-ARC so you may not have to worry about it. If you try this method and start getting EXC_BAD_ACCESS or run into memory issues, then use CFMutableDictionaryRef.

Hope this gets you going in the right direction. You're head is definitely in the right place :)

BASED ON REWRITE

So let me try to better explain what I mentioned above based on your rewrite:

- (void)requestAsGET:(NSMutableURLRequest *)request onSuccess:(void (^)(void))successBlock onFail:(void (^)(void))failBlock {
    [[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:YES];

    [request setHTTPMethod:@"GET"];

    NSURLConnection *connection = [[NSURLConnection alloc] initWithRequest:request
                                                              delegate:self];

    if (connection) {

       /** This is your problem. You are trying to save data to some instance variables
           but there's a data integrity issue here.  If you fire off two requests, the 
           second request will overwrite these two values which would, at the time, contain
           information from the first request */

        onSuccessBlock = successBlock;
        onFailBlock = failBlock;

    } else {
        NSLog(@"NSURLConnection INSTANCE FAILED");
    };
}

So you have a design problem at the very first step, what happens from there really doesn't matter much until you resolve this issue. My suggestion would be to save your ** HTTPRequest** value object to a dictionary using the current NSURLConnection as the key. This way, on each delegate method you can lookup the data specific to that connection and modify it without having to worry about other requests mowing over your data.

Something like this:

if (connection) {

    // Store the data in an object... call it whatever you want
    SomeObject *someObject = [[SomeObject all] init];
    someObject.responseData = [[NSMutableData alloc] init];
    someObject.onSuccessBlock = successBlock;
    someObject.onFailBlock = failBlock;

    /** Have an NSMutableDictionary that stores all the value objects
        under the corresponding connection since ALL NSURLConnectionDelegate
        methods include a reference to the NSURLConnection for the request */
    [requests setValue:someObject forKey:connection];

} else {
    NSLog(@"NSURLConnection INSTANCE FAILED");
};
jerrylroberts
  • 3,419
  • 23
  • 22
  • Please take a look at my second update above. I just finished it right before I read your post and I think I kind-of-sort-of did what you suggested, but going through a different route on it? – Gup3rSuR4c Jul 20 '12 at 06:47
  • I'm making the changes as you're suggesting, but I'm getting an error for the `setValue:forKey:` method because the key is supposed to be a n `NSString` and it wont take the `NSURLConnection`. – Gup3rSuR4c Jul 20 '12 at 19:17
1

You should take inspiration from a simple NSURLConnection wrapper such as https://github.com/nst/STHTTPRequest.

Also, you can use non-ARC code in ARC projects. For this, you can tell the compiler not to use ARC on a file basis Disable Automatic Reference Counting for Some Files.

Community
  • 1
  • 1
nst
  • 3,862
  • 1
  • 31
  • 40