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.
Related
I have subclassed UIButton in order to do some custom drawing with CoreGraphics in drawRect:. In order to respond to touch events, I am adding these target/actions:
[self addTarget:self action:#selector(selectedButton:) forControlEvents:UIControlEventTouchDown | UIControlEventTouchDragInside];
[self addTarget:self action:#selector(deSelectedButton:) forControlEvents:UIControlEventTouchUpInside | UIControlEventTouchDragOutside | UIControlEventTouchUpOutside];
And an example implementation is this:
- (void)selectedButton:(id)sender {
self.isSelected = YES;
[self setNeedsDisplay];
}
This works pretty well. The only problem is that if I touch down/up really fast, the action is still processed, but the selected/deSelected methods get called so quickly that the background is never updated. But it works well if I press down for a little longer.
What am I missing here? How can I make my background update for the slightest of touches? I have tried delaying the deSelect call to update the layout, but it has no effect.
i think you probably need to call setNeedsDisplay on the main thread:
[self performSelectorOnMainThread:#selector(setNeedsDisplay) withObject:nil waitUntilDone:NO];
(i don't think whether waitUntilDone is yes or no will make any difference?...)
My viewController is receiving notifications from background threads and updates the UI accordingly.
Using this piece of code
dispatch_async(dispatch_get_main_queue(), ^{
self.myButton.hidden = NO;
[self.view addsubview:someView];
});
What I observe every now and then (2/5 times) is that myButton is not visible. someView have been added. If I press the area where myButton is it becomes visible.
What is the correct way of updating the UI like this when called from a background thread, I thought this was it but it is obviously not working as I expected.
There is not any issue with this code, You should check that you are not hiding it elsewhere.
So just search for other part or your code where you have code self.myButton.hidden
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'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.
I have a question that we might answer together i have a tickertape in my iphone app (Like those stick tickers) and i use a NSThread to keep the memory away from the main thread so it will not slow down the app. Now the thing is it does its job well but when i scroll on a UITableView that i have on the same view i notice that my ticker tape animation stops to work.
ViewController.m (Main view of this object has the ticker tape on it)
-(void)startTicker {
[NSThread detachNewThreadSelector:#selector(start) toTarget:ticker withObject:nil];
}
TickerView.c (This handles the tickertape animation)
// Called from the viewcontroller
-(void) start {
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
[self performSelectorOnMainThread:#selector(loop) withObject:nil waitUntilDone:YES];
[pool release];
}
-(void)loop {
timerHandle = [NSTimer scheduledTimerWithTimeInterval:.01f target:self selector:#selector(render) userInfo:nil repeats:YES];
}
-(void) render {
// Does a *** load of calculations here and moves the items in the tickertape..
}
My Question: How can i prevent the UITableview or any other view / touch event to block this thread from updating the tickertape animation ?.
Your NSTimer is not running on a background thread, but on the main thread. It will block anytime something else runs on the main thread. -performSelectorOnMainThread: means that anything done within the method called will run on the main thread.
To make your loop truly independent of the main thread, you could set up a while loop within your start method that sleeps for a given interval on every pass, then calls your render method. You'd need to make sure that all user interface updates within your render method get performed on the main thread, but make waitUntilDone NO for those method calls. I've also done this using NSOperations, where as one operation finishes I add another to the queue.
Also, running this render operation 100 times per second is excessive. I'd back that down a bit, or even better, look at using Core Animation for your ticker to make your drawing more efficient.