URLComponents

When writing mobile apps, more often than not we have to communicate with a backend to get some data to present to the user.

The shared language between frontend and backend is usually URLs combined with either a POST body or maybe some URL query parameters, depending on what the backend offers.

In this post we will look at URLComponents, a small but very useful helper that will become your new best friend when composing or decomposing URLs (we promise!)

But before we look at URLComponents, lets first look at a world without URLComponents just to make you extra grateful that such a thing exists.

Setting the Stage

OK, lets assume that we need to use a backend service for searching and showing details about TV shows.

Searching

Our hard working backend developing friends have made a service for us to use. The base URL looks like this:

https://showsknownfrom.tv/search

(no…it doesn’t exist)

And it offers the following query parameters:

  • q: A query string we would like to search for
  • order (optional parameter): Do we want the search results ascending or descending
  • number (optional parameter): Max number of search results to return

Showing Details

To see details about a TV show, we get a URL to a HTML page from the backend which we then present in a WebView.

The URL looks like this:

https://showsknownfrom.tv/12345678

And can have an optional URL parameter called featured. If featured is present and is true, we would like to show a flashing red UILabel with the caption “FEATURED” over our WebView (note: this feature has not been cleared with the designer yet!)

Composing and Decomposing URLs - The Hard Way

Composing URLs for Searching

The stage is set and we are ready to look at how to call our search service in our pre-URLComponents world.

To generate a URL in our iOS app, we could write some code like this:

func searchURL(with q: String, optionalParameters: [String: String] = [:]) -> URL? {
var urlString = "https://showsknownfrom.tv/search"

//Append query
urlString.append("?q=\(q)")

//Append parameters if any
optionalParameters.forEach({key, value in
urlString.append("&\(key)=\(value)")
})

return URL(string: urlString)
}

Granted, it could be worse, but note that we have to know about a “magic” ? if this is the first parameter and & if it is any other parameters.

Lets take it for a spin:

if let url = searchURL(with: "Sopranos") {
print(url) //gives us: https://showsknownfrom.tv/search?q=Sopranos
}

if let url = searchURL(with: "Sopranos", optionalParameters: ["order" : "desc", "number" : "100"]) {
print(url) //gives us: https://showsknownfrom.tv/search?q=Sopranos&number=100&order=desc
}

By now you might be thinking “Hey!! What gives! Why are those morons hyping URLComponents so hard? This clearly works!”

OK, lets look at another example:

if let url = searchURL(with: "Halt and Catch Fire", optionalParameters: ["order" : "desc", "number" : "100"]) {
print(url)
} else {
print("no URL") //We end up here
}

That didn’t work, why?

Yes, you’re right, the query contains spaces!

We need to Percent Encode our query, so lets do that before we try to use it. We add this to the top of our function:

//Append query
guard let encodedQuery = q.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) else { return nil }
urlString.append("?q=\(encodedQuery)")
...

And now we get https://showsknownfrom.tv/search?q=Halt%20and%20Catch%20Fire&number=100&order=desc

Great! Our function seems to be working as expected now, but it took some time and some tries to get here.

Decomposing URLs

Now, lets look at another example.

Remember the details page and the featured parameter?

Here is how we could fetch that from the URL in our iOS app:

func receivedURL(_ url: URL, contains parameter: String) -> String? {
let urlString = url.absoluteString

//add = to parameter
let parameterNameWithEqual = "\(parameter)="

guard let rangeOfParameterName = urlString.range(of: parameterNameWithEqual) else {
return nil
}

//create a substring starting after the parameter name
let parametersAfterParameterName = String(urlString[rangeOfParameterName.upperBound...])

//check if we have more parameters after the one we're looking for
guard let rangeOfNextAmpersand = parametersAfterParameterName.index(of: "&") else {
//we did not, just return the value then
return parametersAfterParameterName
}

//cut the substring where the next parameter starts
return String(parametersAfterParameterName.prefix(upTo: rangeOfNextAmpersand))
}

//test
if let url = URL(string: "https://showsknownfrom.tv/shows/12345678?featured=true"),
let parameter = receivedUrl(url, contains: "featured") {
print("found \(parameter)")
} else {
print("not found")
}

Granted, it works but…wow! So much going on here, magic values and String gymnastics galore.

We could probably make this prettier but…why should we, when something as beautiful as URLComponents exists.

Composing and Decomposing URLs - The Easy Way

By now you should be more than ready for URLComponents so lets dive in.

URLComponents - An Introduction

URLComponents was introduced in iOS 7.0 and macOS 10.9 so it has been around for some time.

To create a URLComponents object you can use either a String or a URL, in both cases you’ll end out with an optional URLComponents object.

Just a quick note about the init method if you are using a URL. As it says in the documentation:

If resolvingAgainstBaseURL is true and url is a relative URL, the components of url.absoluteURL are used. If the url string from the URL is malformed, nil is returned.

To create a new URLComponents object we can write something like this:

let urlString = "https://showsknownfrom.tv/search?q=Sopranos&order=desc"

guard var urlComponents = URLComponents(string: urlString) else {
return nil
}

Now we have access to all parts of the URL and can probe them as we see fit.

Here are some examples:

urlComponents.host   //gives us: showsknownfrom.tv
urlComponents.scheme //gives us: https
urlComponents.query //gives us: q=Sopranos&order=desc

But the best part is this one:

urlComponents.queryItems

Which gives us back an array of URLQueryItem objects, where URLQueryItem objects are basically just key/value objects for the individual query items (documented here).

Oh joy! That makes it so much easier for us to append query parameters, or check if a URL contains a query parameter.

Composing URLs for Searching

Armed with our new knowledge, lets now look at how we can use URLComponents to build a URL with parameters for us:

func searchURL(with q: String, optionalParameters: [String: String] = [:]) -> URL? {
let urlString = "https://showsknownfrom.tv/search"

guard var urlComponents = URLComponents(string: urlString) else {
return nil
}

var queryItems: [URLQueryItem] = [URLQueryItem(name: "q", value: q)]

let optionalURLQueryItems = optionalParameters.map {
return URLQueryItem(name: $0, value: $1)
}
queryItems.append(contentsOf: optionalURLQueryItems)

urlComponents.queryItems = queryItems

return urlComponents.url
}

First we use the urlString to create a new URLComponents object. (Note that we create the urlComponents as a var as we’ll need to write to it later on.)

Next we create an array of URLQueryItem objects and append our only required parameter, the q parameter.

Then we loop through our optionalParameters dictionary, map the elements to URLQueryItem objects and append them to our existing array.

Finally, we ask URLComponents to try and return a URL for us.

Simple and beautiful, all the hard work has been delegated onwards to URLComponents and we don’t even have to worry about whether to use ? or & any more.

Lets see how it works:

if let url = searchURL(with: "Sopranos") {
print(url) //gives us: https://showsknownfrom.tv/search?q=Sopranos
}

if let url = searchURL(with: "Sopranos", optionalParameters: ["order" : "desc", "number" : "100"]) {
print(url) //gives us: https://showsknownfrom.tv/search?q=Sopranos&number=100&order=desc
}

if let url = searchURL(with: "Halt and Catch Fire", optionalParameters: ["order" : "desc", "number" : "100"]) {
print(url) //gives us: https://showsknownfrom.tv/search?q=Halt%20and%20Catch%20Fire&number=100&order=desc
} else {
print("no URL")
}

Perfect!

Decomposing URLs

Remember our example from before?

Now that we know about URLQueryItems, checking if a query parameter exists is soooo much easier:

func receivedURL(_ url: URL, contains parameter: String) -> String? {
guard
let urlComponents = URLComponents(url: url, resolvingAgainstBaseURL: true),
let queryItems = urlComponents.queryItems
else {
return nil
}

let items = queryItems.filter { $0.name == parameter }
return items.first?.value
}

//test
if let url = URL(string: "https://showsknownfrom.tv/shows/12345678?featured=true"),
let parameter = receivedUrl(url, contains: "featured") {
print("found \(parameter)")
} else {
print("not found")
}

Pretty, right? At least compared to the initial version.

Composing and Decomposing URLs - The Easier Way

Well, we made it this far! But can we do better? Of course we can!

Lets write an extension to URL so we can append query parameters and also ask if a URL contains a query parameter:

extension URL {
func append(queryParameters: [String: String]) -> URL? {
guard var urlComponents = URLComponents(url: self, resolvingAgainstBaseURL: true) else {
return nil
}

let urlQueryItems = queryParameters.map {
return URLQueryItem(name: $0, value: $1)
}
urlComponents.queryItems = urlQueryItems
return urlComponents.url
}

func value(forParameter name: String) -> String? {
guard let urlComponents = URLComponents(url: self, resolvingAgainstBaseURL: true),
let queryItems = urlComponents.queryItems else {
return nil
}
let items = queryItems.filter { $0.name == name }
return items.first?.value
}
}

Poetry, no less!

Lets test it out:

if let url = URL(string: "https://showsknownfrom.tv"),
let appendedURL = url.append(queryParameters: ["q": "Halt and Catch Fire", "order" : "desc", "number": "100"]) {
print(appendedURL) //gives us: https://showsknownfrom.tv?q=Halt%20and%20Catch%20Fire&number=100&order=desc
}

if let url = URL(string: "https://showsknownfrom.tv/shows/12345678?featured=true&test=1"),
let value = url.value(forParameter: "featured") {
print("found \(value)") //gives us: found true
}

Fantastic. We’ve composed two simple functions that makes our daily work so much easier when working with URL parameters.

Composing and Decomposing URLs - The Easier Way

Now you might be thinking “Urgh!! Extensions!! Why should I write code myself if I can download something from the internet, written by complete strangers instead!!”

If that is you, then good news pal!

We have made a collection of helper methods and extensions called CodeMine. Next to other nuggets of gold you’ll find two extension methods to URL that looks awfully familiar by now:

public func value(forParameter name: String) -> String?

which will return the value for a query parameter in a URL (if it exists).

And:

public func append(queryParameters: [String: String]) -> URL?

which will append a String: String dictionary to an already existing URL and return a new URL with the query parameters appended and paramter encoded.

So, if you’re into Cocoapods, add this to your podfile:

pod 'Codemine', '~>1.0.0'

And if you’re more of a Carthage person, add this to your Cartfile

github "nodes-ios/Codemine" ~> 1.0

Update and you’re done, you can now check if your URL object contains specific query parameters or you can build your own URL and append parameters easily.

Hey…you’re welcome :)

Parting Words

Whew, we made it!

We started out writing a lot of error prone code ourself for managing URLs more or less as expected.

That was then replaced with URLComponents that does all the hard work for us, and finally we made our own extension to add some building blocks to make our daily work easier.

If you didn’t already know about URLComponents we hope to have convinced you that it is an awesome little helper to add to your toolbelt for when you need to work with URLs.

Thank you for reading along.

This page was updated, please reload to see the changes
This site is now available offline