I would like to update a progressbar while i am instantiating some ui elements that takes some times. I first create my view during the viewLoad method and add my progress bar there. Once my view appeared in the viewDidAppear method I am making several uikit objects instantiation but i would like to update the progress bar in the mean time. I am not sure how to proceed since everything should happen in the main thread as it is ui elements.
Here is part of my code:
-(void) viewDidAppear:(BOOL)animated
{
// precompute the source and destination view screenshots for the custom segue
self.sourceScreenshotView = [[UIImageView alloc] initWithImage:[self.view pw_imageSnapshot]];
[self.progressBar setProgress:.3];
SCLViewController *rvc = [[SCLViewController alloc] init];
UIView *destinationView = rvc.view;
destinationView.frame = CGRectMake(0, 0, kWidthLandscape, kHeightLandscape);
self.destinationScreenshotView = [[UIImageView alloc] initWithImage:[destinationView pw_imageSnapshot]];
[self.progressBar setProgress:.5];
}
In the above code I just need to create two screenshots of views to use them later on. The problem is that i only see the last update (.5) when setting the progress to the progress bar. What is the proper way to do this update?
You can use the performSelectorInBackground:withObject: method in order to instantiate your heavy views. That method (the one that instantiates your views) will have to set your progress bar progress in the main thread.
So your code would look something like this:
- (void)viewDidAppear:(BOOL)animated
{
[self performSelectorInBackground:#selector(instantiateHeavyViews) withObject:nil];
}
- (void)instantiateHeavyViews
{
self.sourceScreenshotView = [[UIImageView alloc] initWithImage:[self.view pw_imageSnapshot]];
[self performSelectorOnMainThread:#selector(updateMyProgressView:) withObject:[NSNumber numberWithFloat:0.3f] waitUntilDone:YES];
SCLViewController *rvc = [[SCLViewController alloc] init];
UIView *destinationView = rvc.view;
destinationView.frame = CGRectMake(0, 0, kWidthLandscape, kHeightLandscape);
self.destinationScreenshotView = [[UIImageView alloc] initWithImage:[destinationView pw_imageSnapshot]];
[self performSelectorOnMainThread:#selector(updateMyProgressView:) withObject:[NSNumber numberWithFloat:0.5f] waitUntilDone:YES];
}
- (void)updateMyProgressView:(NSNumber *)progress
{
[self.progressBar setProgress:[progress floatValue]];
}
Edit: of course, it won't animate your progress bar (I don't know if that is what you wanted). If you want it to move on while your views are being created, you should use a delegate to be notified of the progress, and this can be a bit harder. This way you would be able to update the progress bar every time the delegate is notified.
Related
This question already has an answer here:
Method calling via performSelectorOnMainThread Vs Normal method calling
(1 answer)
Closed 9 years ago.
In my viewDidLoad, I call a function:
[common startActivityIndicatorOnView:self.view];
This method adds a view with Activity indicator, in the center of self.view.
My current view is pushed on a Navigation Stack. This is how the view looks after this method returns (the activity indicator view is not in center):
However, if I call the same method this way:
[common performSelectorOnMainThread:#selector(startActivityIndicatorOnView:) withObject:self.view waitUntilDone:NO];
The view looks like the following image (the activity indicator view is in center):
I do not get, How does it make a difference if the calling line is written in viewDidLoad.
If any one can help me get this, thanks in advance.
Just for reference,
the method looks like this:
-(void) startActivityIndicatorOnView:(UIView *)view {
if ([NSRunLoop currentRunLoop] != [NSRunLoop mainRunLoop]) {
[self performSelectorOnMainThread:#selector(startActivityIndicatorOnView:) withObject:view waitUntilDone:NO];
return;
}
view.userInteractionEnabled = NO;
activityBgView = [[UIView alloc] initWithFrame:CGRectMake((view.frame.size.width/2) - 50, (view.frame.size.width/2) - 50, 100, 100)];
activityBgView.center = view.center;
activityBgView.backgroundColor = [UIColor grayColor];
activityBgView.alpha = 0.8;
spinner = [[UIActivityIndicatorView alloc] initWithFrame:CGRectMake((activityBgView.frame.size.width/2)-10, (activityBgView.frame.size.width/2)-10, 20, 20)];
spinner.activityIndicatorViewStyle = UIActivityIndicatorViewStyleWhiteLarge;
spinner.center = view.center;
[view addSubview:activityBgView];
[view addSubview:spinner];
[spinner startAnimating];
}
ViewDidLoad is called when you load viewControllers (via nib/xib or created programmatically in the loadView method), meaning xcode create all views and instantiate and alloc all objects in xib...
but, your viewController view (and subViews) are still not added in any view...
yourViewController.view.superView = nil;
so, its view has got the frame that you set in xib, but if you tell it to resize inside its superview, when you add it (e.g. with a push or an addsubview), its frame changes, but your
spinner won't change its position.
calling a performSelectorOnMainThread just will call your method later, when your current thread step ahead and may have pushed your viewController.view, so, when executed, yourViewController.view.superView exists, and so view.frame has already changed.
try to move your call to
[common startActivityIndicatorOnView:self.view];
in a viewWillAppear method: at that point yourViewController.view should been already resized to fit its superView
EDIT:
# pavel question:
after what moment yourViewController.view.superView will be not nil?
banally: when you add its view to a view. that is, firts you allocate and init it (init with a nib or via code)
something like:
yourViewControllerInstance = [[YourViewController alloc]initWithNibName:#"yourViewControllerNib" bundle:nil];
at this point the method viewDidLoad in your class is called (but yourViewController.view.superview 0 nil)
later, you usually use your new viewController, so you "push" it in the stack of a navigationController, or you just add its view to the current viewController.view... something like:
[currentViewController.view addSubview:yourViewController.view];
after this line, as you may imagine, yourViewController.view.superView = currentViewController.view, and the method viewWillAppear of yourViewController is called, so you can use superView inside it.
Notice that at this point your viewController.view is still not visible on screen, so you can adjust sizes, move or add views here without users see any changes.
after this, yourViewController will show, and at the end, and the method viewDidAppear of yourViewController is called (for any other code, in case)
I have this piece of code to push a view controller:
// Setup the animation
[self.navigationController pushViewController:self.productView animated:YES];
self.productView.imageURL = [product imageURL];
// Set the title of the view to the product's name
self.productView.title = [product name];
// Set the label text of all the labels in the view
[self.productView.caloriesL setText:[product calories]];
[self.productView.fatL setText:[product fat]];
[self.productView.saturatesL setText:[product saturates]];
[self.productView.sugarL setText:[product sugar]];
[self.productView.fibreL setText:[product fibre]];
[self.productView.saltL setText:[product salt]];
But the delegate method viewDidAppear does not get called when the productView appears. I looked up the problem on google and theres a lot of different solutions, none of which I could apply to my problem.. I had a similar problem in a previous solution but I got around it by manually calling viewDidApear in the viewDidLoad method. Unfortunately in this case I can't do that as viewDidLoad is called only once (on the first push). Does anyone know how to fix this?
Thanks,
Jack Nutkins
EDIT:
Here is the viewDidAppear method in the productView (and selector):
- (void)viewDidAppear:(BOOL)animated{
//Start animating the activity indicator
[indicator startAnimating];
//Perform this method in background
[self performSelectorInBackground:#selector(loadImage) withObject:nil];
}
- (void) loadImage {
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
// Load the animals image into a NSData boject and then assign it to the UIImageView
NSData *imageData = [NSData dataWithContentsOfURL:[NSURL URLWithString:imageURL]];
UIImage *image = [[UIImage alloc] initWithData:imageData];
self.imageView.image = image;
//Stop animating the activity indicator
[indicator stopAnimating];
[pool drain]; //see comment below
}
First: You definitely don't want to be calling any of the standard viewWillLoad, viewDidLoad, viewWillAppear, etc. methods manually. Let the OS do it for you.
Second: Can you show us how your viewDidAppear method is implemented in your self.productView instance? (Just a hunch, you're not expecting this method to be called on your navigation controller, right?) I just want to make sure your method signature is exactly correct. If it's not (due to a mispelling, improper args, etc.) then it definitely won't be called.
Third: I would move your pushViewController: call to after the rest of the code you provided. You don't want the view to be pushed on the screen (so the user can see it) and then have a bunch of on-screen values immediately change. Set your ivars and title property first, then push the view controller. This eliminates any weird flickering.
I solved it, though it doesn't seem conventional, can't believe I didn't try it earlier :
I put this line :
[self.productView viewDidAppear:YES];
Underneath :
// Setup the animation
[self.navigationController pushViewController:self.productView animated:YES];
I also moved the code to set the labels text to run before the above line. (As well as changing my code to send strings to the pushed controller rather that accessing its UI elements.)
Thanks for everyones help,
Jack
I ran into difficulties with SSCollectionView and SSCollectionViewItem.
First of all I'd like to get it initialized from IB. But that won't work for me.
I have a SelectFooViewController which is:
#interface SelectFooViewController : SSCollectionViewController { ... }
and am using it as filesOwner of the corresponding XIB.
SelectFooViewController* selectFooVC = [[SelectFooViewController alloc]
initWithNibName:#"SelectFooViewController" bundle:nil];
But since it wont work I had to initialize its properties inside viewDidLoad() myself.
Furthermore I am not able to display anything except the backgroundColor of my SSCollectionViewItems. What I want is a textLabel and an image .
- (SSCollectionViewItem *)collectionView:(SSCollectionView *)aCollectionView itemForIndexPath:(NSIndexPath *)indexPath {
SSCollectionViewItem *item = [[[SSCollectionViewItem alloc] initWithStyle:SSCollectionViewItemStyleImage reuseIdentifier:itemIdentifier] autorelease];
SSLabel* label = [[SSLabel alloc] init];
[label setText:#"foo"];
item.imageView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:#"foo.png"]];
item.textLabel = label;
[label autorelease];
return item;
}
I can confirm that the delegate methods (for determining the number Of rows, sections and such) are implemented and working as expected. But my items are all empty - but react onclick with the expected popup.
Does anyone see an error in what I did? - Thanks...
EDIT: I was also not able to display a local image by changing SSCatalog project
I just figured out, that I have to set the frame of each property (textLabel, detailTextLabel and imageView) myself. That fixed it.
When you create instance SelectFooViewController just insert this line
selectFooVC.view;
or
selectFooVC.view.hidden = NO;
And then add it to the view.
This is because the view is not initalised until you explicitly access it. Hence your items are loaded only when you click it and not immediately. You can call it a hack but i don't call it one. :-)
I'm trying to add a «loader-view» to my app which shows a spinner while doing stuff.
This works fine the first time, but it doesn't work a second time.
here's what I do:
I have a viewController for the spinner (spinnerViewController) and a nib-file which I made in IB (spinner.xib).
I load the nib in the viewDidLoad-event:
spinnerView = [[spinnerViewController alloc] initWithNibName:#"spinner" bundle:nil];
[spinnerView retain];
spinnerView is declared in the .h-file (spinnerViewController *spinnerView;)
next, I show the spinner-view:
[self.view addSubview:spinnerView.view];
[self.view bringSubviewToFront:spinnerView.view];
which works fine...
And now the trouble starts. No matter what I do, I can't show the spinner view again.
I tried just hiding it (self.view sendSubViewToBack: spinnerView.view) which works for hiding, but when I try to bring it to the front again (self.view bringSubViewToFront: spinnerView.view) it doesn't work.
I also tried removing the spinner-view and add it again with no success (within the spinnerViewController: [self.view removeFromSuperview] and to show it again [self.view addSubview... )
[EDIT]
I changed the whole setup a little and just made the spinner-view a subview in IB - again, hiding works, but showing again fails.
What I found out: After the bringSubViewToFront-command, I call some web-service to get some data. When I comment the following code out and just show the spinnerView, it works. So now I'm trying to figure out how to determine when the spinner-view appeared and then continue with the code - but naturally, this doesn't work (yet) :)
Any ideas what I'm doing wrong??? ;)
Problem solved.
This page gave the answer: http://urenjoy.blogspot.com/2009/05/uiactivityindicatorview.html
Apparently, the update has to happen in a separate thread, as the web-stuff blocks the current one, hence the view did not appear.
[NSThread detachNewThreadSelector:#selector(doWork) toTarget:self withObject:nil];
- (void) doWork {
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
.....Time Consuming Code here .....
[pool release];
}
I might be not exactly on your question, but in general creating a ViewController class in order to show a spinner on the screen is a huge overkill ... just try to discover the logic behind what you do : you create a viewcontroller, but you never use it, you use the view.
So in short I believe you need only a UIView (the view property of the UIViewController)
Why don't you try something like :
... in your class interface...
UIActivityIndicator* activity;
... when the activity needs to happen ...
activity = [[UIActivityIndicator alloc] initWithActivityIndicatorStyle: ....
[activity startAnimating];
[self.view addSubview:activity];
[activity release]
... when the activity is finished
[activity removeFromSuperview]; //this will also decrease the retain count
In my iPhone app, views will often load slowly when transitioning, like if a user clicks a button on the Tab Bar Controller. This happens more if the phone is low on memory. It doesn't really come up on 3GS phones, but it's a big problem on 3G phones.
I suspect that I'm not following some best practices for creating UIViewControllers. I think I might be doing too much in the init functions, not using the viewDidLoad function, or something. It seems to affect all my views, so I think it's a problem with my style in general, not some particular snippet.
Can anyone tell me what i might be doing wrong? Here is some sample code, from a UIViewController subclass:
EDIT: In response to the question: "where is this being called?"
This function gets called in this case when the user clicks a marker on the map:
if(marker.label.tag == SavedBookmarkTag) {
SavedDetailScreen *savedBookmark = [[[SavedDetailScreen alloc] initBookmarkView:
[(NSDictionary *)marker.data objectForKey:#"bookmark"]]autorelease];
[savedBookmark showMap];
[self.navBar pushViewControllerWithBackBar:savedBookmark];
return;
}
END EDIT
-(id)initBookmarkView: (Bookmark *)bm {
self = [self initView];
self.bookmark = bm;
primaryLabel.text = [bm title];
secondaryLabel.text = [self getLineWithLat:[bm lat] AndLon:[bm lon] AndDate:[bm timeCreated]];
return self;
}
- (id)initView {
self = [super init];
self.isWaypoint = NO;
UIImageView *bg = [[UIImageView alloc] initWithImage:[UIImage imageNamed:#"238-beveled-background.png"]];
bg.frame = CGRectMake(0, 0, 320, 376);
[self.view addSubview:bg];
bg = [[UIImageView alloc] initWithImage:[UIImage imageNamed:#"small-label.png"]];
[self.view addSubview:bg];
[bg release];
self.primaryLabel = [[UILabel alloc]init];
primaryLabel.font = TITLE_FONT;
primaryLabel.backgroundColor = [UIColor clearColor];
primaryLabel.textColor = LIGHT_BLUE;
self.secondaryLabel = [[UILabel alloc]init];
secondaryLabel.font = TEXT_FONT;
secondaryLabel.backgroundColor = [UIColor clearColor];
secondaryLabel.textColor = LIGHT_BLUE;
secondaryLabel.lineBreakMode = UILineBreakModeClip;
self.thirdLabel = [[UILabel alloc]init];
thirdLabel.font = TEXT_FONT;
thirdLabel.backgroundColor = [UIColor clearColor];
thirdLabel.textColor = LIGHT_BLUE;
thirdLabel.lineBreakMode = UILineBreakModeCharacterWrap;
[self.view addSubview:primaryLabel];
[self.view addSubview:secondaryLabel];
[self.view addSubview:thirdLabel];
self.loadingBackground = [[UIImageView alloc] initWithImage:[UIImage imageNamed:#"stats-box.png"]];
loadingBackground.frame = CGRectMake(0, 115, loadingBackground.frame.size.width, loadingBackground.frame.size.height);
[self.view addSubview:loadingBackground];
[self.view sendSubviewToBack:loadingBackground];
AnimatedGif *animatedGif = [[[AnimatedGif alloc] init] autorelease];
NSData *data = [NSData dataWithContentsOfFile:[[NSBundle mainBundle] pathForResource:#"35" ofType:#"gif"]];
[animatedGif decodeGIF: data];
UIImageView *loadingImage = [animatedGif getAnimation];
loadingImage.frame = CGRectMake(150,150,loadingImage.frame.size.width,loadingImage.frame.size.height);
[loadingImage startAnimating];
[loadingBackground addSubview:loadingImage];
[loadingImage release];
[self layoutSubviews];
return self;
}
- (void) layoutSubviews {
self.view.frame = CGRectMake(0,0,320,372);
primaryLabel.frame = CGRectMake(30, 30, 260, 18);
secondaryLabel.frame = CGRectMake(30 ,52, 260, 16);
thirdLabel.frame = CGRectMake(30, 72, 260, 16);
}
The slowness that you're seeing can probably be attributed to the fact that constructing objects and reading data from the flash are both expensive processes. Look for opportunities to reuse existing objects rather than constructing them multiple times, and consider deferring especially expensive operations until after the view gets displayed.
In this case, I would start with a couple changes:
Make savedBookmark a member variable so that you can construct it once and reuse it. Replace your initBookmarkView: method with a setBookmarkView: method that you can call after this member variable is constructed to reconfigure your labels for the specific bookmark being displayed.
Take the subview creation code out of initView and put it in loadView. This is the most appropriate place to construct your own view hierarchy programmatically. UIViewController implements lazy loading on its view property to defer construction as long as possible. The view property is nil until the first time it's requested. At that point UIViewController calls loadView to set the property. The default implementation loads the view from a nib file if one is defined. Otherwise it just constructs an empty UIView and makes that the main view. Note that you'll have to construct the container view and set the view property yourself.
In other apps you may get some improvement by moving some initialization code into viewDidLoad:, which gets called after the view property is loaded, whether programmatically or from a nib. If you ever have an especially slow operation like loading images from a remote URL, you might want to start loading the data asynchronously in viewDidLoad:, and then update your subviews once the data finishes loading. In some cases you may also want to defer some code until viewDidAppear:. Just be aware that this method gets called every time the view appears, unlike loadView and viewDidLoad:, which only get called once.