233

I'm trying to run a HTTP Request in Swift, to POST 2 parameters to a URL.

Example:

Link: www.thisismylink.com/postName.php

Params:

id = 13
name = Jack

What is the simplest way to do that?

I don't even want to read the response. I just want to send that to perform changes on my database through a PHP file.

Undo
  • 25,519
  • 37
  • 106
  • 129
angeant
  • 2,785
  • 3
  • 17
  • 9

7 Answers7

477

The key is that you want to:

  • set the httpMethod to POST;
  • optionally, set the Content-Type header, to specify how the request body was encoded, in case server might accept different types of requests;
  • optionally, set the Accept header, to request how the response body should be encoded, in case the server might generate different types of responses; and
  • set the httpBody to be properly encoded for the specific Content-Type; e.g. if application/x-www-form-urlencoded request, we need to percent-encode the body of the request.

E.g., in Swift 3 and later you can:

let url = URL(string: "https://httpbin.org/post")!
var request = URLRequest(url: url)
request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
request.setValue("application/json", forHTTPHeaderField: "Accept")
request.httpMethod = "POST"
let parameters: [String: Any] = [
    "id": 13,
    "name": "Jack & Jill"
]
request.httpBody = parameters.percentEncoded()

let task = URLSession.shared.dataTask(with: request) { data, response, error in
    guard 
        let data = data, 
        let response = response as? HTTPURLResponse, 
        error == nil 
    else {                                                               // check for fundamental networking error
        print("error", error ?? URLError(.badServerResponse))
        return
    }
    
    guard (200 ... 299) ~= response.statusCode else {                    // check for http errors
        print("statusCode should be 2xx, but is \(response.statusCode)")
        print("response = \(response)")
        return
    }
    
    // do whatever you want with the `data`, e.g.:
    
    do {
        let responseObject = try JSONDecoder().decode(ResponseObject<Foo>.self, from: data)
        print(responseObject)
    } catch {
        print(error) // parsing error
        
        if let responseString = String(data: data, encoding: .utf8) {
            print("responseString = \(responseString)")
        } else {
            print("unable to parse response as string")
        }
    }
}

task.resume()

Where the following extensions facilitate the percent-encoding request body, converting a Swift Dictionary to a application/x-www-form-urlencoded formatted Data:

extension Dictionary {
    func percentEncoded() -> Data? {
        map { key, value in
            let escapedKey = "\(key)".addingPercentEncoding(withAllowedCharacters: .urlQueryValueAllowed) ?? ""
            let escapedValue = "\(value)".addingPercentEncoding(withAllowedCharacters: .urlQueryValueAllowed) ?? ""
            return escapedKey + "=" + escapedValue
        }
        .joined(separator: "&")
        .data(using: .utf8)
    }
}

extension CharacterSet { 
    static let urlQueryValueAllowed: CharacterSet = {
        let generalDelimitersToEncode = ":#[]@" // does not include "?" or "/" due to RFC 3986 - Section 3.4
        let subDelimitersToEncode = "!$&'()*+,;="
        
        var allowed: CharacterSet = .urlQueryAllowed
        allowed.remove(charactersIn: "\(generalDelimitersToEncode)\(subDelimitersToEncode)")
        return allowed
    }()
}

And the following Decodable model objects facilitate the parsing of the application/json response using JSONDecoder:

// sample Decodable objects for https://httpbin.org

struct ResponseObject<T: Decodable>: Decodable {
    let form: T    // often the top level key is `data`, but in the case of https://httpbin.org, it echos the submission under the key `form`
}

struct Foo: Decodable {
    let id: String
    let name: String
}

This checks for both fundamental networking errors as well as high-level HTTP errors. This also properly percent escapes the parameters of the query.

Note, I used a name of Jack & Jill, to illustrate the proper x-www-form-urlencoded result of name=Jack%20%26%20Jill, which is “percent encoded” (i.e. the space is replaced with %20 and the & in the value is replaced with %26).


See previous revision of this answer for Swift 2 rendition.

Rob
  • 415,655
  • 72
  • 787
  • 1,044
  • 7
    FYI, if you want to do real requests (including percent escaping, creating complex requests, simplify the parsing of the responses), consider using [AlamoFire](https://github.com/Alamofire/Alamofire), from the author of AFNetworking. But if you just want to do a trivial `POST` request, you can use the above. – Rob Oct 14 '14 at 16:07
  • 3
    Thanks Rob, That was just what I was looking for! Nothing more than a simple POST. Great answer! – angeant Oct 14 '14 at 16:58
  • 1
    After a few hours of looking for several different solutions, lines 3 and 4 is saving my life as I could not for the life of me get NSJSONSerialization.dataWithJSONObject to work! – Zork Sep 09 '16 at 20:42
  • for PHP to see $_POST make sure to use file name in url, e.g. postName.php. – complexi Dec 10 '16 at 16:26
  • 2
    @complexi - Rather than drawing connections between `$_POST` and filenames, I'd reduce this to something simpler: The PHP script won't run at all if you don't get the URL correct. But it's not always the case that you must include the filename (e.g. the server may be doing URL routing or have default filenames). In this case, the OP gave us a URL that included a filename, so I simply used the same URL as he did. – Rob Dec 10 '16 at 22:40
  • Can you help if I want to tell server that `13&name=Jack` is one value for key `id` – Kumar Aug 17 '17 at 12:26
  • You percent encode the value, like shown here: https://stackoverflow.com/a/41092318/1271826. – Rob Aug 17 '17 at 14:04
  • It doesn't work for me. See https://stackoverflow.com/questions/48715002/post-urlrequest-doesnt-work-in-swift-4 – Jay Wang Feb 09 '18 at 22:30
  • Alamofire is a pain in the ass for synchronous requests. – William Yang Apr 06 '18 at 00:43
  • 2
    Alamofire is no better and no worse than `URLSession` in this regard. All networking APIs are inherently asynchronous, as well they should be. Now, if you're looking for other graceful ways of dealing with asynchronous requests, you can consider wrapping them (either `URLSession` requests or Alamofire ones) in asynchronous, custom `Operation` subclass. Or you can use some promises library, like PromiseKit. – Rob Apr 06 '18 at 00:48
  • Rather than force unwrapping your `url`, use `guard let url = URL(string: "www.myurl.com/whatever") else {return}` – DeepBlue May 24 '19 at 06:35
  • 2
    @DeepBlue - I understand what you’re saying, but I respectfully disagree. To silently fail if there is a problem is a _very_ bad idea. Perhaps you could do `guard let url = ... else { fatalError("Invalid URL") }`, but that’s syntactic noise with little benefit. You’re going down the road of writing a lot of error handling code for something that is not a end-user runtime problem, but rather a programming problem mistake. The analogy is implicitly unwrapped `@IBOutlet` references. Do you write tons of `guard let label = ...` code for all of your outlets? No. That would be silly. Same here. – Rob May 24 '19 at 07:40
  • 2
    Don’t get me wrong. If there are things that aren’t immediately obvious or could fail for reasons outside of the programmer’s control (like parsing the JSON response and/or handing network errors), then using forced unwrapping operator is a huge mistake. Definitely safely unwrap those. But for something like an `@IBOutlet` or this URL example, then it’s counterproductive to add that syntactic noise, IMHO. And to do a `guard` with an `else` clause that just does `return`, hiding any underlying issues, is a really bad idea. – Rob May 24 '19 at 07:41
  • @Rob what if I upload an image to the backend as well with this? I'm unable to post an image with data, I'm getting status code --> 400 – coceki Apr 08 '21 at 05:44
  • @coceki - When sending binary payload, such as an image, we will often use a `multipart/form-data` request rather than the above `application/x-www-form-urlencoded` request. See https://stackoverflow.com/a/26163136/1271826 for an example. (Technically you could use the pattern here but base-64 encode the data client-side, manually base-64 decode server-side, and making the payload 33% larger/slower in the process. For reasons that are probably obvious, that's not a great pattern, hence why we often prefer `multipart/form-data` requests.) – Rob Apr 08 '21 at 16:21
  • The more I learn about Swift, I really began to wonder why no one questions the complexity of Alamofire. – quemeful Aug 19 '22 at 02:49
  • For trivial examples, `URLSession` is fine. But as networking code get more complicated (and it gets complicated quickly), Alamofire is a life-saver. – Rob Aug 19 '22 at 05:10
115

Swift 4 and above

func postRequest() {
  
  // declare the parameter as a dictionary that contains string as key and value combination. considering inputs are valid
  
  let parameters: [String: Any] = ["id": 13, "name": "jack"]
  
  // create the url with URL
  let url = URL(string: "www.thisismylink.com/postName.php")! // change server url accordingly
  
  // create the session object
  let session = URLSession.shared
  
  // now create the URLRequest object using the url object
  var request = URLRequest(url: url)
  request.httpMethod = "POST" //set http method as POST
  
  // add headers for the request
  request.addValue("application/json", forHTTPHeaderField: "Content-Type") // change as per server requirements
  request.addValue("application/json", forHTTPHeaderField: "Accept")
  
  do {
    // convert parameters to Data and assign dictionary to httpBody of request
    request.httpBody = try JSONSerialization.data(withJSONObject: parameters, options: .prettyPrinted)
  } catch let error {
    print(error.localizedDescription)
    return
  }
  
  // create dataTask using the session object to send data to the server
  let task = session.dataTask(with: request) { data, response, error in
    
    if let error = error {
      print("Post Request Error: \(error.localizedDescription)")
      return
    }
    
    // ensure there is valid response code returned from this HTTP response
    guard let httpResponse = response as? HTTPURLResponse,
          (200...299).contains(httpResponse.statusCode)
    else {
      print("Invalid Response received from the server")
      return
    }
    
    // ensure there is data returned
    guard let responseData = data else {
      print("nil Data received from the server")
      return
    }
    
    do {
      // create json object from data or use JSONDecoder to convert to Model stuct
      if let jsonResponse = try JSONSerialization.jsonObject(with: responseData, options: .mutableContainers) as? [String: Any] {
        print(jsonResponse)
        // handle json response
      } else {
        print("data maybe corrupted or in wrong format")
        throw URLError(.badServerResponse)
      }
    } catch let error {
      print(error.localizedDescription)
    }
  }
  // perform the task
  task.resume()
}
Suhit Patil
  • 11,748
  • 3
  • 50
  • 60
  • 8
    I get the following error with your code "The data couldn’t be read because it isn’t in the correct format." – applecrusher Dec 13 '16 at 16:28
  • I think you are getting response in String format can you verify? – Suhit Patil Dec 14 '16 at 02:41
  • 1
    i think the problem here in this solution is that you pass the parameter as json serializing and the web service is takeing as formdata parameters – Amr Angry Feb 15 '17 at 11:46
  • yes in solution the params are json, please check with the server if it requires form data then change the content type e.g. request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type") – Suhit Patil Feb 15 '17 at 12:51
  • for multipart params use let boundaryConstant = "--V2ymHFg03ehbqgZCaKO6jy--"; request.addvalue("multipart/form-data boundary=\(boundaryConstant)", forHTTPHeaderField: "Content-Type") – Suhit Patil Feb 15 '17 at 13:05
  • 2
    This should be the correct answer as who wants to form their data in a string like the checked answer suggests... #oldSkool – Erik Grosskurth Apr 24 '17 at 14:01
  • Great! For nested dicts use https://stackoverflow.com/a/45136949/1067147 – WINSergey Feb 06 '19 at 18:29
50

For anyone looking for a clean way to encode a POST request in Swift 5.

You don’t need to deal with manually adding percent encoding. Use URLComponents to create a GET request URL. Then use query property of that URL to get properly percent escaped query string.

let url = URL(string: "https://example.com")!
var components = URLComponents(url: url, resolvingAgainstBaseURL: false)!

components.queryItems = [
    URLQueryItem(name: "key1", value: "NeedToEscape=And&"),
    URLQueryItem(name: "key2", value: "vålüé")
]

let query = components.url!.query

The query will be a properly escaped string:

key1=NeedToEscape%3DAnd%26&key2=v%C3%A5l%C3%BC%C3%A9

Now you can create a request and use the query as HTTPBody:

var request = URLRequest(url: url)
request.httpMethod = "POST"
request.httpBody = Data(query.utf8)

Now you can send the request.

pointum
  • 2,987
  • 24
  • 31
  • 3
    After various examples, only this works for Swift 5. – J A S K I E R Jan 07 '20 at 09:19
  • I mansioned GET request but i wonder how about the POST request ? How to pass parameters into the httpBody or do i need it ? – Mertalp Tasdelen Jan 31 '20 at 22:30
  • 1
    Smart solution! Thanks for sharing @pointum. I'm sure Martalp doesn't need the answer anymore, but for anybody else reading, the above does a POST request. – Allan Spreys May 13 '20 at 22:04
  • 3
    By the way, if you use this technique, please note that it will not correctly percent escape `+` characters. See https://stackoverflow.com/a/27724627/1271826. – Rob Jun 20 '21 at 01:43
  • works perfectly, at last I just added URLSession.shared.dataTask(with: request) { data, HTTPURLResponse, Error in if (data != nil && data?.count != 0) { let response = String(data: data!, encoding: .utf8) print(response!) } }.resume() – Rudrakshya Barman Oct 02 '21 at 17:45
13

Heres the method I used in my logging library: https://github.com/goktugyil/QorumLogs

This method fills html forms inside Google Forms.

    var url = NSURL(string: urlstring)

    var request = NSMutableURLRequest(URL: url!)
    request.HTTPMethod = "POST"
    request.setValue("application/x-www-form-urlencoded; charset=utf-8", forHTTPHeaderField: "Content-Type")
    request.HTTPBody = postData.dataUsingEncoding(NSUTF8StringEncoding)
    var connection = NSURLConnection(request: request, delegate: nil, startImmediately: true)
Esqarrouth
  • 38,543
  • 21
  • 161
  • 168
9
let session = URLSession.shared
        let url = "http://...."
        let request = NSMutableURLRequest(url: NSURL(string: url)! as URL)
        request.httpMethod = "POST"
        request.addValue("application/json", forHTTPHeaderField: "Content-Type")
        var params :[String: Any]?
        params = ["Some_ID" : "111", "REQUEST" : "SOME_API_NAME"]
        do{
            request.httpBody = try JSONSerialization.data(withJSONObject: params, options: JSONSerialization.WritingOptions())
            let task = session.dataTask(with: request as URLRequest as URLRequest, completionHandler: {(data, response, error) in
                if let response = response {
                    let nsHTTPResponse = response as! HTTPURLResponse
                    let statusCode = nsHTTPResponse.statusCode
                    print ("status code = \(statusCode)")
                }
                if let error = error {
                    print ("\(error)")
                }
                if let data = data {
                    do{
                        let jsonResponse = try JSONSerialization.jsonObject(with: data, options: JSONSerialization.ReadingOptions())
                        print ("data = \(jsonResponse)")
                    }catch _ {
                        print ("OOps not good JSON formatted response")
                    }
                }
            })
            task.resume()
        }catch _ {
            print ("Oops something happened buddy")
        }
Osman
  • 1,496
  • 18
  • 22
7

All the answers here use JSON objects. This gave us problems with the $this->input->post() methods of our Codeigniter controllers. The CI_Controller cannot read JSON directly. We used this method to do it WITHOUT JSON

func postRequest() {
    // Create url object
    guard let url = URL(string: yourURL) else {return}

    // Create the session object
    let session = URLSession.shared

    // Create the URLRequest object using the url object
    var request = URLRequest(url: url)

    // Set the request method. Important Do not set any other headers, like Content-Type
    request.httpMethod = "POST" //set http method as POST

    // Set parameters here. Replace with your own.
    let postData = "param1_id=param1_value&param2_id=param2_value".data(using: .utf8)
    request.httpBody = postData

    // Create a task using the session object, to run and return completion handler
    let webTask = session.dataTask(with: request, completionHandler: {data, response, error in
    guard error == nil else {
        print(error?.localizedDescription ?? "Response Error")
        return
    }
    guard let serverData = data else {
        print("server data error")
        return
    }
    do {
        if let requestJson = try JSONSerialization.jsonObject(with: serverData, options: .mutableContainers) as? [String: Any]{
            print("Response: \(requestJson)")
        }
    } catch let responseError {
        print("Serialisation in error in creating response body: \(responseError.localizedDescription)")
        let message = String(bytes: serverData, encoding: .ascii)
        print(message as Any)
    }

    // Run the task
    webTask.resume()
}

Now your CI_Controller will be able to get param1 and param2 using $this->input->post('param1') and $this->input->post('param2')

0-1
  • 702
  • 1
  • 10
  • 30
CanonicalBear
  • 367
  • 8
  • 12
3
@IBAction func btn_LogIn(sender: AnyObject) {

    let request = NSMutableURLRequest(URL: NSURL(string: "http://demo.hackerkernel.com/ios_api/login.php")!)
    request.HTTPMethod = "POST"
    let postString = "email: test@test.com & password: testtest"
    request.HTTPBody = postString.dataUsingEncoding(NSUTF8StringEncoding)
    let task = NSURLSession.sharedSession().dataTaskWithRequest(request){data, response, error in
        guard error == nil && data != nil else{
            print("error")
            return
        }
        if let httpStatus = response as? NSHTTPURLResponse where httpStatus.statusCode != 200{
            print("statusCode should be 200, but is \(httpStatus.statusCode)")
            print("response = \(response)")
        }
        let responseString = String(data: data!, encoding: NSUTF8StringEncoding)
        print("responseString = \(responseString)")
    }
    task.resume()
}
Pang
  • 9,564
  • 146
  • 81
  • 122
Raish Khan
  • 71
  • 5