I have the following routine to simply animate an image across the screen.
-(void)animateGrasland {
[UIView animateWithDuration:5 delay:0.0 options:UIViewAnimationOptionCurveLinear
animations:^{
CGRect newFrame = CGRectMake(100,100,1024,768);
self.grasland.frame = newFrame;
}
completion:^(BOOL finished) {
}
];
}
I want to start the animation immediately after I load the View. And to last for 5 seconds. So I have been experimenting with several options to calls of the function.
Option I - This option immediately shows the image at the new position, no animation!
- (void)viewDidLoad {
[super viewDidLoad];
[self animateGrasland];
}
Option II - This option animates the image to the new position. Bingo.
- (void)viewDidLoad {
[super viewDidLoad];
[NSTimer scheduledTimerWithTimeInterval:0.001 target:self selector:#selector(animateGrasland) userInfo:nil repeats:NO];
}
I have bene playing with the delay in the Time a little and for 0.001 sec the animation will start. If the duration is 0.0001 sec the image is displayed immediately at the new position, so without animation. I expect that the duration of the parsing of the viewDidLoad (and other background routines causes this).
So because the initial position (0,0), which is set in the storyboard, is updated during the viewDidLoad, the Animation is not triggered. And the animation moves immediately to its finished state.
Try calling animateGrasland in viewDidAppear: instead of viewDidLoad, like this:
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated]
[self animateGrasland];
}
// Remove your implementation of the viewDidLoad method or at least the call to animateGrasland
viewDidLoad is called after the view is loaded into the memory, while viewDidAppear: is called after the view is added to the view hierarchy. In your case, this means right after the view is visible. Since you want your animation to start as soon as the view is visible, you should start it in viewDidAppear:
Related
I have a few buttons on my View and when the app starts they should flip around their x-axis. When I start the app from Xcode (Build and then run the current scheme) they flip, but when I open the app on my iPhone they don't flip.
My viewDidLoad in my ViewController looks like this:
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
model =[[Model alloc] init];
GameView *gameView = [[GameView alloc] initWithTiles];
gameView.controller = self;
self.view = gameView;
[self flipButtons];
}
Why don't the buttons flip when I start the app from my phone?
If you need additional information just let me know :)
Edit
This is the method that flips the button and gets called for each button:
- (void)flip:(Color*)color {
int flipDirection = arc4random() % 2;
if(flipDirection == 0) flipDirection = -1;
double duration = 200 + arc4random() % (500 - 200);
duration = duration / 1000;
// Animation
[UIView animateWithDuration:duration
delay:0.0
options:UIViewAnimationOptionCurveEaseInOut | UIViewAnimationOptionAllowUserInteraction
animations:^{
self.layer.transform = CATransform3DMakeRotation(M_PI, 0, flipDirection, 0);
// wait duration /2 and call changeColor
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)((duration / 2) * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[self changeColor:color];
});
}
completion:^(BOOL finished) {
self.layer.transform = CATransform3DMakeRotation(M_PI, 0, 0, 0);
}];
}
viewDidLoad doesn't necessarily mean that the view is on-screen. You should be using viewWillAppear: or viewDidAppear:
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
model =[[Model alloc] init];
GameView *gameView = [[GameView alloc] initWithTiles];
gameView.controller = self;
self.view = gameView;
}
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
[self flipButtons];
}
viewDidLoad only signifies that the view controller's view has been instantiated and by that point all IBOutlets have been wired up. You should only do additional initialization here, and must not rely on when viewDidLoad was called or whether it was called multiple times, etc (e.g. you can unload your view controller's view with self.view = nil if it doesn't have a superview, and it'll be automatically reloaded when it's needed).
Why this is necessary:
UIViewController loads it's view lazily. When you instantiate a UIViewController with e.g initWithNibName:bundle: it won't load the nib right away; that would be a waste of resources. According to Apple's documentation:
The nib file you specify is not loaded right away. It is loaded the
first time the view controller's view is accessed. If you want to
perform additional initialization after the nib file is loaded,
override the viewDidLoad method and perform your tasks there.
So what happens is, when the view controller's view is queried the first time, loading takes place (see: Order of UIViewController initialization and loading ) and the viewDidLoad call is issued so that you can make adjustments to the view with all the subviews and IBOutlets set up.
Once the view is loaded, the view controller's state transitions are described in this diagram:
The initial state is disappeared.
I added about 500 views to my viewController.view.
This action took about 5 seconds on target.
Now I want the screen to refresh after each subview I'm adding, so the user will see them appears one by one on screen.
I tried this in my viewController:
-(void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
for(int i=0; i<500; i++)
{
//...Create aView
[self.view addsubview:aView];
[self.view setNeedsDisplay];
}
}
I run it and nothing happened for 5 seconds then all views appeared at once.
I made sure that [self.view setNeedsDisplay] called from the main thread context.
Any idea how to make those subviews appear one by one?
I found a simple solution. I added a new property to my viewController - 'subviewsCount' witch was initialised to 500. then called the following method from viewDidLoad:
-(void) addSubviewsToMotherView
{
self.subviewsCount -=1;
if (self.subviewsCount >= 0)
{
[UIView animateWithDuration:0.0
delay:0.0
options:UIViewAnimationOptionLayoutSubviews
animations:^{
[self methodToAddSubview];
}
completion:^(BOOL finished){
[self addSubviewsToMotherView];
}
];
}
}
I'm trying to build an animation around a UIButton. The UIButton has a UIImageView that contains an image that I'd like to shrink when the UIButton is held down and then when the UIButton is let go, I'd like to play a separate animation that does a bounce.
The issue I'm experiencing right now is that the 2nd part of the animation doesn't seem to play if I press down and then up very quickly. If I press and hold (wait for the first animation to finish), then let go, it seems to work fine.
Here's the relevant code:
-(void)pressedDown
{
[UIView animateWithDuration:0.2 delay:0 options:UIViewAnimationOptionCurveEaseOut animations:^{
self.heartPart.layer.transform = CATransform3DMakeScale(0.8, 0.8, 1);
} completion:nil];
}
-(void)pressedUp
{
[UIView animateWithDuration:0.8
delay:0.0
usingSpringWithDamping:20
initialSpringVelocity:200
options:UIViewAnimationOptionBeginFromCurrentState
animations:^{
self.heartPart.layer.transform = CATransform3DIdentity;
}
completion:nil];
}];
}
In my ViewDidLoad I add the following:
[self.heartButton addTarget:self
action:#selector(pressedUp)
forControlEvents:UIControlEventTouchUpInside];
[self.heartButton addTarget:self
action:#selector(PressedDown)
forControlEvents:UIControlEventTouchDown];
Any idea how I can get the two animations to sequence and not interrupt each other even on a quick press?
Here's a potential plan of action. Keep two global variables, BOOL buttonPressed and NSDate *buttonPressDate. When the button is pressed, you should set buttonPressed to true and set buttonPressDate to the current date. Now in the touch up method, you would set buttonPressed to false and check whether the time interval between buttonPressDate and the current date is greater than the duration of the animation. If it is, then run the touch up animation; if not, return. In the completion block of the touch down method, you would check if the button was still pressed. If it is, then do nothing; if it's not pressed anymore, then run the touch up animation.
The effect you'll get using this approach should be the following: if you tap the button quickly, it will run the full 0.2-second shrink animation and then run the enlarge animation in sequence.
Now, if you don't want the touch down animation to run in the case of a quick touch, you should probably delay the first animation and check if the button is still pressed when you start it. If it was a quick touch, you would run a modified animation that covered both the touch down and touch up phases.
You could try out the following :-
//Set only one target with TouchUpInside.
[self.heartButton addTarget:self
action:#selector(pressedUp)
forControlEvents:UIControlEventTouchUpInside];
-(void)pressedDown
{
[UIView animateWithDuration:0.2 delay:0 options:UIViewAnimationOptionCurveEaseOut animations:^{
self.heartPart.layer.transform = CATransform3DMakeScale(0.8, 0.8, 1);
}
completion:
[self pressedUp];
//Or
[self performSelector:#selector(pressedUp) withObject:self afterDelay:0.5];
];
}
-(void)pressedUp
{
[UIView animateWithDuration:0.8
delay:0.0
usingSpringWithDamping:20
initialSpringVelocity:200
options:UIViewAnimationOptionBeginFromCurrentState
animations:^{
self.heartPart.layer.transform = CATransform3DIdentity;
}
completion:nil];
}];
}
This way as first animation is completed then next animation will start But my concern is that it won't look appropriate and it would be a bit long animation to show to a user. Rest it's upto your app design and what all it is going to achieve with it.
I assume you are familiar with the concept of operations. If not then NSOperations allow you to keep your code modular and enable you to set the order of execution.
You could create a custom subclass of NSOperation and run animation within its execution block, then add operations with animations on queue each time user interacts with the button.
Apart from documentation from Apple, there is a great example of how to subclass NSOperation available on Github:
https://github.com/robertmryan/AFHTTPSessionOperation/blob/master/Source/AsynchronousOperation.m
Based on that "blueprint" for your custom operations, it's very trivial to achieve what you want, i.e.:
#interface PressDownAnimationOperation : AsynchronousOperation
- (instancetype)initWithView:(UIView *)view;
#end
#implementation PressDownAnimationOperation {
UIView *_view;
}
- (instancetype)initWithView:(UIView *)view {
self = [super init];
if(self) {
_view = view;
}
return self;
}
- (void)main {
// dispatch UIKit related stuff on main thread
dispatch_async(dispatch_get_main_queue(), ^{
// animate
[UIView animateWithDuration:0.2 delay:0 options:UIViewAnimationOptionCurveEaseOut animations:^{
_view.layer.transform = CATransform3DMakeScale(0.8, 0.8, 1);
} completion:^(BOOL finished){
// mark operation as finished
[self completeOperation];
}];
});
}
#end
Now in order to run animations you need to create NSOperationQueue. You can keep it within your view controller, i.e.:
#implementation ViewController {
NSOperationQueue *_animationQueue;
}
- (void)viewDidLoad {
[super viewDidLoad];
_animationQueue = [[NSOperationQueue alloc] init];
// limit queue to run single operation at a time
_animationQueue.maxOperationCount = 1;
}
#end
Now when you want to chain animations, you simply create new operation, add it to queue and that's it, i.e.:
- (void)pressedDown {
NSOperation *animationOperation = [[PressDownAnimationOperation alloc] init];
[_animationQueue addOperation:animationOperation];
}
If user taps too fast and you notice that your operation queue is clogged with animations, you can simply cancel all animations before adding new one, i.e.:
[_animationQueue cancelAllOperations];
Hi everyone,
small question.
I have an UIView class that I created, AnimatedUIView.
In the init, I create a subview that I animate like this:
- (void)animateView{
[UIView animateWithDuration:4.0
delay:1.0
options: nil
animations:^{
// Animation PART 1
}
completion:^(BOOL completed){
[UIView animateWithDuration:4.0
animations:^{
// Animation PART 2
}
completion:^(BOOL completed){
[self animateView];
}
];
}
];
}
I have to call [self animateView] myself in the second completion block rather than using option repeat because I want to delay 1 second only the first animation.
Everything works fine, I alloc and init my view and add it as a subview, and it is animated as should.
But, when I close my superview, I guess ARC does its work and deallocs the animatedView, but my CPU goes 100 % !
I have investigated and if I comment the call to [self animateView], my CPU doesn't skyrocket when I close the superview.
So I've managed to solve the problem (by putting a condition before the call and changing a boolean value before closing the superview, keepAnimating = NO) , I just wanted to understand WHY it does this?
Why does it try to keep animating, and why does it use so much CPU?
If I put an NSLog in the completion block, I first see it every 8 seconds, but when I close the superview, the NSLog just keeps appearing every ms...
BTW: it relates to this question : UIView animation using 90%CPU , which was not really answered. Thanks a lot!
CPU going to 100% almost always means infinite recursion.
We can only guess because only Apple knows what's inside the animation source code.
In this case I guess this could be caused by triggering the animation when the animation cannot run anymore (e.g. there is no layer since we are in dealloc state). That would mean the completion handler is called immediately and that leads to infinite recursion.
There are safer ways to create infinite animations (e.g. using CAAnimation on the layer).
You should not use self in side the block. It will create a retain cycle. If it is necessary to call self create a weak self pointer. Code like this
__weak typeof(self) weakself = self;
[UIView animateWithDuration:4.0
delay:1.0
options: nil
animations:^{
// Animation PART 1
}
completion:^(BOOL completed){
[UIView animateWithDuration:4.0
animations:^{
// Animation PART 2
}
completion:^(BOOL completed){
[weakself animateView];
}
];
}
];
But main issue with your code is you calling animateView recursively without any base case. So i consuming CPU cycles... do not use animateView without base case.
Try using timer
Write following line in your viewDidLoad or any where else from where you want animation to start.
[NSTimer scheduledTimerWithTimeInterval:9 target:self selector:#selector(animateView) userInfo:nil repeats:YES];
// 9 is total animation duration.....
update your animateView method as follows:
- (void)animateView{
[UIView animateWithDuration:4.0
delay:1.0
options: nil
animations:^{
// Animation PART 1
}
completion:^(BOOL completed){
[UIView animateWithDuration:4.0
animations:^{
// Animation PART 2
}
completion:^(BOOL completed){
}
];
}
];
}
I am currently making an app that has a complex loading screen. I have created the loader using UI Animation, but want to add a button that will appear once the loading bar has finished. I have come across the idea of hiding the button for a certain period of time, or making it appear after a certain period of time.
How would I show/hide the button after a period of time?
You could invoke your method to show the button after a certain period of time:
[self performSelector:#selector(showButton) withObject:nil afterDelay:0.5];
Or, probably better, if you want to animate the appearance of the button, you can do both the animation and the delay in a single call, e.g. assuming the button originally has alpha of 0.0:
[UIView animateWithDuration:0.25
delay:0.5
options:nil
animations:^{
myButton.alpha = 1.0;
}
completion:^(BOOL finished){
// if you want to do anything when animation is done, do it here
}
];
Using NSTimer should be the easiest way.
Create NSTimer to do that,
Timer = [NSTimer scheduledTimerWithTimeInterval:5 target:self selector:#selector(hideButton:) userInfo:nil repeats:NO];
-(void)hideButton:(UIButton *)hideButton {
if hideButton.isHidden == false {
hideButton.hidden=TRUE;
} else {
hideButton.hidden = FALSE
}