Building URLs With NSURLComponents and NSURLQueryItems
January 02, 2016

Creating URLs from strings is a minefield for bugs. Just miss a single / or accidentally URL encode the ? in a query and your API call will fail and your app won’t have any data to display (or even crash if you didn’t anticipate that possibility). Since iOS 8 there’s a better way to build URLs using NSURLComponents and NSURLQueryItems. Let’s look at how to do that and when it might be fine to keep using NSURL(string: urlString).

This tutorial was written using Swift 2.0 and Xcode 7.1.

NASA recently released an API with access to lots of their data, including APOD. As a demo app, we’ll build a simple app that retrieves the NASA’s Astronomy Picture of the Day and shows it in Safari.

Before we get started you’ll need an API key. You can get one from NASA. Go do that now so you’ll have it later when you need it.

To create the demo app so we can play with the web service call:

  • Launch Xcode
  • Create a new Single View project
  • Add a new APIController.swift file

In our APIController we’ll implement two functions to create the NSURL: one using a string and one using NSURLComponents. We’ll also need a function to make the API call and launch Safari:


class APIController {
  func createURLWithString(date: NSDate) -> NSURL? {
    // TODO: implement
  }
  
  func createURLWithComponents(date: NSDate) -> NSURL? {
    // TODO: implement  
  }
  
  func getAPOD(date: NSDate) {
    // TODO: implement
  }
}

While we’re here, add your API key so it’ll be easy to include in the requests:


class APIController {
  let API_KEY = "MY_API_KEY"
  ...

To make the network request in getAPOD(), we’ll use NSURLSession like we did in Simple REST API Calls with Swift. Usually we use Alamofire to make our network calls but in this app the call and response handling are so simple that it would be overkill. We just need to create the URL and send the request, then extract the url parameter from the JSON that we get back:


func getAPOD(date: NSDate) {
  guard let url = createURLWithComponents(date) else {
    print("invalid URL")
    return
  }
  
  let urlRequest = NSURLRequest(URL: url)
  
  let config = NSURLSessionConfiguration.defaultSessionConfiguration()
  let session = NSURLSession(configuration: config)
  let task = session.dataTaskWithRequest(urlRequest, completionHandler: { (data, _, _) in
    do {
      guard error == nil else {
        print(error!)
        return
      }
      if let data = data, let json = try NSJSONSerialization.JSONObjectWithData(data, options: []) as? [String: AnyObject] {
        print(json)
        if let imageURLString = json["url"] as? String {
          if let url = NSURL(string: imageURLString) {
            UIApplication.sharedApplication().openURL(url)
            return
          }
        }
      }
    } catch {
      // handle the error
    }
  })
  task.resume()
}

Once we have the image’s URL, we can use UIApplication.sharedApplication().openURL(url) to launch Safari and display the image. Not the most useful app but it’ll work fine for figuring out how to create the URL. If you wanted to get fancy you could rig up a completion handler to pass the URL back to the view controller then use PINRemoteImage to load and display it.

  • In the view controller’s viewWillAppear, set up a call to getAPOD using today’s date:

class ViewController: UIViewController {
  
  override func viewDidAppear(animated: Bool) {
    super.viewDidAppear(animated)

    let apiController = APIController()
    let today = NSDate()
    apiController.getAPOD(today)
  }
}

Basic Query

Here’s how to create an NSURL for https://api.nasa.gov/planetary/apod from a string:


func createURLWithString(date: NSDate) -> NSURL? {
  var urlString: String = "https://api.nasa.gov/planetary/apod"
  return NSURL(string: urlString)
}

And using NSURLComponents:


func createURLWithComponents(date: NSDate) -> NSURL? {
  let urlComponents = NSURLComponents()
  urlComponents.scheme = "https";
  urlComponents.host = "api.nasa.gov";
  urlComponents.path = "/planetary/apod";

  return urlComponents.URL
}

If you’re sure you’ve got the URL string correct and your endpoint is just a static URL then NSURL(string: urlString) works fine. Note that we aren’t using the date parameter yet. By default the API will return today’s image.

Adding a Query

What if we want to specify the parameters for the API call? According to the documentation, we can change the date, request concept tags, request the high resolution version of the image, and provide our API key. Since we’re making the request from a mobile app, we’ll need to at least include the API key parameter. We aren’t using the concept tags or high res image so let’s not retrieve them.

To handle the date parameter we’ll need to convert the NSDate to a string. We can use an NSDateFormatter:


class APIController {
  func dateToString(date: NSDate) -> String {
    let dateFormatter = NSDateFormatter()
    dateFormatter.dateFormat = "yyyy-MM-dd"
    return dateFormatter.stringFromDate(date)
  }

  ...
}

To add the parameters to createURLWithString we can build up the URL string by adding each parameter to it. We can use stringByAppendingString to do that:


func createURLWithString(date: NSDate) -> NSURL? {
  var urlString: String = "https://api.nasa.gov/planetary/apod"
  
  // append params
  urlString = urlString.stringByAppendingString("?date=" + dateToString(date))
  urlString = urlString.stringByAppendingString("&concept_tags=false")
  urlString = urlString.stringByAppendingString("&hd=false")
  urlString = urlString.stringByAppendingString("&api_key=" + API_KEY)
  
  return NSURL(string: urlString)
}

Notice that we have to add the ?, &, and = signs explicitly. If we miss one then our URL will be malformed.

How do we add the parameters to createURLWithComponents? In that case, we can use NSURLQueryItem. We’ll generate a query item for each parameter then pass them as an array to urlComponents.queryItems:


func createURLWithComponents(date: NSDate) -> NSURL? {
  // create "https://api.nasa.gov/planetary/apod" URL using NSURLComponents
  let urlComponents = NSURLComponents()
  urlComponents.scheme = "https";
  urlComponents.host = "api.nasa.gov";
  urlComponents.path = "/planetary/apod";
  
  // add params
  let dateQuery = NSURLQueryItem(name: "date", value: dateToString(date))
  let conceptQuery = NSURLQueryItem(name: "concept_tags", value: "false")
  let hdQuery = NSURLQueryItem(name: "hd", value: "false")
  let apiKeyQuery = NSURLQueryItem(name: "api_key", value: API_KEY)
  urlComponents.queryItems = [dateQuery, conceptQuery, hdQuery, apiKeyQuery]
  
  return urlComponents.URL
}

Save and run. Toggle the guard statement between the two URL creation functions to test them out:


guard let url = createURLWithComponents(date) else {

and


guard let url = createURLWithString(date) else {

You should see the Astronomy Photo of the Day displayed in Safari:

Astronomy Photo of the Day in Safari

To make sure that the date parameter is being used correctly, change the date and run it again to make sure you get a different image:


override func viewWillAppear(animated: Bool) {
  super.viewWillAppear(animated)

  let apiController = APIController()
  let today = NSDate()
  let yesterday = today.dateByAddingTimeInterval(-1*24*60*60)
  apiController.getAPOD(yesterday)
}

What Else Can NSURLComponents Do?

With NSURLQueryItem we don’t need to worry about the & and = or whether we need a trailing /. It also clearly separates the query from the host and path in the URL components. If you were getting parameters passed in from the view controller (e.g., adding switches to select HD and the concept tags), then it would be easy to adjust the query items. In fact, you could easily save the query in the APIController and only change the parameters instead of having to recreate the whole URL from scratch every time:


class APIController {
  var urlComponents = NSURLComponents()
  
  init() {
    urlComponents.scheme = "https";
    urlComponents.host = "api.nasa.gov";
    urlComponents.path = "/planetary/apod";
  }
  
  ...
  
  func createURLWithComponents(date: NSDate) -> NSURL? {
    // add params
    let dateQuery = NSURLQueryItem(name: "date", value: dateToString(date))
    let conceptQuery = NSURLQueryItem(name: "concept_tags", value: "false")
    let hdQuery = NSURLQueryItem(name: "hd", value: "false")
    let apiKeyQuery = NSURLQueryItem(name: "api_key", value: API_KEY)
    urlComponents.queryItems = [dateQuery, conceptQuery, hdQuery, apiKeyQuery]
    
    return urlComponents.URL
  }

  ...
}

That would also make it easier to add additional function. For example if you wanted to have separate functions for retrieving the normal image and the HD one:


func createURLWithComponents(date: NSDate) -> NSURL? {
  // add params
  let dateQuery = NSURLQueryItem(name: "date", value: dateToString(date))
  let conceptQuery = NSURLQueryItem(name: "concept_tags", value: "false")
  let hdQuery = NSURLQueryItem(name: "hd", value: "false")
  let apiKeyQuery = NSURLQueryItem(name: "api_key", value: API_KEY)
  urlComponents.queryItems = [dateQuery, conceptQuery, hdQuery, apiKeyQuery]

  
  return urlComponents.URL
}

func createURLWithComponentsHD(date: NSDate) -> NSURL? {
  // add params
  let dateQuery = NSURLQueryItem(name: "date", value: dateToString(date))
  let conceptQuery = NSURLQueryItem(name: "concept_tags", value: "false")
  let hdQuery = NSURLQueryItem(name: "hd", value: "true")
  let apiKeyQuery = NSURLQueryItem(name: "api_key", value: API_KEY)
  urlComponents.queryItems = [dateQuery, conceptQuery, hdQuery, apiKeyQuery]
  
  return urlComponents.URL
}

Or we can reach different endpoints on the same server by setting the path for each one, for example the NASA Earth imagery API at https://api.nasa.gov/planetary/earth/imagery:


class APIController {
  var urlComponents = NSURLComponents()
  
  init() {
    urlComponents.scheme = "https";
    urlComponents.host = "api.nasa.gov";
  }
  
  ...
  
  func createAPODURLWithComponents(date: NSDate) -> NSURL? {
    urlComponents.path = "/planetary/apod";

    // add params
    // ...
    
    return urlComponents.URL
  }

  func createEarthImageryURLWithComponents(date: NSDate, ...) -> NSURL? {
    urlComponents.path = "/planetary/earth/imagery";

    // add params
    // ...
    
    return urlComponents.URL
  }

  ...
}

Testing

NSURLQueryItems also make it easy to test whether a parameter is being set. We can take the URL and extract the query items then check if the correct ones are included:


import XCTest
@testable import grokURLComponents

class grokURLComponentsTests: XCTestCase {
  func testExample() {
    let apiController = APIController()
    let url = apiController.createURLWithComponents(NSDate())
    XCTAssertNotNil(url)
    let components = NSURLComponents(URL: url!, resolvingAgainstBaseURL: false)
    XCTAssertNotNil(components?.queryItems)
    
    // now we can test the components instead of just comparing the URL to another string
    let queryItems = components!.queryItems!
    XCTAssertTrue(queryItems.count == 4)
    
    let expectedHDQueryItem = NSURLQueryItem(name: "hd", value: "false")
    XCTAssertTrue(queryItems.contains(expectedHDQueryItem))
  }
}

This testing technique lets us test each part of the URL so any test failures immediately pinpoint where the problem is. If this test fails then you know immediately what the problem is:


XCTAssertTrue(components?.scheme == "https")

URL Encoding

Since our parameters haven’t needed URL encoding we’ve been ignoring it. That’s a pretty bad assumption to make. Not URL encoding parameters, especially if they were getting created from user input, could easily cause bugs.

When you set urlComponents.queryItems the query items automatically get URL encoded. That makes it easy to work with plain name and value strings without URL encoding when you’re creating your query items, changing them, or testing them. And it guarantees that the URL will have the query items properly encoded. Here’s how you can test this feature:

First add a query item that needs encoding:


func createURLWithComponents(date: NSDate) -> NSURL? {
  // add params
  let dateQuery = NSURLQueryItem(name: "date", value: dateToString(date))
  let conceptQuery = NSURLQueryItem(name: "concept_tags", value: "false")
  let hdQuery = NSURLQueryItem(name: "hd", value: "false")
  let apiKeyQuery = NSURLQueryItem(name: "api_key", value: API_KEY)
  let queryThatNeedsEncoding = NSURLQueryItem(name: "name has spaces", value: "true plus spaces")
  urlComponents.queryItems = [dateQuery, conceptQuery, hdQuery, apiKeyQuery, queryThatNeedsEncoding]
  
  return urlComponents.URL
}

Then at the end of the test we wrote earlier, compare the output when you print the URL, the query items, and urlComponents.query:


print(url!)
/* prints:
https://api.nasa.gov/planetary/apod?date=2016-01-03&concept_tags=false&hd=false&api_key=MY_API_KEY&name%20has%20spaces=true%20plus%20spaces
*/

print(queryItems)
/* prints:
[ {name = date, value = 2016-01-03},
  {name = concept_tags, value = false},
  {name = hd, value = false}, 
  {name = api_key, value = MY_API_KEY},
  {name = name has spaces, value = true plus spaces}]
*/

print(components!.query!)
/* prints:
date=2016-01-03&concept_tags=false&hd=false&api_key=MY_API_KEY&name has spaces=true plus spaces
*/

In the URL the parameters are URL encoded. They aren’t encoded in the queryItems or the query. If we’re using strings to create our URL then we need to encode the query before adding it to the URL string:


func createURLWithString(date: NSDate) -> NSURL? {
  var urlString: String = "https://api.nasa.gov/planetary/apod"
  
  // append params
  var query = "date=" + dateToString(date)
  query = query.stringByAppendingString("&concept_tags=false")
  query = query.stringByAppendingString("&hd=false")
  query = query.stringByAppendingString("&api_key=" + API_KEY)
  query = query.stringByAppendingString("&name has spaces=true plus spaces")
  guard let encodedQuery = query.stringByAddingPercentEncodingWithAllowedCharacters(.URLHostAllowedCharacterSet()) else {
    return nil
  }
    
  urlString = urlString.stringByAppendingString("?" + encodedQuery)

  return NSURL(string: urlString)
}

To check that’s working add some print statements before the return statement:


print(query)
/*  prints:
date=2016-01-03&concept_tags=false&hd=false&api_key=MY_API_KEY&name has spaces=true plus spaces
*/

print(encodedQuery)
/*  prints:
date=2016-01-03&concept_tags=false&hd=false&api_key=MY_API_KEY&name%20has%20spaces=true%20plus%20spaces
*/

print(urlString)
/*  prints:
https://api.nasa.gov/planetary/apod?date=2016-01-03&concept_tags=false&hd=false&api_key=MY_API_KEY&name%20has%20spaces=true%20plus%20spaces
*/

And That’s All about NSURLComponents and NSURLQueryItems

Working with URL components can make your code more stable and robust, as well as easier to test. Plus it takes care of URL encoding for you. But sometimes it’s overkill, like if you’re just calling a single static URL. Even if you use strings to create your URLs, consider using NSURLComponents to test them.

If you’d like more Swift tutorials on topics like this one, sign up below to get them sent directly to your inbox.

Making your first Swift app that uses a web service can be overwhelming

It seems like every time you try to figure it out you just add more things to learn to your list: REST, Alamofire, parsing JSON, OAuth, App Transport Security, setting headers, …

But it doesn’t have to be confusing. In this free 5-part course, we’ll work through what you really need to know, one step at a time, building our skills as we go. (And get free Swift tutorials, sent straight to your inbox.)

iOS Apps with REST APIs Cover

With iOS Apps with REST APIs ebook I’ll teach you how to build Swift apps that get their data from web services without hundreds of pages about flatmap and Core Whatever. iOS Apps with REST APIs ebook is the book I wish I had on my first iOS contract when I was struggling to figure out how to get the API data showing up in the app I was building.

Other Posts You Might Like