Url minus query string in Objective-C - objective-c

What's the best way to get an url minus its query string in Objective-C? An example:
Input:
http://www.example.com/folder/page.htm?param1=value1&param2=value2
Output:
http://www.example.com/folder/page.htm
Is there a NSURL method to do this that I'm missing?

Since iOS 8/OS X 10.9, there is an easier way to do this with NSURLComponents.
NSURL *url = [NSURL URLWithString:#"http://hostname.com/path?key=value"];
NSURLComponents *urlComponents = [[NSURLComponents alloc] initWithURL:url resolvingAgainstBaseURL:NO];
urlComponents.query = nil; // Strip out query parameters.
NSLog(#"Result: %#", urlComponents.string); // Should print http://hostname.com/path

There's no NSURL method I can see. You might try something like:
NSURL *newURL = [[NSURL alloc] initWithScheme:[url scheme]
host:[url host]
path:[url path]];
Testing looks good:
#import <Foundation/Foundation.h>
int main(int argc, char *argv[]) {
NSAutoreleasePool *arp = [[NSAutoreleasePool alloc] init];
NSURL *url = [NSURL URLWithString:#"http://www.abc.com/foo/bar.cgi?a=1&b=2"];
NSURL *newURL = [[[NSURL alloc] initWithScheme:[url scheme]
host:[url host]
path:[url path]] autorelease];
NSLog(#"\n%# --> %#", url, newURL);
[arp release];
return 0;
}
Running this produces:
$ gcc -lobjc -framework Foundation -std=c99 test.m ; ./a.out
2010-11-25 09:20:32.189 a.out[36068:903]
http://www.abc.com/foo/bar.cgi?a=1&b=2 --> http://www.abc.com/foo/bar.cgi

Here is the Swift version of Andree's answer, with some extra flavour -
extension NSURL {
func absoluteStringByTrimmingQuery() -> String? {
if var urlcomponents = NSURLComponents(URL: self, resolvingAgainstBaseURL: false) {
urlcomponents.query = nil
return urlcomponents.string
}
return nil
}
}
You can call it like -
let urlMinusQueryString = url.absoluteStringByTrimmingQuery()

Swift Version
extension URL {
func absoluteStringByTrimmingQuery() -> String? {
if var urlcomponents = URLComponents(url: self, resolvingAgainstBaseURL: false) {
urlcomponents.query = nil
return urlcomponents.string
}
return nil
}
}
Hope this helps!

What you probably need is a combination of url's host and path components:
NSString *result = [[url host] stringByAppendingPathComponent:[url path]];

You could try using query of NSURL to get the parameters, then strip that value using stringByReplacingOccurrencesOfString of NSString?
NSURL *before = [NSURL URLWithString:#"http://www.example.com/folder/page.htm?param1=value1&param2=value2"];
NSString *after = [before.absoluteString stringByReplacingOccurrencesOfString:before.query withString:#""];
Note, the final URL will still end with ?, but you could easily strip that as well if needed.

I think -baseURL might do what you want.
If not, you can can do a round trip through NSString like so:
NSString *string = [myURL absoluteString];
NSString base = [[string componentsSeparatedByString:#"?"] objectAtIndex:0];
NSURL *trimmed = [NSURL URLWithString:base];

NSURL has a query property which contains everything after the ? in a GET url. So simply subtract that from the end of the absoluteString, and you've got the url without the query.
NSURL *originalURL = [NSURL URLWithString:#"https://winker#127.0.0.1:1000/file/path/?q=dogfood"];
NSString *strippedString = [originalURL absoluteString];
NSUInteger queryLength = [[originalURL query] length];
strippedString = (queryLength ? [strippedString substringToIndex:[strippedString length] - (queryLength + 1)] : strippedString);
NSLog(#"Output: %#", strippedString);
Logs:
Output: https://winker#127.0.0.1:1000/file/path/
The +1 is for the ? which is not part of query.

You might fancy the method replaceOccurrencesOfString:withString:options:range: of the NSMutableString class. I solved this by writing a category for NSURL:
#import <Foundation/Foundation.h>
#interface NSURL (StripQuery)
// Returns a new URL with the query stripped out.
// Note: If there is no query, returns a copy of this URL.
- (NSURL *)URLByStrippingQuery;
#end
#implementation NSURL (StripQuery)
- (NSURL *)URLByStrippingQuery
{
NSString *query = [self query];
// Simply copy if there was no query. (query is nil if URL has no '?',
// and equal to #"" if it has a '?' but no query after.)
if (!query || ![query length]) {
return [self copy];
}
NSMutableString *urlString = [NSMutableString stringWithString:[self absoluteString]];
[urlString replaceOccurrencesOfString:query
withString:#""
options:NSBackwardsSearch
range:NSMakeRange(0, [urlString length])];
return [NSURL URLWithString:urlString];
}
#end
This way, I can send this message to existing NSURL objects and have a new NSURL object be returned to me.
I tested it using this code:
int main(int argc, const char * argv[])
{
#autoreleasepool {
NSURL *url = [NSURL URLWithString:#"http://www.example.com/script.php?key1=val1&key2=val2"];
// NSURL *url = [NSURL URLWithString:#"http://www.example.com/script.php?"];
// NSURL *url = [NSURL URLWithString:#"http://www.example.com/script.php"];
NSURL *newURL = [url URLByStrippingQuery];
NSLog(#"Original URL: \"%#\"\n", [url absoluteString]);
NSLog(#"Stripped URL: \"%#\"\n", [newURL absoluteString]);
}
return 0;
}
and I got the following output (minus the time stamps):
Original URL: "http://www.example.com/script.php?key1=val1&key2=val2"
Stripped URL: "http://www.example.com/script.php?"
Note that the question mark ('?') still remains. I will leave it up to the reader to remove it in a secure way.

We should try to use NSURLComponents
NSURL *url = #"http://example.com/test";
NSURLComponents *comps = [[NSURLComponents alloc] initWithURL:url resolvingAgainstBaseURL:YES];
NSString *cleanUrl = [NSString stringWithFormat:#"%#://%#",comps.scheme,comps.host];
if(comps.path.length > 0){
cleanUrl = [NSString stringWithFormat:#"%#/%#",cleanUrl,comps.path];
}

I think what you're looking for is baseUrl.

Related

How to get an NSURL part without query string? [duplicate]

What's the best way to get an url minus its query string in Objective-C? An example:
Input:
http://www.example.com/folder/page.htm?param1=value1&param2=value2
Output:
http://www.example.com/folder/page.htm
Is there a NSURL method to do this that I'm missing?
Since iOS 8/OS X 10.9, there is an easier way to do this with NSURLComponents.
NSURL *url = [NSURL URLWithString:#"http://hostname.com/path?key=value"];
NSURLComponents *urlComponents = [[NSURLComponents alloc] initWithURL:url resolvingAgainstBaseURL:NO];
urlComponents.query = nil; // Strip out query parameters.
NSLog(#"Result: %#", urlComponents.string); // Should print http://hostname.com/path
There's no NSURL method I can see. You might try something like:
NSURL *newURL = [[NSURL alloc] initWithScheme:[url scheme]
host:[url host]
path:[url path]];
Testing looks good:
#import <Foundation/Foundation.h>
int main(int argc, char *argv[]) {
NSAutoreleasePool *arp = [[NSAutoreleasePool alloc] init];
NSURL *url = [NSURL URLWithString:#"http://www.abc.com/foo/bar.cgi?a=1&b=2"];
NSURL *newURL = [[[NSURL alloc] initWithScheme:[url scheme]
host:[url host]
path:[url path]] autorelease];
NSLog(#"\n%# --> %#", url, newURL);
[arp release];
return 0;
}
Running this produces:
$ gcc -lobjc -framework Foundation -std=c99 test.m ; ./a.out
2010-11-25 09:20:32.189 a.out[36068:903]
http://www.abc.com/foo/bar.cgi?a=1&b=2 --> http://www.abc.com/foo/bar.cgi
Here is the Swift version of Andree's answer, with some extra flavour -
extension NSURL {
func absoluteStringByTrimmingQuery() -> String? {
if var urlcomponents = NSURLComponents(URL: self, resolvingAgainstBaseURL: false) {
urlcomponents.query = nil
return urlcomponents.string
}
return nil
}
}
You can call it like -
let urlMinusQueryString = url.absoluteStringByTrimmingQuery()
Swift Version
extension URL {
func absoluteStringByTrimmingQuery() -> String? {
if var urlcomponents = URLComponents(url: self, resolvingAgainstBaseURL: false) {
urlcomponents.query = nil
return urlcomponents.string
}
return nil
}
}
Hope this helps!
What you probably need is a combination of url's host and path components:
NSString *result = [[url host] stringByAppendingPathComponent:[url path]];
You could try using query of NSURL to get the parameters, then strip that value using stringByReplacingOccurrencesOfString of NSString?
NSURL *before = [NSURL URLWithString:#"http://www.example.com/folder/page.htm?param1=value1&param2=value2"];
NSString *after = [before.absoluteString stringByReplacingOccurrencesOfString:before.query withString:#""];
Note, the final URL will still end with ?, but you could easily strip that as well if needed.
I think -baseURL might do what you want.
If not, you can can do a round trip through NSString like so:
NSString *string = [myURL absoluteString];
NSString base = [[string componentsSeparatedByString:#"?"] objectAtIndex:0];
NSURL *trimmed = [NSURL URLWithString:base];
NSURL has a query property which contains everything after the ? in a GET url. So simply subtract that from the end of the absoluteString, and you've got the url without the query.
NSURL *originalURL = [NSURL URLWithString:#"https://winker#127.0.0.1:1000/file/path/?q=dogfood"];
NSString *strippedString = [originalURL absoluteString];
NSUInteger queryLength = [[originalURL query] length];
strippedString = (queryLength ? [strippedString substringToIndex:[strippedString length] - (queryLength + 1)] : strippedString);
NSLog(#"Output: %#", strippedString);
Logs:
Output: https://winker#127.0.0.1:1000/file/path/
The +1 is for the ? which is not part of query.
You might fancy the method replaceOccurrencesOfString:withString:options:range: of the NSMutableString class. I solved this by writing a category for NSURL:
#import <Foundation/Foundation.h>
#interface NSURL (StripQuery)
// Returns a new URL with the query stripped out.
// Note: If there is no query, returns a copy of this URL.
- (NSURL *)URLByStrippingQuery;
#end
#implementation NSURL (StripQuery)
- (NSURL *)URLByStrippingQuery
{
NSString *query = [self query];
// Simply copy if there was no query. (query is nil if URL has no '?',
// and equal to #"" if it has a '?' but no query after.)
if (!query || ![query length]) {
return [self copy];
}
NSMutableString *urlString = [NSMutableString stringWithString:[self absoluteString]];
[urlString replaceOccurrencesOfString:query
withString:#""
options:NSBackwardsSearch
range:NSMakeRange(0, [urlString length])];
return [NSURL URLWithString:urlString];
}
#end
This way, I can send this message to existing NSURL objects and have a new NSURL object be returned to me.
I tested it using this code:
int main(int argc, const char * argv[])
{
#autoreleasepool {
NSURL *url = [NSURL URLWithString:#"http://www.example.com/script.php?key1=val1&key2=val2"];
// NSURL *url = [NSURL URLWithString:#"http://www.example.com/script.php?"];
// NSURL *url = [NSURL URLWithString:#"http://www.example.com/script.php"];
NSURL *newURL = [url URLByStrippingQuery];
NSLog(#"Original URL: \"%#\"\n", [url absoluteString]);
NSLog(#"Stripped URL: \"%#\"\n", [newURL absoluteString]);
}
return 0;
}
and I got the following output (minus the time stamps):
Original URL: "http://www.example.com/script.php?key1=val1&key2=val2"
Stripped URL: "http://www.example.com/script.php?"
Note that the question mark ('?') still remains. I will leave it up to the reader to remove it in a secure way.
We should try to use NSURLComponents
NSURL *url = #"http://example.com/test";
NSURLComponents *comps = [[NSURLComponents alloc] initWithURL:url resolvingAgainstBaseURL:YES];
NSString *cleanUrl = [NSString stringWithFormat:#"%#://%#",comps.scheme,comps.host];
if(comps.path.length > 0){
cleanUrl = [NSString stringWithFormat:#"%#/%#",cleanUrl,comps.path];
}
I think what you're looking for is baseUrl.

Cannot append to NSString

I'm trying to append the file extension to a the stringValue returned by a subclassed NSTextFieldCell
I've tried everything I knew and could find on the internet, but this is just giving me a headache
the method is the following:
- (void)drawWithFrame:(NSRect)cellFrame inView:(NSView *)controlView {
NSMutableString *filename = [[NSMutableString alloc] init];
[filename appendString:self.stringValue];
NSString *iconFileName = [[[[NSBundle mainBundle] resourcePath] stringByAppendingPathComponent:filename] stringByAppendingPathExtension:#"png"];
NSLog(#"%#", iconFileName);
}
The returned value is without the extension though!
I've also tried the following:
filename = [NSString stringWithFormat: #"%#.png", filename];
This returns the "filename" string without the ".png"
Similarly:
filename = [filename stringByAppendingString: #".png"];
returns just the "filename"
The table column where this cell belongs to is bound to an NSObject, and the method that sends the data to the column is the following:
- (NSString *) nationString {
NSMutableString *string = [[NSMutableString alloc] init];
int index = 0;
if (nationAddress && nationAddress > 0x0) {
index = [[[[controller database] nationsAddressIndex] valueForKey:[NSString stringWithFormat:#"%lli", nationAddress]] intValue];
Nation *nationality = [[[controller database] nations] objectAtIndex:index];
[string appendString:[nationality name]];
}
else {
[string appendString:#"---"];
}
return string;
}
Anyone has any idea why this might be happening, or can suggest any alternatives?
Any help will be appreciated
Thanks
This should return the complete path with extension:
NSString *path = [[[NSBundle mainBundle] resourcePath] stringByAppendingPathComponent:#"Default.png"];
NSLog(#"%#", path);
So, assuming self.stringValue includes the extension, your method should work with this:
- (void)drawWithFrame:(NSRect)cellFrame inView:(NSView *)controlView {
NSString *iconFileName = [[[NSBundle mainBundle] resourcePath] stringByAppendingPathComponent:self.stringValue];
NSLog(#"%#", iconFileName);
}
If it doesn't include the extension, try this:
- (void)drawWithFrame:(NSRect)cellFrame inView:(NSView *)controlView {
NSString *strWithPath = [NSString stringWithFormat:#"%#.png", self.stringValue];
NSString *iconFileName = [[[NSBundle mainBundle] resourcePath] stringByAppendingPathComponent:strWithPath];
NSLog(#"%#", iconFileName);
}
Just for test. Try to use this code and update here a output values:
NSMutableString *filename = [[NSMutableString alloc] init];
[filename appendString:self.stringValue];
NSLog(#"text1: %# ;", filename);
filename = [NSString stringWithFormat: #"%#.png", filename];
filename = [NSString stringWithFormat: #"%#.png%#", filename, filename];
NSLog(#"text2: %# ;", filename);
These should work (barring a typo):
NSString* filename = #"abc";
NSString* result1 = [NSString stringWithFormat:#"%#.png", filename];
NSString* result2 = [filename stringByAppendingString:#".png"];
NSMutableString* result3 = [filename mutableCopy];
[result3 appendString:#".png"];
If they don't appear to be working then you have some problem with how you're initializing or displaying your values.
Hint: Place an NSLog(#"The answer is %#", resultN); statement immediately after each of the above (with "resultN" changed appropriately) to see what you're getting. Keep in mind that if you look from a different object you may be looking at different variables.

NSString unique file path to avoid name collisions

Is there a simple way to take a given file path and modify it in order to avoid name collisions? Something like:
[StringUtils stringToAvoidNameCollisionForPath:path];
that for a given path of type: /foo/bar/file.png, will return /foo/bar/file-1.png and later it will increment that "-1" similarly to what Safari does for downloaded files.
UPDATE:
I followed Ash Furrow's suggestion and I posted my implementation as answer :)
I had a similar problem, and came up with a slightly broader approach, that attempts to name files the same way iTunes would (when you have it set to manage your library and you have multiple tracks with the same name, etc.)
It works in a loop, so the function can be called multiple times and still produce valid output. Explaining the arguments, fileName is the name of the file with no path or extension (e.g. "file"), folder is just the path (e.g. "/foo/bar"), and fileType is just the extension (e.g. "png"). These three could be passed in as one string and be split out after, but in my case it made sense to separate them.
currentPath (which can be empty, but not nil), is useful when you're renaming a file, not creating a new one. For example, if you have "/foo/bar/file 1.png" that you're trying to rename to "/foo/bar/file.png", you would pass in "/foo/bar/file 1.png" for currentPath, and if "/foo/bar/file.png" already exists, you'll get back the path you started with, instead of seeing that "/foo/bar/file 1.png" and returning "/foo/bar/file 2.png"
+ (NSString *)uniqueFile:(NSString *)fileName
inFolder:(NSString *)folder
withExtension:(NSString *)fileType
mayDuplicatePath:(NSString *)currentPath
{
NSUInteger existingCount = 0;
NSString *result;
NSFileManager *manager = [NSFileManager defaultManager];
do {
NSString *format = existingCount > 0 ? #"%# %lu" : #"%#";
fileName = [NSString stringWithFormat:format, fileName, existingCount++];
result = [fileName stringByAppendingFormat:#".%#", [fileType lowercaseString]];
result = [folder stringByAppendingPathComponent:result];
} while ([manager fileExistsAtPath:result] &&
// This comparison must be case insensitive, as the file system is most likely so
[result caseInsensitiveCompare:currentPath] != NSOrderedSame);
return result;
}
I decided to implement my own solution and I want to share my code. It's not the most desirable implementation, but it seems to do the job:
+ (NSString *)stringToAvoidNameCollisionForPath:(NSString *)path {
// raise an exception for invalid paths
if (path == nil || [path length] == 0) {
[NSException raise:#"DMStringUtilsException" format:#"Invalid path"];
}
NSFileManager *manager = [[[NSFileManager alloc] init] autorelease];
BOOL isDirectory;
// file does not exist, so the path doesn't need to change
if (![manager fileExistsAtPath:path isDirectory:&isDirectory]) {
return path;
}
NSString *lastComponent = [path lastPathComponent];
NSString *fileName = isDirectory ? lastComponent : [lastComponent stringByDeletingPathExtension];
NSString *ext = isDirectory ? #"" : [NSString stringWithFormat:#".%#", [path pathExtension]];
NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:#"-([0-9]{1,})$" options:0 error:nil];
NSArray *matches = [regex matchesInString:fileName options:0 range:STRING_RANGE(fileName)];
// missing suffix... start from 1 (foo-1.ext)
if ([matches count] == 0) {
return [NSString stringWithFormat:#"%#-1%#", fileName, ext];
}
// get last match (theoretically the only one due to "$" in the regex)
NSTextCheckingResult *result = (NSTextCheckingResult *)[matches lastObject];
// extract suffix value
NSUInteger counterValue = [[fileName substringWithRange:[result rangeAtIndex:1]] integerValue];
// remove old suffix from the string
NSString *fileNameNoSuffix = [fileName stringByReplacingCharactersInRange:[result rangeAtIndex:0] withString:#""];
// return the path with the incremented counter suffix
return [NSString stringWithFormat:#"%#-%i%#", fileNameNoSuffix, counterValue + 1, ext];
}
... and the following are the tests I used:
- (void)testStringToAvoidNameCollisionForPath {
NSBundle *bundle = [NSBundle bundleForClass:[self class]];
// bad configs //
STAssertThrows([DMStringUtils stringToAvoidNameCollisionForPath:nil], nil);
STAssertThrows([DMStringUtils stringToAvoidNameCollisionForPath:#""], nil);
// files //
NSString *path = [bundle pathForResource:#"bar-0.abc" ofType:#"txt"];
NSString *savePath = [DMStringUtils stringToAvoidNameCollisionForPath:path];
STAssertEqualObjects([savePath lastPathComponent], #"bar-0.abc-1.txt", nil);
NSString *path1 = [bundle pathForResource:#"bar1" ofType:#"txt"];
NSString *savePath1 = [DMStringUtils stringToAvoidNameCollisionForPath:path1];
STAssertEqualObjects([savePath1 lastPathComponent], #"bar1-1.txt", nil);
NSString *path2 = [bundle pathForResource:#"bar51.foo.yeah1" ofType:#"txt"];
NSString *savePath2 = [DMStringUtils stringToAvoidNameCollisionForPath:path2];
STAssertEqualObjects([savePath2 lastPathComponent], #"bar51.foo.yeah1-1.txt", nil);
NSString *path3 = [path1 stringByDeletingLastPathComponent];
NSString *savePath3 = [DMStringUtils stringToAvoidNameCollisionForPath:[path3 stringByAppendingPathComponent:#"xxx.zip"]];
STAssertEqualObjects([savePath3 lastPathComponent], #"xxx.zip", nil);
NSString *path4 = [bundle pathForResource:#"foo.bar1-1-2-3-4" ofType:#"txt"];
NSString *savePath4 = [DMStringUtils stringToAvoidNameCollisionForPath:path4];
STAssertEqualObjects([savePath4 lastPathComponent], #"foo.bar1-1-2-3-5.txt", nil);
NSString *path5 = [bundle pathForResource:#"bar1-1" ofType:#"txt"];
NSString *savePath5 = [DMStringUtils stringToAvoidNameCollisionForPath:path5];
STAssertEqualObjects([savePath5 lastPathComponent], #"bar1-2.txt", nil);
// folders //
NSString *path6 = [DOCUMENTS_PATH stringByAppendingPathComponent:#"foo1"];
NSString *savePath6 = [DMStringUtils stringToAvoidNameCollisionForPath:path6];
STAssertEqualObjects([savePath6 lastPathComponent], #"foo1-1", nil);
NSString *path7 = [DOCUMENTS_PATH stringByAppendingPathComponent:#"bar1-1"];
NSString *savePath7 = [DMStringUtils stringToAvoidNameCollisionForPath:path7];
STAssertEqualObjects([savePath7 lastPathComponent], #"bar1-2", nil);
NSString *path8 = [DOCUMENTS_PATH stringByAppendingPathComponent:#"foo-5.bar123"];
NSString *savePath8 = [DMStringUtils stringToAvoidNameCollisionForPath:path8];
STAssertEqualObjects([savePath8 lastPathComponent], #"foo-5.bar123-1", nil);
}

Objective-C: How to add query parameter to NSURL?

Let's say I have an NSURL? Whether or not it already has an empty query string, how do I add one or more parameters to the query of the NSURL? I.e., does anyone know of an implementation of this function?
- (NSURL *)URLByAppendingQueryString:(NSString *)queryString
So that it satisfies this NSURL+AdditionsSpec.h file:
#import "NSURL+Additions.h"
#import "Kiwi.h"
SPEC_BEGIN(NSURL_AdditionsSpec)
describe(#"NSURL+Additions", ^{
__block NSURL *aURL;
beforeEach(^{
aURL = [[NSURL alloc] initWithString:#"http://www.example.com"];
aURLWithQuery = [[NSURL alloc] initWithString:#"http://www.example.com?key=value"];
});
afterEach(^{
[aURL release];
[aURLWithQuery release];
});
describe(#"-URLByAppendingQueryString:", ^{
it(#"adds to plain URL", ^{
[[[[aURL URLByAppendingQueryString:#"key=value&key2=value2"] query] should]
equal:#"key=value&key2=value2"];
});
it(#"appends to the existing query sting", ^{
[[[[aURLWithQuery URLByAppendingQueryString:#"key2=value2&key3=value3"] query] should]
equal:#"key=value&key2=value2&key3=value3"];
});
});
});
SPEC_END
Since iOS 7 you can use NSURLComponents that is very simple to use. Take a look on these examples:
Example 1
NSString *urlString = #"https://mail.google.com/mail/u/0/?shva=1#inbox";
NSURLComponents *components = [[NSURLComponents alloc] initWithString:urlString];
NSLog(#"%# - %# - %# - %#", components.scheme, components.host, components.query, components.fragment);
Example 2
NSString *urlString = #"https://mail.google.com/mail/u/0/?shva=1#inbox";
NSURLComponents *components = [[NSURLComponents alloc] initWithString:urlString];
if (components) {
//good URL
} else {
//bad URL
}
Example 3
NSURLComponents *components = [NSURLComponents new];
[components setScheme:#"https"];
[components setHost:#"mail.google.com"];
[components setQuery:#"shva=1"];
[components setFragment:#"inbox"];
[components setPath:#"/mail/u/0/"];
[self.webview loadRequest:[[NSURLRequest alloc] initWithURL:[components URL]]];
But you can do many other things with NSURLComponents take a look on NSURLComponents class reference on Apple documentation or on this link: http://nshipster.com/nsurl/
Here's an implementation that passes your specs:
#implementation NSURL (Additions)
- (NSURL *)URLByAppendingQueryString:(NSString *)queryString {
if (![queryString length]) {
return self;
}
NSString *URLString = [[NSString alloc] initWithFormat:#"%#%#%#", [self absoluteString],
[self query] ? #"&" : #"?", queryString];
NSURL *theURL = [NSURL URLWithString:URLString];
[URLString release];
return theURL;
}
#end
And here is an implementation for NSString:
#implementation NSString (Additions)
- (NSURL *)URLByAppendingQueryString:(NSString *)queryString {
if (![queryString length]) {
return [NSURL URLWithString:self];
}
NSString *URLString = [[NSString alloc] initWithFormat:#"%#%#%#", self,
[self rangeOfString:#"?"].length > 0 ? #"&" : #"?", queryString];
NSURL *theURL = [NSURL URLWithString:URLString];
[URLString release];
return theURL;
}
// Or:
- (NSString *)URLStringByAppendingQueryString:(NSString *)queryString {
if (![queryString length]) {
return self;
}
return [NSString stringWithFormat:#"%#%#%#", self,
[self rangeOfString:#"?"].length > 0 ? #"&" : #"?", queryString];
}
#end
The iOS8+ modern way
adding (or replacing 'ref' value if exists) ref=impm to url which is on min60.com
if ([[url host] hasSuffix:#"min60.com"]) {
NSURLComponents *components = [[NSURLComponents alloc] initWithURL:url resolvingAgainstBaseURL:NO];
NSURLQueryItem * newQueryItem = [[NSURLQueryItem alloc] initWithName:#"ref" value:#"impm"];
NSMutableArray * newQueryItems = [NSMutableArray arrayWithCapacity:[components.queryItems count] + 1];
for (NSURLQueryItem * qi in components.queryItems) {
if (![qi.name isEqual:newQueryItem.name]) {
[newQueryItems addObject:qi];
}
}
[newQueryItems addObject:newQueryItem];
[components setQueryItems:newQueryItems];
url = [components URL];
}
Just a friendly post for those who don't want to write boilerplate code while building NSURL with NSURLComponents.
Since iOS8 we have NSURLQueryItem that helps building URL request freaking fast.
I wrote a little handy category to ease the work, that you can grab here: URLQueryBuilder
Here is example of how easy it is to work with it:
NSString *baseURL = #"https://google.com/search";
NSDictionary *items = #{
#"q" : #"arsenkin.com",
#"hl" : #"en_US",
#"lr" : #"lang_en"
};
NSURL *URL = [NSURL ars_queryWithString:baseURL queryElements:items];
// https://google.com/search?q=arsenkin.com&hl=en_US&lr=lang_en
I have an extension to NSURLComponents that add query item, in swift:
extension NSURLComponents {
func appendQueryItem(name name: String, value: String) {
var queryItems: [NSURLQueryItem] = self.queryItems ?? [NSURLQueryItem]()
queryItems.append(NSURLQueryItem(name: name, value: value))
self.queryItems = queryItems
}
}
To use,
let components = NSURLComponents(string: urlString)!
components.appendQueryItem(name: "key", value: "value")
If you're using RestKit it provides additions to NSString. One of which is:
- (NSString *)stringByAppendingQueryParameters:(NSDictionary *)queryParameters
So you could do:
NSDictionary *shopParams = [NSDictionary dictionaryWithKeysAndObjects:
#"limit",#"20",
#"location",#"latitude,longitude",
nil];
NSString *pathWithQuery = [#"/api/v1/shops.json" stringByAppendingQueryParameters:shopParams]
As others have mentioned, you can use NSURLComponents to construct URLs.
#implementation NSURL (Additions)
- (NSURL *)URLByAppendingQueryParameters:(NSDictionary *)queryParameters
{
NSURLComponents *components = [[NSURLComponents alloc] initWithURL:self resolvingAgainstBaseURL:NO];
NSMutableArray *queryItems = [NSMutableArray array:components.queryItems];
for (NSString *key in [queryParameters allKeys]) {
NSURLQueryItem *queryItem = [[NSURLQueryItem alloc] initWithName:key value:queryParameters[key]];
[queryItems addObject:queryItem];
}
components.queryItems = queryItems;
return [components URL];
}
#end
NSURL is not mutable so you cannot implement this functionality directly based on NSURL. Instead you will have to obtain the string representation of the URL, append your parameters to that and then create a new NSURL.
This does not sound like a good solution. Unless there is a good reason, it is better to work with strings until the last moment and only create an NSURL when you have your fully formed request.

Encoding string arguments for URLs

I created a method to build URLs for me.
- (NSString *)urlFor:(NSString *)path arguments:(NSDictionary *)args
{
NSString *format = #"http://api.example.com/%#?version=2.0.1";
NSMutableString *url = [NSMutableString stringWithFormat:format, path];
if ([args isKindOfClass:[NSDictionary class]]) {
for (NSString *key in args) {
[url appendString:[NSString stringWithFormat:#"&%#=%#", key, [args objectForKey:key]]];
}
}
return url;
}
When I try to build something like below, the URLs aren't encoded, of course.
NSDictionary *args = [NSDictionary dictionaryWithObjectsAndKeys:
#"http://other.com", #"url",
#"ABCDEF", #"apiKey", nil];
NSLog(#"%#", [self urlFor:#"articles" arguments:args]);`
The returned value is http://api.example.com/articles?version=2.0.1&url=http://other.com&apiKey=ABCDEF when it should be http://api.example.com/articles?version=2.0.1&url=http%3A%2F%2Fother.com&apiKey=ABCDEF.
I need to encode both key and value. I searched for something and found CFURLCreateStringByAddingPercentEscapes and stringByAddingPercentEscapesUsingEncoding but none of the tests I made worked.
How can I do it?
IIRC, slashes should be interpreted properly when they're in the query part of a URL. Did you test to see if it still works without encoded slashses? Otherwise, do something like:
if ([args isKindOfClass:[NSDictionary class]]) {
for (NSString *key in [args allKeys]) {
NSString *value = [(NSString*)CFURLCreateStringByAddingPercentEscapes(kCFAllocatorDefault, (CFStringRef)[args objectForKey:key], NULL, CFSTR("/?&:=#"), kCFStringEncodingUTF8) autorelease];
[url appendString:[NSString stringWithFormat:#"&%#=%#", key, value]];
[value release];
}
}
return url;
Note the value of the 4th argument to CFURLCreateStringByAddingPercentEscapes.
You should consider using Google Toolbox for Mac's GTMNSString+URLArguments; it's designed for exactly this purpose.
I'd recommend our KSFileUtilities set of classes. Your example would then be:
- (NSString *)urlFor:(NSString *)path arguments:(NSDictionary *)args
{
NSMutableDictionary *parameters = [NSMutableDictionary dictionaryWithDictionary:args];
[parameters setObject:#"2.0.1" forKey:#"version"];
NSURL *result = [NSURL ks_URLWithScheme:#"http"
host:#"api.example.com"
path:path
queryParameters:parameters;
return [result absoluteString];
}