I feel like I'm missing something obvious.
I've got a custom segue class. It does some animation. It works just fine if the current orientation is the same as the nib's orientation, but I can't make it work if the current orientation doesn't match the nib (ie current orientation is landscape but nib is portrait).
The issue is that the destination view controller's frame and orientation is wrong until viewDidAppear. But viewDidAppear is too late. Any attempt to do animation inside the custom segue must happen before viewDidAppear.
Oddly, viewDidLoad and viewWillAppear report the correct self.interfaceOrientation (while unfortunately reporting the wrong frame), but any attempt to draw in these methods will draw at the wrong orientation.
Anyone want to call me an idiot and point out the obvious mistake I'm making? I would greatly appreciate.
To answer my own question, I ultimately gave up on custom segues and opted for using a custom container controller. This doesn't get around the problem of view controllers not knowing their proper frame until after appearing, but did simplify the problem.
A more direct answer to my question is that it's the responsibility of the container controller (or presenting controller) to change the presented controller's frame. And the "best" way to do that, I've found, is to use the presenting/container controller's bounds as a starting point. The presenting/container's frame won't accurately reflect changes in orientation.
As an example in code, from my custom container controller:
- (void)slideToEdit {
// add edit to container and prep home for removal from container
[self.home willMoveToParentViewController:nil];
[self addChildViewController:self.edit];
// the vital frame setting <- this is the bit that solves the problem
self.edit.view.frame = self.view.bounds;
// prep the animation
__block CGRect frame = self.edit.view.frame;
frame.origin.x += frame.size.width;
self.edit.view.frame = frame;
// animate
[self transitionFromViewController:self.home toViewController:self.edit
duration:SLIDE_SPEED options:UIViewAnimationCurveLinear animations:^{
// slide animation
frame.origin.x -= frame.size.width;
self.edit.view.frame = frame;
} completion:^(BOOL done) {
[self.home.view removeFromSuperview];
// finalise container heirarchy
[self.home removeFromParentViewController];
[self.edit didMoveToParentViewController:self];
}];
}
This doesn't feel ideal, in that having to manually modify the frame seems wrong when it could be inferred from the nib + current orientation. But this at least works.
Related
So I realized this myself and found this answer to confirm:
Prevent contentSizeForViewInPopover from animating
Basically I am in a similar boat in that I want to dynamically resize my popover, depending on how much data I have to display. I am also getting this animation action where the popover view moves into place. The problem is, I can't set the popover's content size in the caller/parent, because neither the size of the content view for the text view nor the frame for the text view in my popover is known until viewDidAppear or viewDidLayoutSubviews is called, but of course at this point it's too late, the popover is already on its way to being visible, thus I am getting this unwanted animation.
I am dynamically setting the size of the text views in the popover view controller class to fit the amount of data the text views are displaying, and this only works at the point where their frame size is defined/known/however you want to word it when it hasn't gotten to that point yet in the view lifecycle.
I should mention that I have my view controllers and view all done in a storyboard.
Does this make sense? Hope I'm explaining it well. Any help or suggestions greatly appreciated.
EDIT Here is my viewDidLayoutSubviews method from the view controller I am loading in the popover - hopefully this makes it clear. The first time this loads, if I don't do this in viewDidLayoutSubviews or viewDidAppear the frame for my 2 text views are CGRectZero, the text views don't resize for the content correctly and the popover size isn't right either:
- (void)viewDidLayoutSubviews
{
[super viewDidLayoutSubviews];
self.physicianDataNewValueTextView.text = self.physicianDataNewValue;
CGRect frame = self.physicianDataNewValueTextView.frame;
frame.size.height = self.physicianDataNewValueTextView.contentSize.height;
self.physicianDataNewValueTextView.frame = frame;
self.physicianDataOldValueTextView.text = self.physicianDataOldValue;
frame = self.physicianDataOldValueTextView.frame;
frame.size.height = self.physicianDataOldValueTextView.contentSize.height;
self.physicianDataOldValueTextView.frame = frame;
self.contentSizeForViewInPopover = CGSizeMake(384.0f, self.physicianDataOldValueTextView.frame.size.height + 75.0f);
}
So I decided to use delegation. Instead of resetting the content size in the view controller for the popover, I call my delegate (which is the containing view controller) with the new size so I can set it there with the animated property set to NO. Works well.
I am working on an iOS app. The root view controller contains a UINavigationController which wraps up the main contents of the app, and a footerViewController (audio player) that will compress the main content when it animates up into view.
I’m using auto layout to show and hide this footer like so:
_footerVisibleConstraints = [… #“V:|[navControllerView][footerView(==90)]|" …];
_footerHiddenConstraints = [… #“V:|[navControllerView][footerView(==0)]|" …];
Generally this works well. But I’m struggling with one issue. I have a situation where I need to push a new view on the UINavigationController stack and animate my footer into view the same time:
[self.view layoutIfNeeded];
[UIView animateWithDuration:1.5f animations:^{
[[self view] removeConstraints:_footerHiddenConstraints];
[[self view] addConstraints:_footVisibleConstraints];
[[self view] layoutIfNeeded];
}];
[navigationController pushViewController:newViewController animated:YES];
The problem in this situation is that newViewController is animating in snapped to it's final (compressed) state, and not beginning from the full starting height of the view. So there is a gap at the bottom while the footer animates in.
I’ve slowed down the animation and posted a video here to demonstrate what I am describing.
Also, notice how when I pop back to the root view controller the content in the UINavigationController isn’t compressed either.
So, can someone explain to me what’s going on here? Is there a way to accomplish what I am after?
Just add a variable to the .h of your VC to stipulate whether the footer needs to open or not. Then add the footer animation to the didAppear method with a check on the variable. This will result in performing the actions in the order you want them to happen.
If you want both animations to happen at the same time you will need to subclass a segue and add a custom animation.
I need to change the width of a subview depending on, say, available disk space. So I have a selector that's called upon the view controller's viewWillLayoutSubviews as such:
CGRect rect = self.usageView.bounds;
rect.size.width = self.someCalculatedwidth;
[self.usageView setFrame:rect];
[self.usageView setNeedsDisplay];
Really, I just want to change the width of the subview. It works if the view controller is the initial controller; however, if it is transitioned from, say, a Push Segue it stopped working. What happens is I'd see my desired width rendered for moment but then it gets changed to the original width as per its blueprint on the Storyboard.
The behavior feels like the iOS caches the original (storyboard) view in a block during the push segue animation, and upon completion it executes the block which doesn't have my calculations above; thereby overridden the desired view. Any idea?
There are two approaches.
The first approach is to circumvent the problem, with a side benefit of cool animation. Since I have no clue when or how many times or exactly how iOS enforces the auto-constraints, I simply delay my UIView modifications and smooth it out with animation. So I wrap view animation around the code in my selector:
[UIView animateWithDuration:.25 animations:^{
CGRect rect = self.usageView.bounds;
rect.size.width = self.someCalculatedwidth;
[self.usageView setFrame:rect];
[self.usageView setNeedsDisplay];
}];
Then I'd call my selector in my controller's viewDidAppear:animated:
[self performSelector:#selector(resetUsageView) withObject:self afterDelay:0.0];
Now most would say this is a cop-out or a kludge. And that's fair.
In the second approach I attack the root of the problem - auto layout/constraints. I add a Width constraint to my usageView in the storyboard, and connect its IBOutlet to my code.
#property (weak, nonatomic) IBOutlet NSLayoutConstraint *usageViewWidthConstraint;
Then I modify the constraint as per the calculated width in my controller's viewWillAppear:animated:
[self.usageViewWidthConstraint setConstant:self.someCalculatedWidth];
Voila! A one liner.
Now what if I want animation given I got a taste of it from the first approach? Well, constraint changes are outside the realm of animation so basically I'd need a combination of the first and second approaches. I change the UIView's frame for animation, and modify the constraint as well to "set the record straight":
[UIView animateWithDuration:.25 animations:^{
CGRect rect = self.usageView.bounds;
rect.size.width = self.someCalculatedwidth;
[self.usageView setFrame:rect];
[self.usageViewWidthConstraint setConstant:self.someCalculatedWidth]; // <<<<
[self.usageView setNeedsDisplay];
}];
So I realized this myself and found this answer to confirm:
Prevent contentSizeForViewInPopover from animating
Basically I am in a similar boat in that I want to dynamically resize my popover, depending on how much data I have to display. I am also getting this animation action where the popover view moves into place. The problem is, I can't set the popover's content size in the caller/parent, because neither the size of the content view for the text view nor the frame for the text view in my popover is known until viewDidAppear or viewDidLayoutSubviews is called, but of course at this point it's too late, the popover is already on its way to being visible, thus I am getting this unwanted animation.
I am dynamically setting the size of the text views in the popover view controller class to fit the amount of data the text views are displaying, and this only works at the point where their frame size is defined/known/however you want to word it when it hasn't gotten to that point yet in the view lifecycle.
I should mention that I have my view controllers and view all done in a storyboard.
Does this make sense? Hope I'm explaining it well. Any help or suggestions greatly appreciated.
EDIT Here is my viewDidLayoutSubviews method from the view controller I am loading in the popover - hopefully this makes it clear. The first time this loads, if I don't do this in viewDidLayoutSubviews or viewDidAppear the frame for my 2 text views are CGRectZero, the text views don't resize for the content correctly and the popover size isn't right either:
- (void)viewDidLayoutSubviews
{
[super viewDidLayoutSubviews];
self.physicianDataNewValueTextView.text = self.physicianDataNewValue;
CGRect frame = self.physicianDataNewValueTextView.frame;
frame.size.height = self.physicianDataNewValueTextView.contentSize.height;
self.physicianDataNewValueTextView.frame = frame;
self.physicianDataOldValueTextView.text = self.physicianDataOldValue;
frame = self.physicianDataOldValueTextView.frame;
frame.size.height = self.physicianDataOldValueTextView.contentSize.height;
self.physicianDataOldValueTextView.frame = frame;
self.contentSizeForViewInPopover = CGSizeMake(384.0f, self.physicianDataOldValueTextView.frame.size.height + 75.0f);
}
So I decided to use delegation. Instead of resetting the content size in the view controller for the popover, I call my delegate (which is the containing view controller) with the new size so I can set it there with the animated property set to NO. Works well.
I've been struggling with this for quite a while now.
I have to implement an user profile similar to what Instagram has in their ios app.
When clicking on the first to buttons on that tab bar like thing all the contents downwards from it changes. The tableview that is displayed on the bottom part has dynamic size so they keep account of that also.
I have something implemented where the top part is a UIView with 5 buttons and based on them the bottom part (witch is like a container view) changes content. And these two (top uiview and bottom container view) are part of UIScrollView. But this way I can't get information back in time on the size about the tableview's size that I want to display in the bottom part in order to modify the UIScrollView's size. And I have a feeling this is a flawed way to do it.
I would really appreciate any ideas oh how to implement this king of interaction. Thank you.
I believe it's a headerView on a UITableView or a UICollectionView, depending on which view mode you have selected. When you tap one of the buttons it changes out the UITableView to a UICollectionView or vice versa.
You want to keep track of the current contentOffset for whichever is being displayed (UICollectionView and UITableView are both subclasses of UIScrollView so you will be able to get this from both) and then set the contentOffset on the view you're switching to.
Setup an ivar for the UIView header subclass so you can easily re-use it.
This is what I have. My problem is that I'm mot getting back in useful time the tableview's frame height from the tableview controller to the UserProfileViewController in order to change the latter's scrollview size. I also feel that I'm somehow doing this backwards so any suggestions are more than welcome.
This view has two parts: an upper part and a lower part. The parent view is a scroll view. What I wanted to achieve with this is having a sort of tab bar in the upper part that will controll waht will appear in the lower part.
The upper part has a flip animation when the upper left button is pressed to reveal another view.
The way this is achieved is by having 2 views: a dummy view and the back view. The dummy view has the front view as a child. The front view is the one that containes all the buttons.
The code for this animation is achieved in this way:
- (IBAction)infoButtonPressed:(id)sender
{
[UIView beginAnimations:nil context:NULL];
[UIView setAnimationDuration:1.0];
[UIView setAnimationTransition:UIViewAnimationTransitionFlipFromRight forView:self.hoverView cache:YES];
if ([self.headerView superview]) {
[self.headerView removeFromSuperview];
[self.hoverView addSubview:self.backOfHeaderView];
[self.infoButton removeFromSuperview];
[self.backOfHeaderView addSubview:self.infoButton];
} else {
[self.backOfHeaderView removeFromSuperview];
[self.hoverView addSubview:self.headerView];
[self.infoButton removeFromSuperview];
[self.headerView addSubview:self.infoButton];
}
[UIView commitAnimations];
}
The lower part is made out of a container view that acts as a place holder.
When a button is pressed a different view controller is displayed in the container view.
Each view controller has a container view of it's own. The specific view of that view controller (tableview) is added to it's container view when the controller is loaded. It also makes sure that if the tableview is already added to the container view it will be removed. All this is done in each specific view controller.
In the view controller of the User Profile view there is an instance of the container view and one of a UIViewController that also acts as a placeholder(named currentViewController from now on). When a specific button is pressed it checks if the an instance of the view controller that we want to display already exists. If not it will make one and will set it's tableview's frame to the bounds of the container view. After that it will remove the currentViewController's view from the superview and the currentViewController itself from the parent viewcontroller to make sure that if there is something assigned to these they will not be there. Then it goes and assigns the desired viewcontroller to the currentViewController. It also assigns the desired viewcontroller's containerView instance to the containerview in the parent viewcontroller (the User Profile viewcontroller). At the end it will add the desired viewcontroller as a child to the main viewcontroller (the User Profile viewcontroller) and desired viewcontroller's view to the containerView of the main viewcontroller.
This is the code for one of the buttons:
//Check if there is an instance of the viewcontroller we want to display. If not make one and set it's tableview frame to the container's view bounds
if(!_userWallViewController) {
self.userWallViewController = [[WallViewController alloc] init];
// self.userWallViewController.activityFeedTableView.frame = self.containerView.bounds;
}
[self.userWallViewController.containerView addSubview:self.userWallViewController.activityFeedTableView];
//If the currentviewcontroller adn it's view are already added to the hierarchy remove them
[self.currentViewController.view removeFromSuperview];
[self.currentViewController removeFromParentViewController];
//Add the desired viewcontroller to the currentviewcontroller
self.currentViewController = self.userWallViewController;
//Pass the data needed for the desired viewcontroller to it's instances
self.userWallViewController.searchURLString = [NSString stringWithFormat:#"event/user/%#/", self.userID];
self.userWallViewController.sendCommentURLString = [NSString stringWithFormat:#"event/message/%#", self.userID];
self.userWallViewController.totalCellHeight = ^(float totalCellHeight){
self.userWallViewController.numberOfCells = ^(float numberOfCells){
NSLog(#"The total number of cells: %f", numberOfCells);
NSLog(#"The total cell height: %f", totalCellHeight);
self.scrollView.contentSize = CGSizeMake(320.0, totalCellHeight + 172.0 + 33.0);
CGRect newFrame = self.userWallViewController.containerView.frame;
newFrame.size.height = totalCellHeight + 33.0;
self.userWallViewController.containerView.frame = newFrame;
NSLog(#"Container view: %f", self.containerView.frame.size.height);
NSLog(#"Scroll view: %f",self.scrollView.contentSize.height );
};
};
//Add this containerview to the desired viewcontroller's containerView
self.userWallViewController.containerView = self.containerView;
//Add the needed viewcontroller and view to the parent viewcontroller and the containerview
[self addChildViewController:self.userWallViewController];
[self.containerView addSubview:self.userWallViewController.view];
[self performSelector:#selector(changeScrollView) withObject:self afterDelay:0.5];
//CLEAN UP THE CONTAINER VIEW BY REMOVING THE PREVIOUS ADDED TABLE VIEWS
[self.userFansViewController.userSimpleTableView removeFromSuperview];
[self.fanOfViewController.userSimpleTableView removeFromSuperview];
[self.userPublishedMovellaListViewController.gridView removeFromSuperview];
[self.userPublishedMovellaListViewController removeFromParentViewController];
self.userPublishedMovellaListViewController = nil;
}
I know this answer is over a year late, but I wanted to state my hypothesis on it...just incase it might help someone else later. Im implementing a similar view and came to this conclusion. Anyone is welcomed to correct me if I'm wrong.
I think that perhaps the top view is a header view and the two options that seem like a collection view and a table view are both collection views.
Because the layout of collection views can be fine tuned to the most minute details, I think the view that looks like a table view is just a really specifically designed collection view. And when switching between the views, the collection view's data and properties are being swapped and reloaded.