I am a little confused about something I was just trying for fun. I have written a little method called animateBars_V1 which uses an array of UIImageViews and alters the height of each UIImageView to show a changing set of coloured bars.
- (void)animateBars_V1 {
srandom(time(NULL));
for(UIImageView *eachImageView in [self barArray]) {
NSUInteger randomAmount = kBarHeightDefault + (random() % 100);
CGRect newRect;
CGRect barRect = [eachImageView frame];
newRect.size.height = randomAmount;
newRect.size.width = barRect.size.width;
newRect.origin.x = barRect.origin.x;
newRect.origin.y = barRect.origin.y - (randomAmount - barRect.size.height);
[eachImageView setFrame:newRect];
}
}
This works fine, I then added a UIButton with a UIAction for when the button is pressed. Each time the button is pressed animateBars_V1 is called and the coloured bars update.
- (IBAction)buttonPressed {
for(int counter = 0; counter<5; counter++) {
[self animateBars_V1];
NSLog(#"COUNTER: %d", counter);
}
}
My question is just for fun I decided that each time the button is pressed I would call animateBars_V1 5 times. What happens is that the bars don't change until after the loop has exited. This results in:
Screen as per storyboard
COUNTER: 0
COUNTER: 1
COUNTER: 2
COUNTER: 3
COUNTER: 4
Screen Updates
Is this the correct behaviour? I don't need a fix or workaround as this was just for fun, I was more curious what was happening for future reference.
If you are calling animateBars_V1 multiple times within a loop, the frames of the bars do get set multiple times, but before they can be rendered, animateBars_V1 gets called again, and the frames are set to a new position/size.
The call to render (drawRect: and related methods) doesn't occur until after the loop is finished - since it is an IBAction, it is by necessity called in the main thread, which means that all rendering is blocked until the code is completed.
There are of course several solutions to this. A simple method to do the multi-animation thing is to use UIView animateWithDuration:animations:completion: in the following manner:
- (IBAction)buttonPressed {
[self animateBarsWithCount:5];
}
- (void)animateBarsWithCount:(int)count
{
[UIView animateWithDuration:.25f animations:^{
[self animateBars_V1];
}completion:^(BOOL finished){
[self animateBarsWithCount:count - 1];
}];
}
//animateBars_V1 not repeated
Of course, if you simply wanted to run the animation one time, (but actually animated) you should do it like this:
- (IBAction)buttonPressed {
[UIView animateWithDuration:.25f animations:^{
[self animateBars_V1];
} completion:nil];
}
CrimsonDiego is right
you can try to delay each call with this:
- (IBAction)buttonPressed {
for(int counter = 0; counter<5; counter++) {
float ii = 1.0 * counter / 10;
[self performSelector:#selector(animateBars_V1) withObject:nil afterDelay:ii];
// [self animateBars_V1];
NSLog(#"COUNTER: %d", counter);
}
}
The problem is here
for(int counter = 0; counter<5; counter++) {
[self animateBars_V1];
NSLog(#"COUNTER: %d", counter);
}
This for loop is executed in nano seconds and your eye is not able to catch up that change as eye can detect only 1/16th of a second.
For testing what can run this code in a timer that runs five time.
Edited
Removed sleep call as it will sleep the main thread and everything will stop. So use Timer here
Related
I want to write some code to make the UILabel text updates whenever the text gets changed. I am writing a small demo like:
- (IBAction)ButtonPressed:(id)sender {
for(int i = 0; i < 100000; ++i)
{
[self.Label setText:[NSString stringWithFormat:#"%d",i]];
[self.Label setNeedsDisplay];
}
}
When I click the button, the label only changes once to 99999, but I expect it to display 99999 times from 0 to 99999. Anyone has an idea why the code is not working?
Thanks in advance!
setNeedsDisplay tells the system that your view wants to be drawn, but it doesn't force it to happen. The view will be redrawn on the next draw cycle, which is happening after the for loop completes. If you want all 100,000 changes to be visible, you'll have to delay the label updates by a human-perceptible amount.
The above answer is correct, but to elaborate, if you do want to update the content of the label visibly, you can try the following
- (IBAction)ButtonPressed:(id)sender {
for(int i = 0; i < 100000; ++i)
{
double delayInSeconds = 0.01 * i;
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC));
dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
[self.Label setText:[NSString stringWithFormat:#"%d",i]];
[self.Label setNeedsDisplay];
});
}
}
For your purposes though, this is probably irrelevant. A more tactile approach would be to store a number of times the button has been pressed in an instance variable, and increment it each time it is touched, and set the label value appropriately.
UPDATE
ViewController is not destroyed, new ViewController is not created. This:
TimerViewController * timerViewController = (TimerViewController *)[segue destinationViewController];
does not create a new instance. However, this
TimerViewController *timerViewController = [self.storyboard instantiateViewControllerWithIdentifier:#"theID"];
does. So a new instance is now being used, but the same problem.
END UPDATE
I have a simple "analog digital timer" where the minutes and seconds animate smoothly to emulate the turning of a number wheel. This is simply a ParentViewController with a list of times, selecting a time performs a segue to the TimerViewController. The TimerViewController uses a recursive UIView animation to simulate the countdown of a clock. This works well.
I want the user to be able to transition to the next timer in the list without having to go back to the ParentViewController in order to select it. This does not work well.
I've tried many variations, the basic pattern is for TimerViewController to ask its delegate (ParentViewController) to pop it and then to seque again to the TimerViewController using the next time from the list of times. The initial animation never terminates, the initial TimerViewController instance never "dies". When I segue to TimerViewController the second time, using what I thought was a new instance, the first animation appears to still be running, the duration of the animation which started at almost 1.0s is off the charts (closer to 0.).
I've tried many different, increasingly desperate, ways to stop the original animation and kill the original instance.
ParentViewController.m
-(void) timerViewControllerDidSwipe (TimerViewController *)controller {
[self.navigationController popViewControllerAnimated:NO];
controller = nil; // ?
[self performSegueWithIdentifier:#"ShowTimer" sender:self];
}
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
if ([[segue identifier] isEqualToString:#"ShowTimer"])
{
TimerViewController * timerViewController = (TimerViewController *)[segue destinationViewController];
[timerViewController setDelegate:self];
}
TimerViewController.m
-(void) swipe:(id)selector {
// swipe left
stop = YES; // ivar for ^after to not call [self animate]
[self.delegate timerViewControllerDidSwipe:self];
}
-(void) animate {
// anim block here..
void (^after) (BOOL) = ^(BOOL f) {
if (duration % 60 == 0 && duration >= 60) {
if (minutesAlt.center.y < minutes.center.y)
{
CGPoint a = minutes.center;
a.y -= 2 * displacement;
minutes.center = a;
minutes.text = [NSString stringWithFormat:#"%02d", (duration / 60) -1];
}
else
{
CGPoint a = minutesAlt.center;
a.y -= 2 * displacement;
minutesAlt.center = a;
minutesAlt.text = [NSString stringWithFormat:#"%02d", (duration / 60) -1];
}
}
if (duration == 0)
{
// end
}
else if (secondsAlt.center.y < seconds.center.y)
{
CGPoint a = seconds.center;
a.y -= 2 * displacement;
seconds.center = a;
duration--;
seconds.text = [NSString stringWithFormat:#"%02d",duration % 60];
if (!stop) {[self animate];}
}
else
{
CGPoint a = secondsAlt.center;
a.y -= 2 * displacement;
duration--;
secondsAlt.center = a;
secondsAlt.text = [NSString stringWithFormat:#"%02d", duration % 60];
if (!stop) {[self animate];}
}
};
[UIView animateWithDuration:0.50
delay:0.50
options:UIViewAnimationOptionCurveLinear
animations:anim
completion:after];
}
Any help greatly appreciated.
Fixed here in a related question. It was a simple syntax error (that took me weeks to find).
I have this code in a button click [NSThread detachNewThreadSelector: #selector(spinBegininapp) toTarget:self withObject:nil]; to show a activity indicator for user that the background thread is running ,i put this code to enable the activity-indicator
- (void)spinBegininapp
{
_activityindictor.hidden = NO;
}
and it works fine,when i click the button it shows the activity-indictor animating ,when the thread goes it hides the activity-indicator,but my need is to show a progressView instead of activity-indicator,it progresses according to the thread,and if the thread finishes it need to reach progress completely and self hide.is that possible .
#pragma mark - Loading Progress
static float progress = 0.0f;
-(IBAction)showWithProgress:(id)sender {
progress = 0.0f;
_progressView.progress = progress;
[self performSelector:#selector(increaseProgress) withObject:nil afterDelay:0.3];
}
-(void)increaseProgress {
progress+=0.1f;
_progressView.progress = progress;
if(progress < 1.0f)
[self performSelector:#selector(increaseProgress) withObject:nil afterDelay:0.3];
else
[self performSelector:#selector(dismiss) withObject:nil afterDelay:0.2f];
}
-(void)dismiss {
[self progressCompleted];
}
now call the following function whenever/where ever you want to show progress
[self showWithProgress:nil];
progress range lies between 0.0 and 1.0
1.0 means 100%
you can add a progress view no doubt but usually it used for definite quantities like time or data.. for eg. If you are downloading a 2mb file then you can always tell how much data you have downloaded and show the in the progress view as a factor. So if something similar is happening inside your thread you can use this..
UIProgressView *progressView = [[UIProgressView alloc] initWithProgressViewStyle:whateverStyle];
progressView.progress = 0.75f;
[self.view addSubview: progressView]
[progressView release];
you just need to update your progress as the value changes.... hoping this helps.
I am trying to animate 2 UIButtons in a UITableViewCell called addToPlaylist and removeFromPlayList (they animate off to the right after being swiped on) and am using a block as follows
[UIView animateWithDuration:0.25 animations:^{
self.addToPlaylist.center = CGPointMake(contentsSize.width + (buttonSize.width / 2), (buttonSize.height / 2));
self.removeFromPlaylist.center = CGPointMake(contentsSize.width + (buttonSize.width / 2), (buttonSize.height / 2));
myImage.alpha = 1.0;
}
completion:^ (BOOL finished)
{
if (finished) {
// Revert image view to original.
NSLog(#"Is completed");
self.addToPlaylist.hidden = YES;
self.removeFromPlaylist.hidden = YES;
self.hasSwipeOpen = NO;
}
}];
on completion I want to hide the buttons to attempt to lessen redraw on scroll etc.
This code sits within '-(void) swipeOff' which is called in the UITableViewControllers method scrollViewWillBeginDragging like so:
- (void)scrollViewWillBeginDragging:(UIScrollView *) scrollView
{
for (MediaCellView* cell in [self.tableView visibleCells]) {
if (cell.hasSwipeOpen) {
[cell swipeOff];
}
}
}
The problem is the completion code, if I remove it or set it to nil all is good, if I include it I get an EXC_BAD_ACCESS. even if I include it with any or all of the lines within the if(finished) commented out
Am I using this in the wrong way, any help much appreciated.
Thanks
I had the same problem with animations. I've solved it by removing -weak_library /usr/lib/libSystem.B.dylib from Other Linker flags.
Also, according to this answer, if you need this flag, you may replace it with -weak-lSystem.
Check if you are not calling a UIView (collectionView, Mapview, etc) from inside the UIView block, meaning, it would be a call outside the main thread. If you are, try this:
DispatchQueue.main.async {
self.mapBoxView.setZoomLevel(self.FLYOVERZOOMLEVEL, animated: true
)}
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.