I'm trying to animate UILabel's width. For some reason animation doesn't work and label changes it's size immediately. I use autolayout. Here is the code from the sample project I wrote to reproduce this. Tap on the button changes trailing constrains for both the button and the label.
#interface ViewController ()
#property (assign, nonatomic) BOOL controlsResized;
#property (weak, nonatomic) IBOutlet UIButton *button;
#property (weak, nonatomic) IBOutlet NSLayoutConstraint *buttonTrailingConstraint;
#property (weak, nonatomic) IBOutlet MyLabel *label;
#property (weak, nonatomic) IBOutlet NSLayoutConstraint *labelTrailingConstraint;
- (IBAction)doTapButton:(id)sender;
#end
#implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
[self.button setTranslatesAutoresizingMaskIntoConstraints:NO];
[self.label setTranslatesAutoresizingMaskIntoConstraints:NO];
self.label3.text = #"asdfasdfds sklfjfjls sdlkfj jjjjkfjkfdsjkdfsjklffsjkfdsjkl";
}
- (IBAction)doTapButton:(id)sender
{
if (!self.controlsResized)
{
[UIView animateWithDuration:0.5 animations:^{
self.buttonTrailingConstraint.constant = 0;
[self.button layoutIfNeeded];
self.labelTrailingConstraint.constant = 0;
[self.label layoutIfNeeded];
self.controlsResized = YES;
}];
}
else
{
[UIView animateWithDuration:0.5 animations:^{
self.buttonTrailingConstraint.constant = 100;
[self.button layoutIfNeeded];
self.labelTrailingConstraint.constant = 100;
[self.label layoutIfNeeded];
self.controlsResized = NO;
}];
}
}
#implementation MyLabel
- (void)drawTextInRect:(CGRect)rect
{
NSLog(#"drawTextInRect");
[super drawTextInRect:rect];
}
#end
As you can see on the video, it works for the button, but doesn't work for the label.
I tried to investigate and subclassed label from MyLabel class. It appears, that - (void)drawTextInRect:(CGRect)rect gets called immediately on the button tap, as a reason of [self.label layoutIfNeeded]; in the animation block. Why is it so? I would've expect the frame to be changed animated, as it is set inside the animation block. And it works as expected for the UIButton.
Why UILabel doesn't resize animated? Is there a way to fix it?
EDIT:
I tried the approaches suggested in current answers, but they don't work either. I think the animation code is not a problem here, because it works for the button in my example. The problem seems to be related to UILabel specific. UILabel seems to handle animation of size changes differently than other UIView subclasses, but I can't understand why.
I would post a comment but I do not have the necessary reputation for that. Here is what I would do:
self.buttonTrailingConstraint.constant = x;
self.labelTrailingConstraint.constant = x;
[UIView animateWithDuration:0.5 animations:^{
[self.view layoutIfNeeded];
}];
When you update a constraint you need to take into account that the respective view's constraint you are modifying is not the only one that needs updating, so a layout method call is better made on the parent view.
This worked for me everytime. Hope this helps.
You must set constants of constraints before animate.
Try this :
- (IBAction)doTapButton:(id)sender
{
if (!self.controlsResized)
{
self.buttonTrailingConstraint.constant = 0;
self.labelTrailingConstraint.constant = 0;
[UIView animateWithDuration:0.5 animations:^{
[self.view layoutIfNeeded];
} completion:^(BOOL finished) {
self.controlsResized = YES;
}];
}
else
{
self.buttonTrailingConstraint.constant = 100;
self.labelTrailingConstraint.constant = 100;
[UIView animateWithDuration:0.5 animations:^{
[self.view layoutIfNeeded];
} completion:^(BOOL finished) {
self.controlsResized = NO;
}];
}
}
There are a couple parts to making this 100% work as expected:
Calling layoutIfNeeded first
Setting the constraint constant outside of the animation block
Calling layoutIfNeeded inside the block
Copied from a working implementation:
[self.view layoutIfNeeded];
self.topContainerConstraint.constant = constraintValue;
[UIView animateWithDuration:0.25 animations:^{
[self.view layoutIfNeeded];
} completion:^(BOOL finished){ }];
I'd change your function to something like the following (obviously untested):
- (IBAction)doTapButton:(id)sender
{
if (!self.controlsResized)
{
[self.view layoutIfNeeded];
self.labelTrailingConstraint.constant = 0;
self.buttonTrailingConstraint.constant = 0;
[UIView animateWithDuration:0.5 animations:^{
[self.view layoutIfNeeded];
self.controlsResized = YES;
}];
}
else
{
[self.view layoutIfNeeded];
self.labelTrailingConstraint.constant = 100;
self.buttonTrailingConstraint.constant = 100;
[UIView animateWithDuration:0.5 animations:^{
[self.view layoutIfNeeded];
self.controlsResized = NO;
}];
}
}
Related
I am trying to animate a UITextView to expand the to the width of its containing UIToolbar.
But it animates weird, jumping around.
Can anyone help me make understand why my textbox jumps around?
#define ANIMATION_DURATION 0.5
#define ANIMATION_DELAY 0.03
#interface WebViewController () <UITextFieldDelegate>{
CGRect URLBAR_DEFAULT;
}
-(void)viewDidLoad{
self.txtURLBar = [[UITextField alloc] init];
self.txtURLBar.bounds = CGRectMake(0, 0, 100, 30);
self.txtURLBar.delegate = self;
self.txtURLBar.backgroundColor = [UIColor lightGrayColor];
self.txtURLBar.clearButtonMode = UITextFieldViewModeWhileEditing;
URLBAR_DEFAULT = self.txtURLBar.frame; //KEEP A REFRENCE TO THE ORIGINAL SIZE
}
//START EDITING EXPAND TO FULL WIDTH
- (void)textFieldDidBeginEditing:(UITextField *)textField {
[UIView animateWithDuration:ANIMATION_DURATION delay:ANIMATION_DELAY options:UIViewAnimationOptionCurveLinear animations:^{
[_txtURLBar setFrame:CGRectMake(3, URLBAR_DEFAULT.origin.y, self.view.frame.size.width - 10, URLBAR_DEFAULT.size.height)];
} completion:^(BOOL finished) {
}];
}
//END EDITING SHRINK BACK TO SMALL SIZE
- (void)textFieldDidEndEditing:(UITextField *)textField {
[UIView animateWithDuration:ANIMATION_DURATION delay:ANIMATION_DELAY options:UIViewAnimationOptionCurveEaseOut animations:^{
[_txtURLBar setFrame:CGRectMake(URLBAR_DEFAULT.origin.x, URLBAR_DEFAULT.origin.y, URLBAR_DEFAULT.size.width, URLBAR_DEFAULT.size.height)];
} completion:^(BOOL finished) {
}];
}
The problem may be due to the frame being set in textFieldDidBeginEditing, textFieldDidEndEditing is different. I've done the small POC with same code of yours as below. It's working fine.
#interface ViewController ()
#property (weak, nonatomic) IBOutlet UITextField *resizeableTextField;
#end
#implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
}
- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
//START EDITING EXPAND TO FULL WIDTH
- (void)textFieldDidBeginEditing:(UITextField *)textField {
[UIView animateWithDuration:0.5 delay:0.03 options:UIViewAnimationOptionCurveLinear animations:^{
self.resizeableTextField.frame = CGRectMake(CGRectGetMinX(self.resizeableTextField.frame), CGRectGetMinY(self.resizeableTextField.frame), self.resizeableTextField.frame.size.width + 40, CGRectGetHeight(self.resizeableTextField.frame));
} completion:^(BOOL finished) {
}];
}
//END EDITING SHRINK BACK TO SMALL SIZE
- (void)textFieldDidEndEditing:(UITextField *)textField {
[UIView animateWithDuration:0.5 delay:0.03 options:UIViewAnimationOptionCurveEaseOut animations:^{
self.resizeableTextField.frame = CGRectMake(CGRectGetMinX(self.resizeableTextField.frame), CGRectGetMinY(self.resizeableTextField.frame), self.resizeableTextField.frame.size.width - 40, CGRectGetHeight(self.resizeableTextField.frame));
} completion:^(BOOL finished) {
}];
}
#end
I have a UIView subclass that contains a UITextField as subview. The idea is to animate this UIView and make the UITexField becomeFirstResponder when animation has ended. For some reason, after animation completion block has been executed and the UITextField has becomeFirstResponder, the UIView returns to its initial frame.
Here is the dummy sample code:
#interface ViewController ()
#property (weak, nonatomic) IBOutlet UIView *redView;
#property (strong, nonatomic) UITextField *textField;
#property (strong, nonatomic) UITapGestureRecognizer *gesture;
#end
#implementation ViewController
- (void)viewDidLoad
{
[super viewDidLoad];
self.textField = [[UITextField alloc] initWithFrame:CGRectMake(0.f, 0.f, self.redView.frame.size.width, 30.f)];
self.textField.backgroundColor = [UIColor lightGrayColor];
[self.redView addSubview:self.textField];
self.gesture = [[UITapGestureRecognizer alloc] initWithTarget:self
action:#selector(animateRedView:)];
[self.view addGestureRecognizer:self.gesture];
}
- (IBAction)animateRedView:(id)sender
{
[UIView animateWithDuration:.5
delay:0
usingSpringWithDamping:1.2
initialSpringVelocity:5
options:0
animations:^{
CGRect redViewFrame = self.redView.frame;
[self.redView setFrame:CGRectMake((self.view.frame.size.width - redViewFrame.size.width),
redViewFrame.origin.y,
redViewFrame.size.width,
redViewFrame.size.height)];
} completion:^(BOOL finished) {
[self.textField becomeFirstResponder];
}];
}
#end
Any idea?
Did you create redView in interface builder? If so it's going to have layout constraints attached to it.
You can remove them at runtime. Try adding this before you call setFrame
[self.redView removeConstraints:self.redView.constraints];
Also, you might try calculating the destination frame before the animation block (since you're calculating redFrame's new frame based on its current frame).
- (IBAction)animateRedView:(id)sender
{
CGRect redViewFrame = self.redView.frame;
CGRect newFrame = CGRectMake((self.view.frame.size.width - redViewFrame.size.width),
redViewFrame.origin.y,
redViewFrame.size.width,
redViewFrame.size.height);
[UIView animateWithDuration:.5
delay:0
usingSpringWithDamping:1.2
initialSpringVelocity:5
options:0
animations:^{
[self.redView setFrame:newFrame];
} completion:^(BOOL finished) {
[self.textField becomeFirstResponder];
}];
}
So i have a really weird problem at my hands and hours of search has provided nothing.
I have a uiview containing a uitextfield. Now initially this view is outside of the visible screen with coordinates like x=-500,y=-500.
However in response to a button press this view animates and moves into the center of the screen.
Now whenever i tap on a uitextfield that is the subview of this view.This view moves back to its original coordinates outside the screen.
I have frantically checked my code and there is nothing that is moving the view outside again once its in. Any help in explaining this very unfamiliar behaviour would be really appreciated.
This code moves the view onto the screen
- (IBAction)Register:(id)sender {
//(self.view.frame.size.width/2)-(self.SignUp_Screen.frame.size.width/2);
//self.login_Screen.hidden = YES;
self.blurView.hidden = NO;
//self.SignUp_Screen.layer.zPosition = 5;
NSLog(#"Register");
self.SignUp_Screen.hidden = NO;
[UIView animateWithDuration:0.25 delay:0 options:UIViewAnimationOptionCurveEaseOut animations:^{
self.SignUp_Screen.frame = CGRectMake(35, 50,self.SignUp_Screen.frame.size.width , self.SignUp_Screen.frame.size.height);
} completion:^(BOOL finished) {
}];
}
and these are the delegate methods for the textfield
-(void)textFieldDidEndEditing:(UITextField *)textField
{
NSLog(#"TextFieldEndEditing");
[textField resignFirstResponder];
}
-(BOOL)textFieldShouldReturn:(UITextField *)textField
{
NSLog(#"textFieldShouldReturn");
[textField resignFirstResponder];
return YES;
}
As Wezly hints at, if you are using autolayout, you don't modify the frame directly anymore. That's the old world. You want to have an Outlet / property for the constraint and animate it.
[UIView animateWithDuration:0.25
animations:^{
SignUp_Screen.centerXConstraint.constant = ...;
SignUp_Screen.centerYConstraint.constant = ...;
[SignUp_Screen layoutIfNeeded];
}];
See here and here for more details.
You should not modify frame if you are using auto layout. You should animate view by animating constraint's constant. For example:
NSLayoutConstraint *viewY; //constraint from superview top to view top
viewY.constant = 100;
[self.view setNeedsUpdateConstraints];
[UIView animateWithDuration:0.3f animations:^{
[self.view layoutIfNeeded];
}];
The way i solved this problem was by linking an IBOutlet to the constraint I wanted to change and then animating it's constant value.
.h
#interface ViewController : UIViewController {
IBOutlet NSLayoutConstraint *constraintHandle;
}
.m
[UIView animateWithDuration:0.25 delay:0 options:UIViewAnimationOptionCurveEaseOut animations:^{
constraintHandle.constant = self.view.center.x;
[SignUp_Screen layoutIfNeeded];
} completion:^(BOOL finished) {
}];
Don't forget to link the IBOutlet to your constraint in your storyboard or xib.
I have a UIViewController called ParentViewController.
And i have a UIView custom class called CustomView. It including some ImageView and the function to execute animation.
CustomView.h
#interface CustomView : UIView
#property (weak, nonatomic) IBOutlet UIImageView *human;
#property (weak, nonatomic) IBOutlet UIImageView *shadow;
+ (id)CustomView;
- (void)executeAnimation;
#end
And in CustomView.m i ave executeAnimation as the following:
-(void)executeAnimation{
self.animation1InProgress = YES;
[UIView animateKeyframesWithDuration:3.0 delay:0.0 options:UIViewAnimationCurveLinear animations:^{
self.human.frame = CGRectMake(self.human.frame.origin.x, self.human.frame.origin.y + 300, self.human.frame.size.width, self.human.frame.size.height);
} completion:^(BOOL finished){
self.animation1InProgress = NO;
}];
}
Now in ParentViewController.m, i add CustomView without any animation
//init custom
customView = [CustomView initCustomView];
[self.view addSubview:centerLocationView];
This code is OK. I could init and addSubview to ParentViewController.
But whenever i would like to execute animation about CustomView. I call the following coding in ParentViewController.m:
[customView executeAnimation];
There is nothing changed at Parent View.
Is anybody know the way to execute this animation at ParentViewController?
Thank in advance.
If you really want to use +[UIView animateKeyframesWithDuration:delay:options:animations:completion:], you should add keyframes to your animations block:
-(void)executeAnimation{
self.animation1InProgress = YES;
[UIView animateKeyframesWithDuration:3.0 delay:0.0 options:UIViewAnimationCurveLinear animations:^{
[UIView addKeyframeWithRelativeStartTime:0.0 relativeDuration:1.0 animations:^{
self.human.frame = CGRectMake(self.human.frame.origin.x, self.human.frame.origin.y + 300, self.human.frame.size.width, self.human.frame.size.height);
}];
} completion:^(BOOL finished){
self.animation1InProgress = NO;
}];
}
Otherwise, just use [UIView animateWithDuration:animations:completion:]:
-(void)executeAnimation{
self.animation1InProgress = YES;
[UIView animateWithDuration:3.0 delay:0.0 options:UIViewAnimationCurveLinear animations:^{
self.human.frame = CGRectMake(self.human.frame.origin.x, self.human.frame.origin.y + 300, self.human.frame.size.width, self.human.frame.size.height);
} completion:^(BOOL finished){
self.animation1InProgress = NO;
}];
}
I have a method that drops down a UIView. That works fine. How would I go about having it slide back up when the button is touched a second time?
I also need to deactivate it somehow so that it doesn't repeat the animation if it's already displayed.
- (void)slideDownTableView {
self.tableViewCities = [[UITableView alloc] initWithFrame:self.view.bounds style:UITableViewStylePlain];
self.tableViewCities.frame = CGRectMake(0,0,300,180);
self.tableViewCities.autoresizingMask = ( UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth );
self.tableViewCities.dataSource = self;
self.tableViewCities.delegate = self;
self.tableViewCities.backgroundColor = [UIColor whiteColor];
UIView *myview=[[UIView alloc] initWithFrame: CGRectMake(10, 0,300,180)];
myview.backgroundColor=[UIColor redColor];
[myview addSubview:self.tableViewCities];
[self.view addSubview:myview];
myview.frame = CGRectMake(10, 0,300,-180); // offscreen
[UIView animateWithDuration:0.5
animations:^{
myview.frame = CGRectMake(10, 0,300,180); // final location move
}];
}
iOS provides autoreverse property by which animation reverses automatically.
UIViewAnimationOptionAutoreverse will give you an effect as the animation reverses .
[UIView animateWithDuration:0.5
delay:0.0
options:UIViewAnimationOptionAutoreverse
animations:^{
// do whatever animation you want, e.g.,
someView.frame = someFrame1;
}
completion:NULL];
Using BeginFromCurrentState option worked for me.
if(!enabled){
[UIView animateWithDuration:0.4 delay:0.0 options:UIViewAnimationTransitionCurlUp
animations:^{
myview.frame = CGRectMake(10, 0,300,180);
}
completion:nil];
enabled=YES;
}
else{
[UIView animateWithDuration:0.4 delay:0.0 options:UIViewAnimationTransitionCurlUp
animations:^{
myview.frame = CGRectMake(10, 0,300,-180);
}
completion:nil];
enabled=NO;
}
With a simple BOOL to store the current state of the sliding UIView you can achieve this.
The initial state is not visible if I understand well? So initially the BOOL citiesVisible is set to N0, then the button's action check its value, and performs a different animation accordingly. Simple.
Finally, I also disable the button during the animation to prevent the user to trigger a second animation while a first one is already performing.
I rewrote a bit your code to follow some best practices, and improve readability/maintainability:
Setting up the views beforehand because the button's action responsibility shouldn't be to setup the views, but only to animate them.
Changed the signature of the target action, that's my personal style guide to state in the sender's name and the ControlEvent, it makes it more tidy when you have multiple buttons/actions ;). Also per Apple guidelines, methods receiving actions should have one "id" parameter, the sender.
Store the frames in variables (you could also have them as #define, that'd be even better I think), so you don't have to hard-code them twice, it reduces the risk of breaking the animation by having a view sliding to different positions...
Code update:
#interface ViewController ()
#property (nonatomic, strong) UIButton *showCitiesButton;
#property (nonatomic, strong) CGRect citiesInitialFrame;
#property (nonatomic, strong) CGRect citiesVisibleFrame;
#property (nonatomic, strong) UITableView *tableViewCities;
#property (nonatomic, strong) UIView *citiesContainer;
#property (nonatomic, strong) BOOL citiesVisible;
#end
#implementation ViewController
- (void)viewDidLoad
{
[super viewDidLoad];
// Setting up the cities view
self.citiesInitialFrame = CGRectMake(10, -180, 300, 180);
self.citiesVisibleFrame = CGRectMake(10, 0, 300, 180);
self.tableViewCities = [[UITableView alloc] init ...];
self.citiesContainer = [[UIView alloc] init ...];
// ...
self.citiesVisible = NO;
[self.showCitiesButton addTarget:self
action:#selector(showCitiesButtonDidTouchUpInside:)
forControlEvents:UIControlEventTouchUpInside];
}
- (void)showCitiesButtonDidTouchUpInside:(id)sender
{
[self.showCitiesButton setEnabled:NO];
if (self.citiesVisible)
{
// Cities view is visible, sliding it out of the screen
[UIView animateWithDuration:0.5
animations:^{
self.citiesContainer.frame = self.citiesInitialFrame;
} completion:^(BOOL finished) {
self.citiesVisible = NO;
[self.showCitiesButton setEnabled:YES];
}];
}
else
{
// Cities view is not visible, sliding it in
[UIView animateWithDuration:0.5
animations:^{
self.citiesContainer.frame = self.citiesVisibleFrame;
} completion:^(BOOL finished) {
self.citiesVisible = YES;
[self.showCitiesButton setEnabled:YES];
}];
}
}
#end
You can reverse UIView animation with this code:
if (isMarked) {
[UIView animateWithDuration:3
delay:0
options:UIViewAnimationOptionBeginFromCurrentState
animations:(void (^)(void)) ^{
self.transform=CGAffineTransformMakeScale(1.1, 1.1);
}
completion:nil];
} else {
[UIView animateWithDuration:3
delay:0
options:UIViewAnimationOptionBeginFromCurrentState
animations:(void (^)(void)) ^{
self.transform=CGAffineTransformIdentity;
}
completion:nil];
}