Criação de parâmetros de consulta de URL a partir de objetos NSDictionary em ObjectiveC

90

Com todos os objetos de manipulação de URL disponíveis nas bibliotecas Cocoa padrão (NSURL, NSMutableURL, NSMutableURLRequest, etc), eu sei que devo estar negligenciando uma maneira fácil de compor programaticamente uma solicitação GET.

Atualmente estou acrescentando manualmente "?" seguido por pares de valor de nome unidos por "&", mas todos os meus pares de nome e valor precisam ser codificados manualmente para que NSMutableURLRequest não falhe totalmente ao tentar se conectar ao URL.

Isso parece algo que eu deveria ser capaz de usar uma API pré-preparada para ... há algo pronto para ser usado para anexar um NSDictionary de parâmetros de consulta a um NSURL? Existe outra maneira de abordar isso?

Zev Eisenberg
fonte
Quantas respostas! Muitos votos em perguntas e respostas! E ainda não está marcado como respondido. Uau!
BangOperator

Respostas:

132

Introduzido no iOS8 e no OS X 10.10 é NSURLQueryItem, que pode ser usado para construir consultas. Dos documentos em NSURLQueryItem :

Um objeto NSURLQueryItem representa um único par nome / valor para um item na parte de consulta de um URL. Você usa itens de consulta com a propriedade queryItems de um objeto NSURLComponents.

Para criar um, use o inicializador designado queryItemWithName:value:e, em seguida, adicione-os a NSURLComponentspara gerar um NSURL. Por exemplo:

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 o ponto de interrogação e o e comercial são tratados automaticamente. Criar um a NSURLpartir de um dicionário de parâmetros é tão simples quanto:

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;

Também escrevi uma postagem no blog sobre como construir URLs com NSURLComponentse NSURLQueryItems.

Joe Masilotti
fonte
2
e se o dicionário estiver aninhado?
Hariszaman
1
@hariszaman se você estiver enviando um dicionário aninhado por meio da URL, provavelmente deve pensar em enviá-lo por meio do corpo do POST.
Joe Masilotti
2
apenas para observar, o valor só pode ser do tipo NSString
igrrik
Só quero enfatizar um ponto aqui: o valor de um NSURLQueryItem só pode ser uma string. Isso pode resultar em uma falha se não for o caso!
Pulkit Sharma de
47

Você pode criar uma categoria para NSDictionaryfazer isso - não há uma forma padrão na biblioteca Cocoa que eu possa encontrar. O código que uso é parecido com este:

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

@interface NSDictionary (UrlEncoding)

-(NSString*) urlEncodedString;

@end

com esta implementação:

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

Acho o código bastante simples, mas discuto-o com mais detalhes em http://blog.ablepear.com/2008/12/urlencoding-category-for-nsdictionary.html .

Don McCaughey
fonte
16
Eu não acho que isso funcione. Especificamente, você está usando stringByAddingPercentEscapesUsingEncoding, que não escapa "e" comercial, bem como outros caracteres que devem ser escapados nos parâmetros de consulta, conforme especificado no RFC 3986 . Considere examinar o código de escape da Caixa de ferramentas do Google para Mac .
Dave Peck
Isso funciona para o escape adequado: stackoverflow.com/questions/9187316/…
JoeCortopassi
30

Eu queria usar a resposta de Chris, mas ela não foi escrita para contagem automática de referência (ARC), então eu a atualizei. Pensei em colar minha solução, caso outra pessoa tenha o mesmo problema. Nota: substitua selfpela instância ou nome da classe onde apropriado.

+(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 anônimo
fonte
Eu gosto deste código, a única coisa a acrescentar é que a codificação seja segura para url para todas as chaves / valores.
Pellet
22

As outras respostas funcionam bem se os valores forem strings, no entanto, se os valores forem dicionários ou matrizes, este código tratará disso.

É importante notar que não existe uma forma padrão de passar um array / dicionário através da string de consulta, mas o PHP lida com esta saída muito bem

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

Exemplos

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

foo = bar & traduções [um] = uno & traduções [dois] = dos & traduções [três] = três

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

foo = bar & traduções [] = uno & traduções [] = dos & traduções [] = tres

AlBeebe
fonte
1
Para o caso final de 'else', adicionei uma chamada à descrição para que forneça strings para NSNumbers em vez de vomitar em uma chamada de comprimento: '(CFStringRef) [[params objectForKey: key] description ]' Obrigado!
JohnQ
Ótima resposta. No entanto, você deve substituir as CFURLCreateStringByAddingPercentEscapeschamadas porstringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLQueryAllowedCharacterSet]
S1LENT WARRIOR
8

Refatorei e converti para a resposta ARC de 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
fonte
2
Isso funcionou muito bem para mim, mas mudei a última linha para incluir um? antes do primeiro parâmetro:[NSString stringWithFormat:@"?%@",[pairs componentsJoinedByString:@"&"]];
BFar de
1
Acho que você deve substituir as CFURLCreateStringByAddingPercentEscapeschamadasstringByAddingPercentEncodingWithAllowedCharacters:[NSCharac‌​terSet URLQueryAllowedCharacterSet]
S1LENT WARRIOR
5

Se você já está usando AFNetworking (como foi o meu caso), você pode usar sua classe AFHTTPRequestSerializerpara criar o necessário NSURLRequest.

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

Caso você precise apenas da URL para o seu trabalho, use NSURLRequest.URL.

Ayush Goel
fonte
3

Aqui está um exemplo simples em 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
fonte
1
Muito legal, mas queryItemsestão disponíveis a partir do iOS8.0.
Adil Soomro
Obrigado, adicionou um comentário para esclarecer isso.
Zorayr
3

Eu segui a recomendação de Joel de usar URLQueryItemse me transformei em uma extensão 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)
    }
}

(O self.initmétodo é meio cafona, mas não havia NSURLinit com componentes)

Pode ser usado como

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

Eu tenho outra solução:

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

Você pode usá-lo assim:

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

NSString *urlWithQuerystring = [self addQueryStringToUrl:@"https://myapp.com/login" params:params];
Chris
fonte
4
Poste o código relevante em sua resposta, esse link pode não durar para sempre.
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
fonte
Isso não codifica corretamente caracteres especiais em nomes ou valores (exceto espaço).
jcaron
Não acredito que o espaço deva ser substituído por +, mas sim por% 20 e este é o único caractere alterado
Przemysław Wrzesiński
1

Outra solução, se você usar o RestKit, há uma função RKURLEncodedSerializationchamada RKURLEncodedStringFromDictionaryWithEncodingque faz exatamente o que você deseja.

cicerocamargo
fonte
1

Maneira simples de converter NSDictionary em string de consulta de url em Objective-c

Ex: first_name = Steve & middle_name = Gates & last_name = Jobs & address = Palo Alto, Califórnia

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

Espero que ajude :)

Vidalbenjoe
fonte
Isso é incorreto porque não faz escape caracteres como "&" se eles aparecessem nos valores do dicionário.
Thomas Tempelmann