I have a question about NSURLSession
I have downloaded JSON data using NSURLSession
I want to access the JSON enter code hereData variable from outside this block of code so I manipulate it
this is my code
// variable declared in my class
var jsonData = JSON("")
// my function
func loadCategories(){
var url = NSURL(string: "http://localhost:8888/api/v1/getAllCategories")
var request = NSMutableURLRequest(URL:url!)
request.HTTPMethod = "GET"
NSURLSession.sharedSession().dataTaskWithRequest(request, completionHandler: { (data: NSData!, response: NSURLResponse!, errors: NSError!) in
self.jsonData = JSON(data: data)
}).resume()
}
When I try to get jsonData outside the block of NSURLSession I get empty variable
Any help ?
Check if you are accessing the jsonData on the same thread. Maybe you have a race condition here.
The problem is that you don't understand how async functions with completion blocks work.
Take a look at my answer to this thread. I explain what's going on in detail:
Why does Microsoft Azure (or Swift in general) fail to update a variable to return after a table query?
(Don't let the title of the thread mislead you. It has nothing to do with Microsoft Azure.)
The method you are using, dataTaskWithRequest, invokes your completion closure on the URL session's delegate queue. Unless you've provided a background queue as the delegate queue you don't need to bother with dispatch_async.
I have found the solution
let priority = DISPATCH_QUEUE_PRIORITY_DEFAULT dispatch_async(dispatch_get_global_queue(priority, 0)) { self.jsonData = JSON(data: data) dispatch_async(dispatch_get_main_queue()) { self.collectionView?reloadData() } }
Related
I'm trying to parse csv file from my ios project(swift 2.3) and found this website. In the tutorial code, it has the following section of code :
if let content = String(contentsOfURL: contentsOfURL,
encoding: encoding, error: error) {
...........
}
And I'm not sure what it does. Does it create a String object?
Does it create a String object?
Yes, it creates a string from the contents of the URL given by contentsOfURL and using the character encoding given by encoding. It's analogous to the following Objective-C code:
NSString *content = [NSString stringWithContentsOfURL:contentsOfURL
encoding:encoding
error:&error];
The if let part is a form of conditional statement. let is used to assign a value to an immutable variable. Using it in a conditional as in your example only allows the body of the conditional statement to execute if that assignment succeeds. So, if some error occurs while the data at the given URL is being fetched or if the string cannot be created for some reason, the condition fails and the body isn't executed. The whole snippet might be written like this in Objective-C:
NSString *content = [NSString stringWithContentsOfURL:contentsOfURL
encoding:encoding
error:&error];
if (content != nil) {
// do something with content
}
That code creates a string, but it does it by fetching the contents of a URL. Usually that URL points to a resource on the Internet. In that case it's a very bad way to fetch a string, since it is a synchronous network call that can hang or fail. It's a very bad idea to do synchronous networking calls on the main thread.
You could wrap that code in a GCD call to a background queue, but instead I'd suggest using NSURLSession and submitting a data task. Your search terms would be NSURLSession (or just URLSession in Swift 3) and the function func dataTask(with url: URL). (It might be easier search on it's Objective-C name, dataTaskWithURL since Google searches don't work very well with special characters.)
Take a look at a GitHub project I created called Async_demo. It has a singleton class called DownloadManager that downloads a blob of data from a specified URL. It's written to return the data as a Data object, but it would be a simple matter to convert that result from Data to a String.
The key bit of code is this:
typealias DataClosure = (Data?, Error?) -> Void
func downloadFileAtURL(_ url: URL, completion: #escaping DataClosure) {
//We create a URLRequest that does not allow caching so you can see the download take place
let request = URLRequest(url: url,
cachePolicy: .reloadIgnoringLocalAndRemoteCacheData,
timeoutInterval: 30.0)
let dataTask = URLSession.shared.dataTask(with: request) {
//------------------------------------------
//This is the completion handler, which runs LATER,
//after downloadFileAtURL has returned.
data, response, error in
//Perform the completion handler on the main thread
DispatchQueue.main.async() {
//Call the copmletion handler that was passed to us
completion(data, error)
}
//------------------------------------------
}
dataTask.resume()
//When we get here the data task will NOT have completed yet!
}
I am trying to update a progress bar as my images load. I've read several answers here and tried formatting my code many different ways. I'm trying to read in the images and update the progress bar. Then, will all the images are loaded call the code to process them. The best result I've got was some code that works most of the time. However, if I'm dealing with a situation where it is pulling in a lot of images, I get weird errors. I think it is going ahead and running the continue code before all the images are fully loaded. When I remove the dispatch_async, the code works fine but the progress bar does not update.
func imageLocXML(il:String) {
dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0)) {
let url:NSURL? = NSURL(string: il)
let data:NSData? = NSData(contentsOfURL: url!)
let image = UIImage(data: data!)
pieceImages.append(image!)
self.numImagesLoaded += 1
self.updateProgressBar()
if self.numImagesLoaded == self.numImagesToLoad {
self.continueLoad()
}
}
}
There a number of issues:
This code isn't thread safe, because you have race condition on numImagesLoaded. This could, theoretically, result in continueLoad to be called more than once. You can achieve thread safety by synchronizing numImagesLoaded by dispatching updates to this (and other model objects) back to the main queue.
Like DashAndRest said, you have to dispatch the UI update to the main queue, as well.
When you made this asynchronous, you introduced a network timeout risk when you initiate a lot of requests. You can solve this by refactoring the code to use operation queues instead of dispatch queues and specify maxConcurrentOperationCount.
The images are being added to an array:
Because these tasks run asynchronously, they're not guaranteed to complete in any particular order, and thus the array won't be in order. You should save the images in a dictionary, in which case the order no longer matters.
Just like numImagesLoaded, the pieceImages isn't thread safe.
You are using a lot of forced unwrapping, so if any requests failed, this would crash.
But to address this, we have to step back and look at the routine calling this method. Let's imagine that you have something like:
var pieceImages = [UIImage()]
func loadAllImages() {
for imageUrl in imageURLs {
imageLocXML(imageUrl)
}
}
func imageLocXML(il:String) {
dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0)) {
let url:NSURL? = NSURL(string: il)
let data:NSData? = NSData(contentsOfURL: url!)
let image = UIImage(data: data!)
self.pieceImages.append(image!)
self.numImagesLoaded += 1
self.updateProgressBar()
if self.numImagesLoaded == self.numImagesToLoad {
self.continueLoad()
}
}
}
I'm suggesting that you replace that with something like:
var pieceImages = [String: UIImage]()
func loadAllImages() {
let queue = NSOperationQueue()
queue.maxConcurrentOperationCount = 4
let completionOperation = NSBlockOperation {
self.continueLoad()
}
for imageURL in imageURLs {
let operation = NSBlockOperation() {
if let url = NSURL(string: imageURL), let data = NSData(contentsOfURL: url), let image = UIImage(data: data) {
NSOperationQueue.mainQueue().addOperationWithBlock {
self.numImagesLoaded += 1
self.pieceImages[imageURL] = image
self.updateProgressBar()
}
}
}
queue.addOperation(operation)
completionOperation.addDependency(operation)
}
NSOperationQueue.mainQueue().addOperation(completionOperation)
}
Having said that, I think there are deeper issues here:
Should you be loading images in advance like this at all? We would generally advise lazy loading of images, only loading them when needed.
If you're going to load images into a structure like this, you should gracefully handle memory pressure, purging it upon low memory warning. You then need to gracefully handle what to do when you go to retrieve an image and it's been purged due to memory pressure (leading you right back to a just-in-time lazy loading pattern).
We'd generally advise against synchronous network requests (the NSData(contentsOfURL:_)). We'd generally use NSURLSession which is cancellable, offers richer error handling, etc. Admittedly, that complicates the above code even further (probably leading me down the road of asynchronous NSOperation subclass), but you should at least be aware of the limitations of contentsOfURL.
Try DISPATCH_QUEUE_PRIORITY_DEFAULT this for background queue as:
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)) {
let url:NSURL? = NSURL(string: il)
let data:NSData? = NSData(contentsOfURL: url!)
let image = UIImage(data: data!)
self.pieceImages.append(image!)
self.numImagesLoaded += 1
dispatch_async(dispatch_get_main_queue(), {
//UI must be updated on main thread queue
self.updateProgressBar()
})
if self.numImagesLoaded == self.numImagesToLoad {
self.continueLoad()
}
}
UI must be updated on MAIN thread queue!
You also have to use self for accessing pieceImages.
If I run the following code and let the app in background, the download is still continuing. Finally, when the download is finished, I can get the right callback.
let configuration = NSURLSessionConfiguration.backgroundSessionConfigurationWithIdentifier(SessionProperties.identifier)
let backgroundSession = NSURLSession(configuration: configuration, delegate: self.delegate, delegateQueue: nil)
let url = NSURLRequest(URL: NSURL(string: data[1])!)
let downloadTask = backgroundSession.downloadTaskWithRequest(url)
downloadTask.resume()
But I have a requirement, that is I have to judge what the server returns to me, if it is a json, I don't do the download, so I want to get the response header first, then if it needs to download, I change the data task to download task, so I did as the following code
let configuration = NSURLSessionConfiguration.backgroundSessionConfigurationWithIdentifier(SessionProperties.identifier)
let backgroundSession = NSURLSession(configuration: configuration, delegate: self.delegate, delegateQueue: nil)
let url = NSURLRequest(URL: NSURL(string: data[1])!)
//I change the downloadTaskWithRequest to dataTaskWithRequest
let downloadTask = backgroundSession.dataTaskWithRequest(url)
downloadTask.resume()
Then I can get the response header in the callback, and if it needs to download file, I can change the data task to download task, as following
func URLSession(session: NSURLSession, dataTask: NSURLSessionDataTask, didReceiveResponse response: NSURLResponse, completionHandler: (NSURLSessionResponseDisposition) -> Void) {
if let response = response as? NSHTTPURLResponse {
let contentType = response.allHeaderFields["Content-Type"] as! String
if contentType == "image/jpeg" {
//change the data task to download task
completionHandler(.BecomeDownload)
return
}
}
completionHandler(.Allow)
}
So far so good. When I run the app in the foreground, the effect is like what I thought. But after the app runs in background, the download is stoped, then when I open the app, the console says "Lost connection to background transfer service".
I thought Apple is so smart, he gives us many useful callbacks, but now, I didn't know where I am wrong, and I also see the source code about the AFNetworking and Alamofire, but I didn't find the referring thing.
I also think it is a common requirement, but I can't find any helpful information on the internet, it is too odd.
So hope you can help me out, thanks a billion.
Enable Background Mode in
Xcode->Target->Capabilities->On Background Mode and select the option Background Fetch.
The main issue I see is that you're calling the completionHandler twice. You need to return out of your content-type conditional like so:
if contentType == "image/jpeg" {
//change the data task to download task
completionHandler(.BecomeDownload)
return
}
Otherwise it appears that you are using the logic correctly. Hope that helps.
The problem is evident from your own answer. It's not a bug, you simply couldn't use data tasks for background transfers just download tasks.
Here is the correct full answer.
In one of my apps I need to geocode address string. At first I considered using CLGeocoder. However, after I tried it I stumbled upon a problem which I described in this question.
The solution was to use Google's Geocoding APIs instead. I have now switched to them and managed to get them working by having the following functions:
func startConnection(){
self.data = NSMutableData()
let urlString = "https://maps.googleapis.com/maps/api/geocode/json?address=\(searchBar.text!)&key=MYKEY"
let linkUrl:NSURL = NSURL(string:urlString.stringByAddingPercentEncodingWithAllowedCharacters(NSCharacterSet.URLQueryAllowedCharacterSet())!)!
let request: NSURLRequest = NSURLRequest(URL: linkUrl)
let connection: NSURLConnection = NSURLConnection(request: request, delegate: self, startImmediately: false)!
connection.start()
}
func connection(connection: NSURLConnection!, didReceiveData data: NSData!){
self.data.appendData(data)
}
func connectionDidFinishLoading(connection: NSURLConnection!) {
do {
if let json = try NSJSONSerialization.JSONObjectWithData(data, options: NSJSONReadingOptions.MutableContainers) as? [String: AnyObject] {
print(json)
}
}
catch {
print("error1")
}
}
This works great and resolves the problem which I had with CLGeocoder. However, in addition to extracting the coordinates of place, I need to also use Google's Timezone APIs to extract the timezone for each place.
Doing this with the NSURLConnection or NSURLSession seems to me a bit difficult as I would need to keep track of which session/connection returns. So, I would like to have some solution which uses completion handlers.
I have tried using Alamofire framework (using the correct branch for Swift 2.0). However, it seems like request() function is the wrong one to use in this case. I have tried:
let parameters = ["address":searchBar.text!,"key":"MYKEY"]
Alamofire.request(.GET, "https://maps.googleapis.com/maps/api/geocode/json", parameters: parameters).responseJSON(options:.AllowFragments) { _, _, JSON in
print(JSON)
}
And all I am getting printed is "SUCCESS". I hope that I am doing something wrong and it can be fixed because I would really like to be able to use closures instead of delegate calls.
My questions are:
Is it possible to use Alamofire with Google Geocoding APIs?
If so, can you please tell me what am I doing wrong?
If it is not possible, can you please suggest me how to design a system with NSURSessions or NSURLConnections which would allow me to use completion handlers for each call instead of delegates?
P.S. I am aware that I can use synchronous requests but I would really like to avoid using that option
Update
It was suggested that adding .MutableContainers as an option should make responseJSON work. I tried the code below:
let apiKey = "MYKEY"
var parameters = ["key":apiKey,"components":"locality:\(searchBar.text!)"]
Alamofire.request(.GET, "https://maps.googleapis.com/maps/api/geocode/json", parameters: parameters).responseJSON(options:.MutableContainers) { one, two, JSON in
print(JSON)
}
And all I get printed is "SUCCESS".
Ok, I have finally figured this out (with the help from #cnoon). The value which is returned is of type Result. I couldn't find documentation for it, but the source code is available here.
In order to retrieve JSON below implementation can be used:
Alamofire.request(.GET, "https://mapss.googleapis.com/maps/api/geocode/json", parameters: parameters).responseJSON(options:.MutableContainers) { _, _, JSON in
switch JSON {
case .Failure(_, let error):
self.error = error
break
case .Success(let value):
print(value)
break
}
}
The value printed is the correct representation of response from Geocoding APIs.
I have a Login View Controller, and an Other View Controller. What I'd like to do is: when the user hits login, it sends their credentials to the remote server. The remote server returns a response indicating whether the credentials were good or not, and if they were good, the app redirects to the Other View Controller.
The code below crashes at the call to .performSegueWithIdentifier.
The crash gives an error code of EXC_BAD_ACCESS(code=1, address=0xbbadbeef)
Question: what is the swifty way of doing this?
var request = NSMutableURLRequest(URL: NSURL(string: "http://url.to/my/login/handler")!)
var session = NSURLSession.sharedSession()
request.HTTPMethod = "POST"
//user initialized earlier
bodyData = "email=\(user.username)&password=\(user.password)"
request.HTTPBody = bodyData.dataUsingEncoding(NSUTF8StringEncoding);
var task = session.dataTaskWithRequest(request, completionHandler: {data, response, error -> Void in
// check that log in was successful by looking in 'response' arg
// if login was successful
self.performSegueWithIdentifier("SegueToOtherController", sender: self)
}
task.resume()
}
If it's crashing, you should share the details the crash in order to identify why. Likely problems include that it didn't find a segue of that identifier as the "storyboard id" from the current view controller to the next scene. But it's impossible to say without details on the precise error.
Having said that, there is another problem here: The completion block may not run on the main thread, but all UI updates must happen on the main thread. So make sure to dispatch that back to the main queue, e.g.
let request = NSMutableURLRequest(URL: NSURL(string: "http://url.to/my/login/handler")!)
let session = NSURLSession.sharedSession()
request.HTTPMethod = "POST"
//user initialized earlier
bodyData = "email=\(user.username)&password=\(user.password)"
request.HTTPBody = bodyData.dataUsingEncoding(NSUTF8StringEncoding);
let task = session.dataTaskWithRequest(request) {data, response, error in
// check that log in was successful by looking in 'response' arg
// if login was successful
dispatch_async(dispatch_get_main_queue()) {
self.performSegueWithIdentifier("SegueToOtherController", sender: self)
}
}
task.resume()
Note, I also changed all of those var references to let (as a general rule, use let wherever possible). Also, I haven't tackled it here, but you really should be percent escaping the username and password properties. If, for example, the password included any reserved characters like + or &, this would fail. There are lots of ways of doing that, e.g. something like the method discussed here: https://stackoverflow.com/a/26317562/1271826 or https://stackoverflow.com/a/25154803/1271826.