Xcoding with Alfian

Software Development Videos & Tutorials

Building Safe URL in Swift Using URLComponents and URLQueryItem

Alt text

Constructing URL is a routine task that every Swift developers do when building an iOS application. It’s very important to make sure the URL we construct are safe and correctly encoded using the percent encoding format. The most simple and crash prone approach to construct an URL is using the URL Struct String initalizer passing the raw string.

let searchTerm = "obi wan kenobi"
let format = "wookiee"
let url = URL(string: "https://swapi.co/api/people/?search=\(searchTerm)&format=\(format)")! // Exception(Fatal Error)!

While it works for simple URL without query parameters, the unsafe behaviour comes when we add query parameters manually to the string that is not properly encoded using percent encoding. The process of manually appending the string for every query param is also very error prone.

Enter URLComponents and URLQueryItem

To solve this problem, Apple’s Foundation framework actually provides 2 types of Struct that we can use as a building block to build URL with query parameters safely, they are URLComponents and URLQueryItem. An URL is composed from many parts such as the scheme, host, path and query. Using URLComponents struct we can safely build our URL by specifying the scheme, host, and path manually.

import Foundation

let searchTerm = "obi wan kenobi"
let format = "wookiee"

var urlComponents = URLComponents()
urlComponents.scheme = "https"
urlComponents.host = "swapi.co"
urlComponents.path = "/api/people"
urlComponents.queryItems = [
   URLQueryItem(name: "search", value: searchTerm),
   URLQueryItem(name: "format", value: format)
]

print(urlComponents.url?.absoluteString) 
// https://swapi.co/api/people?search=obi%20wan%20kenobi&format=wookie

To handle query parameters percent encoding safely, URLComponents also exposes queryItems as the property. QueryItems is an array of URLQueryItem, we can use the URLQueryItem initializer passing the name and value of a query parameter. By assigning queryItems array, URLComponents will internally add the percent encoding of the query parameters.

Mapping Dictionary of Key Value String to URLQueryItem Array

Another helpful pattern that we can use is to encapsulate our query parameters inside a Dictionary with String as the Key and Value. We add extension to the URLComponents with a mutating function that accepts a Dictionary then map the dictionary into Array of URLQueryItem using the each item key and value then assign it to the URLComponents queryItems property.

import Foundation

extension URLComponents {
    
    mutating func setQueryItems(with parameters: [String: String]) {
        self.queryItems = parameters.map { URLQueryItem(name: $0.key, value: $0.value) }
    }
}

let queryParams: [String: String] = [
    "search": "obi wan kenobi",
    "format": "wookie"
]

var urlComponents = URLComponents()
urlComponents.scheme = "https"
urlComponents.host = "swapi.co"
urlComponents.path = "/api/people"
urlComponents.setQueryItems(with: queryParams)
print(urlComponents.url?.absoluteString)
// https://swapi.co/api/people?search=obi%20wan%20kenobi&format=wookie

Building SWAPI API Repository with URLComponents and URLQueryItems (an Example)

Here is an example of how we can build a Remote DataRepository Class using URLComponents and URLQuery items as the building blocks for each endpoint. We also add enum with String to each SWAPI Resources and SWAPI Format for Planets, Spaceships, Vehcicles, People, Films, Species, Wookiee format, JSON format that will be used as strongly typed parameter to specify the Resources and Format they want.

Internally, it will constructs the URL using URLComponents and using the resources to set the path and the Dictionary to construct the URLQueryItem for format and search query parameter, then fetch the data using URLSession Data task.

import Foundation

enum StarWarsAPIResources: String {
    case planets, spaceships, vehcicles, people, films, species
}

enum StarWarsAPIFormat: String {
    case wookiee, json
}

struct StarWarsAPIRepository {
    
    private init() {}
    static let shared = StarWarsAPIRepository()
    
    private let urlSession = URLSession.shared
    var urlComponents: URLComponents {
        var urlComponents = URLComponents()
        urlComponents.scheme = "https"
        urlComponents.host = "swapi.co"
        return urlComponents
    }
    
    func fetchData(with resources: StarWarsAPIResources, format: StarWarsAPIFormat, completion: @escaping(Data?, Error?) -> Void) {
        fetch(with: resources, parameters: [
            "format": format.rawValue
        ], completion: completion)
    }
    
    func searchData(with resources: StarWarsAPIResources, format: StarWarsAPIFormat, search: String, completion: @escaping(Data?, Error?) -> Void) {
        fetch(with: resources, parameters: [
            "format": format.rawValue,
            "search": search
        ], completion: completion)
    }
 
    private func fetch(with resources: StarWarsAPIResources, parameters: [String: String], completion: @escaping(Data?, Error?) -> Void) {
        var urlComponents = self.urlComponents
        urlComponents.path = "/api/\(resources)"
        urlComponents.setQueryItems(with: parameters)
        guard let url = urlComponents.url else {
            completion(nil, NSError(domain: "", code: 100, userInfo: nil))
            return
        }
        urlSession.dataTask(with: url) { (data, _, error) in
            completion(data, error)
        }.resume()
    }

}

extension URLComponents {
    
    mutating func setQueryItems(with parameters: [String: String]) {
        self.queryItems = parameters.map { URLQueryItem(name: $0.key, value: $0.value) }
    }
    
}

//https://swapi.co/api/films?format=json
StarWarsAPIRepository.shared.fetchData(with: .films, format: .json) { (data, error) in
    // Convert JSON Data to Object
}

// https://swapi.co/api/people?search=Obi%20Wan%20Kenobi&format=wookiee
StarWarsAPIRepository.shared.searchData(with: .people, format: .wookiee, search: "Obi Wan Kenobi") { (data, error) in
    // Convert JSON Data to Object
}

Conclusion

Building URL in Swift using URLComponenets and URLQueryItem is quite simple and pretty straightforward. There are many improvements we can use to improve our API calling, one of the article that inspired me and recommended for you to read is the Swift by Sundell’s Constructing URLs in Swift where he smartly encapsulate the API Endpoint by creating a simple Endpoint Struct that uses the URLComponents and URLQueryItem to build the URL with query parameters safely.