I'm using the following code to display a page number, when the user switches pages in my iPad app. The number is supposed to show up in a nice transparent gray pane (similar to the "Build Succeeded" message in XCode). However, the image view only shows up about 5% of the time. (I haven't added the code to put a number in the pane yet). The NSLog() message appears every time. I've also set a breakpoint and stepped through, and the UIImageView code is getting called. Sadly, I can't step into the framework to see what it's doing.
The variables used are all locals or constants, so I doubt it has anything to do with a race condition. Also, I can wait minutes between clicks and not get an image, or press the button rapidly and get multiple stacked images (they're transparent, so it's easy to notice a stack).
I tried running a setNeedsDisplay on self.view after the addSubview: (even though I'm pretty sure addSubview: does it), but that didn't do anything.
Calling displayPageNumber: directly, without threading, doesn't make it appear consistently either.
Any ideas?
#define PageDisplayTime 0.5
#define PageDisplayImageName #"PageIndicator.png"
#define PageDisplayImage [UIImage imageNamed: PageDisplayImageName]
...
[NSThread detachNewThreadSelector: #selector(displayPageNumber:)
toTarget: self withObject: index];
...
- (void) displayPageNumber: (NSNumber*) _pageIndex
{
NSLog(#"Page Number: '%d'.", [_pageIndex integerValue] + 1);
NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init];
UIImageView* glassPaneView = [[UIImageView alloc] initWithImage: PageDisplayImage];
glassPaneView.center = CGPointMake(300.0, 300.0);
[self.view addSubview: glassPaneView];
[NSThread sleepForTimeInterval: PageDisplayTime];
[glassPaneView removeFromSuperview];
[glassPaneView release];
[pool release];
}
AFAIK all UI coding must be done on the main thread. doing it on the background thread will at best have unpredictable results, at worse will crash.
Calling the method directly (thus in the main thread) doesn't work because the sleep just stops the current thread, so it is not able to do anything including drawing until you remove the view, thus preventing it from showing.
You have to refactor the method into multiple methods called on the main thread. The first just adds the subview and sets a timer, the timer handling method then removes the subview.
I figured out the issue. In case others run into this, here it is:
The addSubview: / removeFromSuperview pair with the sleepForTimeInterval: between them are basically useless. I knew sleeping blocked the thread, but apparently this thread needs to be unblocked to update the view. I assumed (incorrectly it appears) that the view update happened on the main thread.
Here's my replacement solution:
unhide the subview in the main thread and call detachNewThreadSelector: with a message that sleeps for X seconds and then hides the subview.
And I'm going to add an NSLocked counter that gets incremented on unhide, and decremented in the hider message, but only hides when the counter is down to 0. This will allow repeated activations to extend the duration.
Related
I have a controller that uses an animated UIImageView to display a sequence of 30 512 x 512 frames. When I run the application the view quickly does the following.
- (void)viewDidLoad {
[super viewDidLoad];
[[self imageView] setAnimationImages:[[self dataModel] framesForLOOP]];
[[self imageView] setAnimationDuration:2.5];
[[self imageView] setAnimationRepeatCount:1];
[[self imageView] startAnimating];
NSLog(#"MARKER_001");
}
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
NSLog(#"MARKER_002");
}
This all works fine but what I am trying to work out is that after viewDidLoad: is called there is a 2 second delay before viewDidAppear: is called (between MARKER_001 and MARKER_002).
I was thinking there might be a delay setting up the frames NSArray or after calling setAnimationImages: or maybe after startAnimating.
Is there anyway to reduce/remove this delay, I would prefer to preload the animation at startup and take a hit there rather than having the delay when the viewController loads as it makes the button that fires the segue to instantiate the new controller feel laggy.
Just a few ideas:
reduce the pain - do you need 30x512x512?
distribute the pain - load the first image on viewWillAppear, kick off an operation to load the others and update the animation images as new images are ready (can supply code e.g. if needed)
move the pain - prepare the array of UIImages in app init.
dig deeper - let's have a look at the framesForLoop method, maybe there's some more opportunity to reduce/distribute/move the pain in there.
Note that even if you call startAnimating in viewDidLoad, the animation won't start there. You can't get an animation running in a controller which has not been not displayed yet.
You should call the startAnimating in viewDidAppear instead.
Two seconds delay between these two methods is not anything strange. The delay can be much longer or shorter, depending on what happens inside your application.
Measuring the time between two methods which are not connected doesn't make sense. What about measuring how much time the individual methods take?
If anything is laggy, you should probably paste all the code that happens between the user action and the moment when everything is displayed and the lag happens.
I have the variant of the following codes, triggered by a user input.
NSLog(#"WHY YOU iOS!!!");
[scanButton setSelected:YES];
[overlayImageView setImage:image];
[overlayView setHidden:YES];
The thing is that the above routine executes with seemingly random delay from five seconds to more than a minute.
Sometimes, they don't execute at all, except, mysteriously, the NSLog method. If I set a breakpoint at the NSLog line, it just executes immediately and I can see "WHY YOU ..." printed out in the console. However, even though the debugger runs smoothly past the below three lines without errors or warnings, the effect of the the next three lines takes place after random delay, or infinite delay. What's wrong? How could it be possible that they have random delay?
I suspected that UI part of Cocoa SDK suspends due to my poor program design or whatever, but even during the random delay, other buttons and functionalities work so fine, while the effect of setImage takes place suddenly in 47.2 seconds. What's wrong with iOS, or with me?
What confounds me more is that the above exact routine works fine without delay if the image parameter (UIImage*) comes from a different source. If that's the case, we can attribute the cause of the problem to the image, however, why do scanButton and overlayView also respond with the same delay, even though they have nothing to do with the image? Total mystery to me.
I have no idea on whats going on, as there is almost no hint in the code that you've provided, but why you dont try something like this:
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(#"WHY YOU iOS!!!");
[scanButton setSelected:YES];
[overlayImageView setImage:image];
[overlayView setHidden:YES];
[overlayImageView setNeedsDisplay];
[overlayImageView setNeedsLayout];
[scanButton setNeedsDisplay];
[scanButton setNeedsLayout];
[overlayView setNeedsDisplay];
[overlayView setNeedsLayout];
});
Basically ensure always to update de UI in the main thread (in case that you're using a background thread) and set the UI controls as needed to be redrawn.
I had the UIActivityIndicatorView working fine in simulator and other 3.0 devices in my app. But I found out that it was not spinning (or showing) in the new iphone 4. Basically I need to show the activity indicator when a button is clicked and hide it when the button click event is complete. I was using the approach below.
[NSThread detachNewThreadSelector: #selector(spinBegin) toTarget:self withObject:nil];
from this link. As mentioned, it correctly spins the activity indicator on all except 4.*.. not sure why. To get around this, I also followed another approach something like (from developer.apple.com)
`
(IBAction)syncOnThreadAction:(id)sender
{
[self willStartJob];
[self performSelectorInBackground:
#selector(inThreadStartDoJob:)
withObject:theJobToDo
];
}
(void)inThreadStartDoJob:(id)theJobToDo
{
NSAutoreleasePool * pool;
NSString * status;
pool = [[NSAutoreleasePool alloc] init];
assert(pool != nil);
status = [... do long running job specified by theJobToDo ...]
[self performSelectorOnMainThread:
#selector(didStopJobWithStatus:)
withObject:status
waitUntilDone:NO
];
[pool drain];
}
`
The problem with this was that, it is showing the acitivityVIewIndicator spinning correctly (at least on the simulator) but after it stops, the built in activity indicator in the top bar (where it shows the battery% etc) is still spinning.
I'm new to objective C. I have finished my app completely but for this silly thing. I realize there is no way to display UIActivityView without starting another thread. and finally, just to rant, I don't understand why they have to make it so complicated. I mean they knew it was going to have this problem, why not provide a sample code everyone can use rather than deriving their own solutions.
Finally, can anyone please provide me with a direction or some sample code. I would really appreciate it. I have been searching for a few hours now and have not found anything really that works!
Why are you starting/stopping the indicator on a separate thread? Any methods you send to your UIActivityIndicatorView must be sent on the main (UI) thread.
Any events sent by a button pressed will automatically be run on the main thread. If you're using background threads to complete the process, you could do something like:
- (IBAction)buttonPressed:(id)sender {
// This runs on the main thread
[activityIndicator startAnimating];
[self performSelectorInBackground:#selector(inThreadStartDoJob:) withObject:theJobToDo];
}
- (void)inThreadStartDoJob:(id)theJobToDo {
// Set up autorelease pool
...
// Run your long-running action
...
// Stop the spinner. Since we're in a background thread,
// we need to push this to the UI Thread
[activityIndicator performSelectorOnMainThread:#selector(stopAnimating) withObject:nil waitUntilDone:YES];
}
Edit: As for the activity indicator in the top bar (where the battery is), doesn't this automatically start/stop based on network activity?
I'm trying to use a UIImage as a button which gives the impression of it being turned on then off again within about half a second. This works fine if I switch it on but if I want to switch it off again it doesn't switch on at all. I have a short loop in there to prevent it switching on and off so fast I can't see it but it doesn't switch on at all. I've tried it with and without the [flashingButton release]. Is there something I'm misunderstanding here? Can I addSubview and removeFromSuperView at the same time even with a short delay?
if ( some conditional statements in here .......) {
UIImage *estimateButton1 = [UIImage imageNamed:#"FlashingButton.png"];
flashingButton = [[UIImageView alloc] initWithImage:flashingButton1];
flashingButton.frame = CGRectMake (146,8,165,30);
[self.view addSubview:flashingButton];
// [flashingButton release];
// short loop in here to delay urning the button off
[self.flashingButton removeFromSuperview];
User interface drawing doesn't happen until later in the main run loop. Your call to addSubview adds flashingButton to self.view but doesn't draw it. Your short loop blocks the main run loop, so it still doesn't get to the drawing part. And then, you remove the button before the main run loop gets to draw it.
A solution is to let the main run loop continue after you've added the flashing button (so it will get drawn), but create a timer that will remove that button in the future. You can use performSelector:withObject:afterDelay: to do this.
[self.flashingButton performSelector:#selector(removeFromSuperview) withObject:nil afterDelay:0.5f];
You can read about run loops in "Threading Programming Guide" and about how drawing gets done in "View Programming Guide for iOS."
Looping within the main thread will just hang the program temporarily and prevent any drawing from taking place. Instead, use an NSTimer.
I have a segmented control being used as a toggle. When toggled, I switch a bunch of contents around in a table view which takes a tiny but noticeable amount of time (inserting/deleting sections in the table view, animating the change, etc). I want the segmented control to respond to the selection immediately. So in my action handling code for the segmented control's UIControlEventValueChanged event, I do the following:
- (IBAction)groupingChanged:(id)sender {
UISegmentedControl *seg = sender;
[tableModel toggleOn:[seg selectedSegmentIndex] == ToggleOnIndex];
[self performSelectorOnMainThread:#selector(updateGrouping)
withObject:nil
waitUntilDone:NO];
}
Where updateGrouping is:
- (void)updateGrouping {
MXAssertMainThread();
[tableView beginUpdates];
... several table updates
[tableView endUpdates];
}
Setting waitUntilDone:NO allows the groupingChanged method to complete before the updateGrouping is called, but this doesn't seem to be sufficient to repaint the view. The segmented control sticks until the table is done updating, then it switches over.
So I tried modifying groupingChanged: to create a thread for the update like so:
- (void)delayed {
[self performSelectorOnMainThread:#selector(updateGrouping)
withObject:nil
waitUntilDone:NO];
}
- (IBAction)groupingChanged:(id)sender {
UISegmentedControl *seg = sender;
[tableModel toggleOn:[seg selectedSegmentIndex] == ToggleOnIndex];
[self performSelectorInBackground:#selector(delayed) withObject:nil];
}
And this does work. The segmented control toggles instantly and the table soon follows. But I'm not at all confident of the result. Is it simply a side-effect of giving the main thread a reprieve while the new thread started up? Is this just how I need to queue updates to the UI? It's clearly hacky. I'm hoping someone has a better pattern they're following for this situation.
Think of it this way - everything you do is generally done on the main thread, including UI updates.
So in your original code, the code that updated the table view was reached before the code that did the UI update on the segment.
So, you were correct that giving the main thread a break allowed more time to complete the updates to the UI, because the main thread was allowed to complete the UI updates while the background thread handled the table updates.
An alternative you could try is to use performSelector:withObject:afterDelay with a 0.0 delay (which allows the main thread to process other things before proceeding with the selector). It may work where performSelectorOnMainThread did not, as that call may be more immediate even though they end up doing something very similar.
If you just want to make sure the segmented control gets repainted real quick, I probably wouldn't dive into threads.
Instead I would just set a timer with a low value like 0.1, and that should be sufficient enough to get the control updated without any noticeable delay to the user.
I've used this when I have a lot of work to do but need a quick UI update.
Still a little "hacky", but without the introduction of threads.
So...
- (IBAction)groupingChanged:(id)sender {
UISegmentedControl *seg = sender;
[tableModel toggleOn:[seg selectedSegmentIndex] == ToggleOnIndex];
[NSTimer scheduledTimerWithTimeInterval:0.1
target:self
selector:#selector(updateGrouping)
userInfo:nil
repeats:NO];
}
I hit the same problem and solved it by subclassing the UISegmentedControl to create a Delayed UISegmentedControl.
In the delayed control I overrode addTarget:action:forControlEvents: to capture the target & action. Then when the segment event occurs I run an NSTimer to launch the captured target & action after a set delay. The result is that the UI gets updated to display the segment clicked and I can use the DelayedUISegmentedControl like I would a UISegmentedControl:
// Follows all normal initialization patterns of UISegmentedControl
UISegmentedControl *segmentedControl = [[DelayedUISegmentedControl alloc]
initWithItems:[NSArray arrayWithObjects: #"First", #"Second", nil]];
// Adds a delay to the selector; default is 0.25
[segmentedControl addTarget:self action:#selector(segmentAction:)
forControlEvents:UIControlEventValueChanged];
If you want to download the control I've open sourced it on google code.