How to correctly display a "progress" sheet modally while using Grand Central Dispatch to process something? - objective-c

I'm trying to display a sheet on a window containing a single progress bar, to show the progress of some long function running asynchronously using Grand Central Dispatch. I've almost got it, but can't get the sheet to appear to be in focus, probably because I haven't used runModalForWindow: or similar.
This is approximately what I'm doing at the moment, it happens as a result of a button press on the main window:
// Prepare sheet and show it...
[NSApp beginSheet:progressSheet modalForWindow:window modalDelegate:nil didEndSelector:NULL contextInfo:NULL];
[progressSheet makeKeyAndOrderFront:self];
[progressBar setIndeterminate:NO];
[progressBar setDoubleValue:0.f];
[progressBar startAnimation:self];
// Start computation using GCD...
dispatch_async(dispatch_get_global_queue(0, 0), ^{
for (int i = 0; i < 1000; i ++) {
// Do some large computation here
// ...
// Update the progress bar which is in the sheet:
dispatch_async(dispatch_get_main_queue(), ^{
[progressBar setDoubleValue:(double)i];
});
}
// Calculation finished, remove sheet on main thread
dispatch_async(dispatch_get_main_queue(), ^{
[progressBar setIndeterminate:YES];
[NSApp endSheet:progressSheet];
[progressSheet orderOut:self];
});
});
This works, except the main window is still in focus, the sheet is out of focus, and the progress bar doesn't animate (unless I use setUsesThreadedAnimation:YES on it).
The problem I think I'm having is that I'm not sure how to run the sheet modally without blocking the main thread before I start the asynchronous computation?

As stated by Brad, it should work.
To do a quick test, I created a sheet programmatically (normally, you would probably use a nib file, but they are hard to paste into this text). If I call the code below from a button in a normal Cocoa window, it works as expected. Notice that the text field on the sheet is first responder, and if you type on the keyboard while it is open, it will accept the input.
#define maxloop 1000
- (IBAction)startTask:(id)sender
{
// Prepare sheet and show it...
breakLoop = NO;
NSRect sheetRect = NSMakeRect(0, 0, 400, 114);
NSWindow *progSheet = [[NSWindow alloc] initWithContentRect:sheetRect
styleMask:NSTitledWindowMask
backing:NSBackingStoreBuffered
defer:YES];
NSView *contentView = [[NSView alloc] initWithFrame:sheetRect];
NSProgressIndicator *progInd = [[NSProgressIndicator alloc] initWithFrame:NSMakeRect(143, 74, 239, 20)];
NSTextField *inputField = [[NSTextField alloc] initWithFrame:NSMakeRect(145, 48, 235, 22)];
NSButton *cancelButton = [[NSButton alloc] initWithFrame:NSMakeRect(304, 12, 82, 32)];
cancelButton.bezelStyle = NSRoundedBezelStyle;
cancelButton.title = #"Cancel";
cancelButton.action = #selector(cancelTask:);
cancelButton.target = self;
[contentView addSubview:progInd];
[contentView addSubview:inputField];
[contentView addSubview:cancelButton];
[progSheet setContentView:contentView];
[NSApp beginSheet:progSheet
modalForWindow:self.window
modalDelegate:nil
didEndSelector:NULL
contextInfo:NULL];
[progSheet makeKeyAndOrderFront:self];
[progInd setIndeterminate:NO];
[progInd setDoubleValue:0.f];
[progInd startAnimation:self];
// Start computation using GCD...
dispatch_async(dispatch_get_global_queue(0, 0), ^{
for (int i = 0; i < maxloop; i++) {
[NSThread sleepForTimeInterval:0.01];
if (breakLoop)
{
break;
}
// Update the progress bar which is in the sheet:
dispatch_async(dispatch_get_main_queue(), ^{
[progInd setDoubleValue: (double)i/maxloop * 100];
});
}
// Calculation finished, remove sheet on main thread
dispatch_async(dispatch_get_main_queue(), ^{
[progInd setIndeterminate:YES];
[NSApp endSheet:progSheet];
[progSheet orderOut:self];
});
});
}
- (IBAction)cancelTask:(id)sender
{
NSLog(#"Cancelling");
breakLoop = YES;
}
Apologies for the ugly sheet, but apart from that this code works as expected, so the issue you are seeing is probably unrelated to GCD.

I had exactly the same problem. With some trial and error, I found the solution. Make sure your sheet's window is (a) an NSWindow not an NSPanel (this may not matter) and that the window has a Title Bar (which, as it's a sheet you're using) will not be displayed.
I turned the Title Bar off for that reason, but somehow it's required to correctly achieve focus. Ticking the Title Bar checkbox gives my progress bar sheet focus.

Related

Disable focus on second screen in objective-c

i have an app and i try it on multi screen. I have two screens and i want to disable focus on the second screen when the both sreens are enterFullscreen, i want to force the focus on the main screen.
I tried solutions i found here but doesn't change anything.
following code shows how i enterFullScreen for my mainWindow and my second Window
[self.window.contentView enterFullScreenMode:[[NSScreen screens] firstObject] withOptions:nil];
[windowArray insertObject:self.window atIndex:0];
NSRect screenRect;
NSArray *screenArray = [NSScreen screens];
for (NSInteger index = 1; index < [screenArray count]; index++)
{
NSScreen *screen = [screenArray objectAtIndex: index];
screenRect = CGRectMake(0, 0, screen.frame.size.width , screen.frame.size.height);
NSWindow *window = [[NSWindow alloc] initWithContentRect:screenRect styleMask:NSBorderlessWindowMask backing:NSBackingStoreBuffered defer:NO screen:screen];
[window.contentView setWantsLayer:YES];
window.contentView.layer.backgroundColor = [NSColor blackColor].CGColor;
[window.contentView enterFullScreenMode:[[NSScreen screens] objectAtIndex:index] withOptions:nil];
[windowArray addObject:window];
}
When both screens are in fullScreen mode, when i click on the second window i have the focus on the second window (normal event) but i want to disable that and force to put the focus on my main Window. I tried to disable mouse event on the second screen but.. not working.
If someone can help me ! thanks in advance
I don't know if it's the best answer for you, but you can iterate through all the subviews of the content view, and call setEnabled: on all of them. You can see a better and complete answer here: https://stackoverflow.com/a/16336624/2923506
for (NSView *object in [self.window.contentView subviews]) {
// Check it out the object
// set enable to NO [(id)object setEnabled:NO];
}

Wait until UI has updated before removing UIActivityIndicator

I am having an issue with an iPad app I am developing. I have the following code:
CFTimeInterval startTime = CFAbsoluteTimeGetCurrent();
[self showSpinnerWithMessage:#"Loading settings..."]; // Shows a UIActivityIndicator on screen
dispatch_async(dispatch_get_global_queue(0, 0), ^
{
//Updating items on screen here
[label1 setText:#"Test1"];
[label2 setText:#"Test3"];
//...
CFTimeInterval difference = CFAbsoluteTimeGetCurrent() - startTime;
if (difference < MINIMUM_INDICATOR_SECONDS)
{
[NSThread sleepForTimeInterval:MINIMUM_INDICATOR_SECONDS - difference];
}
dispatch_async(dispatch_get_main_queue(), ^(void)
{
[self removeSpinner]; // Removes UIActivityIndicator from screen
});
};
The problem is that sometimes the UI takes a while to update, and in these particular instances the spinner (UIActivityIndicator) goes away before the UI has actually updated. I realize this is because the items on screen do not actually update until the next run loop so my question is, how can make my [self removeSpinner] call wait until the UI has updated?
Try putting the UI-updating code in the block with removeSpinner.
dispatch_async(dispatch_get_main_queue(), ^(void)
{
[label1 setText:#"Test1"];
[label2 setText:#"Test3"];
[self removeSpinner]; // Removes UIActivityIndicator from screen
});
Put a [self.view setNeedsDisplay]; just before the [self removeSpinner].

Program crashes when segueing back and dispatch queue has animation method

Ok so i have a uitableview and when an item is selected it segues to a new view controller to show an image (inside of a uiscrollview). The image begins downloading in a dispatch_queue from prepareforsegue. To be clear I am doing all UI updates in the main queue. The problem is that if i hit the back button quick enough then my program crashes with an exc_bad_address. I think the problem has to deal with zoomToRect animated:YES because when i set animated to NO i cant get it to crash. Plus the call stack below deals with animation. What is the best way to go about fixing this and is there a better way to get what i need done?
Also when debugging the problem print 'Block completed' and then crashes shortly after.
stack trace
Here is the method called. It is called in a setter of the destination controller in prepareForSegue.
-(void) updateDisplay {
dispatch_queue_t queue = dispatch_queue_create("Load Flickr Photo", NULL);
UIActivityIndicatorView *spinner = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleGray];
self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithCustomView:spinner];
[spinner startAnimating];
dispatch_async(queue, ^{
UIImage *image = [FlickrFetcher imageForPhoto:self.currentPhoto format:FlickrPhotoFormatLarge];
dispatch_async(dispatch_get_main_queue(), ^{
self.navigationItem.rightBarButtonItem = nil;
self.imageView.image = image;
self.title = [FlickrFetcher titleForPhoto:self.currentPhoto];
CGAffineTransform transform = CGAffineTransformMakeScale(1.0, 1.0);
self.imageView.transform = transform;
self.imageView.frame = CGRectMake(0, 0, self.imageView.image.size.width, self.imageView.image.size.height);
self.scrollView.maximumZoomScale = 4.0;
self.scrollView.minimumZoomScale = .2;
self.scrollView.zoomScale = 1;
self.scrollView.contentSize = self.imageView.bounds.size;
//i think problem is here
[self.scrollView zoomToRect:self.imageView.frame animated:YES];
NSLog(#"Block completed");
});
});
}
I think a potential problem could be that while your block retains your self the view controller while its active in queue...
If you send -zoomToRect:animated:NO, the block will still be active and blocking on the call which ensures that all the objects are still valid in memory.
If you send -zoomToRect:animated:YES, the block will exit potentially generating a race condition where your view controller would be released since storyboards will also release your view controller when you go back in a segue leaving it with an effective retain count of zero.

How to use UIActivityIndicatorView on custom UIButton?

I want to use UIActivityIndicatorView on my custom UIButton.
Here is my code:
if (sender.tag == 1)
{
// Start animating
activityIndicator.hidden = NO;
[activityIndicator startAnimating];
// Check if the network is available
if ([self reachable]) {
// Stop animating
activityIndicator.hidden = YES;
[activityIndicator stopAnimating];
}
}
What I want to do here is:
Once the user touch the button, I want to start the ActivityIndicatior while Reachable checking the network availability. Once it done pass it to the next view.
Update
UIActivityIndicator is on top of my custom UIButton. It build successfully, but ActivityIndicator is not showing when I touch the button.
I'm sure you had your answer since then.
After 4 years, language has changed to Swift, but I had the same problem : UIActivityIndicatorView is not showing when I add it through :
self.myButton.addSubview(self.myIndicatorView)
I had to climb a level up :
self.myButton.superview!.addSubview(self.myIndicatorView)
Assuming everything else is correct in your project, the problem here is that you're showing and hiding an activity indicator in the same event loop, without giving a pause to draw it. Let me explain:
If you have the code:
UIView* view = someView;
view.backgroundColor = [UIColor redColor];
// various synchronous operations
view.backgroundColor = [UIColor yellowColor];
The view will only ever have a background color of yellow.
To answer your question, you probably want to animate the spinner, do some asynchronous operation, then stop the animation. The key being to not stall on the main event loop while your asynchronous task is running. If your comfortable with blocks it would look something like this:
if (sender.tag == 1) {
// Start animating
activityIndicator.hidden = NO;
[activityIndicator startAnimating];
// Check if the network is available
[self checkReachableWithCallback:^{
// Stop animating
activityIndicator.hidden = YES;
[activityIndicator stopAnimating];
}];
}

forcing a button to swap the images

I'm working on a matching program for iPad and when a user selects a button an image is "uncovered" and then when the user selects a second button,another image is uncovered. I then programmatically check for a match and if not, revert both button images back to their initial state.
This is working fine except when a match is NOT made, the switch happens so fast that you do not have time to see what you have "uncovered". I tried to make it sleep but the image doesn't ever swap to the uncovered state... Thoughts?
The code for this is as follows:
//Take action on the tap of one of the buttons
if(isFirstSelection)
{
firstSelection = [(UIButton *)sender tag];
tempImageItem = [tileArray objectAtIndex:firstSelection];
tempImage = [tempImageItem tileImage];
firstSelectionName = [[NSString alloc] initWithString:[tempImageItem tileName]];
[(UIButton *)sender setImage:tempImage forState:UIControlStateNormal];
tempButton = sender;
isFirstSelection = NO;
}else{
secondSelection = [(UIButton *)sender tag];
tempImageItem = [tileArray objectAtIndex:secondSelection];
tempImage = [tempImageItem tileImage];
secondSelectionName = [[NSString alloc] initWithString:[tempImageItem tileName]];
[(UIButton *)sender setImage:tempImage forState:UIControlStateNormal];
//Two game pieces have been removed so check to see if they are a match
if([firstSelectionName isEqualToString:secondSelectionName])
{
//Match found
//do something
}else{
**//NO MATCH FOUND
[NSThread sleepForTimeInterval:3];
//Display the checker board pieces again
[(UIButton *)sender setImage:[UIImage imageNamed:#"originalImage"] forState:UIControlStateNormal];**
}
//Reset isFirstSelection Flag to YES for next selection
isFirstSelection = YES;
}
From what i understand you want to put the button's original image after 3 seconds so the user has the time to see what happened.
You should look at the NSTimer class to trigger that code
[(UIButton *)sender setImage:[UIImage imageNamed:#"originalImage"] forState:UIControlStateNormal];
in 3 seconds from now.
For "run this code in X seconds" I prefer:
[self performSelector:#selector(spinWheel:) withObject:[NSNumber numberWithUnsignedInt:0] afterDelay:delay];
no need to muck around with timer objects.