Creación de parámetros de consulta de URL a partir de objetos NSDictionary en ObjectiveC

90

Con todos los objetos de manejo de URL que se encuentran en las bibliotecas estándar de Cocoa (NSURL, NSMutableURL, NSMutableURLRequest, etc.), sé que debo estar pasando por alto una manera fácil de componer programáticamente una solicitud GET.

Actualmente estoy agregando manualmente "?" seguido de pares de nombre y valor unidos por "&", pero todos mis pares de nombre y valor deben codificarse manualmente para que NSMutableURLRequest no falle por completo cuando intenta conectarse a la URL.

Esto se siente como algo para lo que debería poder usar una API prefabricada ... ¿hay algo fuera de la caja para agregar un NSDictionary de parámetros de consulta a una NSURL? ¿Hay otra forma de abordar esto?

Zev Eisenberg
fuente
¡Tantas respuestas! ¡Tantos votos en preguntas y respuestas! Y todavía no está marcado como respondido. ¡Woah!
BangOperator

Respuestas:

132

Introducido en iOS8 y OS X 10.10 es NSURLQueryItem, que se puede utilizar para crear consultas. De los documentos en NSURLQueryItem :

Un objeto NSURLQueryItem representa un único par de nombre / valor para un elemento en la parte de consulta de una URL. Utiliza elementos de consulta con la propiedad queryItems de un objeto NSURLComponents.

Para crear uno, use el inicializador designado queryItemWithName:value:y luego agréguelos NSURLComponentspara generar un NSURL. Por ejemplo:

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

Observe que el signo de interrogación y el ampersand se manejan automáticamente. La creación NSURLde un diccionario de parámetros es tan simple como:

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;

También escribí una publicación de blog sobre cómo crear URL con NSURLComponentsy NSURLQueryItems.

Joe Masilotti
fuente
2
¿y si el diccionario está anidado?
hariszaman
1
@hariszaman si está enviando un diccionario anidado a través de la URL, probablemente debería pensar en enviarlo a través del cuerpo POST.
Joe Masilotti
2
solo para tener en cuenta, el valor solo puede ser de tipo NSString
igrrik
Solo quiero enfatizar un punto aquí: el valor de un NSURLQueryItem solo puede ser una cadena. ¡Podría resultar en un accidente si no es el caso!
Pulkit Sharma
47

Puede crear una categoría para NSDictionaryhacer esto; tampoco hay una forma estándar en la biblioteca de Cocoa que pueda encontrar. El código que utilizo se ve así:

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

@interface NSDictionary (UrlEncoding)

-(NSString*) urlEncodedString;

@end

con esta implementación:

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

Creo que el código es bastante sencillo, pero lo analizo con más detalle en http://blog.ablepear.com/2008/12/urlencoding-category-for-nsdictionary.html .

Don McCaughey
fuente
16
No creo que esto funcione. Específicamente, está utilizando stringByAddingPercentEscapesUsingEncoding, que no escapa a los símbolos de unión, así como a otros caracteres que deben escaparse en los parámetros de consulta como se especifica en RFC 3986 . En su lugar, considere buscar el código de escape de Google Toolbox para Mac .
Dave Peck
Esto funciona para un escape adecuado: stackoverflow.com/questions/9187316/…
JoeCortopassi
30

Quería usar la respuesta de Chris, pero no estaba escrito para el Conteo automático de referencias (ARC), así que lo actualicé. Pensé en pegar mi solución en caso de que alguien más tuviera el mismo problema. Nota: reemplácelo selfcon el nombre de la instancia o la clase cuando corresponda.

+(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;
}
Desarrollador anónimo
fuente
Me gusta este código, lo único que se debe agregar es que la codificación sea segura para todas las claves / valores.
Pellet
22

Las otras respuestas funcionan muy bien si los valores son cadenas, sin embargo, si los valores son diccionarios o matrices, este código se encargará de eso.

Es importante tener en cuenta que no hay una forma estándar de pasar una matriz / diccionario a través de la cadena de consulta, pero PHP maneja esta salida 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:@"&"];
}

Ejemplos

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

foo = barra y traducciones [uno] = uno y traducciones [dos] = dos y traducciones [tres] = tres

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

foo = barra y traducciones [] = uno y traducciones [] = dos y traducciones [] = tres

AlBeebe
fuente
1
Para el caso final 'else', agregué una llamada a la descripción para que proporcione cadenas para NSNumbers en lugar de vomitar en una llamada a la longitud: '(CFStringRef) [[params objectForKey: key] description ]' ¡Gracias!
JohnQ
gran respuesta. Sin embargo, debe reemplazar las CFURLCreateStringByAddingPercentEscapesllamadas constringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLQueryAllowedCharacterSet]
S1LENT WARRIOR
8

Refactoré y convertí a la respuesta ARC por 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
fuente
2
Esto funcionó muy bien para mí, pero cambié la última línea para incluir un? antes del primer [NSString stringWithFormat:@"?%@",[pairs componentsJoinedByString:@"&"]];
parámetro
1
Creo que deberías reemplazar las CFURLCreateStringByAddingPercentEscapesllamadasstringByAddingPercentEncodingWithAllowedCharacters:[NSCharac‌​terSet URLQueryAllowedCharacterSet]
S1LENT WARRIOR
5

Si ya está usando AFNetworking (como fue mi caso), puede usar su clase AFHTTPRequestSerializerpara crear el archivo NSURLRequest.

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

En caso de que solo necesite la URL para su trabajo, utilice NSURLRequest.URL.

Ayush Goel
fuente
3

Aquí hay un ejemplo simple en 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
fuente
1
Bastante ordenado, pero queryItemsestán disponibles desde iOS8.0.
Adil Soomro
Gracias, agregó un comentario para aclarar esto.
Zorayr
3

Seguí la recomendación de Joel de usar URLQueryItemsy la convertí en una extensión 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)
    }
}

(El self.initmétodo es un poco cursi, pero no había NSURLinit con componentes)

Puede usarse como

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

Tengo otra solución:

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

Luego puedes usarlo así:

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

NSString *urlWithQuerystring = [self addQueryStringToUrl:@"https://myapp.com/login" params:params];
Chris
fuente
4
Publique el código relevante en su respuesta, es posible que ese enlace no esté disponible para siempre.
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
fuente
Esto no codifica correctamente los caracteres especiales en nombres o valores (que no sean espacios).
jcaron
No creo que el espacio deba reemplazarse por +, sino por% 20 y este es el único carácter que ha cambiado
Przemysław Wrzesiński
1

Otra solución más, si usa RestKit, hay una función RKURLEncodedSerializationllamada RKURLEncodedStringFromDictionaryWithEncodingque hace exactamente lo que desea.

cicerocamargo
fuente
1

Manera simple de convertir NSDictionary a una cadena de consulta de URL en Objective-c

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

La esperanza ayudará :)

vidalbenjoe
fuente
Esto es incorrecto porque no escapa caracteres como "&" si aparecerían en los valores del diccionario.
Thomas Tempelmann