I have this loop on a background thread, of which I want to inform the user of its progress.
So, in this loop, I call
[self performSelectorOnMainThread:#selector(updateProgressBarto:) withObject:#(value) waitUntilDone:YES];
I put some logging before and after the loop, and made the following observations:
If I enable the above line in order to show the progress, my logging console shows:
2017-06-17 16:43:49.675 myApp[8523:551864] Start Import
2017-06-17 16:43:59.119 myApp[8523:551864] Done Importing
that's a delta of roughly 9.5 seconds
without the progress bar, it looks like
2017-06-17 16:47:06.052 myApp[8611:556572] Start Import
2017-06-17 16:47:12.776 myApp[8611:556572] Done Importing
down to 6.7 seconds
As a comparison, if the loop is run in a Background Fetch, where there is no UI involved at all, logging shows:
2017-06-17 16:45:12.199 myApp[8523:553684] Start Import
2017-06-17 16:45:13.084 myApp[8523:553684] Done Importing
which is less than a second.
if I set
waitUntilDone:NO
I get the unwanted side effect that the progress bar is update only 3 times, rather than 50+ times.
The technical question: is this something I/the user has to live with, or are there any perceptional tricks to solve this?
The psychological question:
Would you/the user prefer six seconds without visual feedback over 9 seconds with feedback?
Your insights are very welcome.
why dont you do the UI update async using the GDC. There should be very little overhead in the background thread and little latency in the UI:
dispatch_async(dispatch_get_main_queue(), ^{
[self updateProgressBarto:#(value)];
});
Also, maybe you dont need to update the UI in every iteration, but only for every 10 or 100
if !(i % 10) {
// update UI progress bar
}
Related
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.
I would like to flood the main thread with random tasks for a certain amount of time in order to figure out how another part of my application would in this circumstances. How can I achieve this?
You could just create an while-loop that never ends. Something like:
BOOL contunue = YES;
while (contunue) {
// Code
}
That would run until you stop the program yourself.
Edit:
To add a timer to the above code, you could use NSTimer to change the value of continue to NO after a determined amount of time
Recently I got requirement from my client that we need to process some execution in background. This execution may be for 10 min or 20 min long.
Is there any way to achieve this? I know iOS has listed few apps type & only those can be run in background. But I checked & my app is not fitting in that listed all apps type. So now I am doubtful in this.
Any one has solution?
Thanks
Shyam
// Try this:
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0ul), ^(void) {
// Perform long running process
dispatch_async(dispatch_get_main_queue(), ^{
// Update the UI
});
});
// Continue doing other stuff
Use Background fetch is available in Capabilities.
Background fetch mode is execute every 10 min.
Refer to the sample Code here : https://mobisoftinfotech.com/resources/mguide/background-fetch-ios/
Forgot this method beginBackgroundUploadTask from above tutorial.
Hi in my application i have two types of syncs. 1.Auto sync 2.Manual Sync. In both the syncs i am downloding a bunch of files from server. If I choose auto sync all files will get download.
Code is like this
for(int i=0;i<filescount;i++)
{
[self downloadfiles];
}
-(void)download files
{
//Here i am creating `NSInvocationOperation`.
if(!synchingfilecount)
totalreceiveddata=0;
}
Based on totalreceiveddata I am updating progress bar. Now the issue is if it autosync it is working fine.While downloading files using autosync and in middle if i click manual sync that time [self downloadfiles]; method will get called but the issue is synchingfilescount is not updating immediately it's completeing the autosyncfiles download and synchingfilescount become 0 due to this reason totalreceiveddata become 0 and progress bar is disappearing. After complete this opertiona again synchingfilecount becomes 4 but i cannot able to see the progress bar due to above situation. Please any one help me how can I come out from this situation.
Ok, if I understand your question correctly, it sounds like you need to create some flags so that you can manage the flow of your code. You can do this by making a boolean property and setting it as you need in the completion block of these sync methods. That way you can call a method or only execute a method after the call is complete.
I noticed that all my UI tests fail when the network is slow. For instance a user would try to login and then the next screen wouldn't load fast enough in order for another UIElement to be on screen.
How can I handle a slow network connection without using a delay() ?
You should definitely take a look at multi-threading. When handling network connections, you should make all this processing in a secondary thread. If not, the main thread will be blocked and the app will become unresponsive to the user.
Multi-threading is a very big subject. I recommend you to start looking at Apple's reference for this. You can also refer to a great course on iTunes U (lecture 11).
If you just want to give it a shot, here's the actual code (similar) that you will need:
dispatch_queue_t newQueue = dispatch_queue_create("networkQueue", NULL);
dispatch_async(newQueue, ^{
// here you need to call the networking processes
dispatch_async(dispatch_get_main_queue(), ^{
// if you need to update your UI, you need to get back to the main queue.
// This block will be executed in your main queue.
});
});
The only way I know of is using a delay. I usually have a activity indicator when loading stuff from the internet. So I add a delay while the activity indicator is displaying
while (activityIndicator.isVisible())
{
UIALogger.logMessage("Loading");
UIATarget.localTarget().delay(1);
}
Check out pushTimeout and popTimeout methods in the UIATarget. You can find the docs here.
Here is one code example from our iOS app UIAutomation tests:
// Tap "Post" button, which starts a network request
mainWindow.buttons()["post.button.post"].tap();
// Wait for maximum of 30 seconds to "OKAY" button to be valid
target.pushTimeout(30);
// Tap the button which is shown from the network request success callback
mainWindow.buttons()["dialog.button.okay"].tap();
// End the wait scope
target.popTimeout();