Performing many UIKit operations blocking main thread - ios

when the app is launched, I'm programmatically creating many complex UIViews with transparent background colour, each being a 'screen' in the app (has to be this way since there's animated background underneath). This causes the app to stay on the static launch image for quite long (about 4 seconds).
I would like to create all the UIViews while an animation is shown. I can't create UIViews on background thread. How can I accomplish this?
Attempt 1:
-(void)viewDidLoad {
[super viewDidLoad];
dispatch_async( dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
UIView *baseView = [[UIView alloc] initWithFrame:CGRectMake(20, 20, 200, 100)];
[baseView setBackgroundColor:[UIColor blueColor]];
UIButton *button = [[UIButton alloc] initWithFrame:CGRectMake(10, 10, 180, 80)];
[button setBackgroundColor:[UIColor redColor]];
[button setTitle:#"button" forState:UIControlStateNormal]; //doesn't work
[baseView addSubview:button];
dispatch_async( dispatch_get_main_queue(), ^{ //Finished
[self.view addSubview:baseView];
});
});
}
The button is added, but the title is never set. It appears after tapped.

UIViews are not thread safe in normal usage, and all interaction should occur on the main thread. That said, you should be able to, on some background thread (dispatch queue), create the view, modify its parameters, set complex background colors, etc - as long as the view is not connected to anything (and thus receiving events from iOS). Once the view is completely setup and wired, then dispatch a block with it to the main queue and only then add it as a subview to some other view.
What I would do is dispatch all this work to the normal priority queue after having set up my initial video clip. When all the views have been posted back to the main queue (and thus this work is complete), you can stop the video and start interacting with the user.

Thanks for help. Here's the final solution that I tested and works perfectly.
I created a task manager class, FCTaskManager:
//Header File
typedef void (^TaskBlock)(void);
typedef void (^TaskCompletedBlock)(void);
#interface FCTaskManager : NSObject
-(void)performUITask:(TaskBlock)taskBlock completionBlock:(TaskCompletedBlock)taskCompletedBlock;
-(void)performBackgroundTask:(TaskBlock)taskBlock completionBlock:(TaskCompletedBlock)taskCompletedBlock;
#end
//Implementation
#import "FCTaskManager.h"
#implementation FCTaskManager
-(void)performUITask:(TaskBlock)taskBlock completionBlock:(TaskCompletedBlock)taskCompletedBlock {
dispatch_async( dispatch_get_main_queue(), ^{
#autoreleasepool {
taskBlock();
dispatch_async( dispatch_get_main_queue(), ^{
taskCompletedBlock();
});
}
});
}
-(void)performBackgroundTask:(TaskBlock)taskBlock completionBlock:(TaskCompletedBlock)taskCompletedBlock {
dispatch_async( dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
#autoreleasepool {
taskBlock();
dispatch_async( dispatch_get_main_queue(), ^{
taskCompletedBlock();
});
}
});
}
#end
Then I can use the task manager like so:
-(void)viewDidLoad {
[super viewDidLoad];
//Create taskManager
taskManager = [[FCTaskManager alloc] init];
//Create UIViews, etc WITHOUT blocking main queue
[taskManager performUITask:^{
button = [[UIButton alloc] initWithFrame:CGRectMake(30, 30, 300, 300)];
[button setBackgroundColor:[UIColor redColor]];
[button setTitle:#"Working" forState:UIControlStateNormal];
[button addTarget:self action:#selector(buttonAction) forControlEvents:UIControlEventTouchUpInside];
} completionBlock:^{
NSLog(#"CREATED BUTTON");
[self.view addSubview:button];
}];
}

Related

Why does loading images asynchronously take forever?

I have a UICollectionView displaying a bunch of images. If I don't load the images asynchronously the scrolling is very choppy and provides a poor user experience. When I load the images asynchronously the scrolling is smooth but it takes a good 5 to 10 seconds to load each image.
Why does it take so long for images to appear when loaded in the background? Here is my code for the background thread which is inside of the cellForItemAtIndexPath delegate:
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
UIImageView *bg = (id)self.backgroundView;
UIImageView *selbg = (id)self.selectedBackgroundView;
if (![bg isKindOfClass:[UIImageView class]])
bg = [[UIImageView alloc] initWithImage:thumb];
else
[bg setImage:thumb];
if (![selbg isKindOfClass:[UIImageView class]]){
selbg = [[UIImageView alloc] initWithImage:thumb];
coloroverlay = [[UIView alloc] initWithFrame:selbg.bounds];
[coloroverlay setAutoresizingMask:UIViewAutoresizingFlexibleWidth|UIViewAutoresizingFlexibleHeight];
[selbg addSubview:coloroverlay];
} else
[selbg setImage:thumb];
[bg setContentMode:UIViewContentModeScaleAspectFill];
[bg setTag: 1];
[coloroverlay setBackgroundColor:[col colorWithAlphaComponent:0.33f]];
[selbg setContentMode:UIViewContentModeScaleAspectFill];
dispatch_sync(dispatch_get_main_queue(), ^{
[self setBackgroundView:bg];
[self setSelectedBackgroundView:selbg];
});
});
EDIT: As #geraldWilliam pointed out, I shouldn't be accessing views from the secondary thread. Here is what I have updated my code to and fixed the issue of images getting set to the wrong cell:
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
UIImageView *bg = (id)self.backgroundView;
UIImageView *selbg = (id)self.selectedBackgroundView;
if (![bg isKindOfClass:[UIImageView class]]) bg = [[UIImageView alloc] initWithImage:thumb];
if (![selbg isKindOfClass:[UIImageView class]]){
selbg = [[UIImageView alloc] initWithImage:thumb];
coloroverlay = [[UIView alloc] initWithFrame:selbg.bounds];
[coloroverlay setAutoresizingMask:UIViewAutoresizingFlexibleWidth|UIViewAutoresizingFlexibleHeight];
[selbg addSubview:coloroverlay];
}
dispatch_sync(dispatch_get_main_queue(), ^{
[bg setImage:thumb];
[selbg setImage:thumb];
[bg setContentMode:UIViewContentModeScaleAspectFill];
[bg setTag: 1];
[coloroverlay setBackgroundColor:[col colorWithAlphaComponent:0.33f]];
[selbg setContentMode:UIViewContentModeScaleAspectFill];
[self setBackgroundView:bg];
[self setSelectedBackgroundView:selbg];
});
});
Most of the code you have here is fine for the main queue. The loading of the image should be on a global queue, but the rest, especially setting the image view's image, should be on the main queue. What's going on in your code is that you're dispatching back to the main queue to set the background view but leaving the assignment of the image property in the background. So, try something like:
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
UIImage *image = [UIImage imageWithData:[NSData dataWithContentsOfURL:myImageURL]];
dispatch_sync(dispatch_get_main_queue(), ^{
imageView.image = image;
[self setBackgroundView:imageView];
});
});
I strongly recommend watching WWDC 2012 Session 211: Building Concurrent User Interfaces on iOS, which is the best WWDC session ever. It’s full of clearly presented, practical advice.
Doing stuff with UIImageView off the main queue is worrying and should be fixed, but is probably not the cause of slowness. You haven’t showed us where thumb comes from, which is likely the slow bit.

How to preform an action while the button is pressed?

I have a button and i need to change the label while the button is pressed to #"PRESSED" and if it's let go I need to change it to #"LET GO".
I've found many answers, but i have no idea how to implement them in Xcode 5.
That are the ones i tried:
You can start your action on the touchDownInside and stop it on the touchUpInside actions - you can hook them up in IB.
or
Add targets to your button for both control states, UIControlEventTouchDown and UIControlEventTouchUpInside or UIControlEventTouchUpOutside
or even
Add a dispatch source iVar to your controller...
dispatch_source_t _timer;
Then, in your touchDown action, create the timer that fires every so many seconds. You will do your repeating work in there.
If all your work happens in UI, then set queue to be
dispatch_queue_t queue = dispatch_get_main_queue();
and then the timer will run on the main thread.
- (IBAction)touchDown:(id)sender {
if (!_timer) {
dispatch_queue_t queue = dispatch_get_global_queue(
DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
_timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
// This is the number of seconds between each firing of the timer
float timeoutInSeconds = 0.25;
dispatch_source_set_timer(
_timer,
dispatch_time(DISPATCH_TIME_NOW, timeoutInSeconds * NSEC_PER_SEC),
timeoutInSeconds * NSEC_PER_SEC,
0.10 * NSEC_PER_SEC);
dispatch_source_set_event_handler(_timer, ^{
// ***** LOOK HERE *****
// This block will execute every time the timer fires.
// Do any non-UI related work here so as not to block the main thread
dispatch_async(dispatch_get_main_queue(), ^{
// Do UI work on main thread
NSLog(#"Look, Mom, I'm doing some work");
});
});
}
dispatch_resume(_timer);
}
Now, make sure to register for both touch-up-inside and touch-up-outside
- (IBAction)touchUp:(id)sender {
if (_timer) {
dispatch_suspend(_timer);
}
}
Make sure you destroy the timer
- (void)dealloc {
if (_timer) {
dispatch_source_cancel(_timer);
dispatch_release(_timer);
_timer = NULL;
}
}
I am new to iOS and have no idea whatsoever how to do any of that! Thanks a lot for any help in advance!
I would do this with a GestureRecognzier. Here is some code:
- (void)viewDidLoad
{
[super viewDidLoad];
UILongPressGestureRecognizer *gr = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:#selector(buttonIsPressed:)];
gr.minimumPressDuration = 0.1;
[self.button addGestureRecognizer:gr];
}
-(IBAction)buttonIsPressed:(UILongPressGestureRecognizer*)gesture {
if (gesture.state == UIGestureRecognizerStateBegan) {
self.label.text = #"Button is pressed!";
}
else if (gesture.state == UIGestureRecognizerStateEnded) {
self.label.text = #"Button is not pressed";
}
}
You need an IBOutlet for the label and the button.
The first method does what you want, here's an example implementation:
- (void)viewDidLoad
{
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
UIButton *myButton = [[UIButton alloc] initWithFrame:CGRectMake(self.view.bounds.size.width / 2.0 - 50.0, self.view.bounds.size.height / 2.0 - 25.0, 100.0, 50.0)];
myButton.backgroundColor = [UIColor colorWithRed:0.2 green:0.3 blue:0.8 alpha:1.0];
[myButton setTitle:#"PRESS ME" forState:UIControlStateNormal];
[myButton addTarget:self action:#selector(setPressed:) forControlEvents:UIControlEventTouchDown];
[myButton addTarget:self action:#selector(setLetGo:) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:myButton];
}
-(void)setPressed:(id)sender
{
UIButton *myButton = (UIButton *)sender;
[myButton setTitle:#"PRESSED" forState:UIControlStateNormal];
}
-(void)setLetGo:(id)sender
{
UIButton *myButton = (UIButton *)sender;
[myButton setTitle:#"LET GO" forState:UIControlStateNormal];
}
If you are new to xCode this can be difficult to understand. Follow this and you'll be allright.
Go to Storyboard and put a button in the view now go to the Inspector and change State config from Default to Highlighted
In placeholder 'highlighted title' put 'Pressed'
Now click on the Assistant Editor to open the .m file next to Storyboard
CNTRL + Click and drag from the Button to the .m file code to create a TouchUpInside event, mine called buttonPressed
Put this code in the method body and you are good to go!
This is what it will look like, pressed and released:

Updating label text (dynamically) while executing another method

I am an iOS developer currently developing an iphone application. I have a simple question regarding updating the contents of a ViewController and I would greatly appreciate it if I can get someone’s feedback who is aware of this issue or have a suggested solution.
I’m writing a method that constantly updates the text of a label (and other components such as the background colour of a UIImage “box1” and “box2” ).
The issue : update to the text of the label (along with other changes) only takes effect at the end when the code was done execution. So I have written a simple infinite loop that does this as follows:
-(void) stateTest{
int i=0;
for(;;){
if (i==5) {
self.stateLabel.text=#"Opened"; //stateLabel previously declared
[self.box2 setBackgroundColor:[UIColor whiteColor]]; // box2 declared as UIImage
[self.box1 setBackgroundColor:[UIColor purpleColor]];
}
if (i==10){
self.stateLabel.text=#"Closed";
[self.box2 setBackgroundColor:[UIColor redColor]];
[self.box1 setBackgroundColor:[UIColor whiteColor]];
break;
}
i++;
}
}
and in the viewDidLoad method:
- (void)viewDidLoad
{
[super viewDidLoad];
self.stateLabel.text=#“View did Load";
[self.box1 setBackgroundColor:[UIColorwhiteColor]];
[self.box2 setBackgroundColor:[UIColorwhiteColor]];
[self stateTest];
}
I’m stepping through the code (breakpoints) and this is the issue I’m facing:
when i==5 the label text does NOT get updated (same as the color of the boxes)
when i==10, the label text does NOT get updated
The update (both the label text and the box color) show up after the code is done executing with
stateLabel : "Closed",
Box 2: red background color
I have tried calling upon several functions at the end of the stateTest method (after the i++) hoping that it will “REFRESH” the contents of the view, label and/or the UIImage:
[self.stateLabel setNeedsLayout];
[self.stateLabel setNeedsDisplay];
[self.box1 setNeedsDisplay];
[self.box2 setNeedsDisplay];
[self.view setNeedsDisplay];
Unfortunately non of these trials worked. I have also put an NSLog that outputs the text of the label and that works just fine, as I want. But the problem is with the dynamic update of the contents of the view controller during execution of code/method.
I would greatly appreciate any help and I am open to any suggestion
Ideally, this code is used in parallel with a face detection algorithm that detects the state of the mouth. There is a processImage method that processes the image every frame. I call [self stateTest] every time the image is processed; if the mouth is opened → stateLabel.text=#”Open”…etc
Thank you in advance for your contribution
Cheers!
The UI update will happen only when the run loop is completed. normally i run the infinite loop in background thread and post the UI update in the main thread. This will take care of the run loop.
Below code is for reference.
-(void) viewDidLoad
{
[super viewDidLoad];
self.subView = [[UIView alloc] initWithFrame:CGRectMake(50, 50, 30, 30)];
self.subView.backgroundColor = [UIColor blueColor];
[self.view addSubview:self.subView];
dispatch_queue_t newQueue = dispatch_queue_create("newQueue", nil);
dispatch_async(newQueue, ^{
[self stateTest];
});
}
-(void) stateTest{
int i=0;
for(;;){
if (i==5) {
[self performSelectorOnMainThread:#selector(purpleColor) withObject:self waitUntilDone:NO];
}
if (i==10){
[self performSelectorOnMainThread:#selector(blueColor) withObject:self waitUntilDone:NO];
// break;
}
if (i==15){
[self performSelectorOnMainThread:#selector(redColor) withObject:self waitUntilDone:NO];
//break;
}
if (i==20){
[self performSelectorOnMainThread:#selector(greenColor) withObject:self waitUntilDone:NO];
break;
}
sleep(1);
i++;
}
}
-(void) purpleColor
{
[self.subView setBackgroundColor:[UIColor purpleColor]];
}
-(void) blueColor
{
[self.subView setBackgroundColor:[UIColor blueColor]];
}
-(void) redColor
{
[self.subView setBackgroundColor:[UIColor redColor]];
}
-(void) greenColor
{
[self.subView setBackgroundColor:[UIColor greenColor]];
}
it's not a very good idea to run a infinite loop on the main thread (uithread). Instead can I suggest you to use a timer or a dispatch event ? below is some code you can use for a timer. and I would also point you out that NSTimer cannot be re used. hence if you wish to do this operation after you invalidated it you need to re create the timer & add it into a runloop.
// properties in your controller
#property (nonatomic) BOOL state;
#property (nonatomic, strong) NSTimer *timer;
// where to create it . timer interval is in seconds & use decimals if you want split seconds.
- (void)viewDidLoad
{
[super viewDidLoad];
self.timer = [NSTimer timerWithTimeInterval:0.1 target:self selector:#selector(onTimer:) userInfo:nil repeats:YES];
[[NSRunLoop mainRunLoop] addTimer:self.timer forMode:NSDefaultRunLoopMode];
}
// timer callback method
int count = 0;
- (void)onTimer:(NSNotification *)sender
{
self.state = !self.state;
if (self.state)
{
self.box1.backgroundColor = [UIColor redColor];
self.box2.backgroundColor = [UIColor blueColor];
}
else
{
self.box1.backgroundColor = [UIColor yellowColor];
self.box2.backgroundColor = [UIColor greenColor];
}
count++;
if (count == 100)
{
[self.timer invalidate];
[[[UIAlertView alloc] initWithTitle:#"timer done" message:#"timer finished" delegate:nil cancelButtonTitle:#"OK" otherButtonTitles:nil, nil] show];
}
}

change UI at runtime from view controller

I have a view controller that loads a custom view(which in turn draws UI elements, then spawns a thread to do some things in the back ground.
If the "things that run in the background", encounter an error, my view controller catches it, and at this time I want to change UI elements, like bgcolor or add a new label.
But any changes I make are not showing up. This is what I'm trying:
[self performSelectorOnMainThread:#selector(onCompleteFail) withObject:nil waitUntilDone:YES];
- (void)onCompleteFail
{
NSLog(#"Error: Device Init Failed");
mLiveViewerView.backgroundColor= [UIColor whiteColor];
//self.view.backgroundColor = [UIColor whiteColor];
UILabel *tmpLabel = [[UILabel alloc] initWithFrame:CGRectMake(20, 20, 200, 30)];
tmpLabel.text = #"Failed to init";
[self.view addSubview:tmpLabel];
}
You need to make any UI-related calls on the main thread: UIKit is not thread-safe and you’ll see all kinds of weird behavior from acting as if it is. That could be as simple as switching something from
[self onCompleteFail];
to
[self performSelectorOnMainThread:#selector(onCompleteFail) withObject:nil waitUntilDone:NO];
…or if -onCompleteFail has to be called on the background thread for other reasons, you can wrap your UI calls in a dispatch to the main queue, like this:
- (void)onCompleteFail
{
NSLog(#"Error: Device Init Failed");
dispatch_async(dispatch_get_main_queue(), ^{
mLiveViewerView.backgroundColor= [UIColor whiteColor];
//self.view.backgroundColor = [UIColor whiteColor];
UILabel *tmpLabel = [[UILabel alloc] initWithFrame:CGRectMake(20, 20, 200, 30)];
tmpLabel.text = #"Failed to init";
[self.view addSubview:tmpLabel];
});
}

Delay On IPad When Calling From A Background Thread

I am converting my IPhone app to IPad and I'm having an issue with the conversion. I have a background thread that creates some thumbnails.
The problem seems to be that the app loops though the array and outputs all the items but only the last item seems to be loading with the UIButton text present. There is a 5-15 second delay before all the previous buttons have there text displayed.
UIImage *tmp = [self thumbnailImage:[effect objectAtIndex:0] thumbnailType:#"category"];
for (id effect in effectArray) {
UIImage *tmp = [self thumbnailImage:[effect objectAtIndex:0] thumbnailType:#"category"];
dispatch_async(dispatch_get_main_queue(), ^{
UIView *effectView = [[UIView alloc] initWithFrame:CGRectMake(item_x_axis, current_row_height, item_width, item_height)];
UIImageView *imagepreview = [[UIImageView alloc] init];
[imagepreview setFrame:CGRectMake(20, 20, 100, 100)];
[imagepreview setImage:tmp];
imagepreview.contentMode = UIViewContentModeScaleAspectFit;
imagepreview.layer.shadowColor = [UIColor blackColor].CGColor;
imagepreview.layer.shadowOffset = CGSizeMake(0, 1);
imagepreview.layer.shadowOpacity = 1;
imagepreview.layer.shadowRadius = 2.0;
imagepreview.clipsToBounds = NO;
[effectView addSubview:imagepreview];
if([[effect objectAtIndex:3] isEqualToString:#"iap"]){
UIImageView *buttonPlus = [[UIImageView alloc] init];
[buttonPlus setFrame:CGRectMake(54, 66, 23, 23)];
[buttonPlus setImage:[UIImage imageNamed:#"ButtonPlus.png"]];
[effectView addSubview:buttonPlus];
}
UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom];
[button addTarget:self action:NSSelectorFromString([effect objectAtIndex:1]) forControlEvents:UIControlEventTouchDown];
[button setTitle:[effect objectAtIndex:2] forState:UIControlStateNormal];
button.frame = CGRectMake(0, 0, item_width, item_height);
button.titleLabel.font = [UIFont systemFontOfSize:11];
[button setTitleShadowColor:[UIColor blackColor] forState:UIControlStateNormal];
[button.titleLabel setShadowOffset:CGSizeMake(1.0f, 1.0f)];
[button setTitleEdgeInsets:UIEdgeInsetsMake(120, 0, 0.0, 0.0)];
[effectView addSubview:button];
[self.viewScrollCategories addSubview:effectView];
button = nil;
});
if(current_items_per_row < 3){
current_items_per_row++;
item_x_axis = item_x_axis + item_width;
}else {
current_row_height = current_row_height + item_height;
current_items_per_row = 1;
item_x_axis = 0;
}
}
Any idea how to solve this issue? it seemed to work fine on the IPhone.
EDIT
This is the code that is calling the background thread (shortned)
- (void)imagePickerController:( UIImagePickerController *)picker didFinishPickingMediaWithInfo: (NSDictionary *)info {
//[self generatingThumbnailMessageShow];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[self loadAllEffects];
dispatch_async(dispatch_get_main_queue(), ^{
[self generatingThumbnailMessageHide];
});
});
}
Well, you are asking for a delay with dispatch_async(dispatch_get_main_queue().... That code means "Please do this whenever you have time, sometime after my current code finishes running." Once you say that, you are basically saying you don't care when this code runs. You hope it will be some time pretty soon, but you've left the runtime to take care of the details.
So perhaps it is this "hope" that is the issue. The architecture of the device processors is different and the threading model for the device might change the way the device responds to having a bunch of these delayed requests piling up.
I'm not at all clear on what you're trying to accomplish in this code (sorry, tl:dr) but the use of delayed performance in the middle of it and your complaint that there is a " 5-15 second delay" seem to go together somehow...
EDIT: OK, I see now that your code is running in the background and you are using dispatch_async to step out to the main thread in order to modify the interface. So perhaps the problem is not in the code you quote, but in the code you are using to manage your background threading in the first place.
ANOTHER EDIT: Just a wild and crazy idea here. If what takes time is the image processing, why don't you do all the image processing and then do your interface updating on the main thread once? What I mean is, you're doing this:
for (id effect in effectArray) {
UIImage *tmp = // ...
dispatch_async(dispatch_get_main_queue(), ^{
// ...
[imagepreview setImage:tmp];
So you're getting back on the main thread repeatedly each time through the loop. Why not try this:
dispatch_async(dispatch_get_main_queue(), ^{
for (id effect in effectArray) {
UIImage *tmp = // ...
// ...
[imagepreview setImage:tmp];
Now you're just getting back on the main thread once. There will be a delay while the images process, but then interface should just update, badda bing badda boom (technical programming term).

Resources