Création de paramètres de requête URL à partir d'objets NSDictionary dans ObjectiveC

90

Avec tous les objets de gestion d'URL qui traînent dans les bibliothèques Cocoa standard (NSURL, NSMutableURL, NSMutableURLRequest, etc.), je sais que je dois négliger un moyen facile de composer par programme une requête GET.

Actuellement, j'ajoute manuellement "?" suivi de paires nom-valeur jointes par "&", mais toutes mes paires nom et valeur doivent être codées manuellement afin que NSMutableURLRequest n'échoue pas entièrement lorsqu'il tente de se connecter à l'URL.

Cela ressemble à quelque chose pour lequel je devrais pouvoir utiliser une API pré-cuite ... y a-t-il quelque chose hors de la boîte pour ajouter un NSDictionary de paramètres de requête à un NSURL? Y a-t-il une autre façon d'aborder cela?

Zev Eisenberg
la source
Tant de réponses! Tant de votes sur questions et réponses! Et toujours sa réponse non marquée. Woah!
BangOperator

Réponses:

132

Introduit dans iOS8 et OS X 10.10 est NSURLQueryItem, qui peut être utilisé pour créer des requêtes. À partir de la documentation sur NSURLQueryItem :

Un objet NSURLQueryItem représente une seule paire nom / valeur pour un élément dans la partie requête d'une URL. Vous utilisez des éléments de requête avec la propriété queryItems d'un objet NSURLComponents.

Pour en créer un, utilisez l'initialiseur désigné queryItemWithName:value:, puis ajoutez-les NSURLComponentspour générer un fichier NSURL. Par exemple:

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

Notez que le point d'interrogation et l'esperluette sont automatiquement gérés. Créer un à NSURLpartir d'un dictionnaire de paramètres est aussi simple que:

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;

J'ai également écrit un article de blog sur la création d'URL avec NSURLComponentset NSURLQueryItems.

Joe Masilotti
la source
2
et si le dictionnaire est imbriqué?
hariszaman
1
@hariszaman si vous envoyez un dictionnaire imbriqué via l'URL, vous devriez probablement penser à l'envoyer via le corps POST à ​​la place.
Joe Masilotti
2
juste à noter, la valeur ne peut être que de type NSString
igrrik
Je veux juste souligner un point ici: la valeur d'un NSURLQueryItem ne peut être qu'une chaîne. Cela pourrait entraîner un crash sinon le cas!
Pulkit Sharma
47

Vous pouvez créer une catégorie pour NSDictionaryce faire - il n'y a pas non plus de moyen standard dans la bibliothèque Cocoa que je puisse trouver. Le code que j'utilise ressemble à ceci:

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

@interface NSDictionary (UrlEncoding)

-(NSString*) urlEncodedString;

@end

avec cette implémentation:

// 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

Je pense que le code est assez simple, mais j'en discute plus en détail à http://blog.ablepear.com/2008/12/urlencoding-category-for-nsdictionary.html .

Don McCaughey
la source
16
Je ne pense pas que cela fonctionne. Plus précisément, vous utilisez stringByAddingPercentEscapesUsingEncoding, qui n'échappe pas les esperluettes, ainsi que d'autres caractères qui doivent être échappés dans les paramètres de requête comme spécifié dans la RFC 3986 . Envisagez plutôt de consulter le code d'échappement de Google Toolbox pour Mac .
Dave Peck
Cela fonctionne pour un échappement correct: stackoverflow.com/questions/9187316/…
JoeCortopassi
30

Je voulais utiliser la réponse de Chris, mais elle n'était pas écrite pour le comptage automatique des références (ARC), alors je l'ai mise à jour. J'ai pensé coller ma solution au cas où quelqu'un d'autre aurait le même problème. Remarque: remplacez selfpar le nom de l'instance ou de la classe, le cas échéant.

+(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;
}
Dev anonyme
la source
J'aime ce code, la seule chose à ajouter est que l'encodage soit sûr pour toutes les clés / valeurs.
Pellet
22

Les autres réponses fonctionnent très bien si les valeurs sont des chaînes, mais si les valeurs sont des dictionnaires ou des tableaux, ce code gérera cela.

Il est important de noter qu'il n'y a pas de moyen standard de passer un tableau / dictionnaire via la chaîne de requête, mais PHP gère cette sortie très bien

-(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:@"&"];
}

Exemples

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

toto = bar & traductions [un] = uno & traductions [deux] = dos & traductions [trois] = tres

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

toto = bar & traductions [] = uno & traductions [] = dos & traductions [] = tres

AlBeebe
la source
1
Pour le dernier cas 'else', j'ai ajouté un appel à la description afin qu'elle fournisse des chaînes pour NSNumbers plutôt que de barfing sur un appel à length: '(CFStringRef) [[params objectForKey: key] description ]' Merci!
JohnQ
très bonne réponse. Cependant, vous devriez remplacer les CFURLCreateStringByAddingPercentEscapesappels parstringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLQueryAllowedCharacterSet]
S1LENT WARRIOR
8

J'ai refactoré et converti en réponse ARC par 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
la source
2
Cela a très bien fonctionné pour moi, mais j'ai changé la dernière ligne afin d'inclure un? avant le premier [NSString stringWithFormat:@"?%@",[pairs componentsJoinedByString:@"&"]];
paramètre
1
Je pense que vous devriez remplacer les CFURLCreateStringByAddingPercentEscapesappelsstringByAddingPercentEncodingWithAllowedCharacters:[NSCharac‌​terSet URLQueryAllowedCharacterSet]
S1LENT WARRIOR
5

Si vous utilisez déjà AFNetworking (comme c'était le cas avec moi), vous pouvez utiliser sa classe AFHTTPRequestSerializerpour créer le fichier requis NSURLRequest.

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

Si vous n'avez besoin que de l'URL pour votre travail, utilisez NSURLRequest.URL.

Ayush Goel
la source
3

Voici un exemple simple dans 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
la source
1
Assez soigné, mais queryItemssont disponibles à partir d'iOS8.0.
Adil Soomro
Merci, a ajouté un commentaire pour clarifier cela.
Zorayr
3

J'ai suivi la recommandation de Joel d'utiliser URLQueryItemset je suis devenu une extension Swift (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)
    }
}

(La self.initméthode est un peu ringarde, mais il n'y avait pas d' NSURLinit avec des composants)

Peut être utilisé comme

URL(string: "http://www.google.com/", parameters: ["q" : "search me"])
Pouvez
la source
2

J'ai une autre 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;
}

Vous pouvez ensuite l'utiliser comme ceci:

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

NSString *urlWithQuerystring = [self addQueryStringToUrl:@"https://myapp.com/login" params:params];
Chris
la source
4
Veuillez publier le code pertinent dans votre réponse, ce lien peut ne pas être là pour toujours.
Madbreaks
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;
}
Filippo Ozino Caligaris
la source
Cela n'encode pas correctement les caractères spéciaux dans les noms ou les valeurs (autres que l'espace).
jcaron
Je ne pense pas que l'espace devrait être remplacé par + mais plutôt par% 20 et c'est le seul caractère changé
Przemysław Wrzesiński
1

Encore une autre solution, si vous utilisez RestKit, il y a une fonction RKURLEncodedSerializationappelée RKURLEncodedStringFromDictionaryWithEncodingqui fait exactement ce que vous voulez.

cicerocamargo
la source
1

Moyen simple de convertir NSDictionary en chaîne de requête URL dans Objective-c

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

    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);

L'espoir aidera :)

vidalbenjoe
la source
Ceci est incorrect car il n'échappe pas les caractères tels que "&" s'ils apparaissent dans les valeurs du dictionnaire.
Thomas Tempelmann