Yet another UIProgressView update from background thread problem...

Discussion in 'iOS Programming' started by bauhaus9mf, Jun 9, 2009.

  1. bauhaus9mf macrumors newbie

    Joined:
    Jun 9, 2009
    #1
    I'm trying to update a UIProgressView (that was created in IB) as I get chunks of data from an Asynch download using NSURLConnection. Without putting the async download into a background thread, the progress view update is all or nothing--that is, you don't see any activity in the progress view UNTIL the download is finished.

    So I have the 4 key methods needed to do an asynch download: didReceiveData, didReceiveResponse, didFailWithError, and connectionDidFinishLoading. And I've got these 4 methods in the implementation of my ViewController class. I've tried to use "performSelectorInBackground" method to try to put such activity into a background thread but when I do (as the last line of code in my "viewDidLoad()" method), it just goes into la-la land and never executes any of the 4 callbacks mentioned above. I have a matching "performSelectorOnMainThread" call in the body of my "didReceiveData" method that invokes a simple method to update the UIProgressView attribute after calculating how much data was pulled down in the current chunk. When NOT using the "performSelectorInBackground" and "performSelectorOnMainThread" the 4 callback methods get called just fine and the download works. Some questions:

    1. The method signature that I pass as the "selector" to the "performSelectorInBackground" is "-(void) startIt". Should this be "-(SEL) startIt" ? I get no complaints from the compiler using (void). Specifically, the call I use is:

    Code:
    [self performSelectorInBackground:@selector(startIt) withObject:nil];
    and the details of my "startIt" method are:

    Code:
    - (void) startIt {
    	NSAutoreleasePool	 *autoreleasepool = [[NSAutoreleasePool alloc] init];
    	downloadURL = [[NSURL URLWithString:@"http://www.gutenberg.org/files/135/135.txt"] retain];
    	data = [[NSMutableData data] retain];
    	myConnection = [[[NSURLConnection alloc] initWithRequest: [NSURLRequest requestWithURL: downloadURL] delegate: self ] retain];
    	[autoreleasepool release];
    }
    2. Do I need to do any kind of thread creation or setup in my "startIt" method.

    3. Is there any more direct way of connecting a UIProgressView to asynch NSURLConnection activity?

    4. What XCode tools are there for monitoring thread activity in specific and the Cocoa framework in general that are recommended. I'd like to know where my app goes when it just sits there in "la-la land".
     
  2. Luke Redpath macrumors 6502a

    Joined:
    Nov 9, 2007
    Location:
    Colchester, UK
    #2
    Because the URL loading system is asynchronous and takes care of the threading for you, the performSelectorInBackground call is redundant and will probably cause problems. Just call startIt normally (and no need for the extra auto release pool).

    Any updates to your UI from your async delegate callbacks should be performed on the main thread (as should all updates).
     
  3. bauhaus9mf thread starter macrumors newbie

    Joined:
    Jun 9, 2009
    #3
    Tried that...that's how I started...

    Just calling "startIt" was where I started with this issue. Here is my "didReceiveData" method:

    Code:
    - (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)mdata {
    	[data appendData: mdata];
    	cumDataReceived += [mdata length];
    	//[self performSelectorOnMainThread:@selector(updateProgressView) withObject:nil waitUntilDone:NO];
    	[self updateProgressView];
    }
    If I put a breakpoint in the middle of the "didReceiveData" method, it stops each time the NSURLConnection thread invokes my "didReceiveData" callback and it then invokes the following method...

    Code:
    -(void) updateProgressView {
    	if (bookLen > 0) 
    		progressBar.progress = cumDataReceived / bookLen;
    }
    (where progressBar is of type UIProgressView)

    But the UI never updates until after it is all over! Am I missing some crucial step?

    BTW, thanks for the quick reply...
     
  4. dejo Moderator

    dejo

    Staff Member

    Joined:
    Sep 2, 2004
    Location:
    The Centennial State
    #4
    Reread that last bit from Luke Redpath. I believe you're so close.
     
  5. bauhaus9mf thread starter macrumors newbie

    Joined:
    Jun 9, 2009
    #5
    I am on the main thread...

    Ok, I modified the "didReceiveData" method to tell me if, when I get called-back from NSUrlConnection, whether or not I am on the main thread:

    Code:
    - (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)mdata {
    	[data appendData: mdata];
    	cumDataReceived += [mdata length];
    	//[self performSelectorOnMainThread:@selector(updateProgressView) withObject:nil waitUntilDone:NO];
    	if (YES == [NSThread isMainThread])
    		NSLog(@"I'm on the main thread and I've received %d bytes so far.",cumDataReceived);
    	
    	[self updateProgressView];
    }
    
    and in the console window, I get messages like the following...

    Code:
    2009-06-09 13:06:49.920 ASR3[2974:20b] I'm on the main thread and I've received 3316512 bytes so far.
    2009-06-09 13:06:49.922 ASR3[2974:20b] I'm on the main thread and I've received 3319144 bytes so far.
    2009-06-09 13:06:49.977 ASR3[2974:20b] I'm on the main thread and I've received 3320460 bytes so far.
    2009-06-09 13:06:49.978 ASR3[2974:20b] I'm on the main thread and I've received 3321776 bytes so far.
    2009-06-09 13:06:49.981 ASR3[2974:20b] I'm on the main thread and I've received 3322623 bytes so far.
    2009-06-09 13:06:49.981 ASR3[2974:20b] Finished (http://www.gutenberg.org/files/135/135.txt) 3322623 bytes.
    
    so, sure enough, I AM on the main thread when I try to update the UIProgressView, so NOW what...?
     
  6. dejo Moderator

    dejo

    Staff Member

    Joined:
    Sep 2, 2004
    Location:
    The Centennial State
    #6
    Are you still doing this?:
    Code:
    [self performSelectorInBackground:@selector(startIt) withObject:nil];
    or are you doing this?:
    Code:
    [self startIt];
     
  7. bauhaus9mf thread starter macrumors newbie

    Joined:
    Jun 9, 2009
    #7
    I have gone back to using the regular:

    Code:
    [self startIt];
    Please help, oh demi-god!
     
  8. dejo Moderator

    dejo

    Staff Member

    Joined:
    Sep 2, 2004
    Location:
    The Centennial State
    #8
    Then, I believe, based on Luke Redpath's advice:
    Code:
    [self performSelectorOnMainThread:@selector(updateProgressView) withObject:nil waitUntilDone:NO];
     
  9. bauhaus9mf thread starter macrumors newbie

    Joined:
    Jun 9, 2009
    #9
    NOTHING CHANGED <sigh>

    The "didReceiveData" callback is in the SAME class as the "updateProgressView" method--could this be the problem? Commenting out the "[self updateProgressView] in exchange for the recommended

    Code:
    [self performSelectorOnMainThread:@selector(updateProgressView) withObject:nil waitUntilDone:NO];
    changed nothing. And it makes sense to me that nothing would change. If am already running on the main thread, then telling the framework to run my "updateProgressView" method on the main thread has no impact because the method already IS part of a class that is running on the main thread.

    Maybe if I post the entire source file someone might spot something silly I am doing...(thanks for hanging in there!)

    Here is the .H file (ignore the NSOperationQueue attribute and the timer method plz):

    Code:
    #import <UIKit/UIKit.h>
    
    @interface ASR3ViewController : UIViewController {
    	IBOutlet UIProgressView * progressBar;
    	NSOperationQueue *queue;
    
    	NSURLConnection *myConnection;
    	NSMutableData *data;
    	NSURL *downloadURL;
    	
    	BOOL executing;
    	BOOL finished;
    	NSInteger cumDataReceived, bookLen;
    	
    }
    
    @property (nonatomic,assign) 	IBOutlet UIProgressView * progressBar;
    
    
    - (void)timedMethod:(NSTimer*)theTimer;
    
    -(void) updateCompletedState;
    -(void) updateProgressView;
    
    
    - (void) startIt;
    
    @end
    
    ------------- and here is the implementation file (.M file) ---------------

    Code:
    #import "ASR3ViewController.h"
    
    @implementation ASR3ViewController
    
    
    @synthesize progressBar;
    
    
    - (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)mdata {
    	[data appendData: mdata];
    	cumDataReceived += [mdata length];
    	[self performSelectorOnMainThread:@selector(updateProgressView) withObject:nil waitUntilDone:NO];
    	//if (YES == [NSThread isMainThread])
    	//	NSLog(@"I'm on the main thread and I've received %d bytes so far.",cumDataReceived);
    	
    	//[self updateProgressView];
    }
    
    - (void)connectionDidFinishLoading:(NSURLConnection *)connection {
    	NSLog(@"Finished (%@) %d bytes.", [downloadURL absoluteString], [data length]);
    	[self updateCompletedState];
    }
    
    - (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response {
    	NSHTTPURLResponse * resp = (NSHTTPURLResponse*) response;
    	NSDictionary * headers = [resp allHeaderFields];
    	NSString * contentLength = [headers objectForKey:@"Content-Length"];
    	bookLen = [contentLength intValue];
    }
    
    
    - (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
    	NSLog(@"Failed (%@)", [downloadURL absoluteString]);
    	[self updateCompletedState];
    }
    
    - (void) startIt {
    	downloadURL = [[NSURL URLWithString:@"http://www.gutenberg.org/files/135/135.txt"] retain];
    	data = [[NSMutableData data] retain];
    	myConnection = [[[NSURLConnection alloc] initWithRequest: [NSURLRequest requestWithURL: downloadURL] delegate: self ] retain];
    }
    
    - (void)timedMethod:(NSTimer*)theTimer {
    }
    
    // Implement viewDidLoad to do additional setup after loading the view, typically from a nib.
    - (void)viewDidLoad {
        [super viewDidLoad];
    	//[self performSelectorInBackground:@selector(startIt) withObject:nil];
    	[self startIt];
    }
    
    -(void) updateProgressView {
    	if (bookLen > 0) 
    		progressBar.progress = cumDataReceived / bookLen;
    }
    
    
    - (void)didReceiveMemoryWarning {
        [super didReceiveMemoryWarning]; // Releases the view if it doesn't have a superview
        // Release anything that's not essential, such as cached data
    }
    
    - (void)updateCompletedState {
    	[self willChangeValueForKey: @"isExecuting"];
    	executing = NO;
    	[self didChangeValueForKey: @"isExecuting"];
    	
    	[self willChangeValueForKey: @"isFinished"];
    	finished = YES;
    	[self didChangeValueForKey: @"isFinished"];
    }
    
    - (void)dealloc {
    	[downloadURL release];
    	[data release];
    	[myConnection release];	
        [super dealloc];
    }
    
    
    
    @end
    
     
  10. pranathi macrumors newbie

    Joined:
    Jun 18, 2009
    #10
    >>> progressBar.progress = cumDataReceived / bookLen;
    You need to cast the result of the division to a float value and assign it. Otherwise, the progress always remains zero causing the progress bar to not update.
     
  11. radshag macrumors newbie

    Joined:
    Dec 31, 2009
    #11
    Solution to UIProgressView display problems

    Use this:
    [self performSelectorInBackground:mad:selector(updateProgressView) withObject:nil];

    performSelectorInBackground will run the selector in the background and your UIProgressView will be updated.

    All your other code should be the same just change the above line with the line that has the performSelectorInMainThread and you are good to go
    :)
     
  12. Luke Redpath macrumors 6502a

    Joined:
    Nov 9, 2007
    Location:
    Colchester, UK
    #12
    No; UI updates should never be called on a background thread. UIKit is not thread-safe.
     
  13. radshag macrumors newbie

    Joined:
    Dec 31, 2009
    #13
    Please Explain

    Why not in a background thread? Please explain.

    None of the solutions on this page work. Mine did.

    Thanks....

    Perhaps I should have the DB Request and SOAP Request running in a seperate thread. That makes more sense....
     
  14. PhoneyDeveloper macrumors 68030

    PhoneyDeveloper

    Joined:
    Sep 2, 2008
    #14
    This is a six month old thread. I think he's figured it out by now, or given up.
     
  15. kainjow Moderator emeritus

    kainjow

    Joined:
    Jun 15, 2000
    #15
    Because that's The Way It Is™. Don't handle any thing from UIKit in a thread other than the main thread.

    http://developer.apple.com/mac/libr...aviortoaCocoaProgram/AddingBehaviorCocoa.html
    If you're handling HTTP data, use NSURLConnection and it works asynchronously automatically. If you're still noticing a lag when dealing with the data, then you could consider threading it, but you should read up more on it before you do that.
     
  16. troyerrdt macrumors newbie

    Joined:
    Jun 29, 2011
    #16
    Confused

    I know this thread is old, but I have the same problem: I read everywhere that you should use performSelectorOnMainThread, but the progress view doesn't get updated visually until it is at 100%, but if I use performSelectorInBackground or detachNewThreadSelector, it works, like radshag said. What gives?
     
  17. spenceu41 macrumors newbie

    Joined:
    May 18, 2012
    #17
    Found a solution for me

    It's funny. I ran into the same problem 2 years later. The problem has very little to do with threading but more about the floating value I send to update UIProgressView. For some reason sending the percentage wrapped in NSNumber doesn't do it. I need to multiply 100. Then inside updateProgressView convert that to float and divided by 100.

    Here the solution that works for me.

    Call startIt like you normally would
    Code:
    [self startIt];
    Then inside connection delegate
    Code:
    - (void)connection:(NSURLConnection *)connection didSendBodyData:(NSInteger)bytesWritten totalBytesWritten:(NSInteger)totalBytesWritten totalBytesExpectedToWrite:(NSInteger)totalBytesExpectedToWrite
    {
        float percent = 100*totalBytesWritten/totalBytesExpectedToWrite;
        [self performSelectorOnMainThread:@selector(updateProgressView:) withObject:[NSNumber numberWithFloat:percent] waitUntilDone:NO];
    
    }
    - (void) updateProgressView:(NSNumber *)percent
    {
        progressBar.progress    = [percent floatValue]/100;
    }
    
     
  18. dejo Moderator

    dejo

    Staff Member

    Joined:
    Sep 2, 2004
    Location:
    The Centennial State
    #18
    I think the reason you feel the need to multiply by 100, is that without it, your division (highlighted in red above) is done using integer mathematics, the result of which is then converted to a float. So, you're losing all the precision in your division. I believe if you use the line below you shouldn't have to multiply in and divide out that 100.

    Code:
    float percent = [COLOR="Red"][B](float)[/B][/COLOR]totalBytesWritten / totalBytesExpectedToWrite;
     

Share This Page