I want to start a task that runs on another thread "just in case it is needed" to minimize the time the user will have to wait on it later. If there is time for it to complete, the user will not have to wait, but if it has not completed then waiting would be necessary.
Something like, opening a database in viewDidLoad: that will be needed when and if the user pushes a button on the screen. If I wait to open the database until the user actually pushes the button there is a lag. So I want to open it early. Since I don't know how long it will take to open and I don't know how long until the user hits the button, I need a way of saying, if that other task has not completed yet then wait, otherwise just go ahead.
For example:
#implementation aViewController
- (void) viewDidLoad {
[self.dbManager openOrCreateDbWithCompletionHandler: ^(NSError *err) {
if( err ) NSLog( #"There was a problem opening the database" );
}];
}
- (IBAction) goButtonTouched: (id) sender {
// Wait here until the database is open and ready to use.
if( ???DatabaseNotAvailableYet??? ) {
[self putSpinnerOnScreen];
???BlockProgressHereUntilDatabaseAvailable???
[self takeSpinnerOffScreen];
}
// Use the database...
NSManagedObjectContext *context = [self theDatabaseContext];
// Build the search request for the attribute desired
NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName: NSStringFromClass([Destinations class])];
request.predicate = [NSPredicate predicateWithFormat: #"dId == %#", sender.tag];
request.sortDescriptors = nil;
// Perform the search
NSError *error = nil;
NSArray *matches = [context executeFetchRequest: request error: &error];
// Use the search results
if( !matches || matches.count < 1 ) {
NSLog( #"Uh oh, got a nil back from my Destination fetch request!" );
UIAlertView *alert = [[UIAlertView alloc] initWithTitle: #"No Info"
message: #"The database did not have information for this selection"
delegate: nil
cancelButtonTitle: #"OK"
otherButtonTitles: nil];
[alert show];
} else {
MyOtherViewController *movc = [[MyOtherViewContoller alloc] init];
movc.destDetails = [matches lastObject];
[self.navigationController pushViewController: movc animated: YES];
}
}
#end
My hope is that there is never a spinner on the screen and never any delay for the user but, since I don't know how long it will take for the database connection to be established, I have to be prepared for it not being ready when the user pushes the button.
I can't use the call back for when openOrCreateDbWithCompletionHandler: completes since I don't want to do anything then, only when the user pushes the button.
I thought about using a semaphore but it seems like I would only signal it once (in the completion handler of the openOrCreateDbWithCompletionHandler: call) but would wait on it every time a button was pushed. That seems like it would only work for the first button push.
I thought about using dispatch_group_async() for openOrCreateDbWithCompletionHandler: then dispatch_group_wait() in goButtonTouched: but since openOrCreateDbWithCompletionHandler: does its work on another thread and returns immediately, I don't think the wait state would be set.
I can simply set a my own flag, something like before the openOrCreateDbWithCompletionHandler:, self.notOpenYet = YES;, then in its completion handler do self.notOpenYet = NO;, then in goButtonTouched: replace ???DatabaseNotAvailableYet??? with self.notOpenYet, but then how do I block progress on its state? Putting in loops and timers seems kludgy since I don't know if the wait will be nanoseconds or seconds.
This seems like a common enough situation, I am sure that you have all done this sort of thing commonly and it is poor education on my side, but I have searched stackOverflow and the web and have not found a satisfying answer.
I think, blocking execution is a bad habit unless you are building your own event loop, which is rarely necessary. You also don't need to do any GCD stuff in your case. Just get a feeling for async.
The following should work:
#implementation aViewController
- (void) viewDidLoad {
self.waitingForDB = NO;
self.databaseReady = NO;
[self.dbManager openOrCreateDbWithCompletionHandler: ^(NSError *err) {
if( err ){
NSLog( #"There was a problem opening the database" )
}else{
[self performSelectorOnMainThread:#selector(handleDatabaseReady) withObject:nil waitUntilDone:NO];
};
}];
}
- (void)handleDatabaseReady{
self.databaseReady = YES;
if(self.waitingForDB){
[self takeSpinnerOffScreen];
[self go];
}
}
- (IBAction) goButtonTouched: (id) sender {
// Wait here until the database is open and ready to use.
if( !self.databaseReady ) {
self.waitingForDB = YES;
[self putSpinnerOnScreen];
else{
[self go];
}
}
-(void)go{
// Use the database...
NSManagedObjectContext *context = [self theDatabaseContext];
// Build the search request for the attribute desired
NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName: NSStringFromClass([Destinations class])];
request.predicate = [NSPredicate predicateWithFormat: #"dId == %#", sender.tag];
request.sortDescriptors = nil;
// Perform the search
NSError *error = nil;
NSArray *matches = [context executeFetchRequest: request error: &error];
// Use the search results
if( !matches || matches.count < 1 ) {
NSLog( #"Uh oh, got a nil back from my Destination fetch request!" );
UIAlertView *alert = [[UIAlertView alloc] initWithTitle: #"No Info"
message: #"The database did not have information for this selection"
delegate: nil
cancelButtonTitle: #"OK"
otherButtonTitles: nil];
[alert show];
} else {
MyOtherViewController *movc = [[MyOtherViewContoller alloc] init];
movc.destDetails = [matches lastObject];
[self.navigationController pushViewController: movc animated: YES];
}
}
#end
Performing the call to handleDatabaseReady on the main thread guarantees that no race conditions in setting/reading your new properties will appear.
I'd go with the flag. You don't want to block the UI, just show the spinner and return from the goButtonTouched. However, you do need to cancel the spinner, if it is active, in openOrCreateDbWithCompletionHandler:.
This is rather a simple scenario. You make a method that does the stuff. Lets call it doStuff. From main thread, you call performSelectorInBackgroundThread:#selector(doStuff). Do not enable the button by default. Enable it at the end of doStuff so that user won't tap on it until you are ready. To make it more appealing, you can place a spinner in the place of the button and then replace it with the button when doStuff completes.
There are a number of classes and APIs you can use to achieve this kind of thing. You can use NSThread with synchronization primitives like semaphores and events to wait for it to finish when the user actually presses the button. You can use an NSOperation subclass (with an NSOperationQueue), and you can use GCD queues.
I would suggest you take a look at some the information in the Concurrency Programming Guide from Apple.
In your case you would probably be best served adding the operation to a GCD background queue using dispatch_async in combination with a semaphore which you can wait on when the user taps the button. You can check out the question "How do I wait for an asynchronously dispatched block to finish?" for an example.
Related
I have a weird issue with Zebra printer. On the big picture, I have codes to fetch items to print one by one from the queue. So if when the printing initiated, and there's 3 items on the queue, the code will loop and fetch the first data in the queue, send it to the printer, and delete the first data in the queue. Sort of like dequeueing.
The problem is, if it's the code that looping and sending the data directly to the printer, the printer will only print the first item. The next item is gone, even though NSLog shows that the printer connection opened, data sent, printing successful, and printer connection closed, for every single item.
But if each time the code print one label, and then the app shows message box like "press OK to print next label", and then the user tap the OK button, it can print the second and the rest of the label after every tap of the button.
I have then tried to emulate this. I've tried to use timer to "push the button programmatically" [btnPrint sendActionsForControlEvents:UIControlEventTouchUpInside], I also use timer to call the function directly, or giving delay to the thread, but none works. It has to be initiated from a button which tapped from human touch. I don't know why.
Here's the code:
// main function to print
-(void) printLabel {
if ([dataToPrint count] > 0) {
[self printWithData:[dataToPrint objectAtIndex:0]];
}
}
-(void)printWithData:(NSString*) data;
{
NSString *zplString = data;
// do something with zplString
NSLog(#"Sending data to printer");
printHandler = [[PrintingHandler alloc] init];
[printHandler setDelegate:self];
[printHandler initialize];
[printHandler printToSerial:bluetoothSerialNumber withData:zplString];
}
// delegate to call if the print is success
-(void) printIsSuccess
{
[dataToPrint removeObjectAtIndex:0];
// in here, I just use sleep code instead of button tap emulation to avoid unnecessarily too long code
[NSThread sleepForTimeInterval:2.0f];
[self printLabel];
}
// this is method inside PrintingHandler class that get called by PrintingHandler (printToSerial:)
-(void) printLabelWithData:(NSString*) zplData toPrinter:(NSString*) serial withSender:(id) sender
{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^ {
// Instantiate connection to Zebra Bluetooth accessory
id<ZebraPrinterConnection, NSObject> thePrinterConn = [[MfiBtPrinterConnection alloc] initWithSerialNumber:serial];
// Open the connection - physical connection is established here.
BOOL success = [thePrinterConn open];
NSError *error = nil;
// Send the data to printer as a byte array.
success = success && [thePrinterConn write:[zplData dataUsingEncoding:NSUTF8StringEncoding] error:&error];
[NSThread sleepForTimeInterval:1.0f];
//Dispath GUI work back on to the main queue!
dispatch_async(dispatch_get_main_queue(), ^{
if (success != YES || error != nil) {
[delegate printFailed];
UIAlertView *errorAlert = [[UIAlertView alloc] initWithTitle:#"Error" message:[error localizedDescription] delegate:nil cancelButtonTitle:#"Ok" otherButtonTitles:nil];
[errorAlert show];
[errorAlert release];
}
else if (success != YES) {
NSLog(#"Print is not success, but no error raised");
[delegate printSuccess];
}
else
{
NSLog(#"Print is success");
[delegate printSuccess];
}
});
// Close the connection to release resources.
NSLog(#"printer connection closed");
[thePrinterConn close];
[thePrinterConn release];
});
}
Sorry, I've found the solution. The problem is it's too fast between opening the connection to the printer and sending data to the printer. I was putting some delay, but I put the delay at wrong position.
So the step to print currently is:
Open bluetooth printer connection
Send data
Delay
Close printer connection
When the correct one should be:
Open bluetooth printer connection
Delay
Send data
Close printer connection
Here I put the answer so this can help other people with same problem.
-(void) printLabelWithData:(NSString*) zplData toPrinter:(NSString*) serial withSender:(id) sender
{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^ {
// Instantiate connection to Zebra Bluetooth accessory
id<ZebraPrinterConnection, NSObject> thePrinterConn = [[MfiBtPrinterConnection alloc] initWithSerialNumber:serial];
// Open the connection - physical connection is established here.
BOOL success = [thePrinterConn open];
NSError *error = nil;
[NSThread sleepForTimeInterval:1.0f]; // this is the important one
// Send the data to printer as a byte array.
success = success && [thePrinterConn write:[zplData dataUsingEncoding:NSUTF8StringEncoding] error:&error];
//Dispath GUI work back on to the main queue!
dispatch_async(dispatch_get_main_queue(), ^{
if (success != YES || error != nil) {
[delegate printFailed];
UIAlertView *errorAlert = [[UIAlertView alloc] initWithTitle:#"Error" message:[error localizedDescription] delegate:nil cancelButtonTitle:#"Ok" otherButtonTitles:nil];
[errorAlert show];
[errorAlert release];
}
else if (success != YES) {
NSLog(#"Print is not success, but no error raised");
[delegate printSuccess];
}
else
{
NSLog(#"Print is success");
[delegate printSuccess];
}
});
// Close the connection to release resources.
NSLog(#"printer connection closed");
[thePrinterConn close];
[thePrinterConn release];
});
}
From what I've been reading about the largely undocumented NSAsynchronousFetchRequest, it is supposed to be cancelable. In Apple's video "What's New in Core Data" from WWDC 2014, there is an example of it being done (right around 17:40). But nowhere have I found how this is supposed to be done.
I've tried setting it up to cancel a fetch when a new fetch comes in, but I have been, seemingly, unsuccessful in getting this to work. The reason I say "seemingly" is because when I debug the code, it hits the cancel method of NSAsyncronousFetchResult's NSProgress property (and the property is not nil). However, after several previous fetches have been "cancelled" the app freezes for approximately the amount of time it would have taken to perform all the fetches. So, it doesn't seem like the fetches are being canceled. Here is what I am trying to cancel the fetch:
if (self.asyncFetchResult) {
[self.asyncFetchResult.progress cancel];
}
NSFetchRequest* fetchRequest = [[NSFetchRequest alloc] initWithEntityName:#"OfflineFeature"];
fetchRequest.predicate = [NSPredicate predicateWithFormat:#"layers.layerName in %# AND xMax >= %lf AND xMin <= %lf AND yMax >= %lf AND yMin <=%lf", allLayerNames, bufferedEnvelope.xmin,bufferedEnvelope.xmax,bufferedEnvelope.ymin,bufferedEnvelope.ymax];
NSAsynchronousFetchRequest* asyncFetchRequest = [[NSAsynchronousFetchRequest alloc] initWithFetchRequest:fetchRequest completionBlock:^(NSAsynchronousFetchResult* result) {
if (![result.progress isCancelled]) {
allFeatures = result.finalResult;
dispatch_async(dispatch_get_main_queue(), ^{
//Bunch of code to use the results
});
}
}];
MVAppDelegate* appDelegate = (MVAppDelegate*)[[UIApplication sharedApplication] delegate];
__weak typeof(self) weakSelf = self;
[appDelegate.managedObjectContext performBlock:^{
NSProgress* progress = [NSProgress progressWithTotalUnitCount:1];
[progress becomeCurrentWithPendingUnitCount:1];
NSError* error;
weakSelf.asyncFetchResult = [appDelegate.managedObjectContext executeRequest:asyncFetchRequest error:&error];
if (error) {
NSLog(#"Error performing asynchronous fetch request.\n%#", error);
}
[progress resignCurrent];
}];
I would appreciate any thoughts on what I'm doing wrong or if there's something else I could try that may be more appropriate. Thanks in advance.
I Added ios-8's new touchID API to my app.
It usually works as expected, BUT when entering app while my finger is already on home-button - API's success callback is called but pop-up still appears on screen. after pressing CANCEL UI becomes non-responsive.
I also encountered the same issue, and the solution was to invoke the call to the Touch ID API using a high priority queue, as well as a delay:
// Touch ID must be called with a high priority queue, otherwise it might fail.
// Also, a dispatch_after is required, otherwise we might receive "Pending UI mechanism already set."
dispatch_queue_t highPriorityQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 0.75 * NSEC_PER_SEC), highPriorityQueue, ^{
LAContext *context = [[LAContext alloc] init];
NSError *error = nil;
// Check if device supports TouchID
if ([context canEvaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics error:&error]) {
// TouchID supported, show it to user
[context evaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics
localizedReason:#"Unlock Using Touch ID"
reply:^(BOOL success, NSError *error) {
if (success) {
// This action has to be on main thread and must be synchronous
dispatch_async(dispatch_get_main_queue(), ^{
...
});
}
else if (error) {
...
}
}];
}
});
When testing our app, we found a delay of 750ms to be optimal, but your mileage may vary.
Update (03/10/2015): Several iOS developers, like 1Password for example, are reporting that iOS 8.2 have finally fixed this issue.
Whilst using a delay can potentially address the issue, it masks the root cause. You need to ensure you only show the Touch ID dialog when the Application State is Active. If you display it immediately during the launch process (meaning the Application is still technically in an inactive state), then these sorts of display issues can occur. This isn't documented, and I found this out the hard way. Providing a delay seems to fix it because you're application is in an active state by then, but this isn't guarenteed.
To ensure it runs when the application is active, you can check the current application state, and either run it immediately, or when we receive the applicationDidBecomeActive notification. See below for an example:
- (void)setup
{
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(applicationDidBecomeActive:)
name:UIApplicationDidBecomeActiveNotification
object:nil];
}
- (void)dealloc
{
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
// We need to be in an active state for Touch ID to play nice
// If we're not, defer the presentation until we are
if([UIApplication sharedApplication].applicationState == UIApplicationStateActive)
{
[self presentTouchID];
}
else
{
__weak __typeof(self) wSelf = self;
_onActiveBlock = ^{
[wSelf presentTouchID];
};
}
}
-(void)applicationDidBecomeActive:(NSNotification *)notif
{
if(_onActiveBlock)
{
_onActiveBlock();
_onActiveBlock = nil;
}
}
- (void)presentTouchID
{
_context = [[LAContext alloc] init];
_context.localizedFallbackTitle = _fallbackTitle;
[_context evaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics
localizedReason:_reason
reply: ^(BOOL success, NSError *authenticationError)
{
// Handle response here
}];
}
This accepted answer does not address the underlying cause of the problem: invoking evaluatePolicy() twice, the second time while the first invocation is in progress. So the current solution only works sometimes by luck, as everything is timing dependent.
The brute-force, straightforward way to work around the problem is a simple boolean flag to prevent subsequent calls from happening until the first completes.
AppDelegate *delegate = [[UIApplication sharedApplication] delegate];
if ( NSClassFromString(#"LAContext") && ! delegate.touchIDInProgress ) {
delegate.touchIDInProgress = YES;
LAContext *localAuthenticationContext = [[LAContext alloc] init];
__autoreleasing NSError *authenticationError;
if ([localAuthenticationContext canEvaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics error:&authenticationError]) {
[localAuthenticationContext evaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics localizedReason:kTouchIDReason reply:^(BOOL success, NSError *error) {
delegate.touchIDInProgress = NO;
if (success) {
...
} else {
...
}
}];
}
I started getting the "Pending UI mechanism already set." error mentioned as well, so I decided to see if other apps were affected. I have both Dropbox and Mint set up for Touch ID. Sure enough Touch ID wasn't working for them either and they were falling back to passcodes.
I rebooted my phone and it started working again, so it would seem the Touch ID can bug out and stop working. I'm on iOS 8.2 btw.
I guess the proper way to handle this condition is like those apps do and fallback to password / passcode.
In my modal UI there is a "DONE" button linked with IBAction -done:, it will upload a text to (lets say Dropbox server). Its code looks like this
- (IBAction)done:(id)sender {
// must contain text in textview
[[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:YES];
if (![_textView.text isEqualToString:#""]) {
// check to see if we are adding a new note
if (!self.note) {
DBFile *newNote = [[DBFile alloc] init];
newNote.root = #"dropbox";
self.note = newNote;
}
_note.contents = _textView.text;
_note.path = _filename.text;
// - UPLOAD FILE TO DROPBOX - //
NSLog(#"Initializing URL...");
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{
NSURL *url = [Dropbox uploadURLForPath:self.note.path];
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url];
[request setHTTPMethod:#"PUT"];
NSData *noteContents = [self.note.contents dataUsingEncoding:NSUTF8StringEncoding];
NSLog(#"Creating session task...");
NSURLSessionUploadTask *uploadTask = [self.session uploadTaskWithRequest:request
fromData:noteContents
completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
NSHTTPURLResponse *resp = (NSHTTPURLResponse *) response;
if (!error && resp.statusCode == 200) {
NSLog(#"OK");
dispatch_async(dispatch_get_main_queue(), ^{
[self.delegate noteDetailsViewControllerDoneWithDetails:self];
});
} else {
NSLog(#"Status code: %d", resp.statusCode);
}
}];
[uploadTask resume];
});
} else {
UIAlertView *noTextAlert = [[UIAlertView alloc] initWithTitle:#"No text"
message:#"Need to enter text"
delegate:nil
cancelButtonTitle:#"Ok"
otherButtonTitles:nil];
[noTextAlert show];
}
}
The delegate method noteDetailsViewControllerDoneWithDetails: of this class is look like this
-(void)noteDetailsViewControllerDoneWithDetails:(NoteDetailsViewController *)controller{
// refresh to get latest
[self dismissViewControllerAnimated:YES completion:nil];
[self notesOnDropbox];}
(notesOnDropbox is a time-consuming task). When DONE button is tapped, I expect this modal VC/UI to dismiss immediately and it fetches data on background (by notesOnDropbox method). However, when I try tapping DONE button, my UI stop responding for about seconds, after that the modal UI is dismissed. I cannot figure out where I misuse the GCD. Please help me.
if you want to dismiss your modal VC/UI immediately, just ask the delegate to dismiss,
like is:
- (IBAction)done:(id)sender {
[self.delegate noteDetailsViewControllerDoneWithDetails:self];
// ...
}
In your sample code,
you do the dismiss action after the upload task completed, but the upload task is asynchronous.
and you ask the delegate to dismiss use GCD dispatch_async, this is asynchronous task, too.
After all, you have to consider the what time to do upload, who to do upload task and what time to invoke notesOnDropbox.
First, if notesOnDropbox is a time-consuming task, then you should not be performing it on the main thread (as you are doing). If it is sufficiently time-consuming and you do it on the main thread, the WatchDog process will kill your app dead right before the user's eyes.
Second, there is no need to get off the main thread to do an upload. If you use NSURLSession correctly, it will be asynchronous.
Your code only calls noteDetailsViewControllerDoneWithDetails when the whole upload task is completed, because that's how you wrote your code. Actually, the situation seems worse. If the upload task has any kinds of problems, noteDetailsViewControllerDoneWithDetails will never be called.
You need to call noteDetailsViewControllerDoneWithDetails as soon as possible, and then think about what you are going to do when the upload fails - which might easily happen a long time later.
I have created a class NetCalculator which I am calling when a button is pressed. The method calculate network it gets 2 NSStrings and returns an id object (either "Network" object or "UIAlertView". Then I am checking which object is and I present the data. When I am using the UIAlertView the app is crashing after showing 2-3 alerts.
Any ides why this happens? On terminal it doesnt show any error just some random hexadecimal.
-(IBAction)calculate:(id)sender {
id result;
Network *network = [[Network alloc]init];
NetCalculator *netCalculated = [[NetCalculator alloc] init];
result = [netCalculated calculateNetworkWithIP:ipLabel.text andSubnet:subnetLabel.text];
if([result isKindOfClass:[Network class]]){
network = result;
NSLog(#"network %#",network.networkIP);
}
else if([result isKindOfClass:[UIAlertView class]]) {
UIAlertView *alert;
alert = result;
[alert show];
}
};
Your code is quite strange to me. Your method calculateNetworkWithIP could return a Network result or a UIAlertView result. I wouldn't follow such an approach.
If the problem relies on memory you should show us hot that method is implemented.
Anyway, I would propose some changes (The following code does not take into account ARC or non ARC code). In particular, I would modify the calculateNetworkWithIP to return a Network result. An error will populated if a problem arises and it is passed as an argument.
- (Network*) calculateNetworkWithIP:(NSString *)ip subnet:(NSString*)subnet error:(NSError**)error
If all is ok, the result would be be a Network and so print it or reused it somewhere. Otherwise an NSError would be returned and based on that create and show an alert view.
So, here pseudo code to do it.
NetCalculator *netCalculated = [[NetCalculator alloc] init];
NSError* error = nil;
Network* networkResult = [netCalculated calculateNetworkWithIP:ipLabel.text subnet:subnetLabel.text error:&error];
if(error != nil) {
// create and show an alert view with the error you received
} else {
// all ok so, for example, save the result in a instance variable
}
To follow a similar approach you can take a look at why is "error:&error" used here (objective-c).