Search code examples
swiftswift-package-manager

Swift - function with many params that need to be added to url query params


Im building a swift package that will be a wrapper to an api. Since its going to be used by many people with many different needs, I have to fully support what the api offers.

There are 10 different query parameters all of which are optional to make the network request. I have a couple different ways of doing this, but I want to see if there is an optimal way im unaware of.

// only showing 3 params but I have 10 optional params
func getData(query: String, limit: Int, page: Int) {
    let url: String = "https://baseurl.com"
    if let query = query {
        url += "?query=\(query)"
    }
    if let limit = limit {
        url += "?limit=\(limit)"
    }
    // do this for all 10 query params
    // make network request with url
}

next option was something like creating an object for my params but i think id be in the same position for unpacking the data

struct Params {
    var query: String?
    var limit: Int?
    var page: Int
    
    func createStringFromParams() -> String {
        let queryParams: String = ""
        if let query = query {
            url += "?query=\(query)"
        } 
        // do this for all params
    }
}

var params: Params()
params.query = "my query"
func getData(params: Params) {
    var url: String = "https://baseurl.com/\(params.createStringFromParams())"
}

I also will be doing this for many different functions as every api endpoint has 0-many query params.


Solution

  • I'd suggest to use URLQueryItem & URLComponents.

    Basic code being:

    let baseUrl: String = "https://baseurl.com/"
    var components = URLComponents(string: baseUrl)
    let queryParameters = [URLQueryItem(name: "query", value: "myQuery")]
    //If empty and set, it will add a "?" at the end of the URL, start of the query with no parameters
    if !queryParameters.isEmpty {
        components?.queryItems = queryParameters
    }
    let finalURL = components?.url
    

    Now, since you want to limit the values/calls, you could use an enum with associated values:

    enum Params {
        case query(String)
        case limit(Int)
        case page(Int)
        
        func queryItem() -> URLQueryItem {
            switch self {
            case .limit(let limit):
                return URLQueryItem(name: "limit", value: "\(limit)")
            case .page(let page):
                return URLQueryItem(name: "page", value: "\(page)")
            case .query(let query):
                return URLQueryItem(name: "page", value: query)
            }
        }
    
    }
    
    func createURL(with params: [Params]) {
        let baseUrl: String = "https://baseurl.com/"
        var components = URLComponents(string: baseUrl)
        
        //If empty and set, it will add a "?" at the end of the URL, start of the query with no parameters
        if !queryParameters.isEmpty {
            components?.queryItems = queryParameters.map { $0.queryItem() }
        }
        
        let finalURL = components?.url
        print(finalURL!.absoluteString)
    }
    

    If you still want to use your parameter struct:

    struct Params {
        let query: String?
        let limit: Int?
        let page: Int?
        
        
        init(query: String? = nil, limit: Int? = nil, page: Int? = nil) {
            self.query = query
            self.limit = limit
            self.page = page
        }
        
        func queryItems() -> [URLQueryItem] {
            var items: [URLQueryItem] = []
            if let query = query {
                items.append(URLQueryItem(name: "query", value: query))
            }
            
            if let limit = limit {
                items.append(URLQueryItem(name: "limit", value: "\(limit)"))
            }
            
            if let page = page {
                items.append(URLQueryItem(name: "page", value: "\(page)"))
            }
            
            return items
        }
    }
    
    func createURL(with params: Params) {
        let baseUrl: String = "https://baseurl.com/"
        var components = URLComponents(string: baseUrl)
        
        //If empty and set, it will add a "?" at the end of the URL, start of the query with no parameters
        let queryParameters = params.queryItems()
        if !queryParameters.isEmpty {
            components?.queryItems = queryParameters
        }
        
        let finalURL = components?.url
        print(finalURL!.absoluteString)
    }
    

    In my opinion, both approach are valid. There are just less if let in the enum solution.