Networking with URLSession - Part 5: URLSession | Ray Wenderlich

Learn about URLRequest and HTTP headers, then create a URLSessionDataTask to POST to a REST API. Also build an Alamofire-inspired PostRouter to create URLRequests.


This is a companion discussion topic for the original entry at https://www.raywenderlich.com/3986-networking-with-urlsession/lessons/5

Hi @audrey I am really enjoying this course. In the 05_Challenge_Starter.playground, you refer to Sources/PostRouter.swift. Apparently I am missing something. I cannot find that folder or file in the provided course materials. Please help. Thank you.

hi Michael: open the project navigator (command-0), then the 05_Challenge_Starter folder:

you had me worried :slight_smile:

Hi @audrey. Thank you so much for your quick reply. I am familiar with Xcode and Swift, but not very familiar with playgrounds. This has been a great learning experience for that as well. For me anyway, the provided Playgrounds open with the Navigator closed. When I opened the Navigator, there was the Sources folder, just as you said! … Thanks again, This is a great course and you are a great teacher!

Is this the correct way to post questions to you or do I not have sufficient rights yet to do so?

Questions are always welcome!

1 Like

Thank you for your prompt response and thank you for your excellent video tutorials on Networking with URLSession. The best exposition I have seen on the topic for someone at my level of iOS knowledge. This is my first attempt at reaching out for assistance so if I am doing anything wrong feel free to point it out to me. While I have programming experience in the .NET world, iOS is totally new to me. Again, thank you for any help

The Application

An iPad data entry form that saves the form data to Core data and then when a timer is triggered the code checks if we have Wi-Fi, checks if the WEBAPI URL will resolve, gets a record from Core data, constructs a JSON string from it and POSTs using URLSession dataTask.
Depending on the status code returned in the completion handler “response” = 201 or 500 – the code deletes from CORE data the record used to create the JSON string (for 201), checks for next record and repeats process until record count equals 0, or aborts processing (for 500) until next timer event.

The Problem

All of this worked correctly – I think - until the recent updates to
Swift 3.1
iOS 10.3.1
Xcode 8.3.2.
The build setting is iOS 10.3
Now, when the first record of the series (from 1 to X) is POSTed to the Web API, I do not appear to receive a response HTTP Status Code. My code loops to the second record to be POSTed, and I DO receive a response HTTP Status Code for this one, and those that follow.
Now whatever I do the first record retrieved from Core data gets inserted twice into the SQL Server database that the WEBAPI is talking to. When I debug the code, on first iteration, when the code gets to
URLSesion.dataTask
it jumps directly to
.resume()
And the value for “response” is not set so I cannot implement the logic of what to do with the CORE data record – delete or abort. On the next iteration, “response” has a value and I can implement the logic. I believe that it is for this reason that the first record is inserted twice into the SQL database. Everything else works correctly except for the doubling of first record.

Not the Problem

Both the database and the WEBAPI is in house and we have complete control over them and they have not been changed.

Limitations

While we are proficient in WEBAPI, .NET, C#, SQL Server development, this is our first IOS application.

The Code

//This function gets called by the timer event
func getData(){
let request = NSFetchRequest(entityName: “WCVisitors”)
do{
let results = try managerContext.fetch(request)
let visitors = results as! [NSManagedObject]
print("Visitors count: " + String(visitors.count))
if visitors.count == 0{
return
}
//fetch all
for visitor in visitors {
SourcePostalCode = visitor.value(forKey: “sourcePostalCode”) as! String
SourceCountry = visitor.value(forKey: “sourceCountry”) as! String
SourceState = visitor.value(forKey: “sourceState”) as! String
DestinationState = visitor.value(forKey: “destinationState”) as! String
VisitorPartyCount = visitor.value(forKey: “visitorPartyCount”) as! String
NewsletterYesNo = visitor.value(forKey: “newsletterYesNo”) as! String
FirstName = visitor.value(forKey: “firstName”) as! String
LastName = visitor.value(forKey: “lastName”) as! String
EmailAddress = visitor.value(forKey: “emailAddress”) as! String
Comments = visitor.value(forKey: “comments”) as! String
EnteredTimeStamp = visitor.value(forKey: “enteredTimeStamp”) as! String
IpadName = visitor.value(forKey: “ipadName”) as! String
Model = visitor.value(forKey: “model”) as! String
iOSVersion = visitor.value(forKey: “iOSVersion”) as! String
//if no Wi-Fi, no delete and return
let didPost = tryToPOST()
if didPost == “false” {
return
}
//We call the function to POST the data stored in the variables
postTry()
//whatCode is a class level variable that is to be set by the completion handler, and interrogated for “what next”
print("What code: " + whatCode)
//if whatCode returns “false” then we got return code 500 and we abort. Had to add empty string because that is what returns every time at first entry into the URLSession dataTask. This I think is the problem but how to solve it?
if whatCode == “” || whatCode == “false”{
return
}
//returned 201 so we can delete the record and loop for next record, if any
if whatCode == “true”{
managerContext.delete(visitor)
do{
try managerContext.save()
}catch let error as NSError{
print(“Could not save delete (error)”)
}
}
}
}catch{
print(“Fetch error: (error)”)
}
//return
}//End function called by the timer event

//function called from getData
func postTry() {
//I also tried ephemeral
let session = URLSession(configuration: .default)
let url = URL(string: “https://dx.nccommerce.com/xxxxxxx/xxx/XXX”)

   let postString = "SourcePostalCode=" + SourcePostalCode + "&SourceCountry=" + SourceCountry + "&SourceState=" + SourceState + "&DestinationState=" + DestinationState + "&VisitorPartyCount=" + VisitorPartyCount + "&FirstName=" + FirstName + "&LastName=" + LastName + "&EmailAddress=" + EmailAddress + "&NewsletterYesNo=" + NewsletterYesNo + "&Comments=" + Comments + "&iPadName=" + IpadName + "&Model=" + Model + "&iOSVersion=" + iOSVersion + "&EnteredTimeStamp=" + EnteredTimeStamp
   
   var request = URLRequest(url: url!)
   request.httpMethod = "POST"
   request.httpBody = postString.data(using: String.Encoding.utf8)
   //on first iteration the code will jump from the below line to postTask.resume() and response is empty string
  //on second entry everything works fine. Yes, I know that the task starts in a suspended state but I don’t know what to do with that    
 //information to solve my problem.
   let postTask = session.dataTask(with: request){data, response, error in   
       guard let response = response as? HTTPURLResponse else {
           print("No status code created")
           return
       }
       if (response.statusCode == 201){
               self.whatCode = "true"
               print("Status code: 201 - true")
           }else if (response.statusCode == 500){
               self.whatCode = "false"
               print("Status code: 500 - false")
           }
   }
   postTask.resume()

}
//End function postTry

Do your server logs show the first request being received and a response sent?

I can’t think of any reason, but people have been reporting other bugs in Xcode 8.3, so it’s possible you’ve found one.

I’ll take a closer look later today.

btw “it jumps directly to .resume()” — this is normal, but also put a breakpoint at the guard statement in the completion handler: when it breaks at the dataTask line, just continue execution, and it should stop at the guard line.

also, if it does seem to be an Xcode bug, you could verify that your code works in 8.2, and include this in your bug report. Xcode 8.2 is available on the Apple Developer additional downloads page

and one more: later in this course, probably in Background Sessions, I give some general advice that URLSession works better with large data transfers. In particular, if some of your POST tasks will be happening while the app is suspended, the system will create new tasks as discretionary, and with longer timeouts between wake-ups.

1 Like

Thank you for your prompt response. As for your first question, I do see the record being created in the database table but I will have to talk to the WEBAPI developer if he can see anything in the server logs.

Will put the breakpoint at the dataTask and guard statement and see what happens.

OK, I am admin of the Enterprise Developer license so I know I can download Xcode 8.2. I will just have to learn how to do a dual Xcode deployment and test the code.

In reference to “URLSession works better with large data transfers” the transfer will never happen when the app is in suspended mode because these iPads are completely controlled buy us for internal use only. Also, If not URLSession, what should I use in this scenario?

Thank you again for reaching out and helping me. I am a one man shop in all thing Apple in my shop and it can be overwhelming.

I usually just rename the older Xcode Xcode82

1 Like

I downloaded Xcode 8.2, Xcode 8.2.1 and did a parallel install next to Xcode 8.3.1 and I am getting the same results.

I did get at the IIS logs and the database table and they both confirm insertion in the database, and a POST and a 201 in IIS and though I saved to Core data 2 records, both IIS and the database table show 3 records as a result of executing URLSession dataTask.

In the database table, you can see that the date/time stamp for insertion into Core data is the same (2017-04-24 07:02:43.000) for record 86744 and 86743 indicating that the same record from Core data has been inserted twice into the SQL database.

The problem is with my code because I cannot decide if the record should be deleted from Core data on first loop into URLSession dataTask because the variable “whatCode” is not set on first loop through the code, but is on next entry into the code.

Do I need to initialize URLSession earlier? Or is my code just rubbish? I can fix this problem at the database end with a stored procedure but that feels like a workaround.

Please help if you can.

The IIS Log

Fields: date time s-ip cs-method cs-uri-stem cs-uri-query s-port cs-username c-ip cs(User-Agent) cs(Referer) sc-status sc-substatus sc-win32-status time-taken
2017-04-24 11:34:26 149.168.36.20 POST /WCGBWebAPI/api/WCGB - 443 - 10.236.6.135 NCWelcomeCenters/1+CFNetwork/811.4.18+Darwin/16.5.0 - 201 0 0 765
2017-04-24 11:35:26 149.168.36.20 POST /WCGBWebAPI/api/WCGB - 443 - 10.236.6.135 NCWelcomeCenters/1+CFNetwork/811.4.18+Darwin/16.5.0 - 201 0 0 15
2017-04-24 11:35:26 149.168.36.20 POST /WCGBWebAPI/api/WCGB - 443 - 10.236.6.135 NCWelcomeCenters/1+CFNetwork/811.4.18+Darwin/16.5.0 - 201 0 0 15

The database table Records

86745 2 United States North Carolina NULL North Carolina NULL NULL 1 N/A N/A N/A 0 N/A Louis’s MacBook Pro iPad 10.3 2017-04-24 07:02:50.000 2017-04-24 07:35:26.067 86745
86744 1 United States North Carolina NULL North Carolina NULL NULL 1 N/A N/A N/A 0 N/A Louis’s MacBook Pro iPad 10.3 2017-04-24 07:02:43.000 2017-04-24 07:35:26.063 86744
86743 1 United States North Carolina NULL North Carolina NULL NULL 1 N/A N/A N/A 0 N/A Louis’s MacBook Pro iPad 10.3 2017-04-24 07:02:43.000 2017-04-24 07:34:26.767 86743

what happens when you add the breakpoint inside your data task completion handler and step through? do you get a response for the first POST?

maybe there’s a race condition somewhere? at the end of the demo in part 11 of the concurrency video course, I show how to turn on Xcode’s Thread Sanitizer: see if that finds anything?

1 Like

No response value for the first POST, but after that, yes.

I enabled the Thread Sanitizer and it did not report any issues.

You have been very helpful but I am reticent to burden you with my problems any further.

As things stand, we will implement a workaround at the backend and get started with our test deployment as we are begging to run late with delivering the solution.

Once the problem gets resolved I will push an update to our production iPads which will be a good learning experience.

We have 2 help tickets with Apple as part of our Enterprise License so maybe I will use one of them.

In your estimate, was my presentation of the problem adequate or how should I go about presenting it to enhance the possibility of a solution.

Thank you again.

sorry, I didn’t see your latest post until now

by any chance, is the response to the second POST, actually the (delayed) response to the first POST? and so on, for all subsequent POSTs?

please re-post if Apple finds the problem?

your presentation was great! you explained the problem and what you tried, very clearly.

Hi Audrey,
I downloaded the Materials.
You say open Sources/PostRouter.swift and follow the TODO instructions
But there have no any project in the materials.
where can I find the project?
thanks!

open the project navigator (command-1) and expand the playground: there’s a Sources folder.

1 Like

Hi Audrey, thank you for this tutorial, this maybe a little off topic, when I check the API of some website, they are presented as curl format, such as Github, I already tried GET with Github API, works great.

But while some other website, for POST method, the curl has some options, like this:

curl https://some_website-api \
 --user api:YOUR_API_KEY \
 --data-binary @some_file \
 --dump-header /dev/stdout

This can be easily done in Terminal, but I’m confused how to pass these curl option parameters while doing the POST task with URLSession.

I would really appreciate if you can give me some cue on how to do this.

possibly/probably as custom HTTP headers?

1 Like

Hi
I have a question in the PostRouter.
Why not move the computed variable ‘method’ inside the ‘asUrlRequest()’ method?
Or in the contrary,
Why not move relative path and parameters variables from inside ‘asUrlRequest()’ method and define them as computed variables?

Thanks,

hi Abdullah: often, there are several ways to do the same thing in Swift. I set up PostRouter this way to match GrokSwift’s code. She blogs a lot about AlamoFire, and I figured many readers would be familiar with the way she sets up her routers.