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];
});
}
Related
I have an issue which is not a big deal but driving me crazy.
I have a view which subclasses UITableView.
Pressing on one of the cells leader to an lengthy HTTP requests, which I run on another thread using this code in didSelectRowAtIndexPath:
case SECTION_SEND:
[self addActivityIndicatorToView];
[self performSelectorInBackground:#selector(sendEmail) withObject:nil];
break;
The activityIndicator is behaving correctly. Displaying on the screen and then removing itself when the operation ends. But if the app goes to the background or another tab, the activity indicator disappears, even though the task did not finish.
Here are the methods I am using:
Then:
-(UIActivityIndicatorView*)createActivityIndicator {
//Setting up activity indicator
UIActivityIndicatorView *spinner = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhiteLarge];
spinner.color = [UIColor whiteColor];
spinner.backgroundColor = [UIColor blackColor];
spinner.alpha = 0.8;
CGFloat width = 120;
spinner.frame = CGRectMake(self.tableView.bounds.size.width/2-width/2, self.tableView.bounds.size.height/2-width/2, width, width);
//spinner.center = CGPointMake( [UIScreen mainScreen].bounds.size.width/2,[UIScreen mainScreen].bounds.size.height/2);
spinner.layer.cornerRadius = 20;
spinner.layer.masksToBounds = YES;
return spinner;
}
-(void)addActivityIndicatorToView {
//[[[[UIApplication sharedApplication] delegate] window] addSubview:ai];
_ai = [self createActivityIndicator];
[self.tableView.superview addSubview:_ai];
[_ai startAnimating];
}
-(void) removeActivityIndicatorFromView {
[_ai stopAnimating];
[_ai removeFromSuperview];
}
I am calling [self removeActivityIndicatorFromView] when the operation ends.
As I said, it behaves properly unless I leave the screen. I have check my viewdidload and viewwilldisappear viewwillappear and init methods and I have nothing overriding _ai.
Any ideas?
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];
}];
}
I have a method which is calling a external webservice.
Now I want to show an UIActivityIndicatorView in the middle of the screen, while the webservice-call is running. I call the webservice-method asynchronously. But with my code which you will find at the bottom, I have two problems:
the current view is not locked. That means, that the user can interact with the current view. I want to block all till the animation is stopped
the animation is not centered
Here is my code which I have tried now:
-(void)saveDataOnServer {
UIActivityIndicatorView *activity = [[UIActivityIndicatorView alloc]initWithFrame:CGRectMake(225, 115, 30, 30)];
[activity setBackgroundColor:[UIColor clearColor]];
[activity setActivityIndicatorViewStyle:UIActivityIndicatorViewStyleGray];
[self.view addSubview:activity];
[activity startAnimating];
// ...
[NSURLConnection sendAsynchronousRequest: request
queue: queue
completionHandler: ^(NSURLResponse *response, NSData *data, NSError *error) {
[activity stopAnimating];
// ...
}
];
}
You have to put another view when activity indicator start :
UIView *overlayView;
overlayView = [[UIView alloc] initWithFrame:[UIScreen mainScreen].bounds];
overlayView.backgroundColor = [UIColor colorWithRed:0 green:0 blue:0 alpha:0.5];
[[UIApplication sharedApplication].keyWindow addSubview:overlayView];
And remove that view(overlayView) when activity indicator stops :
[overlayView removeFromSuperview];
This OverlayView will cover current view.So user can not interact with current view.
Hi there i have a good solution for you ..
there i a library called MBProgressHUD
which does all you want , WITH Style
This is The link for the library HERE
And its easy to use .. here is an example i used
self.hud = [MBProgressHUD showHUDAddedTo:self.view animated:YES];
self.hud.mode = MBProgressHUDModeIndeterminate;
self.hud.labelText = #"Fetching News";
//self.hud.detailsLabelText=#"Fetching News";
self.hud.dimBackground = YES;
and there is a demo you can see .. Cheers :D
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).
I'm trying to load views asynchronously. The problem is that the frame of the views to be loaded depends on the data that's loaded asynchronously. In other words there are some long calculations that decide where to actually display the UIViews.
I know that there are problems when trying to actually display a UIView in a thread and that you should always load them back in the main thread, so this is the code I've been trying out:
asyncQueue = [[NSOperationQueue alloc] init];
[asyncQueue addOperationWithBlock:^{
// Do work to load the UIViews and figure out where they should be
UIButton *test = [[UIButton alloc] initWithFrame:[self doWorkToGetFrame]];
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
[self addSubview:test];
}
}];
}];
This all resides in a UIView container.
Try something like this:
UIButton *test = [[UIButton alloc] initWithFrame:CGRectZero];
test.hidden = YES;
test.alpha = 0.0f;
[self addSubview:test];
dispatch_queue_t downloadQueue = dispatch_queue_create("myDownloadQueue",NULL);
dispatch_async(downloadQueue, ^
{
// do work to load UIViews and calculate frame
CGRect frameButton = ...;
dispatch_async(dispatch_get_main_queue(), ^
{
test.hidden = NO;
[UIView animateWithDuration:0.4f animations:^
{
test.frame = frameButton;
test.alpha = 1.0f;
}
completion:^(BOOL finished){}];
});
});
dispatch_release(downloadQueue);
This adds your button in the main thread, but makes it invisible initially. After your background work is done, you'll make it visible and set the frame using an animation.