UI Slow to Update *ONLY* If Called In Response To Change Occurring in NSURLSessionUploadTask Completion Block - ios

The app interacts with php scripts on my server. A user can create a booking, and details are written to a database. Subsequently, they can cancel that booking.
Within the app, a booking is an object, responsible for gathering its own details from the server. A booking can also cancel itself (at the user's request) - pressing "the cancel button" in BookingViewController calls the booking's (void)cancelBooking method, which posts to bookings-cancel.php using an NSURLSessionUploadTask with json data rolled from #{ #"invoiceNumber": self.invoiceNumber }.
The database is updated, the uploadSession returns new details, and the booking updates itself according. All of this is nicely responsive - up and back in less than a second, consistently.
The problem comes when I attempt to update the UI on BookingViewController (labels for delivery date and price) using values read from the booking object after it has updated itself (within the uploadSession completion block).
BookingViewController is assigned as the booking's delegate. The booking is setup for KVO on its own "price" property. Whenever the price changes, the booking calls a delegate method priceDidChange:(NSString *)updatedPrice on BookingViewController, triggering an NSLog and updates to deliveryLabel.text and priceLabel.text.
- (void)priceDidUpdate:(NSString *)updatedPrice {
NSLog(#"priceDidUpdate delegate notification - updatedPrice is: %#", updatedPrice);
[self.deliveryLabel setText:#"N/A"];
[self.priceLabel setText:updatedPrice];
}
Testing has shown that if I update the price directly from the "cancel" button, or with any other explicit command (e.g., self.price = #"123.45") within the cancelBooking method outside of the uploadTask, then the UI updates just as quickly as the NSLog is written out (i.e., near instantaneously).
However, if the price is updated within the uploadTask completion block, the NSLog will write out just as responsively but the updates to deliveryLabel.text and priceLabel.text are very slow to occur - the delay varies between 5 and 12 seconds, approximately.
I've got NSLogs all over the place, and am confident this is not merely about a delay getting the updated value to or from the booking object. Easily twenty times already I have seen "priceDidUpdate delegate notification - updatedPrice is: 0.00" (the updated price), then counted to 10 before self.priceLabel.text is actually set to #"0.00". Totally stumped.
In case it matters, the NSURLSession is configured using ephemeralSessionConfiguration with no adjustments.
Is there any reason why the UI updates in BookingViewController should take longer to occur based on whether or not the call to priceDidChange comes from inside or outside the uploadTask completion block?
Thanks in advance!

Use main queue to update the UI. Use following code:
- (void)priceDidUpdate:(NSString *)updatedPrice
{
dispatch_async(dispatch_get_main_queue(), ^()
{
//Add method, task you want perform on mainQueue
//Control UIView, IBOutlet all here
NSLog(#"priceDidUpdate delegate notification - updatedPrice is: %#", updatedPrice);
[self.deliveryLabel setText:#"N/A"];
[self.priceLabel setText:updatedPrice];
});
}

Related

UI not updating (in Swift) during intensive function on main thread

I wondered if anyone could provide advice on how I can ‘force’ the UI to update during a particularly intensive function (on the main thread) in Swift.
To explain: I am trying to add an ‘import’ feature to my app, which would allow a user to import items from a backup file (could be anything from 1 - 1,000,000 records, say, depending on the size of their backup) which get saved to the app’s CodeData database. This function uses a ‘for in’ loop (to cycle through each record in the backup file), and with each ‘for’ in that loop, the function sends a message to a delegate (a ViewController) to update its UIProgressBar with the progress so the user can see the live progress on the screen. I would normally try to send this intensive function to a background thread, and separately update the UI on the main thread… but this isn't an option because creating those items in the CoreData context has to be done on the main thread (according to Swift’s errors/crashes when I initially tried to do it on a background thread), and I think this therefore is causing the UI to ‘freeze’ and not update live on screen.
A simplified version of the code would be:
class CoreDataManager {
var delegate: ProgressProtocol?
// (dummy) backup file array for purpose of this example, which could contain 100,000's of items
let backUp = [BackUpItem]()
// intensive function containing 'for in' loop
func processBackUpAndSaveData() {
let totalItems: Float = Float(backUp.count)
var step: Float = 0
for backUpItem in backUp {
// calculate Progress and tell delegate to update the UIProgressView
step += 1
let calculatedProgress = step / totalItems
delegate?.updateProgressBar(progress: calculatedProgress)
// Create the item in CoreData context (which must be done on main thread)
let savedItem = (context: context)
}
// loop is complete, so save the CoreData context
try! context.save()
}
}
// Meanwhile... in the delegate (ViewController) which updates the UIProgressView
class ViewController: UIViewController, ProgressProtocol {
let progressBar = UIProgressView()
// Delegate function which updates the progress bar
func updateProgressBar(progress: Float) {
// Print statement, which shows up correctly in the console during the intensive task
print("Progress being updated to \(progress)")
// Update to the progressBar is instructed, but isn't reflected on the simulator
progressBar.setProgress(progress, animated: false)
}
}
One important thing to note: the print statement in the above code runs fine / as expected, i.e. throughout the long ‘for in’ loop (which could take a minute or two), the console continuously shows all the print statements (showing the increasing progress values), so I know that the delegate ‘updateProgressBar’ function is definitely firing correctly, but the Progress Bar on the screen itself simply isn’t updating / doesn’t change… and I’m assuming it’s because the UI is frozen and hasn’t got ‘time’ (for want of a better word) to reflect the updated progress given the intensity of the main function running.
I am relatively new to coding, so apologies in advance if I ask for clarification on any responses as much of this is new to me. In case it is relevant, I am using Storyboards (as opposed to SwiftUI).
Just really looking for any advice / tips on whether there are any (relatively easy) routes to resolve this and essentially 'force' the UI to update during this intensive task.
You say "...Just really looking for any advice / tips on whether there are any (relatively easy) routes to resolve this and essentially 'force' the UI to update during this intensive task."
No. If you do time-consuming work synchronously on the main thread, you block the main thread, and UI updates will not take effect until your code returns.
You need to figure out how to run your code on a background thread. I haven't worked with CoreData in quite a while. I know it's possible to do CoreData queries on a background thread, but I no longer remember the details. That's what you're going to need to do.
As to your comment about print statements, that makes sense. The Xcode console is separate from your app's run loop, and is able to display output even if your code doesn't return. The app UI can't do that however.

How to implement a search queue

I am new in swift3.0 I am implementing a custom search box. I wish to know how can i make a search queue such that on text change in searchbox i need to perform search operation with new text and if there is an existing search operation going on cancel that. I also want to include threshold ontextchanged. So that search operation does not get fired very frequently
Your question is somehow general, but let me tell you how I accomplished this in Swift 3 and AFNetworking (this assumes you wish to search for the data on the server).
I hold a reference of the networking manager in the properties of the view controller:
//The network requests manager. Stored here because this view controller extensively uses AFNetworking to perform live search updates when the input box changes.
var manager = AFHTTPRequestOperationManager()
Afterwards, using UISearchController I check to see if there is any text entered in the search box at all and, if it is, I want to make sure there aren't any other ongoing AFNetworking tasks from now by closing any of them which are still running:
//Called when the something is typed in the search bar.
func updateSearchResults (for searchController: UISearchController) {
if !SCString.isStringValid(searchController.searchBar.text) {
searchController.searchResultsController?.view.isHidden = false
tableView.reloadData()
return
}
data.searchText = searchController.searchBar.text!
/**
Highly important racing issue solution. We cancel any current request going on because we don't want to have the list updated after some time, when we already started another request for a new text. Example:
- Request 1 started at 12:00:01
- We clear the containers because Request 2 has to start
- Request 2 started at 12:00:02
- Request 1 finished at 12:00:04. We update the containers because data arrived
- Request 2 finished at 12:00:05. We update the containers because data arrived
- Now we have data from both 1 and 2, something really not desired.
*/
manager.session.getTasksWithCompletionHandler { (dataTasks, uploadTasks, downloadTasks) in
dataTasks.forEach { $0.cancel() }
}
/**
Reloads the list view because we have to remove the last search results.
*/
reloadListView()
}
In the end, I also check in the failure closure if the code of the error is not NSURLErrorCancelled. Because, if that happened, I don't display any error message or toast.
//The operation might be cancelled by us on purpose. In this case, we don't want to interfere with the ongoing logic flow.
if (operation?.error as! NSError).code == NSURLErrorCancelled {
return
}
self.retrieveResultListFailureNetwork()
Hope it helps!

FEventType.ChildAdded event only fired once

my firebase data structure looks like the following
user
|__{user_id}
|__userMatch
|__{userMatchId}
|__createdAt: <UNIX time in milliseconds>
I'm trying to listen for the child added event under userMatch since a particular given time. Here's my swift code:
func listenForNewUserMatches(since: NSDate) -> UInt? {
NSLog("listenForNewUserMatches since: \(since)")
var handle:UInt?
let userMatchRef = usersRef.childByAppendingPath("\(user.objectId!)/userMatch")
var query = userMatchRef.queryOrderedByChild("createdAt");
query = query.queryStartingAtValue(since.timeIntervalSince1970 * 1000)
handle = query.observeEventType(FEventType.ChildAdded, withBlock: { snapshot in
let userMatchId = snapshot.key
NSLog("New firebase UserMatch created \(userMatchId)")
}, withCancelBlock: { error in
NSLog("Error listening for new userMatches: \(error)")
})
return handle
}
What's happening is that the event call back is called only once. Subsequent data insertion under userMatch didn't trigger the call. Sort of behaves like observeSingleEventOfType
I have the following data inserted into firebase under user/{some-id}/userMatch:
QGgmQnDLUB
createdAt: 1448934387867
bMfJH1bzNs  
createdAt: 1448934354943
Here are the logs:
2015-11-30 17:32:38.632 listenForNewUserMatches since:2015-12-01 01:32:37 +0000
2015-11-30 17:45:55.163 New firebase UserMatch created bMfJH1bzNs
The call back was fired for bMfJH1bzNs but not for QGgmQnDLUB which was added at a later time. It's very consistent: after opening the app, it only fires for the first event. Not sure what I'm doing wrong here.
Update: Actually the behavior is not very consistent. Sometimes the call back is not fired at all, not even once. But since I persist the since time I should use when calling listenForNewUserMatches function. If I kill the app and restart the app, the callback will get fired (listenForNewUserMatches is called upon app start), for the childAdded event before I killed the app. This happens very consistently (callback always called upon kill-restart the app for events that happened prior to killing the app).
Update 2: Don't know why, but if I add queryLimitedToLast to the query, it works all the time now. I mean, by changing userMatchRef.queryOrderedByChild("createdAt") to userMatchRef.queryOrderedByChild("createdAt").queryLimitedToLast(10), it's working now. 10 is just an arbitrary number I chose.
I think the issue comes from the nature of time based data.
You created a query that says: "Get me all the matches that happened after now." This should work when the app is running and new data comes in like bMfJH1bzNs. But older data like QGgmQnDLUB won't show up.
Then when you run again, the since.timeIntervalSince1970 has changed to a later date. Now neither of the objects before will show up in your query.
When you changed your query to use queryLimitedToLast you avoided this issue because you're no longer querying based on time. Now your query says: "Get me the last ten children at this location."
As long as there is data at that location you'll always receive data in the callback.
So you either need to ensure that since.timeIntervalSince1970 is always earlier than the data you expect to come back, or use queryLimitedToLast.

Avoid multiple web service call in ios

I am inserting data to server database from my app.
on submit button I am calling inserdata web services.
My data inserted in database. but I have one problem,
before I get first result back when I tap on submit button again then same record inserted multiple times in database.
Please help how can I avoid this. (also by mistake I have tapped multiple times on submit button then same record inserted multiple time).
when web service triggers first time.... set button.selected = yes and in function check if button isSelected then not perform action. Use this bool value to distinguish between both conditions.
When user pressed submit button, show progress hud or an activity Indicator View (loading overlay). This will let user know that some processing is going on and make them not trigger any other action.
Also set synchronous property to that object from which you are calling this WebService. (When you execute something synchronously, you wait for it to finish before moving on to another task. When you execute something asynchronously, you can move on to another task before it finishes.)
And when you got response, clear all data from your form or your app. Because If data is cleared, then user can't insert same record into Database.
So, No need to disable that submit button.
Try this.
- (IBAction)doneCollectionSaveAction:(id)sender {
if([activity isAnimating]==YES){
[ALToastView toastInView:[UIApplication sharedApplication].keyWindow withText:#"Please wait..."];
}else{
[self addToCollection];
}
}
or
- (IBAction)doneCollectionSaveAction:(UIButton *)sender {
if (sender.selected==YES) {
//do nothing
[ALToastView toastInView:[UIApplication sharedApplication].keyWindow withText:#"Please wait..."];
}else{
//send data to server
self.buttonDoneOutlet.selected=YES;
[self addToCollection];
}
}
//and self.buttonDoneOutlet.selected=NO; //failure network call

NSRunLoop makes app non-responsive sometimes

In my app, I need to make https calls to a restful web api and process the results upon return. The number of simultaneous service calls is never fixed, hence the related code has been written accordingly. The data fetched from the service is temporarily stored on an SQLite DB within the app. Following is the structure how it works.
When the user navigates to any screen or UI component thereof for which data needs to be fetched, the view controller calls a method on its designated model object. This method then checks whether the data is already present in the DB or it needs to be fetched. In case data is present, it returns the same to the view controller. Otherwise, it initiates an asynchronous service request and waits till the response comes, after which it returns the data to the VC. Therefore, the VC initialises a loading indicator before calling the specified model, and dismisses the same after control is returned from this function.
Here it is important that the function on the model waits till the response is received from the web api. This is done by registering for an NSNotification which will be issued by the service module once returned data is written to the DB. A boolean variable it set to false upon making the service request and set to true once the response is received. An NSRunLoop runs on the false condition of this boolean variable. Hence once the variable is set to true, the rest of the processing can continue.
Following are the relevant pieces of code in which all this is implemented:
[serviceModule initServiceCall:#"25" withDictionary:[NSDictionary dictionaryWithObjects:#[asOfDate] forKeys:#[#"toDate"]]];
dataReady=NO;
NSString *notificationName = #"dataReady";
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(useNotificationFromServiceModule:) name:notificationName object:nil];
NSRunLoop *theRL = [NSRunLoop currentRunLoop];
while (!dataReady && [theRL runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]);
The rest of the function continues after this.
This is the function that handles the notification:
-(void)useNotificationFromServiceModule:(NSNotification *)notification {
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0ul);
dispatch_async(queue, ^{
dataReady=YES;
});}
The usual process is that once the notification is sent, the NSRunLoop quits and the rest of the method completes, returning to the view controller which then dismissed the loading indicator. The problem is that sometimes this does not happen. While the notification is issued (I can see the console log), the NSRunLoop does not end. The loading indicator continues to appear on the screen and stays that way until the screen is tapped once. When the screen is tapped, the NSRunLoop ends and the rest of the process continues randomly.
This does not happen always. It happens quite randomly, maybe about 4-5 times out of 10. Kindly provide some inputs/pointers to indicate why this may be happening.
If you are using the run loop directly, you are either very clever or very stupid. In the first case, you'll find the answer yourself. In the second case, it would be much much better if you followed the same pattern as everyone else does, which is running your networking code on a background thread and using dispatch_async when the results arrive.

Resources