The key is that you want to:
- set the httpMethodtoPOST;
- optionally, set the Content-Typeheader, to specify how the request body was encoded, in case server might accept different types of requests;
- optionally, set the Acceptheader, to request how the response body should be encoded, in case the server might generate different types of responses; and
- set the httpBodyto be properly encoded for the specificContent-Type; e.g. ifapplication/x-www-form-urlencodedrequest, 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.