92

With all the URL-handling objects lying around in the standard Cocoa libraries (NSURL, NSMutableURL, NSMutableURLRequest, etc), I know I must be overlooking an easy way to programmatically compose a GET request.

Currently I'm manually appending "?" followed by name value pairs joined by "&", but all of my name and value pairs need to be manually encoded so NSMutableURLRequest doesn't fail entirely when it tries to connect to the URL.

This feels like something I should be able to use a pre-baked API for.... is there anything out of the box to append an NSDictionary of query parameters to an NSURL? Is there another way I should approach this?

Zev Eisenberg
  • 8,080
  • 5
  • 38
  • 82

13 Answers13

134

Introduced in iOS8 and OS X 10.10 is NSURLQueryItem, which can be used to build queries. From the docs on NSURLQueryItem:

An NSURLQueryItem object represents a single name/value pair for an item in the query portion of a URL. You use query items with the queryItems property of an NSURLComponents object.

To create one use the designated initializer queryItemWithName:value: and then add them to NSURLComponents to generate an NSURL. For example:

NSURLComponents *components = [NSURLComponents componentsWithString:@"http://stackoverflow.com"];
NSURLQueryItem *search = [NSURLQueryItem queryItemWithName:@"q" value:@"ios"];
NSURLQueryItem *count = [NSURLQueryItem queryItemWithName:@"count" value:@"10"];
components.queryItems = @[ search, count ];
NSURL *url = components.URL; // http://stackoverflow.com?q=ios&count=10

Notice that the question mark and ampersand are automatically handled. Creating an NSURL from a dictionary of parameters is as simple as:

NSDictionary *queryDictionary = @{ @"q": @"ios", @"count": @"10" };
NSMutableArray *queryItems = [NSMutableArray array];
for (NSString *key in queryDictionary) {
    [queryItems addObject:[NSURLQueryItem queryItemWithName:key value:queryDictionary[key]]];
}
components.queryItems = queryItems;

I've also written a blog post on how to build URLs with NSURLComponents and NSURLQueryItems.

Joe Masilotti
  • 16,815
  • 6
  • 77
  • 87
47

You can create a category for NSDictionary to do this -- there isn't a standard way in the Cocoa library that I could find either. The code that I use looks like this:

// file "NSDictionary+UrlEncoding.h"
#import <cocoa/cocoa.h>

@interface NSDictionary (UrlEncoding)

-(NSString*) urlEncodedString;

@end

with this implementation:

// file "NSDictionary+UrlEncoding.m"
#import "NSDictionary+UrlEncoding.h"

// helper function: get the string form of any object
static NSString *toString(id object) {
  return [NSString stringWithFormat: @"%@", object];
}

// helper function: get the url encoded string form of any object
static NSString *urlEncode(id object) {
  NSString *string = toString(object);
  return [string stringByAddingPercentEscapesUsingEncoding: NSUTF8StringEncoding];
}

@implementation NSDictionary (UrlEncoding)

-(NSString*) urlEncodedString {
  NSMutableArray *parts = [NSMutableArray array];
  for (id key in self) {
    id value = [self objectForKey: key];
    NSString *part = [NSString stringWithFormat: @"%@=%@", urlEncode(key), urlEncode(value)];
    [parts addObject: part];
  }
  return [parts componentsJoinedByString: @"&"];
}

@end

I think the code's pretty straightforward, but I discuss it in some more detail at http://blog.ablepear.com/2008/12/urlencoding-category-for-nsdictionary.html.

Don McCaughey
  • 9,532
  • 3
  • 30
  • 36
  • 16
    I don't think this works. Specifically, you're using stringByAddingPercentEscapesUsingEncoding, which does not escape ampersands, as well as other characters that must be escaped in query parameters as specified in [RFC 3986](http://www.ietf.org/rfc/rfc3986.txt). Consider looking at the [Google Toolbox For Mac escaping code](http://code.google.com/p/google-toolbox-for-mac/source/browse/trunk/Foundation/GTMNSString%2BURLArguments.m) instead. – Dave Peck Feb 22 '11 at 04:39
  • This works for proper escaping: http://stackoverflow.com/questions/9187316/string-wont-url-encode-in-ios/9232341#9232341 – JoeCortopassi Mar 26 '13 at 23:53
30

I wanted to use Chris's answer, but it wasn't written for Automatic Reference Counting (ARC) so I updated it. I thought I'd paste my solution in case anyone else has this same issue. Note: replace self with the instance or class name where appropriate.

+(NSString*)urlEscapeString:(NSString *)unencodedString 
{
    CFStringRef originalStringRef = (__bridge_retained CFStringRef)unencodedString;
    NSString *s = (__bridge_transfer NSString *)CFURLCreateStringByAddingPercentEscapes(NULL,originalStringRef, NULL, (CFStringRef)@"!*'\"();:@&=+$,/?%#[]% ", kCFStringEncodingUTF8);
    CFRelease(originalStringRef);
    return s;
}


+(NSString*)addQueryStringToUrlString:(NSString *)urlString withDictionary:(NSDictionary *)dictionary
{
    NSMutableString *urlWithQuerystring = [[NSMutableString alloc] initWithString:urlString];

    for (id key in dictionary) {
        NSString *keyString = [key description];
        NSString *valueString = [[dictionary objectForKey:key] description];

        if ([urlWithQuerystring rangeOfString:@"?"].location == NSNotFound) {
            [urlWithQuerystring appendFormat:@"?%@=%@", [self urlEscapeString:keyString], [self urlEscapeString:valueString]];
        } else {
            [urlWithQuerystring appendFormat:@"&%@=%@", [self urlEscapeString:keyString], [self urlEscapeString:valueString]];
        }
    }
    return urlWithQuerystring;
}
Pellet
  • 2,254
  • 1
  • 28
  • 20
Anonymous Dev
  • 301
  • 3
  • 3
22

The other answers work great if the values are strings, however if the values are dictionaries or arrays then this code will handle that.

Its important to note that there is no standard way of passing an array/dictionary via the query string but PHP handles this output just fine

-(NSString *)serializeParams:(NSDictionary *)params {
    /*

     Convert an NSDictionary to a query string

     */

    NSMutableArray* pairs = [NSMutableArray array];
    for (NSString* key in [params keyEnumerator]) {
        id value = [params objectForKey:key];
        if ([value isKindOfClass:[NSDictionary class]]) {
            for (NSString *subKey in value) {
                NSString* escaped_value = (NSString *)CFURLCreateStringByAddingPercentEscapes(NULL,
                                                                                              (CFStringRef)[value objectForKey:subKey],
                                                                                              NULL,
                                                                                              (CFStringRef)@"!*'();:@&=+$,/?%#[]",
                                                                                              kCFStringEncodingUTF8);
                [pairs addObject:[NSString stringWithFormat:@"%@[%@]=%@", key, subKey, escaped_value]];
            }
        } else if ([value isKindOfClass:[NSArray class]]) {
            for (NSString *subValue in value) {
                NSString* escaped_value = (NSString *)CFURLCreateStringByAddingPercentEscapes(NULL,
                                                                                              (CFStringRef)subValue,
                                                                                              NULL,
                                                                                              (CFStringRef)@"!*'();:@&=+$,/?%#[]",
                                                                                              kCFStringEncodingUTF8);
                [pairs addObject:[NSString stringWithFormat:@"%@[]=%@", key, escaped_value]];
            }
        } else {
            NSString* escaped_value = (NSString *)CFURLCreateStringByAddingPercentEscapes(NULL,
                                                                                          (CFStringRef)[params objectForKey:key],
                                                                                          NULL,
                                                                                          (CFStringRef)@"!*'();:@&=+$,/?%#[]",
                                                                                          kCFStringEncodingUTF8);
            [pairs addObject:[NSString stringWithFormat:@"%@=%@", key, escaped_value]];
            [escaped_value release];
        }
    }
    return [pairs componentsJoinedByString:@"&"];
}

Examples

[foo] => bar
[translations] => 
        {
            [one] => uno
            [two] => dos
            [three] => tres
        }

foo=bar&translations[one]=uno&translations[two]=dos&translations[three]=tres

[foo] => bar
[translations] => 
        {
            uno
            dos
            tres
        }

foo=bar&translations[]=uno&translations[]=dos&translations[]=tres

AlBeebe
  • 8,101
  • 3
  • 51
  • 65
  • 1
    For the final 'else' case, I added a call to description so that it will provide strings for NSNumbers rather than barfing on a call to length: '(CFStringRef)[[params objectForKey:key] **description**]' Thanks! – JohnQ Dec 06 '13 at 15:47
  • great answer. However you should replace `CFURLCreateStringByAddingPercentEscapes` calls with `stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLQueryAllowedCharacterSet]` – S1LENT WARRIOR Feb 07 '17 at 12:59
8

I refactored and converted to ARC answer by AlBeebe

- (NSString *)serializeParams:(NSDictionary *)params {
    NSMutableArray *pairs = NSMutableArray.array;
    for (NSString *key in params.keyEnumerator) {
        id value = params[key];
        if ([value isKindOfClass:[NSDictionary class]])
            for (NSString *subKey in value)
                [pairs addObject:[NSString stringWithFormat:@"%@[%@]=%@", key, subKey, [self escapeValueForURLParameter:[value objectForKey:subKey]]]];

        else if ([value isKindOfClass:[NSArray class]])
            for (NSString *subValue in value)
            [pairs addObject:[NSString stringWithFormat:@"%@[]=%@", key, [self escapeValueForURLParameter:subValue]]];

        else
            [pairs addObject:[NSString stringWithFormat:@"%@=%@", key, [self escapeValueForURLParameter:value]]];

}
return [pairs componentsJoinedByString:@"&"];

}

- (NSString *)escapeValueForURLParameter:(NSString *)valueToEscape {
     return (__bridge_transfer NSString *) CFURLCreateStringByAddingPercentEscapes(NULL, (__bridge CFStringRef) valueToEscape,
             NULL, (CFStringRef) @"!*'();:@&=+$,/?%#[]", kCFStringEncodingUTF8);
}
Renetik
  • 5,887
  • 1
  • 47
  • 66
  • 2
    This worked great for me, but I changed the last line in order to include a ? before the first param: `[NSString stringWithFormat:@"?%@",[pairs componentsJoinedByString:@"&"]];` – BFar Aug 22 '14 at 22:54
  • 1
    i think you should replace `CFURLCreateStringByAddingPercentEscapes` calls `stringByAddingPercentEncodingWithAllowedCharacters:[NSCharac‌​terSet URLQueryAllowedCharacterSet]` – S1LENT WARRIOR Feb 07 '17 at 13:00
6

If you are already using AFNetworking (as was the case with me), you can use it's class AFHTTPRequestSerializer to create the required NSURLRequest.

[[AFHTTPRequestSerializer serializer] requestWithMethod:@"GET" URLString:@"YOUR_URL" parameters:@{PARAMS} error:nil];

In case you only require the URL for your work, use NSURLRequest.URL.

Ayush Goel
  • 3,134
  • 27
  • 36
3

Here is a simple example in Swift (iOS8+):

private let kSNStockInfoFetchRequestPath: String = "http://dev.markitondemand.com/Api/v2/Quote/json"

private func SNStockInfoFetchRequestURL(symbol:String) -> NSURL? {
  if let components = NSURLComponents(string:kSNStockInfoFetchRequestPath) {
    components.queryItems = [NSURLQueryItem(name:"symbol", value:symbol)]
    return components.URL
  }
  return nil
}
Zorayr
  • 23,770
  • 8
  • 136
  • 129
3

I took Joel's recommendation of using URLQueryItems and turned into a Swift Extension (Swift 3)

extension URL
{
    /// Creates an NSURL with url-encoded parameters.
    init?(string : String, parameters : [String : String])
    {
        guard var components = URLComponents(string: string) else { return nil }

        components.queryItems = parameters.map { return URLQueryItem(name: $0, value: $1) }

        guard let url = components.url else { return nil }

        // Kinda redundant, but we need to call init.
        self.init(string: url.absoluteString)
    }
}

(The self.init method is kinda cheesy, but there was no NSURL init with components)

Can be used as

URL(string: "http://www.google.com/", parameters: ["q" : "search me"])
Can
  • 8,502
  • 48
  • 57
2
-(NSString*)encodeDictionary:(NSDictionary*)dictionary{
    NSMutableString *bodyData = [[NSMutableString alloc]init];
    int i = 0;
    for (NSString *key in dictionary.allKeys) {
        i++;
        [bodyData appendFormat:@"%@=",key];
        NSString *value = [dictionary valueForKey:key];
        NSString *newString = [value stringByReplacingOccurrencesOfString:@" " withString:@"+"];
        [bodyData appendString:newString];
        if (i < dictionary.allKeys.count) {
            [bodyData appendString:@"&"];
        }
    }
    return bodyData;
}
2

I've got another solution:

http://splinter.com.au/build-a-url-query-string-in-obj-c-from-a-dict

+(NSString*)urlEscape:(NSString *)unencodedString {
    NSString *s = (NSString *)CFURLCreateStringByAddingPercentEscapes(NULL,
        (CFStringRef)unencodedString,
        NULL,
        (CFStringRef)@"!*'\"();:@&=+$,/?%#[]% ",
        kCFStringEncodingUTF8);
    return [s autorelease]; // Due to the 'create rule' we own the above and must autorelease it
}

// Put a query string onto the end of a url
+(NSString*)addQueryStringToUrl:(NSString *)url params:(NSDictionary *)params {
    NSMutableString *urlWithQuerystring = [[[NSMutableString alloc] initWithString:url] autorelease];
    // Convert the params into a query string
    if (params) {
        for(id key in params) {
            NSString *sKey = [key description];
            NSString *sVal = [[params objectForKey:key] description];
            // Do we need to add ?k=v or &k=v ?
            if ([urlWithQuerystring rangeOfString:@"?"].location==NSNotFound) {
                [urlWithQuerystring appendFormat:@"?%@=%@", [Http urlEscape:sKey], [Http urlEscape:sVal]];
            } else {
                [urlWithQuerystring appendFormat:@"&%@=%@", [Http urlEscape:sKey], [Http urlEscape:sVal]];
            }
        }
    }
    return urlWithQuerystring;
}

You can then use it like so:

NSDictionary *params = @{@"username":@"jim", @"password":@"abc123"};

NSString *urlWithQuerystring = [self addQueryStringToUrl:@"https://myapp.com/login" params:params];
Anupdas
  • 10,211
  • 2
  • 35
  • 60
Chris
  • 39,719
  • 45
  • 189
  • 235
1

Yet another solution, if you use RestKit there's a function in RKURLEncodedSerialization called RKURLEncodedStringFromDictionaryWithEncoding that does exactly what you want.

cicerocamargo
  • 1,046
  • 9
  • 9
1

Simple way of converting NSDictionary to url query string in Objective-c

Ex: first_name=Steve&middle_name=Gates&last_name=Jobs&address=Palo Alto, California

    NSDictionary *sampleDictionary = @{@"first_name"         : @"Steve",
                                     @"middle_name"          : @"Gates",
                                     @"last_name"            : @"Jobs",
                                     @"address"              : @"Palo Alto, California"};

    NSMutableString *resultString = [NSMutableString string];
            for (NSString* key in [sampleDictionary allKeys]){
                if ([resultString length]>0)
                    [resultString appendString:@"&"];
                [resultString appendFormat:@"%@=%@", key, [sampleDictionary objectForKey:key]];
            }
NSLog(@"QueryString: %@", resultString);

Hope will help :)

vidalbenjoe
  • 921
  • 13
  • 37
0

If you are already using AFNetwork, you can use their built in serializer to to produce an encoded URL;

NSString *baseURL = @"https://api.app.com/parse";
NSDictionary *mutableParameters = [[NSMutableDictionary alloc] initWithObjectsAndKeys:@"true",@"option1", data, @"option2", token, @"token", @"3.0", @"app", nil];
NSURLRequest *request = [[AFHTTPRequestSerializer serializer] requestWithMethod:@"GET" URLString:baseURL parameters:mutableParameters error:nil];
NSString *urlPath = request.URL.absoluteString;
NSLog(@"%@", urlPath); // https://api.app.com/parse?option1=true&option2=datavalue&token=200%3ATEST%3AENCODE ....

Note; this is an extension to an above answer. The edit queue is full so cannot be added to the existing answer.

Ralpharoo
  • 789
  • 9
  • 21