I'm currently working on an update to one of my apps and I have come across a very strange issue to do with UITabBarController.
In my storyboard I have about 8 view controllers and in my UITabBarController subclass I add another 4 view controllers that are loaded programmatically. Most of these views need to have to UINavigationController to keep consistency when rotating as some views come out from the "More" tab into the main bar, in order to do this I have embeded them in a UINavigationController.
If you choose view 6 in portrait and the rotate the UINavigationController goes black when the view gets its own button in the tab bar, however when it returns to "more" the view comes back. In my investigation of these it seems that the UINavigationController losses the UIViewController as it root view controller.
Working as expected on a view that does not enter the "More" tab: imgur.com/gVB8wTF
Black screen if the view came from the "More" tab: http://imgur.com/WaoNoL1
I made a quick sample project that has this issue: https://github.com/joshluongo/UITabBarController-Issues
Any ideas on how to fix this?
I ran into the same issue.
I was able to come up with a workaround that works really well. I've pushed it up to Github here: https://github.com/jfahrenkrug/UITabBarControllerMoreBugWorkaround
Any improvements are welcome.
The bug happens because the stack of your UINavigationController is removed from it and put into the private UIMoreNavigationController. But upon rotating back to regular width, that stack is not correctly put back into its original UINavigationViewController.
The solution is to subclass UITabBarController and replacing its willTransitionToTraitCollection:withTransitionCoordinator: with this one:
- (void)willTransitionToTraitCollection:(UITraitCollection *)newCollection withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator
{
//#define MORE_TAB_DEBUG 1
#ifdef MORE_TAB_DEBUG
#define MoreTabDLog(fmt, ...) NSLog((#"[More Tab Debug] " fmt), ##__VA_ARGS__);
#else
#define MoreTabDLog(...)
#endif
MoreTabDLog(#"-- before willTransitionToTraitCollection");
/*
There is a bug when going in and out of the compact size class when a tab bar
controller has more than 5 tabs. See http://www.openradar.me/25393521
It comes down to this: When you have more than 5 tabs and a view controller on a tab
beyond the 4th tab is a UINavigationController, you have a problem.
When you are on this tab in compact and push one or more VCs onto the stack and then
change back to regular width, only the top most view controller will be added back onto the
stack.
This happens because the stack of your UINavigationController is taken out of that NavVC and put
into the private UIMoreNavigationController. But upon rotating back to regular, that stack is not
correctly put back into your own NavVC.
We have 3 cases we have to handle:
1) We are on the "More" tab in compact and are looking at the UIMoreListController and then change to
regular size.
2) While in compact width, we are on a tab greater than the 4th and are changing to regular width.
3) While in regular width, we are on a tab greater than the 4th and are changing to compact width.
*/
if ((self.traitCollection.horizontalSizeClass != newCollection.horizontalSizeClass) ||
(self.traitCollection.verticalSizeClass != newCollection.verticalSizeClass))
{
/*
Case 1: We are on the "More" tab in compact and are looking at the UIMoreListController and then change to regular size.
*/
if ([self.selectedViewController isKindOfClass:[UINavigationController class]] && [NSStringFromClass([self.selectedViewController class]) hasPrefix:#"UIMore"]) {
// We are on the root of the MoreViewController in compact, going into regular.
// That means we have to pop all the viewControllers in the MoreViewController to root
#ifdef MORE_TAB_DEBUG
UINavigationController *moreNavigationController = (UINavigationController *)self.selectedViewController;
UIViewController *moreRootViewController = [moreNavigationController topViewController];
MoreTabDLog(#"-- going OUT of compact while on UIMoreList");
MoreTabDLog(#"moreRootViewController: %#", moreRootViewController);
#endif
for (NSInteger overflowVCIndex = 4; overflowVCIndex < [self.viewControllers count]; overflowVCIndex++) {
if ([self.viewControllers[overflowVCIndex] isKindOfClass:[UINavigationController class]]) {
UINavigationController *navigationController = (UINavigationController *)self.viewControllers[overflowVCIndex];
MoreTabDLog(#"popping %# to root", navigationController);
[navigationController popToRootViewControllerAnimated:NO];
}
}
} else {
BOOL isPotentiallyInOverflow = [self.viewControllers indexOfObject:self.selectedViewController] >= 4;
MoreTabDLog(#"isPotentiallyInOverflow: %i", isPotentiallyInOverflow);
if (isPotentiallyInOverflow && [self.selectedViewController isKindOfClass:[UINavigationController class]]) {
UINavigationController *selectedNavController = (UINavigationController *)self.selectedViewController;
NSArray<UIViewController *> *selectedNavControllerStack = [selectedNavController viewControllers];
MoreTabDLog(#"Selected Nav: %#, selectedNavStack: %#", selectedNavController, selectedNavControllerStack);
UIViewController *lastChildVCOfTabBar = [[self childViewControllers] lastObject];
if ([lastChildVCOfTabBar isKindOfClass:[UINavigationController class]] && [NSStringFromClass([lastChildVCOfTabBar class]) hasPrefix:#"UIMore"]) {
/*
Case 2: While in compact width, we are on a tab greater than the 4th and are changing to regular width.
We are going OUT of compact
*/
UINavigationController *moreNavigationController = (UINavigationController *)lastChildVCOfTabBar;
NSArray *moreNavigationControllerStack = [moreNavigationController viewControllers];
MoreTabDLog(#"--- going OUT of compact");
MoreTabDLog(#"moreNav: %#, moreNavStack: %#, targetNavStack: %#", moreNavigationController, moreNavigationControllerStack, selectedNavControllerStack);
if ([moreNavigationControllerStack count] > 1) {
NSArray *fixedTargetStack = [moreNavigationControllerStack subarrayWithRange:NSMakeRange(1, moreNavigationControllerStack.count - 1)];
MoreTabDLog(#"fixedTargetStack: %#", fixedTargetStack);
dispatch_async(dispatch_get_main_queue(), ^{
NSArray *correctVCList = [NSArray arrayWithArray:self.viewControllers];
[selectedNavController willMoveToParentViewController:self];
[selectedNavController setViewControllers:fixedTargetStack animated:NO];
// We need to do this because without it, the selectedNavController doesn't
// have a parentViewController anymore.
[self addChildViewController:selectedNavController];
// We need to do this because otherwise the previous call will cause the given
// Tab to show up twice in the UIMoreListController.
[self setViewControllers:correctVCList];
});
} else {
MoreTabDLog(#"popping to root");
dispatch_async(dispatch_get_main_queue(), ^{
[selectedNavController popToRootViewControllerAnimated:NO];
});
}
} else {
/*
Case 3: While in regular width, we are on a tab greater than the 4th and are changing to compact width.
We are going INTO compact
*/
MoreTabDLog(#"-- going INTO compact");
if ([selectedNavControllerStack count] > 0) {
[coordinator animateAlongsideTransition:^(id<UIViewControllerTransitionCoordinatorContext> _Nonnull context) {
// no op
} completion:^(id<UIViewControllerTransitionCoordinatorContext> _Nonnull context) {
UIViewController *parentViewControllerOfTopVC = [[selectedNavControllerStack lastObject] parentViewController];
MoreTabDLog(#"parentViewControllerOfTopVC: %#", parentViewControllerOfTopVC);
if ([parentViewControllerOfTopVC isKindOfClass:[UINavigationController class]] && [NSStringFromClass([parentViewControllerOfTopVC class]) hasPrefix:#"UIMore"]) {
UINavigationController *moreNavigationController = (UINavigationController *)parentViewControllerOfTopVC;
NSArray *moreNavigationControllerStack = [moreNavigationController viewControllers];
BOOL isOriginalRootVCInMoreStack = [moreNavigationControllerStack containsObject:[selectedNavControllerStack firstObject]];
MoreTabDLog(#"moreNav: %#, moreNavStack: %#, isOriginalRootVCInMoreStack: %i", moreNavigationController, moreNavigationControllerStack, isOriginalRootVCInMoreStack);
if (!isOriginalRootVCInMoreStack) {
NSArray *fixedMoreStack = [#[moreNavigationControllerStack[0]] arrayByAddingObjectsFromArray:selectedNavControllerStack];
MoreTabDLog(#"fixedMoreStack: %#", fixedMoreStack);
[selectedNavController setViewControllers:selectedNavControllerStack animated:NO];
dispatch_async(dispatch_get_main_queue(), ^{
[moreNavigationController setViewControllers:fixedMoreStack animated:NO];
});
}
}
}];
}
}
}
}
}
[super willTransitionToTraitCollection:newCollection withTransitionCoordinator:coordinator];
MoreTabDLog(#"-- after willTransitionToTraitCollection");
}
Enjoy!
Johannes
I have found a workaround that seems to get around this issue.
By overriding the UITraitCollection in a UITabBarController subclass you can force the horizontalSizeClass to always be UIUserInterfaceSizeClassCompact. This will make the UITabBar only ever have 5 items regardless of the orientation.
Here some sample Objective-C code:
- (UITraitCollection *)traitCollection {
if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPhone) {
// Workaround to fix the iPhone 6 Plus roatation issue.
UITraitCollection *curr = [super traitCollection];
UITraitCollection *compact = [UITraitCollection traitCollectionWithHorizontalSizeClass:UIUserInterfaceSizeClassCompact];
return [UITraitCollection traitCollectionWithTraitsFromCollections:#[curr, compact]];
}
return [super traitCollection];
}
Then if you need access to real traits then override -traitCollection in your UIViewController to return the traits from [UIScreen mainScreen].
Here some example Objective-C code to do that:
- (UITraitCollection *)traitCollection {
return [UIScreen mainScreen].traitCollection;
}
This not an ideal solution but until Apple decides to fix this bug, this will do the job.
I hope this helps someone.
rdar://21297168
Related
I am debugging legacy code which is always fun. The old code tried to mock the splitView delegate methods, causing all sorts of issues - mainly crashing: on a Plus device in Portrait, rotating to landscape caused the crash - if there was no detail view set, old code attempted to create one in a dodgy hack and it was just useless...
My app is UISplitViewController based, where I have a navigation stack in both master and detail sides of the splitView.
By reading though SO and using this example and was able to implement UISplitViewController delegate methods and everything is working correctly in regards to rotation, and showing the correct master/detail views when appropriate. Here is my implementation: (apologies for wall of code snippets)
- (BOOL)splitViewController:(UISplitViewController *)splitViewController collapseSecondaryViewController:(UIViewController *)secondaryViewController ontoPrimaryViewController:(UIViewController *)primaryViewController {
if ([secondaryViewController isKindOfClass:[UINavigationController class]]
&& [[(UINavigationController *)secondaryViewController topViewController] isKindOfClass:[AECourseHTMLTableViewController class]]
&& ([(AECourseHTMLTableViewController *)[(UINavigationController *)secondaryViewController topViewController] htmlContentEntry] == nil)) {
// If the detail controller doesn't have an item, display the primary view controller instead
return YES;
}
return NO;
}
And the other splitView delegate method - see comments in code for where I'm stuck.
- (UIViewController *)splitViewController:(UISplitViewController *)splitViewController separateSecondaryViewControllerFromPrimaryViewController:(UIViewController *)primaryViewController {
// If detail view already exists
if ([primaryViewController isKindOfClass:[UINavigationController class]]) {
for (UIViewController *controller in [(UINavigationController *)primaryViewController viewControllers]) {
if ([controller isKindOfClass:[UINavigationController class]] && [[(UINavigationController *)controller visibleViewController] isKindOfClass:[AECourseHTMLTableViewController class]]) {
return controller;
}
}
}
// Create detail view
UINavigationController *navController = [self.storyboard instantiateViewControllerWithIdentifier:#"CourseHTMLNav"];
if ([navController.viewControllers.firstObject isKindOfClass:[AECourseHTMLTableViewController class]]) {
AECourseHTMLTableViewController *courseViewController = navController.viewControllers.firstObject;
[self configureViewController:courseViewController entry:self.contentSection.sections[0] indexPath:courseViewController.currentIndexPath];
}
// Enable back button
UIViewController *controller = [navController visibleViewController];
controller.navigationItem.leftBarButtonItem = self.splitViewController.displayModeButtonItem;
controller.navigationItem.leftItemsSupplementBackButton = YES;
if (!self.splitViewController.isCollapsed) {
UINavigationController *navController = self.splitViewController.viewControllers.firstObject;
AEContentMenuTableViewController *contentMenuVC = navController.viewControllers.firstObject; // This controller needs to be master in Landscape
NSMutableArray<UIViewController *> *controllers = [navController.viewControllers mutableCopy]; // Contains 3 controllers, first needs removed
NSMutableArray *toDelete = [NSMutableArray new];
for (UIViewController *viewController in controllers)
if ([viewController isKindOfClass:[contentMenuVC class]] || [viewController isKindOfClass:[AECourseHTMLTableViewController class]]) {
[toDelete addObject:viewController]; // Remove first VC, so master should become AEContentMenuVC?
break;
}
// Remove the object
[controllers removeObjectsInArray:toDelete];
// Set viewControllers
navController.viewControllers = controllers;
}
return navController;
}
AECourseHTMLTableViewController has next/prev buttons to select the next row in the tableview of the tableview menu class class (AEContentMenuTableViewController). I have a delegate function which can tell me the current indexPath in which AECourseHTML... is using from AEContentMenu..., and when calling it, it selects the menu tableview row and instantiates a new AECourseHTML... and pushes it.
This is where I'm stuck. In Portrait, pressing next/prev is fine, it selects the correct row and works as expected. But once I rotate the device, both master and detail views show the detail view. I can press "Back" on the master view, and it takes me to the correct AEContentMenu... class. As noted in the code snippet comments, I need to remove a ViewController from the master stack (the first object actually), and AEContentMenu... should become the first object of that stack - so when rotating, that should be the master view.
Apologies for such a long post, I've been banging my head with this for weeks now and I want to include as much info as possible in this question. Thanks in advance.
I found a solution which works well for my use cases. It may not be the cleanest code, but I'm happy with what I've got.
splitViewController:collapseSecondaryViewController:ontoPrimaryViewController:
remains unchanged. I have updated my splitViewController:separateSecondaryViewControllerFromPrimaryViewController: delegate method with the solution. Any feedback is welcome.
- (UIViewController *)splitViewController:(UISplitViewController *)splitViewController separateSecondaryViewControllerFromPrimaryViewController:(UIViewController *)primaryViewController {
// If detail view already exists
if ([primaryViewController isKindOfClass:[UINavigationController class]]) {
for (UIViewController *controller in [(UINavigationController *)primaryViewController viewControllers]) {
if ([controller isKindOfClass:[UINavigationController class]] && [[(UINavigationController *)controller visibleViewController] isKindOfClass:[AECourseHTMLTableViewController class]]) {
return controller;
}
}
}
// Return CourseVC
UINavigationController *navController = splitViewController.viewControllers.firstObject;
UIViewController *viewController;
for (viewController in navController.viewControllers) {
if ([navController.viewControllers.lastObject isKindOfClass:[AECourseHTMLTableViewController class]]) {
return viewController;
} else {
// Create detail view
UINavigationController *navController = [self.storyboard instantiateViewControllerWithIdentifier:#"CourseHTMLNav"];
if ([navController.viewControllers.firstObject isKindOfClass:[AECourseHTMLTableViewController class]]) {
// Enable back button
UIViewController *controller = [navController visibleViewController];
controller.navigationItem.leftBarButtonItem = self.splitViewController.displayModeButtonItem;
controller.navigationItem.leftItemsSupplementBackButton = YES;
AECourseHTMLTableViewController *courseViewController = navController.viewControllers.firstObject;
// If next/prev has been tapped, configure current ContentHTML
if (self.currentContentHTML) {
[self configureViewController:courseViewController entry:self.currentContentHTML indexPath:courseViewController.currentIndexPath];
} else {
// Create new ContentHTML from first row of AEContentMenuVC
[self configureViewController:courseViewController entry:self.contentSection.sections[0] indexPath:[NSIndexPath indexPathForRow:0 inSection:0]];
}
return navController;
}
}
}
return navController;
}
Your top if statement should return nil. Since you were returning the nested navigation controller you were missing out on the default behaviour of popping the master navigation's top controller which is required so it can then be placed on the right.
The default behaviour will find that nested nav controller and pop it. However the reason you still need to search for it yourself is if it isn't there then you need to load the detail nav from the storyboard as you have done.
This is related to another question of mine, iOS 8 + interactive transition + nav bar shown = broken?, but is different.
On iOS 8, when one is doing an interactive transition from view A to view B via the NavigationControllerDelegate / UIViewControllerInteractiveTransitioning method, and view A has a navbar, and view B does NOT, then what is the correct method to hide / unhide the nav bar?
I tried to do this in the ViewController like this:
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
[[self transitionCoordinator] animateAlongsideTransition:^(id<UIViewControllerTransitionCoordinatorContext> context) {
if (self.navigationController) {
[self.navigationController setNavigationBarHidden:YES animated:animated];
}
} completion:^(id<UIViewControllerTransitionCoordinatorContext> context) {
NSArray *debugViews = [context containerView].subviews;
NSLog(#"%#", debugViews);
if ([context isCancelled] ) {
if( self.navigationController ) {
[self.navigationController setNavigationBarHidden:NO animated:animated];
}
}
}];
}
- (void)viewWillDisappear:(BOOL)animated {
[[self transitionCoordinator] animateAlongsideTransition:^(id<UIViewControllerTransitionCoordinatorContext> context) {
if (self.navigationController) {
[self.navigationController setNavigationBarHidden:NO animated:animated];
}
} completion:^(id<UIViewControllerTransitionCoordinatorContext> context) {
if ([context isCancelled] ) {
if( self.navigationController ) {
[self.navigationController setNavigationBarHidden:YES animated:animated];
}
}
}];
[super viewWillDisappear:animated];
}
... but there are two big problems:
The view (mainly the navbar) "flickers" sometimes when animation is completing. This is really ugly if you have a complex view underneath.
If the user cancels the interactive transition (ie. by not dragging far enough or pinching enough) then the navbar goes away forever, even though I can see in the code that it's being told to unhide.
Here is some soure-code to show this happening: https://github.com/xaphod/UIViewControllerTransitionTut
--> un-pinch to go from one view controller to another; the first view has a nav bar, the second one does not. When you complete the transition, you can sometimes see flickering (problem 1 above). When you un-pinch just a little bit and let go, that's a cancelled transition: although you're still on view 1, the navbar has disappeared (problem 2 above).
The right way to hide the navigation bar will be to use Navigation's controller delegate,make sure you set the window's navigation controller delegate to self before using the following delegate method:-
Just do this in the AppDelegate.m
- (BOOL)application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
self.window.rootViewController.navigationController.delegate=self;
//do your rest code...
}
-(void)navigationController:(UINavigationController *)navController
willShowViewController:(UIViewController *)viewController
animated:(BOOL)animated
{
[navController setNavigationBarHidden:([viewController isKindOfClass:[CustomViewController class]])
animated:animated]; // just mention the view controller class type for which you want to hide
}
Referred from this SFO's
If you wants to hide navigation bar in particular viewcontroller you can use this method in wilAppear.
//Unhide
-(void)viewWillAppear:(BOOL)animated
{
self.navigationController.navigationBarHidden = NO;
}
//Hide
-(void)viewWillAppear:(BOOL)animated
{
self.navigationController.navigationBarHidden = YES;
}
I have used the following code for deleting my login page from the navigationcontroller(viewcontrollers) so that it will not come into the view again when going back (back button).
if(UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPhone)
{
NSMutableArray *VCs = [NSMutableArray arrayWithArray:self.navigationController.viewControllers];
if([[VCs objectAtIndex:[VCs count] - 2] isKindOfClass:[loginViewController class]]&&(VCs.count>=4))
{
[VCs removeObjectAtIndex:[VCs count] - 2];
[VCs removeObjectAtIndex:[VCs count] - 2];
[self.navigationController setViewControllers: VCs];
}
}
This works perfectly for iPhone. But for iPad, since we are using splitViewController, if we code like
NSMutableArray *VCs = [NSMutableArray arrayWithArray:self.splitViewController.viewControllers];
What we will be getting is an array of navigationControllers. Is there a genuine logic by which we can delete a particular viewcontroller from the splitviewcontroller?
Your split view controller, as you said, will return an array of nav controllers (depending on the project setup). Once you have a reference to those, you can manipulate them however you want.
UISplitViewController *splitViewController = (UISplitViewController *)self.window.rootViewController;
UINavigationController *masterNavVC = (UINavigationController *)splitViewController.viewControllers.firstObject;
UINavigationController *detailNavVC = (UINavigationController *)splitViewController.viewControllers.lastObject;
//Now you have the master and detail navigation controllers, get your VC you need to manipulate
NSMutableArray *masterVCs = masterNavVC.viewControllers;
NSMutableArray *detailVCs = detailNavVC.viewControllers;
//Remove the ones you need to - this example is arbitrary. Put your logic here
if(masterVCs.count > 0 && [masterVCs[0] isKindOfClass:[LoginViewController class]])
{
//Remove or add
}
I am using a split view controller in a simple app. Leaving everything as default works fine. In other words, the master view controller always shows in landscape and overlays the detail view controller in portrait when the back button is pressed.
What I wanted to do was make the master view controller mimic the same functionality in landscape as it does in portrait. In other words, when the device is in landscape, I want the master view controller to be hidden until I hit the back button and then I want it to overlay the detail view controller.
I figured the best way to do this was to use the following code:
- (BOOL)splitViewController:(UISplitViewController *)svc shouldHideViewController: (UIViewController *)vc inOrientation:(UIInterfaceOrientation)orientation
{
return self.bHideMaster;
}
This worked in that it hid the master view controller in landscape mode. I then used the following code to make it reappear:
- (void)hideUnhidePagesController:(id)sender
{
[UIView beginAnimations:nil context:NULL];
[UIView setAnimationDuration:0.30f];
UISplitViewController* spv = self.splitViewController;
// Change hide to unhide or vica versa
self.bHideMaster= !self.bHideMaster;
// Hide the button if master is visible
if(self.bHideMaster)
{
self.navigationItem.leftBarButtonItem = self.pagesBarButton;
}
else
{
self.navigationItem.leftBarButtonItem = nil;
}
[spv.view setNeedsLayout];
[spv willRotateToInterfaceOrientation:self.interfaceOrientation duration:0];
[[self.splitViewController.viewControllers lastObject] view].frame = self.splitViewController.view.frame;
[UIView commitAnimations];
}
This ALMOST worked. I have 2 problems:
The transition from hide-to-unhide and unhide-to-hide the master view controller isn't animated and is much to stark. I added animation code (see above) but it only animates the detail view controller and not the master view controller. The master appears and dis-appears instantly (leaving a black box on disappear) until the detail view controller slides over.
This also shows my second problem. I want the master view controller to overlap the detail view controller when it appears in landscape mode, leaving the detail view controller as is. Instead, it resizes the detail view controller (the same way it does in landscape mode before I started). I want the master view controller to interact the same way it does in portrait mode: Master slides in over the top of the detail controller and slides back out when it an item is selected.
If I could solve problem 2, then I don't have to worry about problem 1. It seems like there should be a method in the split view controller that would slide in the master from the left (overlapping the detail view controller). It does it in portrait mode so the code must be there. How can I call that same code in landscape mode?
Thanks!
---------EDIT 1---------
I have refactored hideUnhidePagesController and am getting closer. The window now overlays in both portrait and landscape. There is still a problem if the master is visible on rotation. It gets confused and inverts the expected behavior. I'm working on it. Here the amended code:
- (void)hideUnhidePagesController:(id)sender
{
// Change hide to unhide or vica versa
self.bMasterIsHidden= !self.bMasterIsHidden;
NSArray *controllers = self.splitViewController.viewControllers;
UIViewController *rootViewController = [controllers objectAtIndex:0];
UIView *rootView = rootViewController.view;
CGRect rootFrame = rootView.frame;
if(self.bMasterIsHidden)
{
rootFrame.origin.x -= rootFrame.size.width;
}
else
{
rootFrame.origin.x += rootFrame.size.width;
}
[UIView beginAnimations:#"hideUnhideView" context:NULL];
rootView.frame = rootFrame;
[UIView commitAnimations];
}
In ios 8.0
self.splitViewController.preferredDisplayMode = UISplitViewControllerDisplayModePrimaryHidden;
to hide master view
To get the effect you describe I had to add the following code to my DetailViewController.
- (BOOL)splitViewController:(UISplitViewController *)svc shouldHideViewController:
(UIViewController *)vc inOrientation:(UIInterfaceOrientation)orientation
{
return YES;
}
Then my split view works the same in portrait and landscape mode.
I'm putting in the code that I ended up using. Hope this helps someone else.
// ***************************************************************************************************
//
// hideUnhideMasterViewControllerButtonPressed
//
// ***************************************************************************************************
- (void)hideUnhideMasterViewControllerButtonPressed:(id)sender {
if([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPhone) {
[self.navigationController popViewControllerAnimated:YES];
}
else {
if(bMasterIsHidden)
[self hideMasterViewController:NO];
else
[self hideMasterViewController:YES];
}
}
// ***************************************************************************************************
//
// hideMasterViewController
//
// ***************************************************************************************************
- (void)hideMasterViewController:(BOOL)bHideMaster {
// Change hide to unhide or vica versa
self.bMasterIsHidden= !self.bMasterIsHidden;
NSArray *controllers = self.splitViewController.viewControllers;
UIViewController *rootViewController = [controllers objectAtIndex:0];
UIView *rootView = rootViewController.view;
CGRect rootFrame = rootView.frame;
if(bHideMaster) {
if(self.tapRecognizer) {
rootFrame.origin.x -= rootFrame.size.width;
[self.view removeGestureRecognizer:self.tapRecognizer];
self.tapRecognizer = nil;
}
}
else {
rootFrame.origin.x += rootFrame.size.width;
self.tapRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(tapRecognized:)];
self.tapRecognizer.numberOfTapsRequired = 1;
[self.view addGestureRecognizer:self.tapRecognizer];
self.tapRecognizer.delegate = self;
}
// Log resulting frame
NSString *hiddenString = self.bMasterIsHidden ? #"YES" : #"NO";
NSLog(#"Page=%# Class=%# MasterIsHidden=%# Origin(x,y)=(%f, %f) Size(width,height)=(%f, %f)", self.pageDefinition.pageName, [self class], hiddenString, rootFrame.origin.x, rootFrame.origin.y, rootFrame.size.width, rootFrame.size.height);
[UIView beginAnimations:#"hideUnhideView" context:NULL];
rootView.frame = rootFrame;
[UIView commitAnimations];
}
Maybe I am too late to answer this but... here is the solution..
You can get the reference of your masterviewcontroller from the method in every orientation change
-(BOOL)splitViewController:(UISplitViewController *)svc shouldHideViewController:(UIViewController *)vc inOrientation:(UIInterfaceOrientation)orientation {
myVCForPopOverController = vc;
//always hide the controller
return YES;
}
now you can show this "myVCForPopOverController" from any barbutton items click.
-(void)onBarButtonClick:(id)sender {
if(!self.popOverController.popoverVisible) {
self.popOverController = [[UIPopoverController alloc]initWithContentViewController:myVCForPopOverController];
[self.popOverController presentPopoverFromBarButtonItem:showDetailsBarButton permittedArrowDirections:UIPopoverArrowDirectionUp animated:YES];
self.popOverController.passthroughViews = nil;
}
else {
[self.popOverController dismissPopoverAnimated:YES];
}
}
I have implemented this and it works.
I'm using UIPageViewController and story board in my application.
When in portrait, i'm using following code to jump from one page to another page and its working fine.
int direction = UIPageViewControllerNavigationDirectionForward;
if ([self.modelController currentPage] < pagenum)
{
direction = UIPageViewControllerNavigationDirectionForward;
}
else if ([self.modelController currentPage] > pagenum)
{
direction = UIPageViewControllerNavigationDirectionReverse;
}
[self.pageViewController setViewControllers:[NSArray arrayWithObject:[self.modelController viewControllerAtIndex:pagenum storyboard:self.storyboard]] direction:direction animated:YES completion:NULL];
But the same code is not working when we are in landscape mode. How to turn the pages when in landscape?
If you look at the Page-Based Application Template in Xcode you find the following UIPageViewControllerDelegate method:
- (UIPageViewControllerSpineLocation)pageViewController:(UIPageViewController *)pageViewController spineLocationForInterfaceOrientation:(UIInterfaceOrientation)orientation
{
if (UIInterfaceOrientationIsPortrait(orientation)) {
// In portrait orientation: Set the spine position to "min" and the page view controller's view controllers array to contain just one view controller. Setting the spine position to 'UIPageViewControllerSpineLocationMid' in landscape orientation sets the doubleSided property to YES, so set it to NO here.
UIViewController *currentViewController = [self.pageViewController.viewControllers objectAtIndex:0];
NSArray *viewControllers = [NSArray arrayWithObject:currentViewController];
[self.pageViewController setViewControllers:viewControllers direction:UIPageViewControllerNavigationDirectionForward animated:YES completion:NULL];
self.pageViewController.doubleSided = NO;
return UIPageViewControllerSpineLocationMin;
}
// In landscape orientation: Set set the spine location to "mid" and the page view controller's view controllers array to contain two view controllers. If the current page is even, set it to contain the current and next view controllers; if it is odd, set the array to contain the previous and current view controllers.
DataViewController *currentViewController = [self.pageViewController.viewControllers objectAtIndex:0];
NSArray *viewControllers = nil;
NSUInteger indexOfCurrentViewController = [self.modelController indexOfViewController:currentViewController];
if (indexOfCurrentViewController == 0 || indexOfCurrentViewController % 2 == 0) {
UIViewController *nextViewController = [self.modelController pageViewController:self.pageViewController viewControllerAfterViewController:currentViewController];
viewControllers = [NSArray arrayWithObjects:currentViewController, nextViewController, nil];
} else {
UIViewController *previousViewController = [self.modelController pageViewController:self.pageViewController viewControllerBeforeViewController:currentViewController];
viewControllers = [NSArray arrayWithObjects:previousViewController, currentViewController, nil];
}
[self.pageViewController setViewControllers:viewControllers direction:UIPageViewControllerNavigationDirectionForward animated:YES completion:NULL];
return UIPageViewControllerSpineLocationMid;
}
When returning UIPageViewControllerSpineLocationMid from the above method, you are telling the UIPageViewController that it should expect to display two UIViewControllers side by side (i.e. two pages).
The key here is that you must pass the correct number of UIViewControllers when calling the UIPageViewController's setViewControllers:direction:animated:completion: method.
Showing two pages? Pass two UIViewControllers.
Showing one page? Pass one UIViewController.
The code you presented will never pass more than one UIViewController to the UIPageViewController.
If the UIPageViewControllerSpineLocation does not correspond with the amount of UIViewControllers you are passing it will crash or just do nothing.
Let me know if you need further help.