RESTful API requests using Objective-C (works on iOS and Mac)

In a recent article we showed how HTTP requests are formed in a low level. We demonstrated how to create a simple HTTP GET request, a URL encoded POST request, and an upload request (multipart POST HTTP request).

In this article you will see how to create those low level examples of HTTP requests programmatically using Objective-C, the language for iOS and Mac applications.

Simple HTTP GET requests

Lets jump right into the code. The following function is the simplest form of an HTTP request we can make

- (void)send_http_get_request:(NSString *)url_str {
    NSURL *url = [NSURL URLWithString:url_str];
    NSURLRequest *request = [[NSURLRequest alloc] initWithURL:url];
    [NSURLConnection connectionWithRequest:request delegate:self];
}

And here is an example on how we can use this code:

- (IBAction)on_button_click:(id)sender {
    NSString *url_str = @"http://www.example.com/path/to/page.php";
    [self send_http_get_request:url_str];
}

In our Xcode implementation this call generates the following request:

GET /path/to/page.php HTTP/1.1
Host: www.example.com
User-Agent: LowLevelHttpRequest/1 CFNetwork/596.6 Darwin/12.5.0 (x86_64) (MacBookPro6%2C1)
Accept: */*
Accept-Language: en-us
Accept-Encoding: gzip, deflate
Cookie: __utma=111872281.1884546802.1372687790.1372687790.1380098347.2; __atuvc=1%7C35
Connection: keep-alive

Notice that this basic HTTP request is a GET request. Everything seems well, except that we don't need cookies, or at least those particular cookies. We would also like to use a more distinctive name for a user agent, as our vanity demands it. Here is with a slightly better version of the code:

- (void)send_http_get_request:(NSString *)url_str {
    NSURL *url = [NSURL URLWithString:url_str];
    NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url];
    [request setHTTPShouldHandleCookies:NO];
    [request setValue:@"Agent name goes here" forHTTPHeaderField:@"User-Agent"];
    [NSURLConnection connectionWithRequest:request delegate:self];
}

The generated request is now improved:

GET /path/to/page.php HTTP/1.1
Host: www.example.com
User-Agent: Agent name goes here
Accept: */*
Accept-Language: en-us
Accept-Encoding: gzip, deflate
Connection: keep-alive

We can use the code above to add URL variables manually. Or, we may want to use an associative array (NSDictionary) with our custom variables. In this case our code will need to add the necessary URL escape format in the variables. This can be done in a couple of ways in Objective-C, unfortunately all of them are problematic. In the following code we will use our own version of URL encoding for Objective-C strings:

- (NSString *)urlencode:(NSString *)input {
    const char *input_c = [input cStringUsingEncoding:NSUTF8StringEncoding];
    NSMutableString *result = [NSMutableString new];
    for (NSInteger i = 0, len = strlen(input_c); i < len; i++) {
        unsigned char c = input_c[i];
        if (
            (c >= '0' && c <= '9')
            || (c >= 'A' && c <= 'Z')
            || (c >= 'a' && c <= 'z')
            || c == '-' || c == '.' || c == '_' || c == '~'
        ) {
            [result appendFormat:@"%c", c];
        }
        else {
            [result appendFormat:@"%%%02X", c];
        }
    }
    return result;
}

- (void)send_http_get_request:(NSString *)url_str vars:(NSDictionary *)vars {
    NSMutableString *url_str_with_vars = [NSMutableString stringWithString:url_str];
    if (vars != nil && vars.count > 0) {
        BOOL first = YES;
        for (NSString *key in vars) {
            [url_str_with_vars appendString:(first ? @"?" : @"&")];
            [url_str_with_vars appendString:[self urlencode:key]];
            [url_str_with_vars appendString:@"="];
            [url_str_with_vars appendString:[self urlencode:[vars valueForKey:key]]];
            first = NO;
        }
    }

    NSURL *url = [NSURL URLWithString:url_str];
    NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url];
    [request setHTTPShouldHandleCookies:NO];
    [request setValue:@"Agent name goes here" forHTTPHeaderField:@"User-Agent"];
    [NSURLConnection connectionWithRequest:request delegate:self];
}

And an example on how to use this enhanced version:

- (IBAction)on_button_click:(id)sender {
    NSString *url_str = @"http://www.example.com/path/to/page.php";

    NSMutableDictionary *vars = [NSMutableDictionary new];
    [vars setObject:@"value1" forKey:@"key1"];
    [vars setObject:@"value2" forKey:@"key2"];

    [self send_http_get_request:url_str vars:vars];
}

The generated request now includes variables next to the URL:

GET /path/to/page.php?key1=value1&key2=value2 HTTP/1.1
Host: www.example.com
User-Agent: Agent name goes here
Accept: */*
Accept-Language: en-us
Accept-Encoding: gzip, deflate
Connection: keep-alive

URL encoded HTTP POST requests

The following example will show an example of a POST request. The differences from GET requests are: a) change the HTTP verb "GET" to "POST" in the first line of the request, b) move the variables in the content of the request, although we can still keep GET variables in the URL as with the previous example, c) set the content type to "application/x-www-form-urlencoded", and d) set the content length as in this case there is a body in the request.

Some of those requirements are taken care automatically. Here is the new function for POST requests:

- (NSString *)urlencode:(NSString *)input {
    const char *input_c = [input cStringUsingEncoding:NSUTF8StringEncoding];
    NSMutableString *result = [NSMutableString new];
    for (NSInteger i = 0, len = strlen(input_c); i < len; i++) {
        unsigned char c = input_c[i];
        if (
            (c >= '0' && c <= '9')
            || (c >= 'A' && c <= 'Z')
            || (c >= 'a' && c <= 'z')
            || c == '-' || c == '.' || c == '_' || c == '~'
        ) {
            [result appendFormat:@"%c", c];
        }
        else {
            [result appendFormat:@"%%%02X", c];
        }
    }
    return result;
}

- (void)send_url_encoded_http_post_request:(NSString *)url_str vars:(NSDictionary *)vars {
    NSMutableString *vars_str = [NSMutableString new];
    if (vars != nil && vars.count > 0) {
        BOOL first = YES;
        for (NSString *key in vars) {
            if (!first) {
                [vars_str appendString:@"&"];
            }
            first = NO;

            [vars_str appendString:[self urlencode:key]];
            [vars_str appendString:@"="];
            [vars_str appendString:[self urlencode:[vars valueForKey:key]]];
        }
    }

    NSURL *url = [NSURL URLWithString:url_str];
    NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url];
    [request setHTTPMethod:@"POST"];
    [request setHTTPShouldHandleCookies:NO];
    [request setValue:@"Agent name goes here" forHTTPHeaderField:@"User-Agent"];
    [request setHTTPBody:[vars_str dataUsingEncoding:NSUTF8StringEncoding]];
    [NSURLConnection connectionWithRequest:request delegate:self];
}

And an example on how to use this code is this:

- (IBAction)on_button_click:(id)sender {
    NSString *url_str = @"http://www.example.com/path/to/page.php";

    NSMutableDictionary *vars = [NSMutableDictionary new];
    [vars setObject:@"value1" forKey:@"key1"];
    [vars setObject:@"value2" forKey:@"key2"];

    [self send_url_encoded_http_post_request:url_str vars:vars];
}

This code will produce the following HTTP request:

POST /path/to/page.php HTTP/1.1
Host: www.example.com
User-Agent: Agent name goes here
Content-Length: 23
Accept: */*
Accept-Language: en-us
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded
Connection: keep-alive

key1=value1&key2=value2

Multipart HTTP POST requests

Finally, lets see an example on how to send multipart HTTP POST requests. The previous type of POST requests used URL encoding, which makes the request very large when we send non latin characters or non numbers. With this type of POST request its size is more reasonable. The most significant implication is that we can upload files and avoid sending a request three times the size of those files.

The headers of parts for this type of request have attributes that need to be escaped. The escape method can be a little complicated (according to RFC 5987), that is why we created a dedicated function for them.

- (NSString *)http_attribute_encode:(NSString *)input attribute_name:(NSString *)attribute_name {
    // result structure follows RFC 5987

    BOOL need_utf_encoding = NO;
    NSMutableString *result = [NSMutableString new];
    const char *input_c = [input cStringUsingEncoding:NSUTF8StringEncoding];
    NSInteger i;
    NSInteger len = strlen(input_c);
    unsigned char c;
    for (i = 0; i < len; i++) {
        c = input_c[i];
        if (c == '\\' || c == '/' || c == '\0' || c < ' ' || c > '~') {
            // ignore and request utf-8 version
            need_utf_encoding = YES;
        }
        else if (c == '"') {
            [result appendString:@"\\\""];
        }
        else {
            [result appendFormat:@"%c", c];
        }
    }
    
    if (result.length == 0) {
        need_utf_encoding = YES;
        [result appendString:@"file"];
    }

    if (!need_utf_encoding) {
        // return a simple version
        return [NSString stringWithFormat:@"%@=\"%@\"", attribute_name, result];
    }
    
    NSMutableString *result_utf8 = [NSMutableString new];
    for (i = 0; i < len; i++) {
        c = input_c[i];
        if (
            (c >= '0' && c <= '9')
            || (c >= 'A' && c <= 'Z')
            || (c >= 'a' && c <= 'z')
        ) {
            [result_utf8 appendFormat:@"%c", c];
        }
        else {
            [result_utf8 appendFormat:@"%%%02X", c];
        }
    }
    
    // return enhanced version with UTF-8 support
    return [NSString stringWithFormat:@"%@=\"%@\"; %@*=utf-8''%@", attribute_name, result, attribute_name, result_utf8];
}

- (void)send_multipart_http_post_request:(NSString *)url_str vars:(NSDictionary *)vars files:(NSArray *)files {
    NSString *str;
    NSMutableData *data = [NSMutableData new];
    
    NSMutableString *boundary = [NSMutableString new];
    [boundary appendString:@"__-----------------------"];
    [boundary appendFormat:@"%ld", arc4random() % 2147483648];
    [boundary appendFormat:@"%ld", (long) ([[NSDate new] timeIntervalSince1970] * 1000)];
    
    NSData *boundary_body = [boundary dataUsingEncoding:NSUTF8StringEncoding];
    NSData *boundary_delimiter = [@"--" dataUsingEncoding:NSUTF8StringEncoding];
    NSData *new_line = [@"\r\n" dataUsingEncoding:NSUTF8StringEncoding];

    // add variables
    if (vars != nil) {
        for (NSString *key in vars) {
            // add boundary
            [data appendData:boundary_delimiter];
            [data appendData:boundary_body];
            [data appendData:new_line];
            
            // add header
            NSString *form_name = [self http_attribute_encode:key attribute_name:@"name"];
            str = [NSString stringWithFormat:@"Content-Disposition: form-data; %@", form_name];
            [data appendData:[str dataUsingEncoding:NSUTF8StringEncoding]];
            [data appendData:new_line];
            
            str = @"Content-Type: text/plain";
            [data appendData:[str dataUsingEncoding:NSUTF8StringEncoding]];
            [data appendData:new_line];
            
            // add header to body splitter
            [data appendData:new_line];
            
            // add variable content
            str = [vars valueForKey:key];
            [data appendData:[str dataUsingEncoding:NSUTF8StringEncoding]];
            [data appendData:new_line];
        }
    }
    
    // add files
    if (files != nil) {
        for (NSInteger i = 0; i < files.count; i++) {
            NSMutableDictionary *file_parameters = [files objectAtIndex:i];
            
            NSString *variable_name = [file_parameters objectForKey:@"variable_name"];
            NSString *request_filename = [file_parameters objectForKey:@"request_filename"];
            NSString *mime_type = [file_parameters objectForKey:@"mime_type"];
            NSString *local_filename = [file_parameters objectForKey:@"local_filename"];
            
            // ensure necessary variables are available
            if (
                variable_name == nil || variable_name.length == 0
                || local_filename == nil || local_filename.length == 0
                || ![[NSFileManager defaultManager] fileExistsAtPath:local_filename]
            ) {
                // silent abort current file
                continue;
            }

            // ensure filename for the request
            if (request_filename == nil || request_filename.length == 0) {
                request_filename = [local_filename lastPathComponent];
                if (request_filename.length == 0) {
                    // silent abort current file
                    continue;
                }
            }
            
            // add boundary
            [data appendData:boundary_delimiter];
            [data appendData:boundary_body];
            [data appendData:new_line];
            
            // add header
            str = [NSString stringWithFormat:@"Content-Disposition: form-data; %@; %@",
                [self http_attribute_encode:variable_name attribute_name:@"name"],
                [self http_attribute_encode:request_filename attribute_name:@"filename"]
            ];
            [data appendData:[str dataUsingEncoding:NSUTF8StringEncoding]];
            [data appendData:new_line];
            
            if (mime_type != nil && mime_type.length > 0) {
                str = [NSString stringWithFormat:@"Content-Type: %@", mime_type];
                [data appendData:[str dataUsingEncoding:NSUTF8StringEncoding]];
                [data appendData:new_line];
            }
            
            str = @"Content-Transfer-Encoding: binary";
            [data appendData:[str dataUsingEncoding:NSUTF8StringEncoding]];
            [data appendData:new_line];
            
            // add header to body splitter
            [data appendData:new_line];
            
            // add file content
            [data appendData:[NSData dataWithContentsOfFile:local_filename]];
            [data appendData:new_line];
        }
    }
    
    // add end of body
    [data appendData:boundary_delimiter];
    [data appendData:boundary_body];
    [data appendData:boundary_delimiter];

    // join parts and send the request
    NSURL *url = [NSURL URLWithString:url_str];
    NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url];
    [request setHTTPMethod:@"POST"];
    [request setHTTPShouldHandleCookies:NO];
    [request setValue:@"Agent name goes here" forHTTPHeaderField:@"User-Agent"];
    [request setValue:[NSString stringWithFormat:@"multipart/form-data; boundary=%@", boundary] forHTTPHeaderField:@"Content-Type"];
    [request setHTTPBody:data];
    [NSURLConnection connectionWithRequest:request delegate:self];
}

Here is an example on how to use this code:

- (IBAction)on_button_click:(id)sender {
    NSString *url_str = @"http://www.example.com/path/to/page.php";

    // set up regular variables
    NSMutableDictionary *vars = [NSMutableDictionary new];
    [vars setObject:@"value1" forKey:@"key1"];
    [vars setObject:@"value2" forKey:@"key2"];
    
    // prepare variables for files
    NSMutableArray *files = [NSMutableArray new];
    NSMutableDictionary *file_parameters;
  
    // set up first file
    file_parameters = [NSMutableDictionary new];
    [file_parameters setObject:@"file1" forKey:@"variable_name"];
    [file_parameters setObject:@"/path/to/file1.png" forKey:@"local_filename"];
    [file_parameters setObject:@"file1.png" forKey:@"request_filename"];
    [file_parameters setObject:@"image/png" forKey:@"mime_type"];
    [files addObject:file_parameters];
    
    // set up second file
    file_parameters = [NSMutableDictionary new];
    [file_parameters setObject:@"file2" forKey:@"variable_name"];
    [file_parameters setObject:@"/path/to/file2.png" forKey:@"local_filename"];
    [file_parameters setObject:@"file2.png" forKey:@"request_filename"];
    [file_parameters setObject:@"image/png" forKey:@"mime_type"];
    [files addObject:file_parameters];
    
    // execute request
    [self send_multipart_http_post_request:url_str vars:vars files:files];
}

If you study the code you will see that in your call you can omit the "request_filename" entries for files. If you do omit them, the system will automatically assign the base-name of the "local_filename" variable.

The previous example will generate the following HTTP request:

POST /path/to/page.php HTTP/1.1
Host: www.example.com
User-Agent: Agent name goes here
Content-Length: 686
Accept: */*
Content-Type: multipart/form-data; boundary=__-----------------------9446182961397930864818
Accept-Language: en-us
Accept-Encoding: gzip, deflate
Connection: keep-alive

--__-----------------------9446182961397930864818
Content-Disposition: form-data; name="key1"
Content-Type: text/plain

value1
--__-----------------------9446182961397930864818
Content-Disposition: form-data; name="key2"
Content-Type: text/plain

value2
--__-----------------------9446182961397930864818
Content-Disposition: form-data; name="file1"; filename="file1.png"
Content-Type: image/png
Content-Transfer-Encoding: binary

[... contents of /path/to/file1.png ...]
--__-----------------------9446182961397930864818
Content-Disposition: form-data; name="file2"; filename="file2.png"
Content-Type: image/png
Content-Transfer-Encoding: binary

[... contents of /path/to/file2.png ...]
--__-----------------------9446182961397930864818--

That concludes the last type of POST request you can send to your web-server. You need to add the necessary code from the web-server side to complete the handling of the uploaded variables and files.

It is worth noting that the communication calls in this entire article are asynchronous or non-blocking. This means that the functions return immediately. If you want to use the result from the web server you should use the handler functions of the NSURLConnection delegate.