My setup
I have a UICollectionViewController which displays 2 columns in Portrait and 3 Columns in Landscape, Like So :
. . . .
I am using this code to trigger a layout refresh on rotation :
- (void)updateViewConstraints
{
[super updateViewConstraints];
[[self.collectionView collectionViewLayout] invalidateLayout];
}
Everything is working fine, But...
The issue
1. I am viewing the collectionView in Portrait :
Everythings fine.
2. I click on one of the cells to go to a detail view, which also supports both orientations. Then on the detail view, I switch the orientation to landscape. Still good.
3. Still in Landscape, I now tap < back to come back to the collectionView, and then this is what I see :
OOPS!
I have absolutely no idea what is the cause of this. Searching on SO and Google didn't turn up anything.
What I've Tried
I tried implementing this method in the collectionVC.m file :
- (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator
{
[super blahblah];
[[self.collectionView collectionViewLayout] invalidateLayout];
}
Although it gets called when even I am on the detailView, it doesn't solve the issue.
Notes :
I am not implementing any of those should/will/did methods related to orientation except the two I posted above.
One thing I noticed is that the collectionView is switching to a 2 column layout nd all. It seems like its the view bounds that is getting mixed up.
Sorry for the long post. Here's a potato.
So Any thoughts on what is going wrong??
Update : I grabbed a screenshot of the UI hierarchy, might help.
The problem you are facing is the rotation did happen when the collection viewController is not the visible one. so both updateViewConstraints and viewWillTransitionToSize:withTransitionCoordinator: will not get called.
There is two possible solutations for this probelm.
You can observe the the change of orientation
// in viewDidLoad
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(orientationDidChange:) name:UIDeviceOrientationDidChangeNotification object:nil];
- (void)orientationDidChange:(NSNotification *)notification
{
//TODO: resetup the layout itemSize, etc if your it depend on the collection bounds.
// and make sure to do that after the rotation did happend(i.e in viewWillAppear or orientationDidChange:)
[[self.collectionView collectionViewLayout] invalidateLayout];
}
you can invalidate the layout in viewWillAppear anyway
-(void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
//TODO: resetup the layout itemSize, etc if your it depend on the collection bounds.
// and make sure to do that after the rotation did happend(i.e in viewWillAppear or orientationDidChange:)
[[self.collectionView collectionViewLayout] invalidateLayout];
}
Thanks to Ismail's Answer, which set me on the right path and I figured it out finally:
Upon careful observation of the code, I made some observations :
Firstly, the UICollectionViewController is properly adjusting to the rotation, it's viewWillTransitionToSize: and updateViewConstraints are getting called right on cue. The 3 column/2 column switch is also happening properly and all the required datasource methods are getting called.
So Its not the CollectionViewController
Then I took a closer look at the view hierarchy (Image link at the end of my question).
Interestingly, the tabBar Controller still thinks its in portrait while everything above and below it has switched to landscape. The tab bar and all is still at portrait width.
So I just added this in the CollectionViewController's viewDidAppear method :
-(void) viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
[self.tabBarController.view setNeedsDisplay];
}
Even Better, I subclassed the TabBar controller and added this:
- (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator
{
[super viewWillTransitionToSize:size withTransitionCoordinator:coordinator];
[self.view setNeedsLayout];
}
And that did the trick!
I still have no idea why this is happening. So I will be investing and will update my answer when I find out more.
Related
I'm having a problem adjusting a parent view's layout when orientation changes for it's child view. I have a collection view controller that, when one of the cells are tapped, pushes a child view on top. If an orientation change occurs while the child view is visible and it is dismissed, the parent view's collection view cells haven't adjusted for the new width.
I should note that this works fine if the parent view is visible.
The only thing that has fixed this for me is in the viewDidAppear method of the parent view controller invalidates the collection view layout, but for me it's too late as the user sees the animation of the collection view cells snap into place.
- (void) viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
[self.collectionView.collectionViewLayout invalidateLayout];
[self.collectionView reloadData];
}
I would have preferred to use viewWillAppear, but that doesn't seem to do anything. It sounds like it can only adjust the cells when the parent view is visible.
Is there a way around this?
Referring to this answer, iOS does not send orientation change events to offscreen view controllers, making them an unreliable way to determine whether the view has been resized.
viewWillAppear: isn't working in your case because iOS doesn't resize the offscreen view controller's view until after it calls the method, so your invalidate and reload are being pulled off the wrong values.
I believe the iOS8+ viewWillTransitionToSize:withTransitionCoordinator: method fires even when offscreen, but I'm not positive. In my experience, the size it provides does not reflect the actual size of the view. What I personally like to hook into is viewWillLayoutSubviews, usually guarded with a width check:
#property (nonatomic) CGFloat lastWidth;
- (void)viewWillLayoutSubviews
{
[super viewWillLayoutSubviews];
if (self.lastWidth != CGRectGetWidth(self.view.bounds)) {
self.lastWidth = CGRectGetWidth(self.view.bounds);
// Update your collection view here.
}
}
This way, whenever your view is going to resize (on display, inside an orientation change animation) you can update the size information.
try overriding -(void)viewWillLayoutSubviews: method in your parent view controller. In your case,it goes like this
-(void)viewWillLayoutSubviews {
[self.collectionView.collectionViewLayout invalidateLayout];[self.collectionView reloadData];
}
You could try invalidating your layout and reloading in the rotation handler methods.
For pre-iOS 8
willRotateToInterfaceOrientation:duration:
And for iOS 8+
viewWillTransitionToSize:withTransitionCoordinator:
Am using xib to design my view using auto layout.Run time I want to change the content size of my scrollview in viewDidLayoutSubviews method but its not working.On orientation change the code written inside viewDidLayoutSubviews works but on load its not working.Please advice what could be the issue?
- (void)viewDidLayoutSubviews
{
scroll.contentSize = CGSizeMake(scroll.frame.size.width, attBtn.frame.origin.y+attBtn.frame.size.height+40);//atnBtn is added programatically
}
Usually when you want to have a view redraw you should call:
[self setNeedsDisplay: YES];
Granted I have never build on iOS, on OSX this code works every time. Also, for example, if you want your delegate to call a redraw for a view named someView:
[someView setNeedsDisplay: YES];
On iPad, I have perfectly working UISplitViewController.
I can hide and show its primaryViewController, and splitViewController:willChangeToDisplayMode: is called in appropriate way.
But on iPhone, something is wrong.
I can show primaryViewController, but cannot hide it, because the primaryViewController appears in full screen size. It's so full that I can't touch the secondary view, in that way I can hide the primaryViewController on iPad.
splitViewController:willChangeToDisplayMode: is not called either.
I have a viewDidLoad below, in my custom UISplitViewController class.
// UISplitViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.delegate = self;
self.preferredPrimaryColumnWidthFraction = .1;
CGRect mainScreen = [[UIScreen mainScreen] bounds];
self.minimumPrimaryColumnWidth = 270;
self.maximumPrimaryColumnWidth = mainScreen.size.width - 5;
}
On iPhone, any of these property seems not to be working: preferredPrimaryColumnWidthFraction or minimum/maximumPrimaryColumnWidth
I am adding this splitViewController as rootViewController in AppDelegate.m by the code below.
// AppDelegate.m
[_splitViewCon addChildViewController: tagNaviCon];
[_splitViewCon addChildViewController: mainNaviCon];
self.window.rootViewController = _splitViewCon;
I searched web and found some keywords like "container view".
Is this something I have to do with, when I want to use a UISplitViewController on iPhone ?
I also watched WWDC Video, but I didn't understand "how to code it exactly".
Currently, I do not use any Interface Builder. So I'd be rather glad if someone gives programmaticaly way to code it.
Thank you !
You can have side-by-side UISplitViewController on iPhones 4S, 5, 5S and 6 as well.
To do it you have to embed its view in another view controller (addChildViewController:...didMoveToParentViewController:)
After that you'll be able to control split's behaviour by overriding its trait collection (setOverrideTraitCollection:forChildViewController:). Basically here you have to inspect your current trait collection and change the horizontal size class to regular. This way the UISplitController will be able to show both master and detail views (primary and secondary now called) by setting split's preferredDisplayMode
Then on rotation you can make the same observations about your trait collection and change the preferredDisplayMode and override again if necessary the split's trait collection. This can be done in viewWillTransitionToSize:withTransitionCoordinator: or willTransitionToTraitCollection:withTransitionCoordinator:. The second one won't be called on an iPad as its size classes are alway regular on both orientations.
One note about a problem I'm still not able to resolve. On iPhone 5S for example when rotating in portrait I'm hiding the master controller so to have only one view on the screen and the UISplitViewController should adapt itself to a UINavigationController. That works fine however during the rotation animation the master view is disappearing and you can see a blank ugly space.
One other note as well.
You have to implement UISplitViewControllerDelegate and use methods in order to set which view controller should be visible on app launch and when split is used as a navigation.
Here is a thread about this.
Hope it helps and if I find solution about the problem I have I'll update my answer
The #user1006806 answer worked for me. Here's how I got rid of the ugly blank space during the rotation from within my UISplitViewController's rotation method (iOS 8):
- (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator {
UIInterfaceOrientation theOrientation = [[UIApplication sharedApplication] statusBarOrientation];
if (UIInterfaceOrientationIsPortrait(theOrientation)) {
self.preferredDisplayMode = UISplitViewControllerDisplayModeAllVisible;
} else {
self.preferredDisplayMode = UISplitViewControllerDisplayModeAutomatic;
}
[coordinator animateAlongsideTransition:^(id<UIViewControllerTransitionCoordinatorContext> context) {
} completion:^(id<UIViewControllerTransitionCoordinatorContext> context) {
self.preferredDisplayMode = UISplitViewControllerDisplayModeAutomatic;
}];
[super viewWillTransitionToSize:size withTransitionCoordinator:coordinator];
}
I'm having a problem handling rotation in a view controller.
When the view is topmost and the phone is rotated, it adapts correctly.
When there's a view controller being presented modally over it and the device is rotated, the view controller under is not fully updated for the rotation when the user returns. The biggest problem I appear to be having is that the separator lines don't expand to fill the whole width.
Example:
I've uploaded my test project to GitHub; you can clone it from https://github.com/tewha/ResizeOnRotate.git.
I have no code handling rotation at all. My understanding was that this was supposed to be fully automatic. What am I missing to make this work correctly?
Edit:
Inspired by the answer below, a simple workaround:
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
UITableViewCellSeparatorStyle separatorStyle = self.tableView.separatorStyle;
self.tableView.separatorStyle = UITableViewCellSeparatorStyleNone;
self.tableView.separatorStyle = separatorStyle;
}
I think this may be an Apple bug. You can reproduce it in their own app "Mail".
Steps to reproduce in "Mail" app -
Open Inbox of messages in "Mail" app. (say, in Portrait orientation)
Tap compose message icon (bottom right) and rotate the device (in landscape).
Close the compose message and see the inbox messages view now.
Result: The separator lines are broken.
Workaround for user: Scroll down and up in message inbox (leads to display refresh).
Reproducible up to iOS 7.0.2.
Edit
(Code workaround - 1)
If possible, you could reloadData to refresh the tableview when it appears.
- (void)viewWillAppear:(BOOL)animated
{
[self.tableView reloadData];
}
Code workaround - 2 (reload only the visible cells)
- (void)viewWillAppear:(BOOL)animated
{
NSArray *refreshCells = [self.tableView indexPathsForVisibleRows];
[self.tableView reloadRowsAtIndexPaths:refreshCells withRowAnimation:UITableViewRowAnimationNone];
}
Everybody knows that you can't trust the frame size on a UIViewController init/viewDidLoad method; this:
- (void)viewDidLoad: {
NSLog(#"%d", self.view.frame.size.width);
}
will print wrong sizes in many occasions (in particular it's pretty much broken in landscape mode)
This will actually return always corrected results so it's good to layout the subviews:
- (void)viewWillAppear: {
NSLog(#"%d", self.view.frame.size.width);
}
The problem is that viewWillAppears gets called every time the view appears, so it's not suitable to alloc or add subviews. So you end up declaring every single view in the interface and you end up with huge header files that I don't like at all since most of the items don't need any more manipulations after the initial setup.
So question one is: Is there a better way to handle subviews positioning?
Question two is very related, let's say I have a subclass of UIView including various others subviews. I declare it inside my interface, and i alloc/init it in my init/viewDidLoad method.
- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
{
...
menu = [[SNKSlidingMenu alloc] initWithFrame:CGRectMake(0, 0, self.view.frame.size.width, self.view.frame.size.height);
...
}
As we already know we now need to reposition it in viewWillAppear to get a more accurate reading
- (void)viewWillAppear:(BOOL)animated{
....
menu.frame = CGRectMake(0, 0, self.view.frame.size.width, self.view.frame.size.height);
....
}
The problem is that of course all the subviews needs to be repositioned as well. This is done by the layoutSubviews function that get called automatically, but we got the same problem: All the subviews need to be declared inside the interface of the SNKSlidingMenu class.. Is there a way around this?
Thanks.
If you are targetting iOS 5.0 or better you can use viewWillLayoutSubviews and viewDidLayoutSubviews to make changes.
As for your second question, if you need access to an instance variable in other method than init, you need to keep it around, I don't see a problem with it.
You can, however, try to use Auto Layouts and set up rules between the subviews so it's automatically laid out for you without the need to keep a reference.
viewDidLoad only gets called when your view is created, but lots of things can affect the frame's size, and it doesn't get called again when frame changes.
Instead:
create subviews in viewDidLoad
set their sizes in
viewWillLayoutSubviews.
See some additional discussion here for handling rotation: https://stackoverflow.com/a/16421170/1445366
viewWillLayoutSubviews and viewDidLayoutSubviews can resolve this problem.
But the two methed would be performed more times.
this is my code to get correct self.view.frame.
- (void)viewDidLoad {
[super viewDidLoad];
...
dispatch_async(dispatch_get_main_queue(), ^{
// init you view and set it`s frame. this can get correct frame.
...
}
...
}
This saved my life more than once (Swift 4):
override func viewDidLoad() {
super.viewDidLoad()
self.view.setNeedsLayout()
self.view.layoutIfNeeded()
}
Basically this forces the viewcontroller to correctly layout it's view and from there you can get the correct frames for all your subviews. This particularly helps when doing transition animations and your view controllers are using autolayout and interface builder.
From what I've noticed it looks like the initial frames are set to whatever your interface builder's default size class is set to. I normally edit using the iPhone XS size class so in viewDidLoad it seems that the view's width is always 375 regardless whether you are using an iPhone XR or not. This corrects itself before viewWillAppear though.
The above code will correct this issue and allow you to get the correct frames for your view / subviews before the view controller is rendered to the screen.