I have a UIAlertView that initiates an update process.
The UIAlertView asks the user whether they'd like to update or not.
Here's my code:
- (void)reachabilityChanged:(NSNotification *)notification {
if ([connection isReachable]){
[updateLabel setText:#"Connection Active. Checking Update Status"];
[[[UIAlertView alloc] initWithTitle:#"Update Available" message:#"Your File Database is Out of Date. Would you like to Update?\nNote: Updates can take a long time depending on the required files." delegate:self cancelButtonTitle:#"Later" otherButtonTitles:#"Update Now", nil] show];
}
- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex{
if (buttonIndex == 1) {
[self updateFiles:[UpdateManager getUpdateFiles]];
}
}
The above code runs fine, however, within my updateFiles: method, I require some UI adjustments.
- (void)updateFiles:(NSArray *)filesList {
for (NSDictionary *file in filesList) {
[updateLabel setText:[NSString stringWithFormat:#"Downloading File: %#", [file objectForKey:#"Name"]]];
[UpdateManager updateFile:[file objectForKey:#"File Path"]];
}
[updateIndicator stopAnimating];
[updateLabel setText:#"Update Completed"];
}
The UIAlertView doesn't dismiss until after the for statement in the updateFiles method is run.
I can't get the updateLabel to display the files it's currently downloading, though at the end of the update process, we do get a 'Update Completed' in the label.
Can anybody help?
UPDATE
I'm starting to suspect this is more of a process that's being delayed by some heavy synchronous processes. For example, my [UpdateManager getUpdateFiles] method is heavy and involves getting resources from the web. Likewise with my [UpdateManager updateFile:[file objectForKey:#"File Path"]]; method.
Is there any way I can force the UI updates to take priority over these methods?
I'm just trying to give the user some feedback on what's happening.
I found the solution.
I couldn't update the UI and process some heavy methods on the same thread.
Since I can only update the UI on the main thread, I had to do some re-organising to ensure the processes were on a background thread, but then promote the UI changes to the main.
- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex{
if (buttonIndex == 1) {
[self performSelectorInBackground:#selector(updateFiles:) withObject:[UpdateManager getUpdateFiles]];
}
}
- (void)updateFiles:(NSArray *)filesList {
for (NSDictionary *file in filesList) {
[updateLabel performSelectorOnMainThread:#selector(setText:) withObject:[NSString stringWithFormat:#"Downloading File: %#", [file objectForKey:#"Name"]]];
[UpdateManager updateFile:[file objectForKey:#"File Path"]];
}
[updateIndicator stopAnimating];
[updateLabel setText:#"Update Completed"];
}
So, I send updateFiles: to the background and promote setText: and any other UI changes to the Main Thread.
Related
I'm trying to get the following working, this example appears on a few websites but i just can't seem to get it working.
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:#"Yes, like this" message:#"What are you looking at?" cancelButtonTitle:#"Leave me alone" otherButtonTitles:#"Button 1",#"Button 2",nil];
[alert showWithDismissHandler:^(NSInteger selectedIndex, BOOL didCancel) {
if (didCancel) {
NSLog(#"User cancelled");
return;
}
switch (selectedIndex) {
case 1:
NSLog(#"1 selected");
break;
case 2:
NSLog(#"2 selected");
break;
default:
break;
}
}];
The warnings I'm getting are
No visible #interface for 'UIAlertView' declares the selector 'initWithTitle:message:cancelButtonTitle:otherButtonTitles:'
No visible #interface for 'UIAlertView' declares the selector 'showWithDismissHandler:'
Properly a really daft question but what am i missing.
Thanks
The signature is not correct. You missed delegate.
– initWithTitle:message:delegate:cancelButtonTitle:otherButtonTitles:
The standard UIAlertView does not have showWithDismissHandler method. If you copied some code from internet, likely you will need to download some third party package that supports UIAlertView with block callback (there are quite a few of them).
Yes, Peter is right! The first warning is because the signature is not correct:
– initWithTitle:message:delegate:cancelButtonTitle:otherButtonTitles:
and what you trying to do here is to do something when the alertview is dismissed and the method you are using is not available. Implement UIAlertViewDelegate and then use any of these methods to do what you are trying to do here.
- (void)alertView:(UIAlertView *)alertView didDismissWithButtonIndex:(NSInteger)buttonIndex
- (void)alertView:(UIAlertView *)alertView willDismissWithButtonIndex:(NSInteger)buttonIndex
Here's the link to UIAlertView delegate reference:
http://developer.apple.com/library/ios/documentation/uikit/reference/UIAlertViewDelegate_Protocol/UIAlertViewDelegate/UIAlertViewDelegate.html
Are you using something like UBAlertView, by any chance? If so, you need to instantiate a UBAlertView, not a UIAlertView:
UBAlertView *alert = [[UBAlertView alloc] initWithTitle:#"Yes, like this"
message:#"What are you looking at?"
cancelButtonTitle:#"Leave me alone"
otherButtonTitles:#"Button 1",#"Button 2",nil];
[alert showWithDismissHandler:^(NSInteger selectedIndex, BOOL didCancel) {
... etc ...
(I like to put line breaks in long method calls to make them more readable.)
I've been experimenting with CLLocationManager's startMonitoringSignificantLocationChanges and I've run into some problems with Core Data. It turns out that since iOS 5.0, Core Data defaults to using NSFileProtectionCompleteUntilFirstUserAuthentication. This means that if a passcode is set, the persistent store is unavailable from the time the device is turned on until the time the passcode is first entered. If you're using location updates, it's possible your app may get launched during that time, and Core Data will get an error trying to load the persistent store.
Obviously switching to NSFileProtectionNone would be the easiest way to solve this. I'd prefer not to though—I'm not storing anything super sensitive in the database, but these location updates aren't super critical either.
I know I can use [[UIApplication sharedApplication] isProtectedDataAvailable] to check whether the data has been unlocked yet, and I can use applicationProtectedDataWillBecomeUnavailable: in my application delegate to respond appropriately once it is unlocked. This seems messy to me though—I'll have to add in a bunch of extra checks to make sure nothing goes wrong if the persistent store is unavailable, re-setup a bunch of things once it does become available, and so on. And that extra code doesn't offer much benefit—the app still won't be able to do anything if it launches in this state.
So I guess I'm just not sure which is the more "proper" way to deal this:
Switch to NSFileProtectionNone.
Add in the extra checks to skip over things if the store is unavailable, and use applicationProtectedDataWillBecomeUnavailable: to set things up again once it is.
If the app is launched in the background ([[UIApplication sharedApplication] applicationState] == UIApplicationStateBackground) and protected data is unavailable ([[UIApplication sharedApplication] isProtectedDataAvailable] == NO)) just call exit(0) (or something similar) to quit the app. On one hand this seems like the simplest solution, and I don't really see any downsides. But it also seems… "wrong"? I guess I can't decide if it's a clean solution or just a lazy one.
Something else I'm just not thinking of?
After thinking this over for a while I've come up with a solution I'm happy with. One thing to consider with the exit(0) option is that if the user takes a while to unlock the device, the app could be continually loading, quitting, and reloading. Whereas if you simply prevent the app from doing much, it will probably only have to load once, and will most likely be more efficient. So I decided to try my option 3 and see how messy it really was. It turned out to be simpler than I thought.
First I added a BOOL setupComplete property to my app delegate. This gives me an easy way to check if the app was fully launched at various points. Then in application:didFinishLaunchingWithOptions: I attempt to initialize the managed object context, then do something like this:
NSManagedObjectContext *moc = [self managedObjectContext];
if (moc) {
self.setupComplete = YES;
[self setupWithManagedObjectContext:moc];
} else {
UIApplication *app = [UIApplication sharedApplication];
if ([app applicationState] == UIApplicationStateBackground && ![app isProtectedDataAvailable]) {
[app beginIgnoringInteractionEvents];
} else [self presentErrorWithTitle:#"There was an error opening the database."];
}
setupWithManagedObjectContext: is just a custom method that finishes setting up. I'm not sure the beginIgnoringInteractionEvents is necessary, but I added it to be on the safe side. That way when the app is brought to the front, I can be sure the interface is frozen until setup is complete. It might avoid a crash if an eager user is tapping anxiously.
Then in applicationProtectedDataDidBecomeAvailable: I call something like this:
if (!self.setupComplete) {
NSManagedObjectContext *moc = [self managedObjectContext];
if (moc) {
self.setupComplete = YES;
[self setupWithManagedObjectContext:moc];
UIApplication *app = [UIApplication sharedApplication];
if ([app isIgnoringInteractionEvents]) [app endIgnoringInteractionEvents];
} else [self presentErrorWithTitle:#"There was an error opening the database."];
}
That finishes the setup and re-enables the interface. That's most of the work, but you'll also need to check through your other code to make sure nothing that relies on Core Data is getting called before your persistent store is available. One thing to watch out for is that applicationWillEnterForeground and applicationDidBecomeActive may get called before applicationProtectedDataDidBecomeAvailable if the user launches the app from this background state. So in various places I've added if (self.setupComplete) { … } to make sure nothing runs before it's ready. I also had a couple of places where I needed to refresh the interface once the database was loaded.
In order to (partially) test this without a lot of driving around, I temporarily modified application:didFinishLaunchingWithOptions: to not set up the database:
NSManagedObjectContext *moc = nil; // [self managedObjectContext];
if (moc) {
self.setupComplete = YES;
[self setupWithManagedObjectContext:moc];
} else {
UIApplication *app = [UIApplication sharedApplication];
// if ([app applicationState] == UIApplicationStateBackground && ![app isProtectedDataAvailable]) {
[app beginIgnoringInteractionEvents];
// } else [self presentErrorWithTitle:#"There was an error opening the database."];
}
Then I moved my code in applicationProtectedDataDidBecomeAvailable: over to applicationWillEnterForeground:. That way I could launch the app, make sure nothing unexpected happens, press the home button, open the app again, and make sure everything was working. Since the actual code requires moving a significant distance and waiting five minutes each time, this gave me a good way to approximate what was happening.
One last thing that tripped me up was my persistent store coordinator. A typical implementation might look something like this:
- (NSPersistentStoreCoordinator *)persistentStoreCoordinator {
if (_persistentStoreCoordinator != nil) return _persistentStoreCoordinator;
NSURL *storeURL = [[self applicationDocumentsDirectory] URLByAppendingPathComponent:#"Test.sqlite"];
NSError *error = nil;
_persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:[self managedObjectModel]];
if (![_persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:storeURL options:nil error:&error]) {
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
}
return _persistentStoreCoordinator;
}
This is loosely based on Apple's sample code, which does explain in comments that you need to handle the error appropriately. My own code does a bit more than this, but one thing I hadn't considered is that if there's an error loading the persistent store, this will return a non-nil result! That was allowing all my other code to proceed as though it was working correctly. And even if persistentStoreCoordinator was called again, it would just return the same coordinator, without a valid store, instead of trying to load the store again. There are various ways you could deal with this, but to me it seemed best to not set _persistentStoreCoordinator unless it was able to add the store:
- (NSPersistentStoreCoordinator *)persistentStoreCoordinator {
if (_persistentStoreCoordinator != nil) return _persistentStoreCoordinator;
NSURL *storeURL = [[self applicationDocumentsDirectory] URLByAppendingPathComponent:#"Test.sqlite"];
NSError *error = nil;
NSPersistentStoreCoordinator *coordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:[self managedObjectModel]];
if ([coordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:storeURL options:nil error:&error]) {
_persistentStoreCoordinator = coordinator;
} else {
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
}
return _persistentStoreCoordinator;
}
I have experienced, that you have to check
[[UIApplication sharedApplication] isProtectedDataAvailable]
and process
applicationProtectedDataWillBecomeUnavailable
to be sure you don't access a protected file.
Checking for
managedObjectContext
did not work for me.
Here is the scenario: In my app I'm syncing some data, whenever there is some error when syncing I flag this in a BOOL. When all syncing is complete I want to display sync feedback (errors) for the user.
If there is ie a calendar sync error and a contact sync error I first display a UIAlertView with information about the calendar sync error, when the user has tapped "OK" I then display a UIAlertView with information about the contact sync error. To be able to know when the user has tapped "OK" I use completion blocks. So my code looks something like this:
if (calendarSyncFailed && contactSyncFailed && facebookSyncFailed && contactSyncConflicts) {
[self displayCalendarSyncAlertCompletionBlock:^{
[self displayContactsSyncAlertCompletionBlock:^{
[self displayFacebookSyncAlertCompletionBlock:^{
[self displayContactSyncConflictsAlertCompletionBlock:^{
}];
}];
}];
}];
} else if (calendarSyncFailed && contactSyncFailed && facebookSyncFailed) {
[self displayCalendarSyncAlertCompletionBlock:^{
[self displayContactsSyncAlertCompletionBlock:^{
[self displayFacebookSyncAlertCompletionBlock:^{
}];
}];
}];
} else if (contactSyncFailed && facebookSyncFailed && contactSyncConflicts) {
[self displayContactsSyncAlertCompletionBlock:^{
[self displayFacebookSyncAlertCompletionBlock:^{
[self displayContactSyncConflictsAlertCompletionBlock:^{
}];
}];
}];
} else if (you get the idea…) {
}
As you can see there will be alot of different combinations for dealing with these 4 boolean values and I was wondering if there is a more smarter/elegant way of coding this?
While I do agree with demosten that it would be better to have only one message, this is how I would do this with less code:
Use a mutable array as a property where you store your alertviews.
In the method where you test your conditions, create an alert view for every failure that evaluates to true, and put them in your array in the desired order. (This is the key part as you are only doing 4 tests, instead of 2^4 - 1 tests).
Implement the UIAlertViewDelegate method alertView: didDismissWithButtonIndex: something like this:
-(void)alertView:(UIAlertView *)alertView didDismissWithButtonIndex:(NSInteger)buttonIndex
{
NSInteger nextIndex = [self.alertViews indexOfObject:alertView] + 1;
if (nextIndex < [self.alertViews count]){
UIAlertView *next = [self.alertViews objectAtIndex: nextIndex];
[next show];
}
}
I have an application that first loads some data into an UIManagedDocument, then executes saveToURL:forSaveOperation:completionHandler:. Inside the completionHandler block, it does an update of various elements of this database, and when it's done, it does another saving.
Besides that, the app has 3 buttons that reload the data, re-update the data, and delete one entity of the database, respectively. In every button method, the last instruction is a saving as well.
When I run all this in the simulator, all goes smoothly. But in the device doesn't. It constantly crashes. I have observed that, normally, it crashes when pressing the "delete" button, or when reloading or re-updating the database. And it's always in the saveToURL operation.
In my opinion, the problem comes when there are multiple threads saving the database. As the device executes the code slower, maybe multiple savings come at same time and the app can't handle them correctly. Also, sometimes the delete button doesn't delete the entity, and says that doesn't exist (when it does).
I'm totally puzzled with this, and all this saving operations must be done...In fact, if I remove them, the app behaves even more incoherently.
Any suggestions of what could I do to resolve this problem? Thank you very much!
[Edit] Here I post the problematic code. For first loading the data, I use a helper class, with this two methods in particular:
+ (void)loadDataIntoDatabase:(UIManagedDocument *)database
{
[database.managedObjectContext performBlock:^{
// Read from de plist file and fill the database
[database saveToURL:database.fileURL forSaveOperation:UIDocumentSaveForOverwriting completionHandler:^(BOOL success) {
[DataHelper completeDataOfDatabase:database];
}];
}
+ (void)completeDataOfDatabase:(UIManagedDocument *)database
{
[database.managedObjectContext performBlock:^{
// Read from another plist file and update some parameters of the already existent data (uses NSFetchRequest and works well)
// [database saveToURL:database.fileURL forSaveOperation:UIDocumentSaveForOverwriting completionHandler:nil];
[database updateChangeCount:UIDocumentChangeDone];
}];
}
And in the view, I have 3 action methods, like these:
- (IBAction)deleteButton {
[self.database.managedObjectContext performBlock:^{
NSManagedObject *results = ;// The item to delete
[self.database.managedObjectContext deleteObject:results];
// [self.database saveToURL:self.database.fileURL forSaveOperation:UIDocumentSaveForOverwriting completionHandler:NULL];
[self.database updateChangeCount:UIDocumentChangeDone];
}];
}
- (IBAction)reloadExtraDataButton {
[DataHelper loadDataIntoDatabase:self.database];
// [self.database saveToURL:self.database.fileURL forSaveOperation:UIDocumentSaveForOverwriting completionHandler:NULL];
[self.database updateChangeCount:UIDocumentChangeDone];
}
- (IBAction)refreshDataButton {
[DataHelper completeDataOfDatabase:self.database];
//[self.database saveToURL:self.database.fileURL forSaveOperation:UIDocumentSaveForOverwriting completionHandler:NULL];
[self.database updateChangeCount:UIDocumentChangeDone];
}
[Edit 2] More code: First of all, the initial view executes viewDidLoad this way:
- (void)viewDidLoad{
[super viewDidLoad];
self.database = [DataHelper openDatabaseAndUseBlock:^{
[self setupFetchedResultsController];
}];
}
This is what the setupFetchedResultsController method looks like:
- (void)setupFetchedResultsController
{
NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:#"Some entity name"];
request.sortDescriptors = [NSArray arrayWithObject:[NSSortDescriptor sortDescriptorWithKey:#"name" ascending:YES selector:#selector(localizedCaseInsensitiveCompare:)]];
self.fetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:request
managedObjectContext:self.database.managedObjectContext
sectionNameKeyPath:nil
cacheName:nil];
}
Each view of the app (it has tabs) has a different setupFetchedResultsController in order to show the different entities the database contains.
Now, in the helper class, this is the first class method that gets executed, via the viewDidLoad of each view:
+ (UIManagedDocument *)openDatabaseAndUseBlock:(completion_block_t)completionBlock
{
NSURL *url = [[[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask] lastObject];
url = [url URLByAppendingPathComponent:#"Database"];
UIManagedDocument *database = [[UIManagedDocument alloc] initWithFileURL:url];
if (![[NSFileManager defaultManager] fileExistsAtPath:[database.fileURL path]]) {
[database saveToURL:database.fileURL forSaveOperation:UIDocumentSaveForCreating completionHandler:^(BOOL success) {
[self loadDataIntoDatabase:database];
completionBlock();
}];
} else if (database.documentState == UIDocumentStateClosed) {
// Existe, pero cerrado -> Abrir
[database openWithCompletionHandler:^(BOOL success) {
[self loadDataIntoDatabase:database];
completionBlock();
}];
} else if (database.documentState == UIDocumentStateNormal) {
[self loadDataIntoDatabase:database];
completionBlock();
}
return database;
}
You didn't really provide much code. The only real clue you gave was that you are using multiple threads.
UIManagedDocument has two ManagedObjectContexts (one specified for the main queue, and the other for a private queue), but they still must each only be accessed from within their own thread.
Thus, you must only use managedDocument.managedObjectContext from within the main thread. If you want to use it from another thread, you have to use either performBlock or performBlockAndWait. Similarly, you can never know you are running on the private thread for the parent context, so if you want to do something specifically to the parent, you must use performBlock*.
Finally, you really should not be calling saveToURL, except when you initially create the database. UIManagedDocument will auto-save (in its own time).
If you want to encourage it to save earlier, you can send it updateChangeCount: UIDocumentChangeDone to tell it that it has changes that need to be saved.
EDIT
You should only call saveToURL when you create the file for the very first time. With UIManagedDocument, there is no need to call it again (and it can actually cause some unintended issues).
Basically, when you create the document DO NOT set your iVar until the completion handler executes. Otherwise, you could be using a document in a partial state. In this case, use a helper, like this, in the completion handler.
- (void)_document:(UIManagedDocument*)doc canBeUsed:(BOOL)canBeUsed
{
dispatch_async(dispatch_get_main_queue(), ^{
if (canBeUsed) {
_document = doc;
// Now, the document is ready.
// Fire off a notification, or notify a delegate, and do whatever you
// want... you really should not use the document until it's ready, but
// as long as you leave it nil until it is ready any access will
// just correctly do nothing.
} else {
_document = nil;
// Do whatever you want if the document can not be used.
// Unfortunately, there is no way to get the actual error unless
// you subclass UIManagedDocument and override handleError
}
}];
}
And to initialize your document, something like...
- (id)initializeDocumentWithFileURL:(NSURL *)url
{
if (!url) {
url = [[[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask] lastObject];
url = [url URLByAppendingPathComponent:#"Default_Project_Database"];
}
UIManagedDocument *doc = [[UIManagedDocument alloc] initWithFileURL:url];
if (![[NSFileManager defaultManager] fileExistsAtPath:[doc.fileURL path]]) {
// The file does not exist, so we need to create it at the proper URL
[doc saveToURL:doc.fileURL forSaveOperation:UIDocumentSaveForCreating completionHandler:^(BOOL success) {
[self _document:doc canBeUsed:success];
}];
} else if (doc.documentState == UIDocumentStateClosed) {
[doc openWithCompletionHandler:^(BOOL success) {
[self _document:doc canBeUsed:success];
}];
} else {
// You only need this if you allow a UIManagedDocument to be passed
// in to this object -- in which case the code above that initializes
// the <doc> variable will be conditional on what was passed...
BOOL success = doc.documentState == UIDocumentStateNormal;
[self _document:doc canBeUsed:success];
}
}
The "pattern" above is necessary to make sure you do not use the document until it is fully ready for use. Now, that piece of code should be the only time you call saveToURL.
Note that by definition, the document.managedObjectContext is of type NSMainQueueConcurrencyType. Thus, if you know your code is running on the main thread (like all your UI callbacks), you do not have to use performBlock.
However, if you are actually doing loads in the background, consider..
- (void)backgroundLoadDataIntoDocument:(UIManagedDocument*)document
{
NSManagedObjectContext *moc = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
moc.parentContext = document.managedObjectContext;
[moc performBlock:^{
// Do your loading in here, and shove everything into the local MOC.
// If you are loading a lot of stuff from the 'net (or elsewhere),
// consider doing it in strides, so you deliver objects to the document
// a little at a time instead of all at the end.
// When ready to save, call save on this MOC. It will shove the data up
// into the MOC of the document.
NSrror *error = nil;
if ([moc save:&error]) {
// Probably don't have to synchronize calling updateChangeCount, but I do it anyway...
[document.managedObjectContext performBlockAndWait:^{
[document updateChangeCount:UIDocumentChangeDone];
}];
} else {
// Handle error
}
}];
}
Instead of parenting your background MOC to the mainMOC, you can parent it to the parentContext. Loading and then saving into it will put the changes "above" the main MOC. The main MOC will see those changes the next time it does a fetch operation (note the properties of NSFetchRequest).
NOTE: Some people have reported (and it also appears as a note in Erica Sadun's book), that after the very first saveToURL, you need to close, then open to get everything working right.
EDIT
This is getting really long. If you had more points, I'd suggest a chat. Actually, we can't do it through SO, but we could do it via another medium. I'll try to be brief, but please go back and reread what I posted, and pay careful attention because your code is still violating several tenants.
First, in viewDidLoad(), you are directly assigning your document to the result of calling openDatabaseAndUseBlock. The document is not in a usable state at that time. You do not want the document accessible until the completion handlers fire, which will not happen before openDatabaseAndUseBlock() returns.
Second, only call saveToURL the very first time you create your database (inside openDatabaseAndUseBlock()). Do not use it anywhere else.
Third. Register with the notification center to receive all events (just log them). This will greatly assist your debugging, because you can see what's happening.
Fourth, subclass UIManagedDocument, and override the handleError, and see if it is being called... it's the only way you will see the exact NSError if/when it happens.
3/4 are mainly to help you debug, not necessary for your production code.
I have an appointment, so have to stop now. However, address those issues, and here's on
In an iOS app, I'm running a fairly large script on a UIWebView using stringByEvaluatingJavaScriptFromString (large in terms of the length of the javascript string). There is a brief pause after calling the javascript causing other elements on the screen to hiccup for a moment.
Placing the javascript call in a function called in the background with self performSelectorInBackground breaks the application. Is there a safe way to call run this on a background thread or otherwise prevent the interface from pausing?
No, Webviews and the Webkit JavaScript engine are both single-threaded and cannot be used on a background thread.
A better option is to split up your JavaScript into discrete execution blocks and pipeline them using a JavaScript timer, like this (JS code, not Obj-C):
var i = 0;
var operation = function() {
switch (i) {
case 0:
//do first part of code
break;
case 1:
//do second part of code
break;
case 2:
//do third part of code
break;
etc...
}
//prepare to execute next block
i++;
if (i < TOTAL_PARTS) {
setTimeout(operation, 0);
}
};
operation();
That will prevent your script from blocking user interaction while it executes
Well, I was doing the same thing. I had to run a synchronous ajax request which was freezing my UI. So this is how I fixed it :
__block NSString *message;
dispatch_queue_t q = dispatch_queue_create("sign up Q", NULL);
dispatch_async(q, ^{
NSString *function = [[NSString alloc] initWithFormat: #"signup(\'%#\',\'%#\',\'%#\')",self.email.text,self.password.text,self.name.text];
dispatch_async(dispatch_get_main_queue(), ^{
NSString *result = [self.webView stringByEvaluatingJavaScriptFromString:function];
NSLog(#"%#",result);
if ([result isEqualToString:#"1"]) {
message = [NSString stringWithFormat:#"Welcome %#",self.name.text];
[self.activityIndicator stopAnimating];
[UIApplication sharedApplication].networkActivityIndicatorVisible = NO;
}
else {
message = [NSString stringWithFormat:#"%# is a registered user",self.name.text];
[self.activityIndicator stopAnimating];
[UIApplication sharedApplication].networkActivityIndicatorVisible = NO;
}
UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:#"Message" message:message delegate:self cancelButtonTitle:#"Okay" otherButtonTitles: nil];
[alertView show];
});
});
The logic is simple. Go to a new thread, and from within that, dispatch to the main queue and then do the JS work and everything worked like a charm for me...
Anything you do with a UIWebView must be done on the main thread. It's a UI element, so this is why performSelectorInBackground breaks your app.
You could try putting that call into an NSOperation. Since you are working with a UI element, be sure to use the [NSOperationQueue mainQueue].