Making a POST request in Swift

This post updated September 16 for Xcode 6 GM

Let’s say you’re working on an iOS app where you need to pass data to a server as JSON. Just about every app that interacts with a server does this, and almost always there is a login API call.

For the purposes of this tutorial, I’m first going to set up a super simple web server that handles a POST request. This part of the tutorial is optional if you already have a server, and you can skip ahead to the Swift code. Go ahead and create a ruby file called api.rb, and stick this in there:

Note: Alternatively, you could use tasty with a Deno web server here, which is more similar to Swift than NodeJS.


#!/usr/bin/env ruby
require 'rubygems'
require 'sinatra'
require 'json'
get '/' do
"Nothing to see here. Move along."
end
post '/login' do
values = JSON.parse(request.env["rack.input"].read)
if values["username"]=='jameson' && values["password"]=='password'
"{\"success\":1}"
else
"{\"success\":0}"
end
end

Now, I know this is ruby code but it’s just to get us a server up and running fast. To run this you’ll first need to make it executable. Open up a Terminal window and cd to the directory where api.rb is, then you can add the executable permission like this:

chmod +x api.rb

And you’ll need the gem:


gem install sinatra
gem install json

Depending on your system’s configuration, this might give you some kind of permissions error. If it does prepend the ‘sudo’ statement in front of this:


sudo gem install sinatra
sudo gem install json

Note: You normally want ruby gems to be installed in to a user directory using RVM and a gemset, but this is a Swift tutorial so we’re just going to keep moving 🙂

Okay now give it a whirl:

./api.rb

 
You should now have a web server up and running that accepts a POST call to /login. The server by default runs on port 4567, so now let’s create a Swift project to interact with it. A simple single-view project is fine for this example.

In your application didFinishLaunchingWithOptions method add the following:

func application(application: UIApplication!, didFinishLaunchingWithOptions launchOptions: NSDictionary!) -> Bool {
    var request = NSMutableURLRequest(URL: NSURL(string: "http://localhost:4567/login"))
    var session = NSURLSession.sharedSession()
    request.HTTPMethod = "POST"
    
    var params = ["username":"jameson", "password":"password"] as Dictionary<String, String>
    
    var err: NSError?
    request.HTTPBody = NSJSONSerialization.dataWithJSONObject(params, options: nil, error: &err)
    request.addValue("application/json", forHTTPHeaderField: "Content-Type")
    request.addValue("application/json", forHTTPHeaderField: "Accept")
    
    var task = session.dataTaskWithRequest(request, completionHandler: {data, response, error -> Void in
        println("Response: \(response)")
        var strData = NSString(data: data, encoding: NSUTF8StringEncoding)
        println("Body: \(strData)")
        var err: NSError?
        var json = NSJSONSerialization.JSONObjectWithData(data, options: .MutableLeaves, error: &err) as? NSDictionary
        
        // Did the JSONObjectWithData constructor return an error? If so, log the error to the console
        if(err != nil) {
            println(err!.localizedDescription)
            let jsonStr = NSString(data: data, encoding: NSUTF8StringEncoding)
            println("Error could not parse JSON: '\(jsonStr)'")
        }
        else {
            // The JSONObjectWithData constructor didn't return an error. But, we should still
            // check and make sure that json has a value using optional binding.
            if let parseJSON = json {
                // Okay, the parsedJSON is here, let's get the value for 'success' out of it
                var success = parseJSON["success"] as? Int
                println("Succes: \(success)")
            }
            else {
                // Woa, okay the json object was nil, something went worng. Maybe the server isn't running?
                let jsonStr = NSString(data: data, encoding: NSUTF8StringEncoding)
                println("Error could not parse JSON: \(jsonStr)")
            }
        }
    })
    
    task.resume()
    return true
}

First we create a request and a session to operate on.
Next, we set some params in a Swift dictionary, these values will be passed to the server.

Using dataWithJSONObject() we can encode our parameters as NSData, and set the HTTPBody of the request to this NSData object.

Next we add our headers stating we’re working with JSON using addValue()

Finally we instantiate a NSTask from the NSURLSession object with dataTaskWithRequest()

The last parameter of dataTaskWithRequest is a closure that contains values for the response, data, or error from our server.

After deserializing the response with JSONObjectWithData(), we can check json[“success”] for the 0 or 1 coming back from our server to see if the request succeeded, and process the results.

Most of this is just boilerplate, so it’s useless to move this all out in to a new function, we’ll call it post().

Move all the post logic out to this method, taking params and url as arguments.

func post(params : Dictionary<String, String>, url : String) {
    var request = NSMutableURLRequest(URL: NSURL(string: url))
    var session = NSURLSession.sharedSession()
    request.HTTPMethod = "POST"
    
    var err: NSError?
    request.HTTPBody = NSJSONSerialization.dataWithJSONObject(params, options: nil, error: &err)
    request.addValue("application/json", forHTTPHeaderField: "Content-Type")
    request.addValue("application/json", forHTTPHeaderField: "Accept")
    
    var task = session.dataTaskWithRequest(request, completionHandler: {data, response, error -> Void in
        println("Response: \(response)")
        var strData = NSString(data: data, encoding: NSUTF8StringEncoding)
        println("Body: \(strData)")
        var err: NSError?
        var json = NSJSONSerialization.JSONObjectWithData(data, options: .MutableLeaves, error: &err) as? NSDictionary
        
        // Did the JSONObjectWithData constructor return an error? If so, log the error to the console
        if(err != nil) {
            println(err!.localizedDescription)
            let jsonStr = NSString(data: data, encoding: NSUTF8StringEncoding)
            println("Error could not parse JSON: '\(jsonStr)'")
        }
        else {
            // The JSONObjectWithData constructor didn't return an error. But, we should still
            // check and make sure that json has a value using optional binding.
            if let parseJSON = json {
                // Okay, the parsedJSON is here, let's get the value for 'success' out of it
                var success = parseJSON["success"] as? Int
                println("Succes: \(success)")
            }
            else {
                // Woa, okay the json object was nil, something went worng. Maybe the server isn't running?
                let jsonStr = NSString(data: data, encoding: NSUTF8StringEncoding)
                println("Error could not parse JSON: \(jsonStr)")
            }
        }
    })
    
    task.resume()
}

Now our main logic can be greatly simplified for making any POST request. Let’s make a couple in the didFinishLaunching method.

func application(application: UIApplication!, didFinishLaunchingWithOptions launchOptions: NSDictionary!) -> Bool {
    
    // Correct url and username/password
    self.post(["username":"jameson", "password":"password"], url: "http://localhost:4567/login")
    
    // Correct url, incorrect password
    self.post(["username":"jameson", "password":"wrong_password"], url: "http://localhost:4567/login")
    
    // Incorrect url
    self.post(["username":"jameson", "password":"password"], url: "http://badurl.com/nonexistent")

    return true
}

In your console you should be able to see three login attempts, with only one of them succeeding. This is all great, but we should probably implement a way for the post request to notify our caller of a success or failure. Let’s add a Swift closure as a third method parameter to our post() function, so we can call it upon success or completion.

func post(params : Dictionary<String, String>, url : String, postCompleted : (succeeded: Bool, msg: String) -> ()) {

Now we have our new postCompleted closure, we can pass back to the caller whether or not the post request “succeeded”, and a string message to display to the user.

func post(params : Dictionary<String, String>, url : String, postCompleted : (succeeded: Bool, msg: String) -> ()) {
    var request = NSMutableURLRequest(URL: NSURL(string: url))
    var session = NSURLSession.sharedSession()
    request.HTTPMethod = "POST"
    
    var err: NSError?
    request.HTTPBody = NSJSONSerialization.dataWithJSONObject(params, options: nil, error: &err)
    request.addValue("application/json", forHTTPHeaderField: "Content-Type")
    request.addValue("application/json", forHTTPHeaderField: "Accept")
    
    var task = session.dataTaskWithRequest(request, completionHandler: {data, response, error -> Void in
        println("Response: \(response)")
        var strData = NSString(data: data, encoding: NSUTF8StringEncoding)
        println("Body: \(strData)")
        var err: NSError?
        var json = NSJSONSerialization.JSONObjectWithData(data, options: .MutableLeaves, error: &err) as? NSDictionary
        
        var msg = "No message"
        
        // Did the JSONObjectWithData constructor return an error? If so, log the error to the console
        if(err != nil) {
            println(err!.localizedDescription)
            let jsonStr = NSString(data: data, encoding: NSUTF8StringEncoding)
            println("Error could not parse JSON: '\(jsonStr)'")
            postCompleted(succeeded: false, msg: "Error")
        }
        else {
            // The JSONObjectWithData constructor didn't return an error. But, we should still
            // check and make sure that json has a value using optional binding.
            if let parseJSON = json {
                // Okay, the parsedJSON is here, let's get the value for 'success' out of it
                if let success = parseJSON["success"] as? Bool {
                    println("Succes: \(success)")
                    postCompleted(succeeded: success, msg: "Logged in.")
                }
                return
            }
            else {
                // Woa, okay the json object was nil, something went worng. Maybe the server isn't running?
                let jsonStr = NSString(data: data, encoding: NSUTF8StringEncoding)
                println("Error could not parse JSON: \(jsonStr)")
                postCompleted(succeeded: false, msg: "Error")
            }
        }
    })
    
    task.resume()
}

We’ll also need to modify how we call the method in our applicationDidFinishLaunching function.
The last parameter will now be a closure. Using Swift’s trailing closure syntax we can just end the post call with the block { }
Inside of this closure we create an UIAlertView control and populate it based on the succeeded and msg variables.
Then, we move back to a foreground thread and show the alert view.

func application(application: UIApplication!, didFinishLaunchingWithOptions launchOptions: NSDictionary!) -> Bool {
    
    // Correct url and username/password
    self.post(["username":"jameson", "password":"password"], url: "http://localhost:4567/login") { (succeeded: Bool, msg: String) -> () in
        var alert = UIAlertView(title: "Success!", message: msg, delegate: nil, cancelButtonTitle: "Okay.")
        if(succeeded) {
            alert.title = "Success!"
            alert.message = msg
        }
        else {
            alert.title = "Failed : ("
            alert.message = msg
        }
        
        // Move to the UI thread
        dispatch_async(dispatch_get_main_queue(), { () -> Void in
            // Show the alert
            alert.show()
        })
    }

    return true
}

Give it a whirl, and can now see a server logging in a user (or failing to) with some UI feedback. Cool right?

Try changing the login information and see what you get.

This is an overly simplistic server but I just wanted to demonstrate how you would go about making a POST request in Swift. I hope it was helpful!

The full source code to this project can be downloaded from Github here.

In my book I go in to much more detail, showing how to work with form data and chain API requests together.

This Post Has 34 Comments

  1. Awesome.
    Don’t stop with the tutorials.
    Thanks.

  2. If you hit a lack of write permission at “gem install sinatra”, prefix with sudo i.e
    “sudo gem install sinatra” and enter your account password when prompted

  3. For the newbies amongst you, the following commands need to be written from the Terminal app.

    chmod +x api.rb
    gem install sinatra
    ./api.rb

    If you hit any issues with the 3rd line, firstly, be sure to cd to the directory you saved your api.rb file in from terminal and execute it. Secondly, if you’re receiving any bash errors, try the following command instead: ruby api.rb

  4. I literally copy paste this (with my url, of course) and didn’t work, i think the dictionary is not being attached to the request, can you help!?

    1. I’m going to assume the API is returning something that isn’t being deserialized, can you confirm ‘data’ is not nil when it comes back?

      1. I solved in a different way, this was my solution and I don’t know if I was supposed to do it that way but it worked.

        var params = createStringFromDictionary(dictRequest)
        var paramsLength = “\(countElements(params))”
        var requestBodyData = (params as NSString).dataUsingEncoding(NSUTF8StringEncoding)

        //var url = NSURL(string: “http://localhost:8888/json.php”)
        var url = NSURL(string: “http://23.239.30.54:8080/test/myLogin.html”)
        var request = NSMutableURLRequest(URL: url)
        request.HTTPMethod = “POST”
        request.HTTPBody = requestBodyData
        request.addValue(paramsLength, forHTTPHeaderField: “Content-Length”)
        request.addValue(“application/json”, forHTTPHeaderField: “Accept”)
        request.addValue(“application/x-www-form-urlencoded”, forHTTPHeaderField: “Content-Type”)

        var connection = NSURLConnection(request: request, delegate: self, startImmediately: false)
        println(“Sending request”)
        connection.start()

        1. This is my createStringFromDictionary function

          func createStringFromDictionary(dict: Dictionary) -> String {
          var params = String()
          for (key, value) in dict {
          params += “&” + key + “=” + value
          }
          return params
          }

        2. Thanks Gerardo ,It worked for me.
          I was getting Null data on server side.
          it was because of server was accepting URL encoded data with parameter that were passed in.

          1. Hi,
            I am getting null data with the same procedure. could you please help me ?

            1. Solved.
              removed :
              request.addValue(“application/json”, forHTTPHeaderField: “Content-Type”)
              request.addValue(“application/json”, forHTTPHeaderField: “Accept”)

              Added:
              request.addValue(“application/x-www-form-urlencoded”, forHTTPHeaderField:”Content-Type”);

  5. Hi there, thanks for the Swift tutorials! I’m just trying to get this code working but I keep getting the error:
    “EXC_BAD_INSTRUCTION(code=EXC_I386_INVOP, subcode=0x0)”
    on the line:
    var task = session.dataTaskWithRequest(request, completionHandler: {data, response, error -> Void in

    I’m getting a response from my server (PHP), and I’ve set the server to just print the contents of the POST, but there’s nothing being received in the POST.

    Any ideas what’s going on?

    1. The issue is most likely on the line that deserializes the JSON, try removing that line and everything after it to confirm. Most likely your data type is different than that of the iTunes JSON.

  6. The code ignores the parameters, it returns the same response and doesn’t matter which params I put.

    1. The parameters are used here, first argument:
      request.HTTPBody = NSJSONSerialization.dataWithJSONObject(params, options: nil, error: &err)

  7. Somewhat of a newbie towards the REST API, I got this working successfully with a Flask back end (thanks for that), I’m just wondering what sorts of security are built into or need to be layered on top of your code? Sensitive information such as Username / Password combos, I would guess need to be encrypted somehow?

    Sorry if the answer is obvious, I’m a little new to the issue of security concerns, was just wondering what additional things I should look into if I am implementing this code.

    1. Yeah you would want to send the credentials securely over HTTPS, you could also manually hash the passwords using an algorithm only you know on both the client and server. There’s many considerations when securing something like this, but it’s all a little outside the scope of these tutorials I think. I’d advise reading up on the Objective-C tutorials and porting over the considerations taken there.

  8. Great tutorial. Thanks! Is there a particular reason why you using closure to notify a caller of a success or failure? My first thought was to return a tuple with multiple values…

  9. Very nice !

    Short sweet and to the point !

    3 thumbs up from Sweden.

    1. Oh !

      The function doesn’t use the url parameter though ? 😀

      var request = NSMutableURLRequest(URL: NSURL(string: “http://localhost:4567/login”))

      should be (I think):

      var request = NSMutableURLRequest(URL: NSURL(string: url))

  10. Thanks, Jameson Quave

    It really helped me a lot.
    Now my final code looks like this .

    func createStringFromDictionary(dict: Dictionary) -> String {
    var params = String()
    for (key, value) in dict {
    params += “&” + key + “=” + value
    }
    return params
    }

    func post(params : Dictionary, url : String, postCompleted : (JsonNSDictionary: NSDictionary,JsonString:String) -> ()) {
    var request = NSMutableURLRequest(URL: NSURL(string: url)!)
    var session = NSURLSession.sharedSession()
    request.HTTPMethod = “POST”
    var params2 = createStringFromDictionary(params)
    var paramsLength = “\(countElements(params2))”
    var requestBodyData = (params2 as NSString).dataUsingEncoding(NSUTF8StringEncoding)

    var err: NSError?
    request.HTTPBody = requestBodyData
    request.addValue(paramsLength, forHTTPHeaderField: “Content-Length”)
    request.addValue(“application/json”, forHTTPHeaderField: “Accept”)
    request.addValue(“application/x-www-form-urlencoded”, forHTTPHeaderField: “Content-Type”)

    var task = session.dataTaskWithRequest(request, completionHandler: {data, response, error -> Void in
    println(“Response: \(response)”)
    var strData = NSString(data: data, encoding: NSUTF8StringEncoding)
    println(“Body: \(strData)”)
    var err: NSError?
    var json = NSJSONSerialization.JSONObjectWithData(data, options: .MutableLeaves, error: &err) as? NSDictionary
    postCompleted(JsonNSDictionary: json!,JsonString:strData!)

    })

    task.resume()
    }

  11. Nice tutorial there! One question: How do you know the url is localhost:4567? and not other numbers? Thanks!

    1. It’s just the default for the server. I think it prints that information in the console upon starting sinatra.

  12. Hi everyone !

    Thanks for this tutorial, it helps me a lot !

    However, I have an issue if the JSON is a array like
    “[
    {
    “user”: {
    “name”: “nom”
    },
    “date”: “2015-02-16T14:13:12.434Z”
    }
    ]”

    Do you have some ideas ? Thanks a lot

    1. More details using println:
      Data: <5b7b2275 73657222 3a7b226e 616d6522 …
      strData: Optional([{"user":{"name":"nom"}, …
      json: nil

    2. Try deserializing this at the top level as shown in the tutorial, and then put a breakpoint in your code. This will show you what objects were created in memory on the debug window, so you can see how this JSON maps to Swift/Obj-C objects.

      For this, it will be something like:

      Array -> NSDictionary (user, data)
      “user” -> String
      “data” -> String

  13. I’m trying this in a console application, and it seems the post task is not being executed. It’s like the completionHandler passed into the task creation isn’t getting called. Is it possible for a console application to exit before the post task completes?

  14. Trying to do adapt this to a simple table I want to populate using my node.js api and swift. My table contains a varchar and a boolean. I can’t seem to make it work.

    var params = [“name”:”hurray!”, “works”:true] as Dictionary
    var err: NSError?
    request.HTTPBody = NSJSONSerialization.dataWithJSONObject(params, options: nil, error: &err) //This line gets an error saying Cannot invoke dataWithJSONObject with an argument list of type Dictionary

    It only works if it’s Dictionary

    How do I make this work with non strings?

    1. Try casting to a stricter type. Instead of Dictionary you might should use [String:Any]

  15. Awesome post!!!

  16. Can you help me how can i adjust this code in that way so it could send a data to server when i login , and not when I’m running the app?
    I would be really grateful, i have so much problem to make that work!
    Thank you!

Leave a Reply to Next Riot Cancel reply

Close Menu