Changing a frame in viewDidLayoutSubviews - ios

When a view controller's view is first shown I want to run an animation in which all elements in the view controller slide from outside the bottom of the screen to their natural positions. To achieve this, I do subview.frame.origin.y += self.view.frame.size.height in viewDidLayoutSubviews. I also tried viewWillAppear, but it doesn't work at all. Then I animate them up to their natural positions with subview.frame.origin.y -= self.view.frame.size.height in viewDidAppear.
The problem is that viewDidLayoutSubviews is called several times throughout the view controller's lifespan. As such, when things like showing the keyboard happen all my content gets replaced outside the view again.
Is there a better method for doing this? Do I need to add some sort of flag to check whether the animation has already run?
EDIT: here's the code. Here I'm calling prepareAppearance in viewDidLayoutSubviews, which works, but viewDidLayoutSubviews is called multiple times throughout the controller's life span.
- (void)viewDidLayoutSubviews
{
[super viewDidLayoutSubviews];
[self prepareAppearance];
}
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
[self animateAppearance];
}
- (NSArray *)animatableViews
{
return #[self.createAccountButton, self.facebookButton, self.linkedInButton, self.loginButton];
}
- (void)prepareAppearance
{
NSArray * views = [self animatableViews];
NSUInteger count = [views count];
for (NSUInteger it=0 ; it < count ; ++it) {
UIView * view = [views objectAtIndex:it];
CGRect frame = view.frame;
// Move the views outside the screen, to the bottom
frame.origin.y += self.view.frame.size.height;
[view setFrame:frame];
}
}
- (void)animateAppearance
{
NSArray * views = [self animatableViews];
NSUInteger count = [views count];
for (NSUInteger it=0 ; it < count ; ++it) {
__weak UIView * weakView = [views objectAtIndex:it];
CGRect referenceFrame = self.view.frame;
[UIView animateWithDuration:0.4f
delay:0.05f * it
options:UIViewAnimationOptionCurveEaseOut
animations:^{
CGRect frame = weakView.frame;
frame.origin.y -= referenceFrame.size.height;
[weakView setFrame:frame];
}
completion:^(BOOL finished) {
}];
}
}

If you need to animate something when view will appear and then not touch subviews later, I would suggest the following:
Don't change/touch viewDidLayoutSubviews
Add logic to move elements outside the screen (to their initial position before animation) in viewWillAppear
Add logic to animate elements into their proper position in viewDidAppear
UPDATE:
If you're using auto-layout (which is very good thing), you can't animate views by changing their frames directly (because auto-layout would ignore that and change them again). What you need to do is to expose outlets to constraints responsible for Y-position (in your case) and change that constraints rather then setting frames.
Also don't forget to include call to [weakView layoutIfNeeded] after you update constraints in the animation method.

I did both things in viewDidAppear:. It seems that when viewDidAppear: is called, the view is not actually visible, but about to. So the UI elements never show in their initial position if they are replaced there.

Related

viewDidLayoutSubviews getting called twice and convertPoint improperly converting on first render

I am trying to animate UITabBar items by using a UIImageView and setting its frame in the same frame as my UITabBarItem's when the view first renders to the user. In a different project, it works fine on viewDidAppear, logs showing that the frame is in the same frame as the one in my original project (I have 5 items, and the 5th item is always at 333, 1). On my original project, viewDidAppear will return 0, 0 and returns 333, 1 on viewDidLayoutSubviews. I moved the code to animate on viewDidLayoutSubviews and discovered that it gets called twice, and returns 2 values, 0, 0 on the first call, and 333, 1 on the second call.
Is there a way to start the animation after viewDidLayoutSubviews is finished with the second call?
I have tried moving around the animation code in viewWillAppear, viewDidAppear, and viewDidLayoutSubviews.
viewWillAppear and viewDidAppear resulted in the converted CGPoint as the original CGPoint of the tab bar (0, 617).
viewDidLayoutSubviews resulted in the converted CGPoint as the new CGPoint of the Tab Bar (0, 687) on the first call and starts the animation, and the actual frame after the animation is finished on the second call.
The following code is what I have on my viewDidLayoutSubviews:
-(void)viewDidLayoutSubviews
{
[super viewDidLayoutSubviews];
if(self.fromTabBar) {
NSInteger index = 5;
UIImageView *imageView = [[self.tabBar.subviews lastObject].subviews firstObject];
[self startAnimation:imageView withTag:index];
}
}
The following code is for startAnimation:withTag: :
-(void)startAnimation:(UIImageView *)imageView withTag:(NSInteger)tag {
CGPoint point = [imageView.superview convertPoint:imageView.frame.origin toView:self.view];
UIImageView *hexagon = [[UIImageView alloc]initWithImage:[UIImage imageNamed:#"vector-profile"]];
[hexagon setFrame:CGRectMake(point.x, point.y, imageView.frame.size.width, imageView.frame.size.height)];
hexagon.alpha = 0;
[self.view addSubview:hexagon];
[UIView animateWithDuration:0.5 animations:^{
imageView.transform = CGAffineTransformMakeScale(0.45, 0.45);
} completion:^(BOOL finished) {
[UIView animateWithDuration:0.5 animations:^{
imageView.alpha = 0;
hexagon.alpha = 1;
} completion:nil];
}];
}
the tag parameter is no longer being used in this method.
I expect the output UIImage to be on top of where the intended UITabBarItem image is (In this case, the 5th position in a UITabBar). This has worked on a different project, but my current project seems to process the rendering differently.

Animation of subviews in iOS one at a time

I have 9 custom subviews that I create and lay out programmatically. I want to animate their creation and change of position each one at a time but right now the whole set of 9 subviews is being animated at the same time.
This is my code:
- (void)viewDidLoad{
//create 9 setCardViews and add them to the array setCards to be able to identify them later
for (int i=0; i<9; i++) {
SetCardView *card = [[SetCardView alloc]initWithFrame:CGRectMake(self.gameView.center.x, self.gameView.center.y, 0, 0)];
[self.gameView addSubview:card];
[self.setCards addObject:card];
}
//layout those 9 views
[self updateLayout];
}
Then, the updateLayout method is called. It sets a frame size inside of which the subviews are laid out:
-(void)updateLayout
{
CGRect canvas = [self resizeCanvasWithNumberOfCards:[self.setCards count]];
for (SetCardView *card in self.setCards)
{
[self layOutCard:card withFrame:canvas atIndex:[self.setCards indexOfObject:card]];
}
}
Finally, inside the layOutCard method I calculate the position of the view (I omit that part of the code here) and I animate the change in its frame:
-(void)layOutCard:(SetCardView *)card withFrame:(CGRect)canvas atIndex:(NSUInteger)index
{
//calculate the new frame of the view
CGRect rect = //calculations ;
[SetCardView animateWithDuration:0.5 delay:self.delay options: UIViewAnimationOptionCurveEasyInOut animations:^{card.frame=rect;} completion:nil];
}
So all these animations happen at the same time, maybe because it's inside a loop in the updateLayout method so the controller waits for it to finish? Anyway I can't think of a way to make the animation of the views without using a loop so that they animate one at a time.
How could I do that?
Thanks
One of the way you can make it works is increase delay when you enumerate cards in updateLayout and after that pass it to layOutCard:withFrame:atIndex (of course you have to add delay parameter to that method. Or ft index parameter start from 0 and increase by 1 every time you can use it to calculate delay:
-(void)layOutCard:(SetCardView *)card withFrame:(CGRect)canvas atIndex:(NSUInteger)index
{
//calculate the new frame of the view
CGRect rect = //calculations ;
float myDelay = 0.5 * index; //0.5 is your duration.
[SetCardView animateWithDuration:0.5 delay: myDelay options: UIViewAnimationOptionCurveEasyInOut animations:^{card.frame=rect;} completion:nil];
}
You have it done (almost). One small adjustment on your code. At a run time your for loop is executed very fast leaving you with the impression that all animations are starting at the same time. You are using start with delay on your animation block already so simply increase the delay every time you go into to loop like this.
-(void)updateLayout
{
CGRect canvas = [self resizeCanvasWithNumberOfCards:[self.setCards count]];
for (SetCardView *card in self.setCards)
{
[self layOutCard:card withFrame:canvas atIndex:[self.setCards indexOfObject:card]];
// increase the delay so next animation will start after this one is finished
// check if this card is the last from your array
if([card isEqual:[self.setCards lastObject]])
{
// if card is last object set self.delay back to 0
self.delay = 0;
{
else
{
// if card is not the last object increase the delay time for next animation
self.delay += 0.5; // your current animation duration
}
}
}

iOS UITableView resize height / cant scroll down issue

Beginner iOS Developer here.
What I have is a UITableVIewController that has 1 section with 3 static grouped cells. Under that I have a UIView that has some buttons and text fields, when pressing one of the buttons, the UIView height increases. My problem is I cant scroll down to see the content that is at the bottom of the UIView
Screenshots:
When the green plus button is pressed, these elements are moved down making room for some new elements to be inserted (which I haven't implemented yet as I am stuck on this issue)
EDIT
Here is an example project: https://www.dropbox.com/s/vamxr12n6rrsam7/resizeSample.zip
You problem is that in your method to change the invoice. You only change the constraint of the invoiceBottomView, but you are not changing the size of invoiceBottomView's superview, that is the invoicePickerViewsContainer. So your tableView won't know you want to use more space of the tableFooterView (your invoicePickerViewsContainer).
You can change your code for this:
- (IBAction)invoiceLinesAddLine:(id)sender {
[UIView animateWithDuration:.25 animations:^{
CGRect invoiceBottomViewFrame = self.invoiceBottomView.frame;
NSInteger invoiceBottomViewFrameHeight = invoiceBottomViewFrame.size.height;
NSInteger invoiceLinesTopConstraintValue = invoiceLinesControlsViewTopConstraint.constant;
NSInteger invoiceLinesBottomConstraintValue = invoiceLinesControlsViewBottomConstraint.constant;
invoiceBottomViewFrameHeight = invoiceBottomViewFrameHeight + 40;
invoiceLinesTopConstraintValue = invoiceLinesTopConstraintValue + 40;
//invoiceLinesBottomConstraintValue = invoiceLinesBottomConstraintValue - 40;
invoiceBottomViewFrame.size.height = invoiceBottomViewFrameHeight;
invoiceLinesControlsViewTopConstraint.constant = invoiceLinesTopConstraintValue;
invoiceLinesControlsViewBottomConstraint.constant = invoiceLinesBottomConstraintValue;
CGRect invoicePickerViewsContainerFrame = self.invoicePickerViewsContainer.frame;
invoicePickerViewsContainerFrame.size.height += 40;
self.invoicePickerViewsContainer.frame = invoicePickerViewsContainerFrame;
self.tableView.tableFooterView = self.invoicePickerViewsContainer;
[self.view layoutIfNeeded];
} completion:^ (BOOL finished){
if (finished) {
//actions when animation is finished
}
}];
}
You don't need to change the invoiceLinesBottomConstraintValue, because you will increment the invoicePickerViewsContainer's height.
Then you need to add again the tableFooterView, so the table view will know that the size change, and the table will change the contentSize
Now, you have problems with the pickers... But I don't understand why you have the pickers in this view. If you want to preserve those pickers inside that view, you will need to change the constraint of those or its frame.
The way you setup the - (IBAction)invoiceLinesAddLine:(id)sender {} and the view within the tableview does not update the self.tableView.contentsize as you add more items. so add these lines to the completion function of the animation function within invoiceLinesAddLines and then twice the incremental height addition (i picked 80 arbitrarily)
completion:^ (BOOL finished){
if (finished) {
//actions when animation is finished
CGSize size=self.tableView.contentSize;
size.height+=80;
self.tableView.contentSize=size;
}
}];

move UIView not responding in the viewDidAppear method

I have a UIView that I am trying to move up bit when that parent view controller comes onto the screen. I have been reading up on this and most I see seem to say to use the viewDidAppear method to make any visual adjustments to the layout. I have tried this and it doesn't seem to work. Nothing happens, and went I nslog the origin.y I get back -47,000, which I then maybe assume that something is not initialized yet. Here is what I have tried.
- (void) viewDidAppear:(BOOL)animated
{
// set the save view y postion
saveData.center = CGPointMake( 0.0f, 0.5f );
NSLog(#"This is the y %f", saveData.frame.origin.y);
NSLog(#"This is the center points on load %#", NSStringFromCGPoint(optionalData.center));
}
But if I do something like this where I add a delayed method call in the viewDidLoad method:
[self performSelector:#selector(moveSaveView) withObject:nil afterDelay:0.7f];
and have this, it works
- (void) moveSaveView
{
// set the save buttons y postion
[UIView animateWithDuration:0.5 delay:0.0 options:0 animations:^{
// Animate the alpha value of your imageView from 1.0 to 0.0 here
optionalData.alpha = 0.0f;
} completion:^(BOOL finished) {
// Once the animation is completed and the alpha has gone to 0.0, hide the view for good
optionalData.hidden = YES;
}];
// move the save button up
[UIView animateWithDuration:0.5
animations:^{saveData.center = CGPointMake( 160.0f, 280.5f );}];
saveData.center = CGPointMake( 160.0f, 280.5f );
}
Is this also an issue due to the fact that I am using auto layout? I would just like my view to start in the place I need it to, and not use some delayed call to make that happen.
Edit:
So I gave this a shot and came up with this to try and move my UIView:
- (void) viewDidAppear:(BOOL)animated
{
NSLog(#"this is the constraing %f", saveData.saveButtomConstraint.constant); // gives me 93 which is here its at.
saveData.saveButtomConstraint.constant = 32;
[saveData setNeedsUpdateConstraints];
[saveData layoutIfNeeded];
NSLog(#"this is the constraing %f", saveData.saveButtomConstraint.constant); // gives me 32 which is here its at.
}
The problem is that the view never moves on the screen. What am I missing? Also is it ok to post and edit like this, when its related to the same question? I'm still trying to get the hang of this form.
Yes, your problem is due to the fact you are using auto layout. Frame animation is not compatible with auto layout, so instead you need to animate the constraints on your view. Check out this answer for detailed info, this might also help too. Good luck!
Edit:
So it looks like you have added a property to your saveData UIView called saveButtomConstraint. This is good as it gives you access to that constraint. However are you sure that that constraint is actually a member of the [saveData constraints] array? Generally constraints in Interface Builder are added to the parent UIView. I think the problem is most likely are calling layoutIfNeeded on the wrong view, you need to call it on parent view of saveData, or possibly on the root view of the view controller, [self view].

'UIView' may not respond to 'addSubview:withAnimation:'

I got this warning:
'UIView' may not respond to 'addSubview:withAnimation:'
The line of code which produce that warning is this:
[self.masterView addSubview:self.detailImage withAnimation:def];
And my relevant code is like this:
ExUIViewAnimationDefinition *def = [[ExUIViewAnimationDefinition alloc] init];
def.type = ExUIAnimationTypeTransition;
def.direction = ExUIAnimationDirectionMoveUp;
[self.masterView addSubview:self.detailImage withAnimation:def];
[def release];
I looked on the UIView documentation, i thought addSubview may be deprecated, but it still like this.
Does any one know how to solve this warning? Thanx in advance.
addSubview is a method UIView will respond to. addSubview:withAnimation: is not a method UIView will respond to.
If you want to add a subview with a fade or something like that, try this:
self.detailImage.alpha = 0.0;
[self.masterView addSubview:self.detailImage];
[UIView animateWithDuration:0.3 animations:^{
self.detailImage.alpha = 1.0;
}];
To add a subview to a parent, call the addSubview: method of the parent view. This method adds the subview to the end of the parent’s list of subviews.
To insert a subview in the middle of the parent’s list of subviews, call any of the insertSubview:... methods of the parent view. Inserting a subview in the middle of the list visually places that view behind any views that come later in the list.
[self.masterView addSubView:self.def];
[def release];
This helped me to animate subview by sliding out from down of border of parent view
-(void) addAnimatadView:(UIView *) animatedView toView:(UIView *)aView {
CGRect frame = animatedView.frame;
float origin = frame.origin.y;
frame.origin.y = aView.frame.size.height;
[animatedView setFrame:frame];
[aView addSubview:animatedView];
frame.origin.y = origin;
[UIView animateWithDuration:0.4 animations:^{[animatedView setFrame:frame];}];
}
Just change frame origins as you want to reach free sliding

Resources