Kinda new to iPhone programming and was experimenting with threads
- (void)viewDidLoad {
[super viewDidLoad];
[NSThread detachNewThreadSelector:#selector(changeMain) toTarget:self withObject:nil];
[NSThread detachNewThreadSelector:#selector(changeThread) toTarget:self withObject:nil];
}
- (void)changeMain{
NSAutoreleasePool* arp = [[NSAutoreleasePool alloc] init];
for (int i = 0; i < 1000000; i++) {
[mainValue setText:[NSString stringWithFormat:#"%d",i]];
[self.view setNeedsDisplay];
}
[arp release];
}
- (void)changeThread{
NSAutoreleasePool* arp = [[NSAutoreleasePool alloc] init];
for (int i = 0; i < 1000000; i++) {
[threadValue setText:[NSString stringWithFormat:#"%d",i]];
[self.view setNeedsDisplay];
}
[arp release];
}
mainValue and threadValue are both just UILabels. I expected this to run and see both labels run up to 999999 but instead it starts at some low number (what it is when the screen initally refreshing i assume), pauses for a bit, then updates to 999999. I'm thinking the screen just isn't refreshing.
Is this correct? Am I doing it wrong?
You have to perform any Cocoa Touch operations in main thread, in other case results are unpredictable.
You don't have to call setNeedsDisplay manually.
So I'd recommend to use the following construction:
[threadValue performSelectorOnMainThread:#selector(setText:) withObject:[NSString stringWithFormat:#"%d",i] waitUntilDone:YES];
Additional notes:
1. 100000 runs can overflow the main thread queue so some values will disappear
2. You can use waitUntilDone:NO too
The setNeedsDisplay message triggers a redraw, but it only happens during the next time the main thread becomes active. So your side threads trigger a million redraws but they are queued. As soon as the main thread continues, it "collapses" all requests into one redraw.
Most likely setNeedsDisplay just sets a flag that is checked once during each run of the main loop, so setting it 1000000 to true doesn't influence anything. Try to let the "worker threads" sleep after each iteration to give the main thread some time to redraw.
Don't use for() for animations. The for will process in the same "frame". Maybe just have an ivar i and in changeMain you can have if (i<10000) { mainValue.text = [NSString stringWithFormat:#"%d",i]; i++;} or something like that. This way the setText only happens once per "frame".
I'm not sure if this will work, but you could try to force the setNeedsDisplay method to be executed on the main thread, using e.g. [self performSelectorOnMainThread:#selector(setNeedsDisplay) withObject:nil waitUntilDone:YES]. This should (hopefully, i didn't test it!) update the view after every increment. You could also try to set waitUntiDone:NO, but I'm unsure what will happen then.
See here
Related
I want to update a label from a loop, for example like this:
- (void)viewDidLoad {
[super viewDidLoad];
int i=0;
while (1) {
i++;
[NSThread sleepForTimeInterval:0.05]; // do some computation,
[myLabel setText:[NSString stringWithFormat:#"%d", i]]; // show the result!
[self.view setNeedsDisplay];
}
}
Assume that instead of sleep some heavy computation is done.
I do not want the overhead of doing the computation in the background.
The Windows equivalent for handling this problem would be .DoEvents, as this example shows:
http://www.tek-tips.com/viewthread.cfm?qid=1305106&page=1
Is there a similar solution for this in iOS?
[self.view setNeedsDisplay] does not work at all!
There must be some way to process application events from iOS on a controlled schedule in the main thread... Like .DoEvents in windows, despite all its shortcomings is quite useful for some simple applications.
I guess this is like a game-loop but with UI components.
The way you would do this is as follows:
-(void) incrementCounter:(NSNumber *)i {
[myLabel setText:[NSString stringWithFormat:#"%d", [i intValue]]]; // show the result!
[self performSelector:#selector(incrementCounter:) withObject:[NSNumber numberWithInt:i.intValue+1] afterDelay:0.05];
}
- (void)viewDidLoad {
[super viewDidLoad];
// start the loop
[self incrementCounter:[NSNumber numberWithInt:0]];
}
The basic idea here is to increment the counter after a slight delay 0.05 to give the main UI thread a chance to flush all UI events, which makes up for explicitly calling .DoEvents in the Windows world.
I assume you want to implement a counter using a label? You can use NSTimer to call a method that updates your counter every X milliseconds, for instance.
Use NSTimer in iOS if you want to update UI components.
NSTimer* myTimer = [NSTimer scheduledTimerWithTimeInterval: 60.0 target: self
selector: #selector(callAfterSomeSecond:) userInfo: nil repeats: YES];
Implement the callAfterSomeSecond: as below :
-(void) callAfterSomeSecond:(NSTimer*) timer
{
static int counter = 0;
if(counter == 100)
{
[timer invalidate];
}
[myLabel setText:[NSString stringWithFormat:#"%d", counter ]];
[self.view layoutSubviews];
counter++;
}
in your code, the while loop runs in the Main thread, and the UI update also should be done in Main thread, so while the loop is running, the Main thread is kind of 'blocked'(busy), so UI update cannot be performed.
I think what I want to say is not what you want. to resolve it, you have to put the heavy computing in another thread, using NSOperation or GCD for example.
The best way is to do your computation on a different thread, but when that's not feasible, you can wrap the code that affects the UI in a CATransaction. In my tests this will update the UI immediately rather than needing to wait till the next run loop.
while (1) {
[CATransaction begin];
//Your UI code here
[CATransaction commit];
}
I'm building my own activity indicator like class that's supposed to fade in before a heavy operation, and fade out when the operation is complete. This is working fine, until I run into the following scenario:
[[MyLoaderClass sharedInstance] displayLoaderInView:self.view];
for( int i = 0; i < 1000; i++ ) {
NSLog(#"Performing heavy operation...");
}
[[MyLoaderClass sharedInstance] removeLoaderInView:self.view];
What's happening on the first line is that my loader view is alloced, subviewed and told to fade in with a standard UIView animation. However, the animation doesn't start (as shown by the setAnimationWillStartSelector:) until after the heavy operation is complete.
Now, heavy operations on the main thread are of course to be avoided, but I still want my loader class to work no matter what programmers might throw at it.
I tried moving the loader into a separate thread and animating it from there which worked great, but led to crashes because it's not cool to manipulate views from threads other than the main thread.
My question: Is it possible to do what I want, and/or should I bother with it at all?
As an alternative to Joshua Smith's suggestion, in case being on a different thread messes with your operation, just make sure you drop out to the runloop between starting the UIView animations and starting your heavy code. E.g.
...
[[MyLoaderClass sharedInstance] displayLoaderInView:self.view];
[self performSelector:#selector(performHeavyOperation) withObject:nil afterDelay:0];
}
- (void)performHeavyOperation
{
for( int i = 0; i < 1000; i++ ) {
NSLog(#"Performing heavy operation...");
}
[[MyLoaderClass sharedInstance] removeLoaderInView:self.view];
}
The performSelector:withObject:afterDelay: causes the nomated selector to be scheduled on the runloop in the future. Setting a delay of 0 means it is added to the runloop to occur as soon as possible.
For various reasons, quite a lot of UIView stuff takes effect only if you allow the call stack to unwind all the way to the call stack. That's so that, e.g. if you did:
view.frame = aNewFrame;
view.someOtherPropertyThatWouldCauseAVisibleChange = anotherValue;
Then the UIView will end up redrawing itself only once, not twice.
Put your heavy operation in an NSOperationQueue, then it will not block the main thread.
#interface MyClass : NSOperation {
}
#end
#implementation MyClass
-(void) main {
for( int i = 0; i < 1000; i++ ) {
NSLog(#"Performing heavy operation...");
}
}
#end
Then, in your above code:
[[MyLoaderClass sharedInstance] displayLoaderInView:self.view];
NSOperationQueue *q = [[NSOperationQueue alloc] init];
MyClass *c = [[[MyClass alloc] init] autorelease];
[q addOperation:c];
[q waitUntilAllOperationsAreFinished];
[[MyLoaderClass sharedInstance] removeLoaderInView:self.view];
Read the docs, too, you'll need them: http://developer.apple.com/library/mac/#documentation/cocoa/reference/NSOperationQueue_class/Reference/Reference.html
NSOperationQueue's are awesome, but not exactly intuitive.
Goal is to "launch a spinner graphic at start of viewWillAppear that loads data before showing the tableview" so the user doesn't wonder why there's a delay before seeing the table. I.e. a UIActivityIndicatorView has been added to the window, and I just want to set the alpha to hide/show it.
I get this strange error when starting a thread to make sure the "spinning gears" imageview (tag=333) gets shown, before moving on to load/calculate stuff in viewWillAppear.
I don't get it on every call to [appdel addGearz] and [appdel removeGearz], it happens for both these, and it's random. It can happen after 2 viewWillAppears, or after 15. If I comment out the line that sets the alpha, everything works.
A typical viewWillAppear looks something like this,
[super viewWillappear];
self.title=#"Products listing"; //and other simple things
[appdel addGearz];
[self getProducts];
[self getThumbnails];
[myTableView reloadData]; //in case view already loaded and coming back from subview and data changed
And here is the code that crashes if the lines with .alpha are not commented out
-(void)addGearz {
[NSThread detachNewThreadSelector:#selector(gearzOn) toTarget:self withObject:nil];
}
-(void)removeGearz {
[NSThread detachNewThreadSelector:#selector(gearzOff) toTarget:self withObject:nil];
}
- (void)gearzOn {
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
[window viewWithTag:333].alpha=1.0;
//
// [[window viewWithTag:333] setNeedsDisplay];
[pool drain];
}
- (void) gearzOff {
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
[window viewWithTag:333].alpha=0.0;
//
// [[window viewWithTag:333] setNeedsDisplay];
[pool drain];
}
I've used someone else's code, so... anything obvious you can see? Surely I must be able to change alpha of UIViews in a thread? Do I need to "embed" the alpha-change in some "stop enumerating while I change this"-code?
I made it not crash by moving that alpha-change-line to above the pool alloc or below the [pool drain], but then I get a lot of "autoreleased with no pool in place - just leaking"-messages.
Apparently, there's something I don't understand about this thread code.
You must not try to modify the UI on a separate thread. UI should only be manipulated on the main thread.
Instead of detaching a new thread, you should use performSelectorOnMainThread:withObject:waitUntilDone:. This will ensure that the method will be called on the proper thread.
I'm trying to add a progress meter, or other "I'm busy right now" notification to my view hierarchy right before doing some intense computation that will block the UI. My code looks some thing like:
//create view
[currentTopView addSubView:imBusyView];
//some initialization for the intense computation
[computation startComputing];
Unfortunately, my progress meter doesn't display until after the computation completes. It appears like the views aren't re-drawn until the run loop completes. I'm pretty sure that setNeedsDisplay and setNeedsLayout will also wait until the run loop completes.
How do I get the view to display immediately?
Redrawing only occurs when your code returns control to the run loop. So the easiest way would be for you to schedule the startComputing call with a zero delay. That way, it will be executed during the next run loop iteration (right after redrawing):
[computation performSelector:#selector(startComputing) withObject:nil afterDelay:0.0];
Be aware, though, that unless you do your computation in another thread you will not be able to update the UI during the computation.
If you are doing heavy calculations maybe spawning a new thread is a good idea.
Here I have an activityIndicator displayed and starts a large XMLParse operation in a background thread:
- (void) setSearchParser {
activityIndicator = [[ActivityIndicatorView alloc] initWithActivity];
[self.view addSubview:activityIndicator];
[NSThread detachNewThreadSelector:#selector(getSearchResults:) toTarget:self withObject:[searchParser retain]];
}
then the getSearchResults method:
- (void) getSearchResults: (SearchResultParser *) parser {
NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init];
[parser startParser];
[self performSelectorOnMainThread:#selector(searchResultsReady:) withObject:[parser data] waitUntilDone:NO];
[pool release];
}
So firstly make a new thread:
[NSThread detachNewThreadSelector:#selector(getSearchResults:) toTarget:self withObject:[searchParser retain]];
this means that all code inside the getSearchResults will be executed on a different thread. getSearchResults also get's passed a parameter of "searchParser" thats a large object that just needs startParse called on it to begin.
This is done in getSearchResults. When the [parser startParser] is done, the results is passed back to the main thread method called "searchResultsReady" and the threads autorelease pool is released.
All the time it took from my parser began to it had finished, a gray view covered the screen an an activityIndicator ran.
You can have the small activityIndicator class I wrote:
-(id) initWithActivity {
[self initWithFrame:[self bounds]];
[self setBackgroundColor:[UIColor blackColor]];
[self setAlpha:0.8];
activityView = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhiteLarge];
activityView.center = CGPointMake(160, 240);
[self addSubview:activityView ];
[activityView startAnimating];
return self;
}
- (void) dealloc {
[activityView release];
[super dealloc];
}
Hope it helps you out, even though threads seems a bit confusing, they can help to make the UI not freeze up, which is especially important on the iPhone.
I have a background task that updates a view. That task calls -setNeedsDisplay to have the view drawn.
This works:
- (void) drawChangesTask;
{
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
if (pixels) {
drawChanges((UInt32 *) origPixels, (UInt32 *) pixels, CGBitmapContextGetBytesPerRow(ctx)/4, CGBitmapContextGetHeight(ctx), count--);
if (count < 0) {
count = 150;
}
else
[self performSelectorInBackground:#selector(drawChangesTask) withObject:nil ];
[self performSelectorOnMainThread:#selector(setNeedsDisplay) withObject:nil waitUntilDone:NO ];
}
[pool release];
}
This does not work:
- (void) drawChangesTask;
{
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
if (pixels) {
drawChanges((UInt32 *) origPixels, (UInt32 *) pixels, CGBitmapContextGetBytesPerRow(ctx)/4, CGBitmapContextGetHeight(ctx), count--);
if (count < 0) {
count = 150;
}
else
[self performSelectorInBackground:#selector(drawChangesTask) withObject:nil ];
[self setNeedsDisplay];
}
[pool release];
}
Anyone know why? When I say it doesn't work, I mean that it runs tens of iterations, sometimes I see portions of my image shifted up or down, or entirely blank, and then the deugger give me an “EXC_BAD_ACCESS” somewhere in CoreGraphics.
Also, if I don't handle the autorelease pool myself, then I get leaking error messages. Don't understand why that is either. My drawChanges() doesn't create any new objects. Here's the error:
2009-08-17 11:41:42.358 BlurApp[23974:1b30f] *** _NSAutoreleaseNoPool(): Object 0xd78270 of class NSThread autoreleased with no pool in place - just leaking
UIKit simply isn't thread-safe — you need to call methods that update UIKit controls on the main thread.
I think that this line:
[self performSelectorInBackground:#selector(drawChangesTask) withObject:nil];
Is causing trouble. Have you tried simply calling it again on the current thread? If you need the runloop to execute between the calls, use:
[self performSelector:#selector(drawChangesTask) withObject:nil afterDelay:0.0];
This will call the method on the current thread after the method you're in has finished and the runloop has gone round once.
Problem here is that UIKit is not thread safe, if you tell your UI to do something from a background thread nothign is guaranteed, what you want to do is use the performSelectorOnMainThread method to do updates t o your UI elements