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 togetAPOD
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:
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.