Construct a URL safely using enums instead of raw strings

Daily Coding Tip 002

Here’s how to safely create URLs with the lowest reliance on raw strings possible.

It may seem like a lot but we’re basically combining enums with a raw type of String with URLComponents. This structure provides a way to individually add the scheme, host, and path components, instead of passing a raw string that could fail.

Instead of relying on a hard-coded string to get the right number of dots and slashes in the URL, we are relying on the URLComponents structure to do this for us.

Therefore none of the components we add will have slashes or dots, except for the dot between the subdomain and domain name (such as in maps.google.com).

import Foundation
extension URL {
enum Scheme: String { case http, https }
enum Subdomain: String { case maps, translate, noSubdomain = "" }
enum DomainName: String { case apple, google }
enum DomainExtension: String { case com, couk = "co.uk" }
enum PathComponent: String { case accessibility }
/// Create a URL using cases from the enums above
/// - Parameters:
/// - scheme: Could be https or a custom URL scheme
/// - subdomain: An optional part of the host name that is separated from the domain name automatically (so no '.' should be given in the parameter)
/// - domainName: The main part of the host name that describes the website (without the '.' or extension)
/// - domainExtension: The extension after the host name (without the '.')
/// - pathComponents: The directory path that will be separated by slashes automatically (so no slashes should be given in the parameter)
init?(scheme: Scheme, subdomain: Subdomain? = .noSubdomain, domainName: DomainName, domainExtension: DomainExtension, pathComponents: [PathComponent]? = []) {
//The URL will be constructed from components
var urlComponents = URLComponents()
//Add scheme
urlComponents.scheme = scheme.rawValue
//Add hostname
var hostname = [String]()
if let subdomain = subdomain, !subdomain.rawValue.isEmpty {
hostname = [subdomain.rawValue, domainName.rawValue, domainExtension.rawValue]
}
else { hostname = [domainName.rawValue, domainExtension.rawValue] }
urlComponents.host = hostname.joined(separator: ".")
//Create URL and add path components if any exist
var url = urlComponents.url
if let pathComponents = pathComponents {
for pathComponent in pathComponents {
url?.appendPathComponent(pathComponent.rawValue)
}
}
//Create URL string
guard let urlString = url?.absoluteString else {
return nil
}
//Use existing initialiser with the string
self.init(string: urlString)
}
}
//URL with path components
let appleURL = URL(scheme: .https, domainName: .apple, domainExtension: .com, pathComponents: [.accessibility])
//URL with subdomain
let googleURL = URL(scheme: .https, subdomain: .maps, domainName: .google, domainExtension: .com)
//URL without either
let googleUKURL = URL(scheme: .https, domainName: .google, domainExtension: .couk)
view raw CreateURL.swift hosted with ❤ by GitHub

I have provided three examples of the initializer in action. The Apple URL takes you to apple.com/accessibility, with ‘accessibility’ as the path component. If I wanted to add more path components, such as apple.com/accessibility/vision, accessibility and vision would be separate strings in the array that I passed.

Again, no slashes are necessary as this is done automatically for us by the URLComponents structure.

The Google URL was for maps, and so it uses the optional subdomain maps.

I’ve also added the UK Google website, mainly to show the limitations of using enums for this kind of string. Since enums cannot have dots in their case names, I had to specify that case couk = “co.uk”.

Wherever you can, try to make enum cases reflect the string that is their underlying raw value.

This way Swift handles the raw value for us, and we do not have to state it explicitly as I did in this case.


Get more Daily Coding Tips in your inbox!