My action method:
- (IBAction)buttonPressed {
// animate picker to random row (picker is a UIPickerView)
int row = random() % [self.column1 count];
[picker selectRow:row inComponent:0 animated:YES];
[picker reloadComponent:0];
// display new selected row content
int selectedRow = [picker selectedRowInComponent:0];
NSString *selectedItem = [self.column1 objectAtIndex:selectedRow];
myLabel.text = selectedItem; // UILabel under the picker
}
However
myLabel.text = selectedItem;
gets called before the animation has completed and so it doesn't display the new value.
I found the thread How to get callback from UIPickerView when the selectRow animation is done?, but the answer uses a beginAnimations/commitAnimation block - about which apple says: "Use of this method is discouraged in iOS 4.0 and later. You should use the block-based animation methods to specify your animations instead."
How would I use block-based animation to accomplish this?
It looks like Apple needs to update their UIPickerView API to support a block based completion handler. Until then, just wait the estimated time of the animation.
[picker selectRow:0 inComponent:0 animated:YES];
[self performSelector:#selector(setMyLabel) // setMyLabel - my function
withObject:nil
afterDelay:0.4];
Have you tried something like this:
[UIView animateWithDuration:2.0
delay:0.0
options:UIViewAnimationOptionCurveEaseInOut
animations:^{
// the animation code
[myPickerView selectRow:0 inComponent:0 animated:YES];
}
completion:^(BOOL finished) {
[self performSelector:#selector(setMyLabel)];
}
];
Or you could just set the label directly in the completion block, if that's the only thing you want to do after the animation completes.
EDIT:
The code above is the same as the code in the other thread you've mentioned, but using animation blocks instead. I actually didn't know if the method selectRow:inComponent:animated: will run on the same animation block thread or it will have its own thread. Since it didn't work, that means it runs on its own thread so the code I wrote won't work.
There is one way around to solve this, which is by blocking the main thread for the time needed for the animation to complete. You can call [NSThread sleepForTimeInterval:1.0] after your call to selectRow:inComponent:animated:, and you can adjust the interval until you reach a suitable value. Note that blocking the main thread is highly discouraged by Apple. Personally I would block it only if I couldn't think of another way to achieve what I'm trying to do and if the blocking time is smaller than 1.
Related
first of all sorry for my English :-) not so good.
I have a strange memory leak with the following code (code after the explanation).
I have a class, FLWaitingView. It is a simple view with a waiting indicator (plus a view with background), used to say to the user "wait for the data to be loaded".
It has two simple methods: show and dismiss.
In the show method, I find the main Application Window and add the subviews (the waiting view and a background view, with different animations). In the dismiss method, I remove it from superview.
In every show, I verify that the view isn't already visible using a static bool var (is_visible).
The strange thing is this: In the dismiss method, I use:
[self.view removeFromSuperview];
[self.waitingView removeFromSuperview];
to remove the two views from the Window, to avoid them to be retained. They are correctly removed, I can verify this with NSLog (for cicle on each window subview). But, in INSTRUMENTS, using the "mark heap" function, I see that in every single reload (new instance of FLWaitingView, then show, then dismiss) the old instance remains in memory and continues to increase memory usage. Obviously is not a problem of the calling code, because I correctly release the object:
//CALLING CODE
//customWaitingView is a property retained
self.customWaitingView = [[[FLWaitingView alloc]init]autorelease];
[self.customWaitingView show];
Moreover, and I think that this is the most important information, if I move the view dismission in another method, called by a selector, the leak disappear!!!
Now I show the "wrong" code and, after, the "correction". I would like to understand why it happens.
- (void)show
{
if (!is_visible){
id appDelegate = [[UIApplication sharedApplication] delegate];
UIWindow *window = [appDelegate window];
self.waitingLabel.text = #"Attendere";
self.view.alpha = 1.0;
self.waitingView.alpha = 1.0;
[window addSubview:self.view];
[window addSubview:self.waitingView];
[self.waitingIndicator startAnimating];
self.view.frame = window.frame;
self.waitingView.center = window.center;
// "Pop in" animation for alert
[self doPopInAnimationWithDelegate:self];
// "Fade in" animation for background
[self doFadeInAnimation];
is_visible = YES;
} else {
NSLog(#"FLWaitingView %# already visible, do nothing", self);
}
}
- (void)dismiss
{
[UIView beginAnimations:nil context:nil];
self.view.alpha = 0.0;
self.waitingView.alpha = 0.0;
[UIView commitAnimations];
[self.waitingIndicator stopAnimating];
//here is the problem
[self.view removeFromSuperview];
[self.waitingView removeFromSuperview];
is_visible = NO;
}
the code above is the "wrong" one, but if I add
[self performSelector:#selector(alertDidFadeOut) withObject:nil afterDelay:0.5];
in the dismiss method and a new method (obviously removing the redundant code from dismiss method):
- (void)alertDidFadeOut
{
//here the memory is correctly released
[self.view removeFromSuperview];
[self.waitingView removeFromSuperview];
is_visible = NO;
}
the memory is correctly released.
Why??????
Thank you in advance
Fabio
Your view isn't getting released as you would be expecting because at the moment you're releasing it there are still animations linked to it. You can only properly release it after the animations are finished.
Your second method works because the animation lasts less than 0.5 seconds - the releasing code is called after view is freed of all the animations.
Proper way to animate the view would be to either create an animation and assign its delegate or maybe a bit more elegant soulution is to use block-based animation like this:
- (void)dismiss
{
[[UIApplication sharedApplication] beginIgnoringInteractionEvents];
[UIView animateWithDuration: 0.15
animations: ^{
self.view.alpha = 0.0;
self.waitingView.alpha = 0.0;
}
completion: ^(BOOL finished){
[self.waitingIndicator stopAnimating];
[self.view removeFromSuperview];
[self.waitingView removeFromSuperview];
is_visible = NO;
[[UIApplication sharedApplication] endIgnoringInteractionEvents];
}];
}
I'd like to display an activity indicator BEFORE the work undertaken by willAnimateRotationToInterfaceOrientation:duration: begins. Most of the time in my app, this work is quickly completed and there would be no need for an activity indicator, but occasionally (first rotation, i.e. before I have cached data, when working with a large file) there can be a noticeable delay. Rather than re-architect my app to cope with this uncommon case, I'd rather just show the UIActivityIndicatorView while the app generates a cache and updates the display.
The problem is (or seems to be) that the display is not updated between the willRotateToInterfaceOrientation:duration and the willAnimateRotationToInterfaceOrientation:duration: method. So asking iOS to show UIActivityIndicator view in willRotate method doesn't actually affect the display until after the willAnimateRotation method.
The following code illustrates the issue. When run, the activity indicator appears only very briefly and AFTER the simulateHardWorkNeededToGetDisplayInShapeBeforeRotation method has completed.
Am I missing something obvious? And if not, any smart ideas as to how I could work around this issue?
Update: While suggestions about farming the heavy lifting off to another thread etc. are generally helpful, in my particular case I kind of do want to block the main thread to do my lifting. In the app, I have a tableView all of whose heights need to be recalculated. When - which is not a very common use case or I wouldn't even be considering this approach - there are very many rows, all the new heights are calculated (and then cached) during a [tableView reloadData]. If I farm the lifting off and let the rotate proceed, then after the rotate and before the lifting, my tableView hasn't been re-loaded. In the portrait to landscape case, for example, it doesn't occupy the full width. Of course, there are other workarounds, e.g. building a tableView with just a few rows prior to the rotate and then reloading the real one over that etc.
Example code to illustrate the issue:
#implementation ActivityIndicatorViewController
#synthesize activityIndicatorView = _pgActivityIndicatorView;
#synthesize label = _pgLabel;
- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation
{
return (interfaceOrientation != UIInterfaceOrientationPortraitUpsideDown);
}
- (void) willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration;
{
NSLog(#"willRotate");
[self showActivityIndicatorView];
}
- (void) willAnimateRotationToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration;
{
NSLog(#"willAnimateRotation");
[self simulateHardWorkNeededToGetDisplayInShapeBeforeRotation];
}
- (void) didRotateFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation;
{
NSLog(#"didRotate");
[self hideActivityIndicatorView];
}
- (void) simulateHardWorkNeededToGetDisplayInShapeBeforeRotation;
{
NSLog(#"Starting simulated work");
NSDate* date = [NSDate date];
while (fabs([date timeIntervalSinceNow]) < 2.0)
{
//
}
NSLog(#"Finished simulated work");
}
- (void) showActivityIndicatorView;
{
NSLog(#"showActivity");
if (![self activityIndicatorView])
{
UIActivityIndicatorView* activityIndicatorView = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleGray];
[self setActivityIndicatorView:activityIndicatorView];
[[self activityIndicatorView] setCenter:[[self view] center]];
[[self activityIndicatorView] startAnimating];
[[self view] addSubview: [self activityIndicatorView]];
}
// in shipping code, an animation with delay would be used to ensure no indicator would show in the good cases
[[self activityIndicatorView] setHidden:NO];
}
- (void) hideActivityIndicatorView;
{
NSLog(#"hideActivity");
[[self activityIndicatorView] setHidden:YES];
}
- (void) dealloc;
{
[_pgActivityIndicatorView release];
[super dealloc];
}
- (void) viewDidLoad;
{
UILabel* label = [[UILabel alloc] initWithFrame:CGRectMake(50.0, 50.0, 0.0, 0.0)];
[label setText:#"Activity Indicator and Rotate"];
[label setTextAlignment: UITextAlignmentCenter];
[label sizeToFit];
[[self view] addSubview:label];
[self setLabel:label];
[label release];
}
#end
The app doesn't update the screen to show the UIActivityIndicatorView until the main run loop regains control. When a rotation event happens, the willRotate... and willAnimateRotation... methods are called in one pass through the main run loop. So you block on the hard work method before displaying the activity indicator.
To make this work, you need to push the hard work over to another thread. I would put the call to the hard work method in the willRotate... method. That method would call back to this view controller when the work is completed so the view can be updated. I would put show the activity indicator in the willAnimateRotation... method. I wouldn't bother with a didRotateFrom... method. I recommend reading the Threaded Programming Guide.
Edit in response to a comment: You can effectively block user interaction by having the willAnimateRotation... method put a non functioning interface on screen such as a view displaying a dark overlay over and the UIActivityIndicatorView. Then when the heavy lifting is done, this overlay is removed, and the interface becomes active again. Then the drawing code will have the opportunity to properly add and animate the activity indicator.
More digging (first in Matt Neuberg's Programming iPhone 4) and then this helpful question on forcing Core Animation to run its thread from stackoverflow and I have a solution that seems to be working well. Both Neuberg and Apple issue strong caution about this approach because of the potential for unwelcome side effects. In testing so far, it seems to be OK for my particular case.
Changing the code above as follows implements the change. The key addition is [CATransaction flush], forcing the UIActivityIndicatorView to start displaying even though the run loop won't be ended until after the willAnimateRotationToInterfaceOrientation:duration method completes.
- (void) willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration;
{
NSLog(#"willRotate");
[self showActivityIndicatorView];
[CATransaction flush]; // this starts the animation right away, w/o waiting for end of the run loop
}
- (void) willAnimateRotationToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration;
{
NSLog(#"willAnimateRotation");
[self simulateHardWorkNeededToGetDisplayInShapeBeforeRotation];
[self hideActivityIndicatorView];
}
- (void) didRotateFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation;
{
NSLog(#"didRotate");
}
Try performing you work on a second thread after showing the activity view.
[self showActivityIndicatorView];
[self performSelector:#selector(simulateHardWorkNeededToGetDisplayInShapeBeforeRotation) withObject:nil afterDelay:0.01];
Either execute the heavy lifting in a background thread and post the results in the foreground thread to update the UI (UIKit is only thread safe since iOS 4.0):
[self performSelectorInBackground:#selector(simulateHardWorkNeededToGetDisplayInShapeBeforeRotation) withObject:nil]
Or you can schedule the heavy lifting method to be executed after the rotation took place:
[self performSelector:#selector(simulateHardWorkNeededToGetDisplayInShapeBeforeRotation) withObject:nil afterDelay:0.4]
But these are only hacks and the real solution is to have proper background processing if your UI needs heavy processing to get updated, may it be in portrait or landscape. NSOperation and NSOperationQueue is a good place to start.
I want to update a label from a loop, for example like this:
- (void)viewDidLoad {
[super viewDidLoad];
int i=0;
while (1) {
i++;
[NSThread sleepForTimeInterval:0.05]; // do some computation,
[myLabel setText:[NSString stringWithFormat:#"%d", i]]; // show the result!
[self.view setNeedsDisplay];
}
}
Assume that instead of sleep some heavy computation is done.
I do not want the overhead of doing the computation in the background.
The Windows equivalent for handling this problem would be .DoEvents, as this example shows:
http://www.tek-tips.com/viewthread.cfm?qid=1305106&page=1
Is there a similar solution for this in iOS?
[self.view setNeedsDisplay] does not work at all!
There must be some way to process application events from iOS on a controlled schedule in the main thread... Like .DoEvents in windows, despite all its shortcomings is quite useful for some simple applications.
I guess this is like a game-loop but with UI components.
The way you would do this is as follows:
-(void) incrementCounter:(NSNumber *)i {
[myLabel setText:[NSString stringWithFormat:#"%d", [i intValue]]]; // show the result!
[self performSelector:#selector(incrementCounter:) withObject:[NSNumber numberWithInt:i.intValue+1] afterDelay:0.05];
}
- (void)viewDidLoad {
[super viewDidLoad];
// start the loop
[self incrementCounter:[NSNumber numberWithInt:0]];
}
The basic idea here is to increment the counter after a slight delay 0.05 to give the main UI thread a chance to flush all UI events, which makes up for explicitly calling .DoEvents in the Windows world.
I assume you want to implement a counter using a label? You can use NSTimer to call a method that updates your counter every X milliseconds, for instance.
Use NSTimer in iOS if you want to update UI components.
NSTimer* myTimer = [NSTimer scheduledTimerWithTimeInterval: 60.0 target: self
selector: #selector(callAfterSomeSecond:) userInfo: nil repeats: YES];
Implement the callAfterSomeSecond: as below :
-(void) callAfterSomeSecond:(NSTimer*) timer
{
static int counter = 0;
if(counter == 100)
{
[timer invalidate];
}
[myLabel setText:[NSString stringWithFormat:#"%d", counter ]];
[self.view layoutSubviews];
counter++;
}
in your code, the while loop runs in the Main thread, and the UI update also should be done in Main thread, so while the loop is running, the Main thread is kind of 'blocked'(busy), so UI update cannot be performed.
I think what I want to say is not what you want. to resolve it, you have to put the heavy computing in another thread, using NSOperation or GCD for example.
The best way is to do your computation on a different thread, but when that's not feasible, you can wrap the code that affects the UI in a CATransaction. In my tests this will update the UI immediately rather than needing to wait till the next run loop.
while (1) {
[CATransaction begin];
//Your UI code here
[CATransaction commit];
}
When my main viewController is first clicked, it starts showing a demo (on repeat) as follows:
showingDemo = YES;
[self startDemo];
- (void)startDemo {
if (showingDemo) {
[self performSelector:#selector(stepone) withObject:nil afterDelay:1.5f];
[self performSelector:#selector(steptwo) withObject:nil afterDelay:2.0f];
[self performSelector:#selector(stepthree) withObject:nil afterDelay:3.8f];
[self performSelector:#selector(stepfour) withObject:nil afterDelay:4.3f];
[self performSelector:#selector(startDemo) withObject:nil afterDelay:5.6f];
}
}
When it is clicked a second time, I bring a new ViewController to the screen
showingDemo = NO;
[self.view addSubview:newView];
I thought this would stop the endless loop.
When the user returns back to my main viewController:
[newView.view removeFromSuperview];
And clicks on the screen again:
showingDemo = YES;
[self startDemo];
In testing my app, if I click back quickly (before the loop has had time to end, the program seems to be running through the loop twice - the one that was previously going and the new one - and therefore it looks all weird, with the 'stepthree' function happening before 'stepone' and so forth.
Anybody know a better way to STOP the loop I've programmed for good so that when I go to start it again later, it doesn't run multiple loops thinking that the previous one hasn't been finished?
Thanks so much!
When you set showingDemo to NO, call NSObject's cancelPreviousPerformRequestsWithTarget: to cancel any pending perform requests:
showingDemo = NO;
[NSObject cancelPreviousPerformRequestsWithTarget:self];
[self.view addSubview:newView];
When the user navigates to a different view and returns back to the original view I would like everything to be reset as though they were coming to the view for the first time.
I was able to stop the audio from playing when they leave, but not an animation method.
how would I be able to do this?
-(void)viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
[audioIntro stop];
[mainAudio stop];
}
here's the animation code:
- (void) firstAnimation {
if (playSoundButton.enabled = NO || audioIntro.playing) {
return;
}
UIViewAnimationOptions options = UIViewAnimationOptionCurveLinear;
myImage.hidden = NO;
myImage.alpha = 1.0;
[UIView animateWithDuration:1.0 delay:12.0 options:options animations:^
{
setAnimationBeginsFromCurrentState:YES;
tapImage.alpha = 0.0;
}
completion:^(BOOL finished){
[self performSelector:#selector(secondAnimation) withObject:nil afterDelay:0.2];
}
];
}
thanks for any help.
It seems a bit hacky - but I would try this:
Use the "old style" animation - i.e. not with blocks - use "beginAnimation", setting an animationID for your animation.
In you viewWillDisappear method, call "beginAnimation" again, with the same animationID, but this time:
a. Set the animation duration to ZERO
b. Animate "to" the location where you want the stuff to reset to.
Set some sort of kill-flag in the viewWillDisappear method
Make the completion function look for the "kill flag" - and do not evoke the second animation inside your "secondAnimation" flag if the selector if the "kill flag" is set.
(I am a little unclear - if the "finished" flag would suffice for the "kill-flag" - not having seen your entire code.