Tworzenie parametrów zapytania URL z obiektów NSDictionary w ObjectiveC

90

Ponieważ wszystkie obiekty obsługujące adresy URL znajdują się w standardowych bibliotekach Cocoa (NSURL, NSMutableURL, NSMutableURLRequest itp.), Wiem, że muszę przeoczyć łatwy sposób programistycznego tworzenia żądania GET.

Obecnie ręcznie dołączam „?” po którym następują pary nazwa-wartość połączone znakiem „&”, ale wszystkie moje pary nazwa i wartość muszą być zakodowane ręcznie, aby NSMutableURLRequest nie zakończyło się niepowodzeniem podczas próby połączenia się z adresem URL.

Wydaje mi się, że powinienem być w stanie użyć wstępnie upieczonego interfejsu API do… czy jest coś nieskomplikowanego, aby dołączyć NSDictionary parametrów zapytania do NSURL? Czy jest inny sposób, w jaki powinienem do tego podejść?

Zev Eisenberg
źródło
Tyle odpowiedzi! Tyle głosów na pytanie i odpowiedzi! I nadal nie ma zaznaczonej odpowiedzi. Woah!
BangOperator

Odpowiedzi:

132

Wprowadzony w iOS8 i OS X 10.10 jest NSURLQueryItem, który może służyć do budowania zapytań. Z dokumentacji w NSURLQueryItem :

Obiekt NSURLQueryItem reprezentuje pojedynczą parę nazwa / wartość dla elementu w części adresu URL zawierającej zapytanie. Elementów zapytania używa się z właściwością queryItems obiektu NSURLComponents.

Aby go utworzyć, użyj wyznaczonego inicjatora, queryItemWithName:value:a następnie dodaj je do, NSURLComponentsaby wygenerować plik NSURL. Na przykład:

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

Zwróć uwagę, że znak zapytania i ampersand są obsługiwane automatycznie. Tworzenie NSURLze słownika parametrów jest tak proste, jak:

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;

Napisałem również post na blogu o tym, jak tworzyć adresy URL za pomocą NSURLComponentsi NSURLQueryItems.

Joe Masilotti
źródło
2
co, jeśli słownik jest zagnieżdżony?
hariszaman
1
@hariszaman jeśli wysyłasz zagnieżdżony słownik przez adres URL, prawdopodobnie powinieneś pomyśleć o wysłaniu go za pośrednictwem treści POST.
Joe Masilotti
2
tylko uwaga, wartość może być tylko typu NSString
igrrik
Chcę tylko podkreślić punkt tutaj: wartość NSURLQueryItem może być tylko ciągiem. Jeśli tak nie jest, może to spowodować awarię!
Pulkit Sharma
47

Możesz utworzyć kategorię, aby NSDictionaryto zrobić - w bibliotece Cocoa nie ma standardowego sposobu, który mógłbym znaleźć. Kod, którego używam, wygląda następująco:

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

@interface NSDictionary (UrlEncoding)

-(NSString*) urlEncodedString;

@end

przy tej implementacji:

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

Myślę, że kod jest dość prosty, ale omówię go bardziej szczegółowo pod adresem http://blog.ablepear.com/2008/12/urlencoding-category-for-nsdictionary.html .

Don McCaughey
źródło
16
Myślę, że to nie działa. W szczególności używasz stringByAddingPercentEscapesUsingEncoding, który nie powoduje zmiany znaczenia znaków ampersand, a także innych znaków, które muszą zostać zmienione w parametrach zapytania, jak określono w dokumencie RFC 3986 . Zamiast tego rozważ przyjrzenie się kodowi ucieczki Google Toolbox for Mac .
Dave Peck
To działa dla prawidłowego ucieczki: stackoverflow.com/questions/9187316/…
JoeCortopassi
30

Chciałem użyć odpowiedzi Chrisa, ale nie została napisana dla automatycznego liczenia referencji (ARC), więc zaktualizowałem ją. Pomyślałem, że wkleię moje rozwiązanie na wypadek, gdyby ktoś inny miał ten sam problem. Uwaga:self w razie potrzeby zastąp nazwą instancji lub klasy.

+(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;
}
Anonimowy Dev
źródło
Podoba mi się ten kod, jedyną rzeczą do dodania jest to, aby kodowanie było bezpieczne dla wszystkich kluczy / wartości.
Pellet
22

Pozostałe odpowiedzi działają świetnie, jeśli wartości są łańcuchami, jednak jeśli wartości są słownikami lub tablicami, ten kod poradzi sobie z tym.

Należy zauważyć, że nie ma standardowego sposobu przekazywania tablicy / słownika przez ciąg zapytania, ale PHP obsługuje to wyjście dobrze

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

Przykłady

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

foo = bar i tłumaczenia [jeden] = brak i tłumaczenia [dwa] = polecenia i tłumaczenia [trzy] = tres

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

foo = bar i tłumaczenia [] = uno i tłumaczenia [] = dos & tłumaczenia [] = tres

AlBeebe
źródło
1
W ostatnim przypadku 'else' dodałem wywołanie do opisu, tak aby dostarczało ciągi znaków dla NSNumbers zamiast barfingu na wywołaniu length: '(CFStringRef) [[params objectForKey: key] description ]' Dzięki!
JohnQ
świetna odpowiedź. Jednak należy zamienić CFURLCreateStringByAddingPercentEscapespołączenia nastringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLQueryAllowedCharacterSet]
S1LENT WARRIOR
8

Refaktoryzowałem i przekonwertowałem na odpowiedź ARC autorstwa 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
źródło
2
To działało świetnie dla mnie, ale zmieniłem ostatnią linię, aby dodać? przed pierwszym parametrem:[NSString stringWithFormat:@"?%@",[pairs componentsJoinedByString:@"&"]];
BFar
1
myślę, że powinieneś zmienić CFURLCreateStringByAddingPercentEscapespołączeniastringByAddingPercentEncodingWithAllowedCharacters:[NSCharac‌​terSet URLQueryAllowedCharacterSet]
S1LENT WARRIOR
5

Jeśli korzystasz już z AFNetworking (tak jak w przypadku mnie), możesz użyć jego klasy AFHTTPRequestSerializerdo utworzenia wymaganego plikuNSURLRequest .

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

Jeśli potrzebujesz tylko adresu URL do swojej pracy, użyj NSURLRequest.URL.

Ayush Goel
źródło
3

Oto prosty przykład w 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
źródło
1
Całkiem queryItemsfajne , ale są dostępne od iOS8.0.
Adil Soomro,
Dzięki, dodał komentarz, aby to wyjaśnić.
Zorayr
3

Przyjąłem zalecenie Joela URLQueryItemsi zmieniłem go w 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)
    }
}

( self.initMetoda jest trochę tandetna, ale nie było NSURLinicjalizacji z komponentami)

Może być używany jako

URL(string: "http://www.google.com/", parameters: ["q" : "search me"])
Mogą
źródło
2

Mam inne rozwiązanie:

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

Następnie możesz go użyć w następujący sposób:

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

NSString *urlWithQuerystring = [self addQueryStringToUrl:@"https://myapp.com/login" params:params];
Chris
źródło
4
W odpowiedzi podaj odpowiedni kod, który może nie istnieć wiecznie.
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
źródło
To nie koduje poprawnie znaków specjalnych w nazwach lub wartościach (innych niż spacja).
jcaron
Nie uważam, że miejsce powinno być zastępowane przez +, a raczej% 20 i to jedyna zmieniona postać
Przemysław Wrzesiński
1

Jeszcze inne rozwiązanie, jeśli używasz RestKit, istnieje funkcja o RKURLEncodedSerializationnazwie, RKURLEncodedStringFromDictionaryWithEncodingktóra robi dokładnie to, co chcesz.

cicerocamargo
źródło
1

Prosty sposób konwersji NSDictionary na ciąg zapytania url w Objective-c

Np .: first_name = Steve & middle_name = Gates & last_name = Praca i adres = Palo Alto, Kalifornia

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

Nadzieja pomoże :)

vidalbenjoe
źródło
Jest to niepoprawne, ponieważ nie powoduje zmiany znaczenia znaków, takich jak „&”, jeśli pojawią się one w wartościach słownika.
Thomas Tempelmann