I've been searching around to learn how to get some data using NSURLSession in Swift.
There is a general pattern being repeated on the internet for making an http request using NSURLSession, and it looks a lot like this:
let url = NSURL(string: "myPretendURLthatIsNotThisURL.com")
let task = NSURLSession.sharedSession().dataTaskWithURL(url) {(data, response, error) in println(NSString(data: data, encoding: NSUTF8StringEncoding))}
task.resume()
If I place a real URL in, I can see a lot of JSON output printed, which is the expected response from the server. Nice : ) I can see the data from println(). However, I would rather display the returned data in a text view when I press a button. I do believe my solution relies on using a delegate (somehow).
Is it all right to request an example of how this is done, in the following context:
Code is in Swift
Press a button (IBAction, contains the code above to make the request)
Display the data returned (it's JSON, I'll worry how to parse it later on) in a text view.
Thanks!
Related
I'm a little ashamed to post this as I use URLs all the time but this one has me stumped!
I keep getting an 'Unexpectedly found nil while unwrapping an Optional value'
Just to add some context, I use the same format for over 20 URLs which is making requests to an API. The only difference is the ID at the end of the URL.
Ignore the insecure URL as this is simply a debug endpoint and has no bearing on the issue as I use the same URL in other calls. Also, ignore the first part of the URL, I have purposefully changed it for security purposes.
The code I use is as follows:
func returnEventParticipantsEndPoint(eventID: String) -> URLRequest {
print("URL: \(debugEndPointURL)/api/friendgroups/\(eventID)")
var url: URL!
url = URL(string: "\(debugEndPointURL)/api/friendgroups/\(eventID)")!
let requestURL = URLRequest(url: url)
return requestURL
}
The URL printed in the console (top line) before I try to return the URLRequest prints as follows:
http://blahblahblah.blah.net/api/friendgroups/1c8264b95884409f90b4fd8ba9d830e1
Yet, it keeps returning nil and I'd really appreciate some help with this...thanks.
The reason that it is failing is that there are hidden Unicode characters in your string. Copying and pasting the url string you provided into a hidden Unicode character checker gives the following for your url
http://blahblahblah.blah.net/apiU+200B/friendgroupsU+200B/1c8264b95884409f90b4fd8ba9d830e1
Notice the additional hidden characters at the end of the words api and friendgroups
You can try it here https://www.soscisurvey.de/tools/view-chars.php
Have you copied and pasted the format for this? Examining the string you have some non printable Unicode characters after the words api and friendgroups. Retyping the url path and trying again worked for me
I'm developing an upload project in swift. I'm taking very large files (video, picture with size over 500 MB) with imagepickercontroller and dividing this file into chunks which has a size 1 MB. Then I send these chunks to remote server and make them defragment in server and I'm showing this file to user.
I have no problem if the file size is under 300 MB. But after this size, memory goes up too much and app is being crashed. Actually, in every case memory usage are raising but there is no crash.
When I watch progress on console, I see URLSession task begins. But, because of these tasks are waiting response from completion handler, the task queue is growing and memory usage goes up. Is there a way when a task begins, this task's completion handler begins too? I think if I can make task queue free concurrently, my problem solves. I'm waiting your helps.
let url:URL = URL(string: "\(addressPrefix)UploadFile")!
let session = URLSession.shared
let request = NSMutableURLRequest(url: url)
request.cachePolicy = NSURLRequest.CachePolicy.reloadIgnoringCacheData
request.httpMethod = "POST"
let bodyData = "\(metaDataID)~\(chunkIndex)~\(chunkSize)~\(chunkHash)~\(wholeTicket)~\(fileDataString)"
request.httpBody = bodyData.data(using: String.Encoding(rawValue: String.Encoding.utf8.rawValue));
request.timeoutInterval = .infinity
let task = session.dataTask(with: request as URLRequest, completionHandler: {(data, response, error) in
guard let _:Data = data, let _:URLResponse = response, error == nil else {
var attemptCounter = 1
if attemptCounter <= 3 {
completion("\(attemptCounter).attempt",chunkSize, error)
attemptCounter += 1
}
return
}
let jsonStr = String(data: data!, encoding: String.Encoding(rawValue: String.Encoding.utf8.rawValue))
completion(jsonStr, chunkSize, error)
SingletonConnectionManager.sharedConnectionDataManager.dataTasks["uploadFile"] = nil
})
SingletonConnectionManager.sharedConnectionDataManager.dataTasks["uploadFile"] = task
task.resume()
---I call this URLSession task from this function in a tableview controller
tmpConnection.uploadFile(chunk, metaDataID!, chunkIndex: chunkIndex, completion: {(result, chunkSize, error) in
// I want to enter immediately when 'uploadFile' get called })
The requests aren't really waiting until all of them have been sent. When things are working correctly, each callback happens when the associated request finishes, and it wouldn't make sense for that to happen sooner, because the callback provides the response from the server (which you can't possibly get back until after the request has been fully sent out).
The problem here is that you are completely clogging up the session by starting entirely too many tasks at the same time. There's a known bug in NSURLSession that causes it to start to fall apart when you create a large number of tasks in a single session all at once. When you get too many tasks in a session, IIRC, the session stops calling callbacks entirely, and basically the session becomes unusable. (There's another Stack Overflow question in which this was discussed a couple of years ago, though I can't seem to find it right now.)
And because the tasks never complete, your app ends up leaking all the memory that you're using for the body data, which means your app just allocates more and more memory until it gets evicted.
The only way to fix this problem is to stop adding all of the requests to the session all at once. Start a few tasks at first (for at most eight parts or so), and then wait to send the next part until one of the previous parts finishes or fails. This approach will not only prevent you from bricking the NSURLSession, but also will prevent you from allocating an insane amount of memory to hold all of the request body NSData objects, which are currently all sitting in RAM at once.
I suggest keeping an NSMutableArray of NSNumber object representing each unsent chunk. That way, you know what is still left to send, and you can just loop to 8 and pull off the first 8 numbers, and send the chunks with those numbers. When a request completes successfully, grab the next number out of the array and send the chunk with that number.
Also, you shouldn't stop after a particular number of retries. Instead, when a request fails, check the failure to decide whether to retry (network failure) or give up (server error). Then use reachability to wait until a good time to try again, and try again when it says that the destination host is reachable. Cancel the upload only if the user explicitly asks you to cancel the upload by hitting a cancel button or similar. If the user asks you to cancel the upload, tear down your data structure so you don't start any new requests, then invalidate the URL session.
I copy-pasted the first example of the Alamofire readme (at fa3c6d0) into main.swift:
import Foundation
import Alamofire
Alamofire.request("https://httpbin.org/get").responseJSON { response in
print(response.request) // original URL request
print(response.response) // HTTP URL response
print(response.data) // server data
print(response.result) // result of response serialization
if let JSON = response.result.value {
print("JSON: \(JSON)")
}
}
print("Done")
When I run this, all I get is Done, then the application terminates.
While I see here that I can pick a dispatch queue, this answer seems to suggest that I shouldn't have to.
Anyway, having had a similar issue with "basic" requests I tried the same solution but to no avail: the application now blocks. So, apparently, Alamofire has a different default than URLSession and wants to use the main thread.
What is the best way to have a request executed (and waited for) in an application like this?
We need to do two things.
Execute the request in the background.
Block the main thread until the request is done, i.e. the completion handler ran.
My original code does neither.
The first item is achieved by using .response(queue: DispatchQueue(label: "some-name")) (or one of its variants).
Waiting can be done in several ways.
Using a flag and active waiting, as shown here (won't scale to more than one request).
Use active waiting with countdown latch as shown here (works for multiple requests).
Use DispatchSemaphore as seen e.g. here.
And probably many more.
I'm trying to craft a very specific HTTP request to a server (ie. defining the exact set of HTTP headers), but NSURLSession keeps "helpfully" inserting a bunch of HTTP headers like Accept, Accept-Language and Accept-Encoding.
Consider the following playground (Swift 2.x) which sends a request to a service that just echos the HTTP headers that were sent:
import Foundation
import XCPlayground
XCPlaygroundPage.currentPage.needsIndefiniteExecution = true
let url = NSURL(string: "http://httpbin.org/headers")!
let request = NSMutableURLRequest(URL: url, cachePolicy: .ReloadIgnoringLocalCacheData, timeoutInterval: 30000)
let configuration = NSURLSessionConfiguration.ephemeralSessionConfiguration()
let session = NSURLSession(configuration: configuration)
let task = session.dataTaskWithRequest(request) { (data: NSData?, response: NSURLResponse?, error: NSError?) in
print(NSString(data: data!, encoding: NSUTF8StringEncoding))
XCPlaygroundPage.currentPage.finishExecution()
}
task.resume()
You can see that there are three Accept headers being sent. How can I prevent that?
I've tried setting the header using request.setValue(nil, forHTTPHeaderField: "Accept-Language") but that gets ignored. Tried setting it to "", but no good. I've also tried manipulating the HTTPAdditionalHeaders property on NSURLSessionConfiguration, but no love.
How do I get NSURLSession to not be quite so helpful?
I doubt what you're asking for is possible. NSURLSession (and NSURLConnection) automatically provide a number of headers, and that's one of them.
There's also no valid reason to remove them. All three of those headers have been part of the spec since the original HTTP/0.9 spec (https://www.w3.org/Protocols/HTTP/HTRQ_Headers.html). There's absolutely no excuse for any server not either handling those correctly or ignoring them outright.
With that said, if you provide the wrong value for those fields (and the default may be wrong), the server may refuse to give you results. To solve that problem, first figure out what type of data the server is actually going to provide, and specify that value in the Accept header instead of the default.
For example, you might set Accept to "application/json" or one of the other variants (What is the correct JSON content type?) if you're expecting JSON data.
That said, if you really must avoid having those headers sent, you can always open a socket, construct the request manually, and send it. Assuming the server doesn't require chunked encoding, it is pretty easy to do. (Chunked encoding, however, is a horror of Herculean proportions, so if your server sends that back, you'll pretty much have to resort to adding libcurl into your project and using that to make requests instead.)
I have looked at questions like this or that one but it is still not working.
Also I have a question about what should I enter into fileURL param from function multipartFormData.appendBodyPart?
Should it be a way to image from PC, or image must be added to Images.xcassets? What should I send here?
It looks like you have three issues that you need to fix.
Use .POST instead of POST.
The fileURL needs to be a valid NSURL that points to a file on the file system. You cannot just use the filename.
You are using the responseString serializer, but named the third parameter in the closure JSON. Then you are letting result into s and trying to print it out. The result parameter doesn't even exist anywhere. Instead, you should print(JSON).
Hopefully that helps clear things up a bit.
Try to use .POST not POST
As an alternative solution upload an encoded file and send it as a parameter of POST.
// `data` is NSData
let base64String = data!.base64EncodedStringWithOptions(NSDataBase64EncodingOptions.allZeros)
let parameters = ["image_data": base64String] as [String: AnyObject]
Alamofire.request(.POST, "http://your-url.com", parameters: parameters)
The cons of this method is that the data will get %33 larger due to encoding. If you have bandwidth problems it may not be a good solution.