Swift GET request with parameters
Asked Answered
A

8

113

I'm very new to swift, so I will probably have a lot of faults in my code but what I'm trying to achieve is send a GET request to a localhost server with paramters. More so I'm trying to achieve it given my function take two parameters baseURL:string,params:NSDictionary. I am not sure how to combine those two into the actual URLRequest ? Here is what I have tried so far

    func sendRequest(url:String,params:NSDictionary){
       let urls: NSURL! = NSURL(string:url)
       var request = NSMutableURLRequest(URL:urls)
       request.HTTPMethod = "GET"
       var data:NSData! =  NSKeyedArchiver.archivedDataWithRootObject(params)
       request.HTTPBody = data
       println(request)
       var session = NSURLSession.sharedSession()
       var task = session.dataTaskWithRequest(request, completionHandler:loadedData)
       task.resume()

    }

}

func loadedData(data:NSData!,response:NSURLResponse!,err:NSError!){
    if(err != nil){
        println(err?.description)
    }else{
        var jsonResult: NSDictionary = NSJSONSerialization.JSONObjectWithData(data, options: NSJSONReadingOptions.MutableContainers, error: nil) as NSDictionary
        println(jsonResult)

    }

}
Apriorism answered 31/12, 2014 at 16:48 Comment(0)
L
204

When building a GET request, there is no body to the request, but rather everything goes on the URL. To build a URL (and properly percent escaping it), you can also use URLComponents.

var components = URLComponents(string: "https://www.google.com/search/")!

components.queryItems = [
    URLQueryItem(name: "q", value: "War & Peace")
]

guard let url = components.url else {
    throw URLError(.badURL)
}

The only trick is that most web services need + character percent escaped (because they'll interpret that as a space character as dictated by the application/x-www-form-urlencoded specification). But URLComponents will not percent escape it. Apple contends that + is a valid character in a query and therefore shouldn't be escaped. Technically, they are correct, that it is allowed in a query of a URI, but it has a special meaning in application/x-www-form-urlencoded requests and really should not be passed unescaped.

When I presented this issue to Apple support, they advised to manually percent escaping the + characters:

components.percentEncodedQuery = components.percentEncodedQuery?.replacingOccurrences(of: "+", with: "%2B")

This is an inelegant work-around, but it works, and is what Apple advises if your queries may include a + character and you have a server that interprets them as spaces.

Anyway, combining that with your sendRequest routine, you might end up with something like:

enum WebServiceError: Error {
    case invalidResponse(Data, URLResponse)
    case statusCode(Int, Data)
}

func object<T: Decodable>(from baseUrl: URL, parameters: [String: String]? = nil) async throws -> T {
    guard var components = URLComponents(url: baseUrl, resolvingAgainstBaseURL: false) else {
        throw URLError(.badURL)
    }

    components.queryItems = parameters?.map { (key, value) in
        URLQueryItem(name: key, value: value)
    }
    components.percentEncodedQuery = components.percentEncodedQuery?.replacingOccurrences(of: "+", with: "%2B")

    guard let url = components.url else {
        throw URLError(.badURL)
    }

    let (data, response) = try await URLSession.shared.data(from: url)

    guard let statusCode = (response as? HTTPURLResponse)?.statusCode else {  // is there HTTP response
        throw WebServiceError.invalidResponse(data, response)
    }

    guard 200 ..< 300 ~= statusCode else {                                    // is statusCode 2XX
        throw WebServiceError.statusCode(statusCode, data)
    }

    return try JSONDecoder().decode(T.self, from: data)
}

And you'd call it like:

do {
    let foo: Foo = try await object(from: baseUrl)
    // do something with `foo` here
} catch WebServiceError.statusCode(404, _) {        // if you want, you can catch individual status codes here
    // handle not found error here
} catch {
    // handle other errors here
}

Clearly, there are lots of permutations on the idea, but hopefully this illustrates the basic idea of how to percent encode the parameters into the URL of a GET request.


See previous revisions of this answer for Swift 2, manual percent escaping renditions, and non-Swift concurrency renditions of the above.

Laundryman answered 31/12, 2014 at 17:57 Comment(16)
Thanks for the fantastic answer. I just have one question,I am still a bit confused as to what the extension string is doing to the values ? Also when would I need to use HttpBody then?Apriorism
The string extension is percent escaping the values per RFC 3986. There are certain characters that have special meanings in URLs (e.g. & separates one parameter from the next, so if & occurs in value, you cannot let it go by unescaped). Regarding HTTPBody, you should not use it in GET request; It is used in POST but not GET.Laundryman
This looks so simple, what are the advantages of using something like github.com/xyyc/SwiftSocket instead of this? I'm sorry I'm new to all this.Arnold
That might not be the right comparison, because that's a sockets library and this is HTTP. Closer is something like Alamofire, but that is (a) more flexible (can handle JSON requests, x-www-form-urlencoded requests, multipart, etc.); (b) more functional (e.g. handles authentication challenges); and (c) gets you out of the weeds of HTTP programming. If anything, my answer above is intended as a cautionary tale of the risks of just creating your own NSMutableURLRequest, pointing out that there's more to it than the OP suggests.Laundryman
This is a beautiful solution. Thanks @Rob. Btw is there any difference in supplying parameters like this NSJSONSerialization.dataWithJSONObject(parameters, options: nil, error: nil)? parameters being an array of dictionaries [String: String]Vassaux
It doesn't make much sense to me to be using JSON with GET request. If you're sending information to a server, that's POST, not GET. And you'd specify a different Content-Type header. And you don't need any of this percent-encoding stuff. Bottom line, a lot of the execution details are different.Laundryman
Rob . chance you can update the extension method to swfit 2?Eichhorn
Great answer. I think you can use NSCharacterSet.URLQueryAllowedCharacterSet() instead of manually creating the allowed characters.Clarineclarinet
@EddieSullivan - Not quite. If you do that, you have to make a mutable copy and then remove a few characters, notably & and +, which URLQueryAllowedCharacterSet will allow to pass unescaped. See https://mcmap.net/q/121143/-how-to-http-post-special-chars-in-swift.Laundryman
@Laundryman this is a great answer. Could you update it for Swift 3 please?Africanist
Updated for Swift 3.Laundryman
Just as a note, the dictionary category breaks if the dictionary contains non-string values such as Int data types.Aureomycin
Agreed, I was trying to keep it simple, and you might have rendition that handles a broader array of values. For example, this rendition renders dates, coordinates, booleans, strings, numeric values, etc.Laundryman
@Laundryman this is brilliant - thank you! Everything makes sense except (200 ..< 300) ~= response.statusCode. What does that do please? And what is that operator called - I searched but couldn't find it in Swift. Apologies if that's a really obvious question!Aurelea
It is a “pattern-matching operator”. See the ~= documentation. In this case, it effectively says “does the range of 200 up to (and not including) 300 contain the value statusCode.”Laundryman
Updated for Swift concurrency, JSONDecoder, etc.Laundryman
R
104

Use NSURLComponents to build your NSURL like this

var urlComponents = NSURLComponents(string: "https://www.google.de/maps/")!

urlComponents.queryItems = [
  NSURLQueryItem(name: "q", value: String(51.500833)+","+String(-0.141944)),
  NSURLQueryItem(name: "z", value: String(6))
]
urlComponents.URL // returns https://www.google.de/maps/?q=51.500833,-0.141944&z=6

source: https://www.ralfebert.de/snippets/ios/encoding-nsurl-get-parameters/

Redford answered 7/12, 2015 at 13:46 Comment(2)
In spite of Rob's impressive answer, yours is simpler and works.Henri
This should be the accepted answer. It is recommended to use NSURLComponents along with query items to construct URLs. Much safer and less prone to error.Bifoliate
R
7

I am using this, try it in playground. Define the base urls as Struct in Constants

struct Constants {

    struct APIDetails {
        static let APIScheme = "https"
        static let APIHost = "restcountries.eu"
        static let APIPath = "/rest/v1/alpha/"
    }
}

private func createURLFromParameters(parameters: [String:Any], pathparam: String?) -> URL {

    var components = URLComponents()
    components.scheme = Constants.APIDetails.APIScheme
    components.host   = Constants.APIDetails.APIHost
    components.path   = Constants.APIDetails.APIPath
    if let paramPath = pathparam {
        components.path = Constants.APIDetails.APIPath + "\(paramPath)"
    }
    if !parameters.isEmpty {
        components.queryItems = [URLQueryItem]()
        for (key, value) in parameters {
            let queryItem = URLQueryItem(name: key, value: "\(value)")
            components.queryItems!.append(queryItem)
        }
    }

    return components.url!
}

let url = createURLFromParameters(parameters: ["fullText" : "true"], pathparam: "IN")

//Result url= https://restcountries.eu/rest/v1/alpha/IN?fullText=true
Rubio answered 29/3, 2017 at 16:41 Comment(0)
B
1

Swift 3:

extension URL {
    func getQueryItemValueForKey(key: String) -> String? {
        guard let components = NSURLComponents(url: self, resolvingAgainstBaseURL: false) else {
              return nil
        }

        guard let queryItems = components.queryItems else { return nil }
     return queryItems.filter {
                 $0.name.lowercased() == key.lowercased()
                 }.first?.value
    }
}

I used it to get the image name for UIImagePickerController in func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [String : Any]):

var originalFilename = ""
if let url = info[UIImagePickerControllerReferenceURL] as? URL, let imageIdentifier = url.getQueryItemValueForKey(key: "id") {
    originalFilename = imageIdentifier + ".png"
    print("file name : \(originalFilename)")
}
Bacillary answered 23/12, 2016 at 13:47 Comment(1)
your response doesn't answer OPs questionKorry
L
0

You can extend your Dictionary to only provide stringFromHttpParameter if both key and value conform to CustomStringConvertable like this

extension Dictionary where Key : CustomStringConvertible, Value : CustomStringConvertible {
  func stringFromHttpParameters() -> String {
    var parametersString = ""
    for (key, value) in self {
      parametersString += key.description + "=" + value.description + "&"
    }
    return parametersString
  }
}

this is much cleaner and prevents accidental calls to stringFromHttpParameters on dictionaries that have no business calling that method

Ludwick answered 12/6, 2017 at 0:41 Comment(0)
G
0
func relationsApi(_ search: String) {
      let url = URL(string: getApiUrl)!
      
      let session = URLSession.shared
      let queryItems = [URLQueryItem(name: "search", value: search)]
      var urlComps = URLComponents(string: getApiUrl)!
      urlComps.queryItems = queryItems
      let result = urlComps.url!
      print(result)
      
      var request = URLRequest(url: result)
      
      request.setValue( "Bearer \(tokenName)", forHTTPHeaderField: "Authorization")
      let task = session.dataTask(with: request) { (data, response, error) in
          if let error = error{
              print (error)
          } else if let data = data {
              do {
                  let decoder = JSONDecoder()
                  let responseDatas = try decoder.decode(RelationsAPIModel.self, from: data)
                  //  print(responseDatas)
                  DispatchQueue.main.async {
                      
                      self.namesArr = responseDatas.data.relation
                      //     self.subname = responseDatas.data.relation[Re]
                      self.relationTableView.reloadData()
                  }
              } catch {
                  print(error)
              }
          } else {
              print("something went wrong")
          }
      }
      task.resume()
    }
}
Grog answered 22/11, 2023 at 16:7 Comment(0)
A
-1

This extension that @Rob suggested works for Swift 3.0.1

I wasn't able to compile the version he included in his post with Xcode 8.1 (8B62)

extension Dictionary {

    /// Build string representation of HTTP parameter dictionary of keys and objects
    ///
    /// :returns: String representation in the form of key1=value1&key2=value2 where the keys and values are percent escaped

    func stringFromHttpParameters() -> String {

        var parametersString = ""
        for (key, value) in self {
            if let key = key as? String,
               let value = value as? String {
                parametersString = parametersString + key + "=" + value + "&"
            }
        }
        parametersString = parametersString.substring(to: parametersString.index(before: parametersString.endIndex))
        return parametersString.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed)!
    }

}
Afferent answered 16/11, 2016 at 2:5 Comment(0)
G
-4

I use:

let dictionary = ["method":"login_user",
                  "cel":mobile.text!
                  "password":password.text!] as  Dictionary<String,String>

for (key, value) in dictionary {
    data=data+"&"+key+"="+value
    }

request.HTTPBody = data.dataUsingEncoding(NSUTF8StringEncoding);
Gerrilee answered 22/7, 2016 at 15:10 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.