115

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)

    }

}

8 Answers 8

207

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.

Sign up to request clarification or add additional context in comments.

16 Comments

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?
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.
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.
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.
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]
|
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/

2 Comments

In spite of Rob's impressive answer, yours is simpler and works.
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.
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

Comments

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)")
}

1 Comment

your response doesn't answer OPs question
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

Comments

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()
    }
}

Comments

-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)!
    }

}

Comments

-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);

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.