How do I convert url.query to a dictionary in Swift?

Details

  • Swift 5
  • Xcode Version 10.2.1 (10E1001)

Solution

import Foundation

// MARK: - [URLQueryItem] to [String: Any]

extension Array where Element == URLQueryItem {
    func toDictionary() -> [String: Any] {
        var dictionary = [String: Any]()
        for queryItem in self {
            guard let value = queryItem.value?.toCorrectType() else { continue }
            if queryItem.name.contains("[]") {
                let key = queryItem.name.replacingOccurrences(of: "[]", with: "")
                let array = dictionary[key] as? [Any] ?? []
                dictionary[key] = array + [value]
            } else {
                dictionary[queryItem.name] = value
            }
        }
        return dictionary
    }
}

extension String {

    // MARK: - String to [URLQueryItem]

    func toURLQueryItems() -> [URLQueryItem]? {
        guard let urlString = self.removingPercentEncoding, let url = URL(string: urlString) else { return nil }
        if let querItems = url.toQueryItems() { return querItems }
        var urlComponents = URLComponents()
        urlComponents.query = urlString
        return urlComponents.queryItems
    }

    // MARK: - attempt to cast string to correct type (int, bool...)

    func toCorrectType() -> Any {
        let types: [LosslessStringConvertible.Type] = [Bool.self, Int.self, Double.self]
        func cast<T>(to: T) -> Any? { return (to.self as? LosslessStringConvertible.Type)?.init(self) }
        for type in types { if let value = cast(to: type) { return value } }
        return self
    }
}

// MARK: - URL to [URLQueryItem]

extension URL {
    func toQueryItems() -> [URLQueryItem]? { return URLComponents(url: self, resolvingAgainstBaseURL: false)?.queryItems }
}

// MARK: - create [URLQueryItem] from [AnyHashable: Any] or [any]

extension URLQueryItem {
    private static var _bracketsString: String { return "[]" }
    static func create(from values: [Any], with key: String) -> [URLQueryItem] {
        let _key = key.contains(_bracketsString) ? key : key + _bracketsString
        return values.compactMap { value -> URLQueryItem? in
            if value is [Any] || value is [AnyHashable: Any] { return nil }
            return URLQueryItem(name: _key, value: value as? String ?? "\(value)")
        }
    }

    static func create(from values: [AnyHashable: Any]) -> [URLQueryItem] {
        return values.flatMap { element -> [URLQueryItem] in
            if element.value is [AnyHashable: Any] { return [] }
            let key = element.key as? String ?? "String"
            if let values = element.value as? [Any] { return URLQueryItem.create(from: values, with: key) }
            return [URLQueryItem(name: key, value: element.value as? String ?? "\(element.value)")]
        }
    }
}

 // MARK: - [AnyHashable: Any] to [URLQueryItem]

extension Dictionary where Value: Any {
    func toURLQueryItems() -> [URLQueryItem] { return URLQueryItem.create(from: self) }
}

Usage

url.toQueryItems()
url.toQueryItems()?.toDictionary()

urlString.toURLQueryItems()
urlString.toURLQueryItems()?.toDictionary()

urlQueryString?.toURLQueryItems()
urlQueryString?.toURLQueryItems()?.toDictionary()

let dictionary = ["aaa": [1234], "bbb": ["a", "b", "c"]]
dictionary.toURLQueryItems()

Full sample

var urlString = "https://example.com/l57?condition%5B%5D=31&brand%5B%5D=289&brand%5B%5D=291&brand%5B%5D=32&year%5B%5D=23259&year%5B%5D=23757&ships_from_region%5B%5D=23684&ships_from_region%5B%5D=23683"
let url = URL(string: urlString)!
var urlQueryString = url.query

print("URL:\n\(urlString)")
print("URL query string:\n\(String(describing: urlQueryString))\n")

print("get [URLQueryItem] from URL:\n\(String(describing: url.toQueryItems()))\n")
print("get [String: Any] from URL:\n\(String(describing: url.toQueryItems()?.toDictionary()))\n")

print("get [URLQueryItem] from url string (absoluteString):\n\(String(describing: urlString.toURLQueryItems()))\n")
print("get [String:Any] from url string (absoluteString):\n\(String(describing: urlString.toURLQueryItems()?.toDictionary()))\n")

print("get [URLQueryItem] from url string (only query):\n\(String(describing: urlQueryString?.toURLQueryItems()))\n")
print("get [String:Any] from url string (only query):\n\(String(describing: urlQueryString?.toURLQueryItems()?.toDictionary()))\n")

var dict =  [String: Any]()
dict = ["aaa": [1234], "bbb": [1234: 22], "ccc": ["a", "b", "c"], "ddd": [[1,2,3], [4,5,6]], "eee[]": [1,2,4], "fff": "value", "ggg": 123]
print("Dict: \(dict)")
print("Dict to [URLQueryItem]: \(dict.toURLQueryItems())")
print("Dict to query oriented dictionary: \(dict.toURLQueryItems().toDictionary())")

Full sample output

URL:
https://example.com/l57?condition%5B%5D=31&brand%5B%5D=289&brand%5B%5D=291&brand%5B%5D=32&year%5B%5D=23259&year%5B%5D=23757&ships_from_region%5B%5D=23684&ships_from_region%5B%5D=23683
URL query string:
Optional("condition%5B%5D=31&brand%5B%5D=289&brand%5B%5D=291&brand%5B%5D=32&year%5B%5D=23259&year%5B%5D=23757&ships_from_region%5B%5D=23684&ships_from_region%5B%5D=23683")

get [URLQueryItem] from URL:
Optional([condition[]=31, brand[]=289, brand[]=291, brand[]=32, year[]=23259, year[]=23757, ships_from_region[]=23684, ships_from_region[]=23683])

get [String: Any] from URL:
Optional(["condition": [31], "ships_from_region": [23684, 23683], "year": [23259, 23757], "brand": [289, 291, 32]])

get [URLQueryItem] from url string (absoluteString):
Optional([condition[]=31, brand[]=289, brand[]=291, brand[]=32, year[]=23259, year[]=23757, ships_from_region[]=23684, ships_from_region[]=23683])

get [String:Any] from url string (absoluteString):
Optional(["year": [23259, 23757], "ships_from_region": [23684, 23683], "condition": [31], "brand": [289, 291, 32]])

get [URLQueryItem] from url string (only query):
Optional([condition[]=31, brand[]=289, brand[]=291, brand[]=32, year[]=23259, year[]=23757, ships_from_region[]=23684, ships_from_region[]=23683])

get [String:Any] from url string (only query):
Optional(["condition": [31], "brand": [289, 291, 32], "ships_from_region": [23684, 23683], "year": [23259, 23757]])

Dict: ["ccc": ["a", "b", "c"], "eee[]": [1, 2, 4], "bbb": [1234: 22], "fff": "value", "ddd": [[1, 2, 3], [4, 5, 6]], "aaa": [1234], "ggg": 123]
Dict to [URLQueryItem]: [ccc[]=a, ccc[]=b, ccc[]=c, ggg=123, fff=value, aaa[]=1234, eee[]=1, eee[]=2, eee[]=4]
Dict to query oriented dictionary: ["ccc": ["a", "b", "c"], "aaa": [1234], "fff": "value", "eee": [1, 2, 4], "ggg": 123]

Simple Extension

extension URL {
    var queryDictionary: [String: String]? {
        guard let query = self.query else { return nil}

        var queryStrings = [String: String]()
        for pair in query.components(separatedBy: "&") {

            let key = pair.components(separatedBy: "=")[0]

            let value = pair
                .components(separatedBy:"=")[1]
                .replacingOccurrences(of: "+", with: " ")
                .removingPercentEncoding ?? ""

            queryStrings[key] = value
        }
        return queryStrings
    }
}

USAGE

let urlString = "http://www.youtube.com/video/4bL4FI1Gz6s?hl=it_IT&iv_logging_level=3&ad_flags=0&endscreen_module=http://s.ytimg.com/yt/swfbin/endscreen-vfl6o3XZn.swf&cid=241&cust_gender=1&avg_rating=4.82280613104"
let url = URL(string: urlString)
print(url!.queryDictionary ?? "NONE")

Here is an example using the Swift reduce function. This will turn a string like 'key1=value1&key2=value2&key3=value3' into a dictionary.

let params = queryString.components(separatedBy: "&").map({
    $0.components(separatedBy: "=")
}).reduce(into: [String:String]()) { dict, pair in
    if pair.count == 2 {
        dict[pair[0]] = pair[1]
    }
}