Skip to content

Posts tagged ‘Multi-Threading’

19
Mar

iOS, Blocking User Input and CFRunLoopRun()

Yesterday I worked on new a Lab for my upcoming iPhone course and the topic was how to prevent user input while the application is gathering data from a server. Normally that is not a big issue when you just obtain data but that can be a big issue when you send data because then it could corrupt the server side.

So far I just created an UIView that was semi-transparent on top of the entire view preventing the user to click on anything below. But that did not work properly in every case because as it turned out the user input is not consumed but queued and handled as soon as the main thread becomes available. Therefore if I don’t disable the button or alike the code is executed again. This is the original code:

self.connection = [[[NSURLConnection alloc]
     initWithRequest:request delegate:self] autorelease];
// Now show an animation
UIActivityIndicatorView *spinner = [[UIActivityIndicatorView alloc]
     initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhiteLarge];
UIView *window = [[UIApplication sharedApplication] keyWindow];
UIView *shield = [[UIView alloc] initWithFrame:window.bounds];
shield.backgroundColor = [UIColor blackColor];
shield.alpha = 0.5f;
[window addSubview:shield];
spinner.center = shield.center;
[shield addSubview:spinner];
spinner.hidden = NO;
[spinner startAnimating];
// Block the further execution until all data is received
CFRunLoopRun();
[spinner stopAnimating];
[spinner removeFromSuperview];
[spinner release];
[shield removeFromSuperview];
[shield release];

Soon I got the feeling that I block the main UI thread and so the input is not consumed until the system can execute it. After looking for a good solution I was more or less told that I should not use CFRunLoopRun() but let the main thread run its course and deal with the result when it comes in.

That said the transition was not easy but eventually I got all the callbacks in place and the thing worked fine except that the call was to short to test is properly. Thus I added a sleep inside the callback to give myself time to click multiple times but that did not solve the issue. Through debugging I then saw that when data are returned back from the server that it is still done in the main thread and so preventing the view to consume the user input.

This is the code afterwards:

// 1. When the call comes in and we start the loading
self.spinner = [[UIActivityIndicatorView alloc]
     initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhiteLarge];
[self.spinner release];
UIView *window = [[UIApplication sharedApplication] keyWindow];
self.shield = [[UIView alloc] initWithFrame:window.bounds];
[self.shield release];
shield.backgroundColor = [UIColor blackColor];
shield.alpha = 0.5f;
[window addSubview:shield];
spinner.center = shield.center;
[shield addSubview:spinner];
spinner.hidden = NO;
[spinner startAnimating];
// Now execute the call to the web service
self.connection = [[[NSURLConnection alloc]
     initWithRequest:request delegate:self] autorelease];

// 2. When the all the data is received we do:
[spinner stopAnimating];
[spinner removeFromSuperview];
[shield removeFromSuperview];
// Call back to the service that we received the data
[self.depotService handleResult:root];

Finally I went to the backend web service and made sure that handling of the call is delayed. Then I was able to test it and voila it worked like a charm. I could click multiple times on the button and when the shielding view went away the user input was consumed and did not cause more calls to the server.

There is still some stuff I have to learn how things are done in iOS in contrast to backend Java as I am used to. On the other hand it forces me to understand the underlying concept in details which will be a great asset when given the class.

– Andy