iOS takes long time to update view after HTTP request? - ios

I am very new to learning iOS and Swift, so there may be something very basic here that I don't understand. I am using the agent library to send HTTP requests in Swift. Right now I'm just getting the hang of creating the request and parsing the JSON response.
I have a very simple UI with a button and a UILabel that I want to update with the results of some JSON.
Here is my code:
func updateWithJson() -> Void {
let req = Agent.get("http://xkcd.com/info.0.json")
req.end({ (response: NSHTTPURLResponse!, data: Agent.Data!, error: NSError!) -> Void in
let json = data! as Dictionary<NSString, NSString>
// this takes almost 30 seconds to propagate to the UI...
self.updateMe.text = json["safe_title"]!
})
}
The part I don't understand is the self.updateMe.text = json["safe_title"]! statement which is just updating some text on a UILabel called updateMe. It takes almost a full 30 seconds for this to reflect in the UI.
I know that the HTTP request itself is very fast - if I curl that URL it comes back instantaneously. It also seems like the Agent object is making the connection and returning the response pretty fast.
Am I missing something very basic? Should I be updating the UI in a different way? All pointers are appreciated!
UPDATE
Through another SO post it's my understanding that the block inside of req.end is a background thread and I need to do UI updates on the main thread. I updated my function to look like this and I get the desired result. However, now I'm wondering if there's something incorrect about doing it this way?
func updateWithJson() -> Void {
let req = Agent.get("http://xkcd.com/info.0.json")
req.end({ (response: NSHTTPURLResponse!, data: Agent.Data!, error: NSError!) -> Void in
let json = data! as Dictionary<NSString, NSString>
var text = json["safe_title"]!
dispatch_sync(dispatch_get_main_queue()) {
self.updateMe.text = text
}
})
}

What's happening is that your HTTP request is running on a background thread. When it calls the callback you provide, you're still on that background thread. In iOS all UI work must be done on the main thread. The easiest way to do this is by using GCD's dispatch_async like so:
dispatch_async(dispatch_get_main_queue()) {
[weak self] in
self?.updateMe.text = json["safe_title"]!
return
}
So your entire function would look like:
func updateWithJson() -> Void {
let req = Agent.get("http://xkcd.com/info.0.json")
req.end({ (response: NSHTTPURLResponse!, data: Agent.Data!, error: NSError!) -> Void in
let json = data! as Dictionary<NSString, NSString>
dispatch_async(dispatch_get_main_queue()) {
[weak self] in
self?.updateMe.text = json["safe_title"]!
return
}
})
}
The bit about [weak self] in is so you don't create a strong reference cycle. The return at the end is there because you have a closure with only one expression in it. In that case, Swift will try to implicitly return a value based on that statement and in this case that's not what you want.

Related

Completion handler? async_dispatch?

These are the methods and processes which need to happen:
Download images from Parse
Download associated data (which matches images)
Plot this data on a map
Here is my code from view did load:
override func viewDidLoad() {
imageDownload { () -> () in
print("5-----inside closure")
self.queryParseandSave(callback: self.plotImages)
}
}
Image download function:
func imageDownload(completed: #escaping FinishedDownload){
print("1-----Started Image download")
// Query for places
let query = PFQuery(className:"ViewFinderObjects")
query.whereKey("ImageVerified", equalTo: true)
query.whereKey("coordinates", nearGeoPoint:myGeoPoint)
query.limit = 10
query.findObjectsInBackground { (objects, error) -> Void in
if (error == nil) {
for object in objects! {
print("2-----inside object for block")
let imageFromParse = object["image"] as! PFFile
imageFromParse.getDataInBackground(block: {(imageData, error) -> Void in
print("Searching for Image")
if error == nil {
let obsImage:UIImage = UIImage(data: imageData!)!
self.imageToShow = obsImage
self.closestImage.append(self.imageToShow!)
print("There are \(self.closestImage.count) images in the image array")
}
})
print("3-----Completed object loop")
}
}
print("4-----Calling completed statement")
completed()
}
}
Which then calls another function queryParseandSave(callback: self.plotImages)
with the self.plotImages plotting the images on a map.
I have 1 huge issue:
self.plotImahes is always called before the images have finished downloading
I have researched async_dispatch but have no idea if this is the right thing to do.
I'm not familiar with the implementations of the query.findObjectsInBackground and imageFromParse.getDataInBackground methods, but their naming implies that they both happen asynchronously. Also judging from what you're provided above, the former retrieves the object data, while the latter does the actual image data download. If that is indeed the case, then it looks like you're calling your completion handler inside the body of the first asynchronous method instead of waiting for the second method (what appears to be the actual image download).
A couple of ideas for how to resolve this:
You could move your completion handler into the imageFromParse.getDataInBackground block, but this would only make sense if you're comfortable calling the completion block multiple times, after each image finishes downloading.
You could create your own dispatch or operation queue and wait until all tasks complete, then call the completion handler.
You could set up an observer or notification pattern that will call your completion handler at the appropriate time.
There are many different ways to address the issue, but the key thing to remember is that it sounds like you want to call your completion handler after all of the asynchronous operations have completed. Right now you're calling it after you've retrieved the objects, but all of your images are still downloading in the background when your completion handler is called.

how to execute some code after execution of another class method code?

I have a common web services class. in that class I have written method getcitydetail(). I want to populate that city detail after getting the result.
I have written following code in viewDidload:
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
let objWebService = NTMWebServices()
objWebService.getCityDetail()
}
After execution of getcitydetail, I want to do some operation here.
I think we can do it using closure in swift. but I didn't get an idea how to use it.
To use a closure in this situation try something like
func getCityDetail (completion:()->()){
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0)) {
//background thread, do ws related stuff, the ws should block until finished here
dispatch_async(dispatch_get_main_queue()) {
completion() //main thread, handle completion
}
}
}
then you can use it like
objWebService.getCityDetail {
//do something when the ws is complete
}
You can try any of these:
If getCityDetail() is asynchronous request then, Use Delegate to
respond with the result to the registered class.
Ex:
NSURLConnection.sendAsynchronousRequest(request, queue: queue, completionHandler:{ (response: NSURLResponse!, data: NSData!, error:
NSError!) -> Void in
// Handle incoming data like you would in synchronous request
var reply = NSString(data: data, encoding: NSUTF8StringEncoding)
// Conform to the protocol with the results here
})
iOS Swift Pass Closure as Property? - As here u can use the closure as property and can communicate between the classes

iOS Concurrency issue: method returned before got the pedometer data

like the code below, when I want to return a pedometer data through a method for some connivence, but the method returned earlier than the data be retrieved.I think this maybe a concurrency issue. How could I return the data in a right way for future use?Thx
func queryPedometerTodayTotalData() -> Int {
var pedometerDataOfToday: CMPedometerData?
self.queryPedometerDataFromDate(NSDate.today()!, toDate: NSDate(), withHandler: { (pedometerData, error) in
pedometerDataOfToday = pedometerData!
print("this print after got data in a background thread:\(pedometerDataOfToday)")
})
print("This should print before last print, and there haven't got the data now: \(pedometerDataOfToday)")
return pedometerDataOfToday
}
You're right about it being a concurrency issue. You should use the result inside the handler of the queryPedometerDataFromDate.
One way of achieving this would be to use a completion block for your queryPedometerTodayTotalData method instead of having it return a value, like this:
func queryPedometerTodayTotalData(completion:((CMPedometerData?)->())) {
var pedometerDataOfToday: CMPedometerData?
self.queryPedometerDataFromDate(NSDate.today()!, toDate: NSDate(), withHandler: { (pedometerData, error) in
pedometerDataOfToday = pedometerData!
completion(pedometerData)
})
}
func testQueryPedometerTodayTotalData() {
self.queryPedometerTodayTotalData { (data) in
print(data)
}
}
It is a concurrency issue. queryPedometerDataFromDate is an asynchronous method. it executes the completion block whenever iOS deems it (which would usually be after it has retrieved the data) so that is why the 2nd print line prints first and doesnt return your result.
You need to either use the pedometerData from the completion block inside the completion block, or call a method/delegate that will handle the result of the completion block.

To wait in a loop (that backgrounds anyway) in Swift?

In Swift (that's Swift) there are a number of ways to handle asynchronous,
Say you have a loop like this - it's calling a parse cloud code call which goes to background anyway.
public func runImages()
{
print("test begins...")
for i in 1...3
{
print("Tick tock tick tock ..\(i)")
PFCloud.callFunctionInBackground("blah", withParameters:["bla":i,"bla":"bla] )
{
(response: AnyObject?, error: NSError?) -> Void in
print(".. done! Now go again...")
if let rr = response as? String { print(rr) }
if let err = error { print(err.domain) }
}
}
}
How to make that wait for the end of each PFCloud call?
Really is just an ordinary flag best, or? (Note that (a) I can't get a flag to work in Swift and (b) as Paul points out you're blocking the UI!!)
What is the "Swift way" in the context you see here? I feel it would be very inelegant to use a recursive pattern here, for example.
If you want the three calls to execute serially then you can use a serial dispatch queue and a dispatch_barrier_async to do something when all three are finished -
let dispatchQueue=dispatch_queue_create("queue", DISPATCH_QUEUE_SERIAL)
for i in 1...3 {
dispatch_async(dispatchQueue, { () -> Void in
print("i=\(i)")
let result = PFCloud.callFunction("blah", withParameters:["bla":i,"bla":"bla] )
})
}
dispatch_barrier_async(dispatchQueue, { () -> Void in
print("really done")
})
print(" done")
In order for this to work with your Parse function you need to use the synchronous cloud code call, not an asynchronous. And if you update UI in the dispatch_barrier_async closure you would need to dispatch that on the main queue.

Service API call blocks UI in UITableViewController

I have a UITableViewController and have to load a not so big amount of data on start. In my viewDidLoad, I use a different queue to send the request:
override func viewDidLoad() {
super.viewDidLoad()
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), {
var data = self.getStoresData()
dispatch_async(dispatch_get_main_queue(), {
self.parseStoresData(data)
self.resultsController = PartnerStore.getAllStores()
});
});
}
And these are the methods:
func getStoresData() -> [NSDictionary]
{
var responseData = [NSDictionary]()
self.refreshControl.beginRefreshing()
AppDelegate.appDelegate().httpRequestOperationManager?.GET(
"partner_stores/",
parameters: nil,
success: { (operation: AFHTTPRequestOperation!, responseObject: AnyObject!) in
self.tableView.reloadData();
println("RESPONSE OBJECT IN GET PARTNER STORES: \(responseObject)") },
failure: { (operation: AFHTTPRequestOperation!, error: NSError!) in
println("FAIL IN GET PARTNER STORES: \(error)") })
self.refreshControl.endRefreshing()
return responseData
}
func parseStoresData(storesData: [NSDictionary])
{
for storeDict in storesData {
// just inserts a new object to CoreData
PartnerStore.addNewStore(storeDict)
}
}
The problem is (I think) that the API call is async, so the two functions in dispatch_async are executed before the data is downloaded from the server. But if I put everything in the success block of the GET call, it takes a lot of time and the whole UI is blocked. What is the best way to to the server call, without blocking the UI thread ?
You are right about the two calls in the dispatch_async in viewDidLoad being called before your GET request completes; that's a problem. You also had the right idea when you said you tried putting everything in the success block; that's where it should go flow wise. There are a few other things that need to be moved around as well though.
One good way to go about handling UI updates is to have separate functions for your UI updates and your data fetch. Doing it that way means we'll need to pass a callback to your getStoresData function and then call it appropriately in your GET function's success and error blocks. That will let us know when the data fetch is complete so that we can finish the UI updates. We'll also want to move the dispatch to the background queue out of viewDidLoad and in to getStoresData.
So, lets pull any UI changes out of getStoresData and move that dispatch:
func getStoresData(callback: (error: NSError?) -> Void) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), {
// ... do any setup ...
AppDelegate.appDelegate().httpRequestOperationManager?.GET(
// ... other GET parameters ...
success: { (operation: AFHTTPRequestOperation!, responseObject: AnyObject!) in
var responseData = [NSDictionary]()
// do what you need to convert responseObject to responseData
// then...
// NOTE: we'll dispatch the the main thread here because parseStoresData deals with CoreData.
// This dispatch could be done in parseStoresData itself but
// a callback function would need to be added to it as well
// in that case.
dispatch_async(dispatch_get_main_queue(), {
self.parseStoresData(responseData)
// The response has been dealt with, so call the callback
callback(error: nil)
});
},
failure: { (operation: AFHTTPRequestOperation!, error: NSError!) in
// There was an error, so call the callback with the error object
callback(error: error)
}
)
})
}
Now let's make that new function to handle updating the UI so that the data updates are decoupled from the UI updates. In this function we'll first start the refresh control and call getStoresData. Then, when getStoresData finishes, update the table view and stop the refresh control.
func reloadData() {
// start the refresh control on the main thread so the user knows we're updating
dispatch_async(dispatch_get_main_queue(), {
self.refreshControl.beginRefreshing()
})
// do the actual data fetch...
// (remember this will dispatch to a background thread on its own now)
getStoresData({
(error: NSError?) -> Void in
// since this callback could be called from any thread,
// make sure to dispatch back to the main UI thread to finish the UI updates
dispatch_async(dispatch_get_main_queue(), {
if let actualError = error {
// update the UI appropriately for the error
} else {
// update the data in the table view and reload it
self.resultsController = PartnerStore.getAllStores()
self.tableView.reloadData();
}
// we're done; stop the refresh control
self.refreshControl.endRefreshing()
})
})
}
That makes your viewDidLoad function very simple now:
override func viewDidLoad() {
super.viewDidLoad()
reloadData()
}
This also makes it easier to implement things like pull to refresh since you can simply call reloadData when the user triggers the refresh instead of duplicating the UI update code all over the place.

Resources