1
votes

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?

2
Have you tried on device? It works on iPhone5s simulator but not on iPhone 6s plus simulator. I found a warning log Unbalanced calls to begin/end appearance transitions for <UIViewController: 0x7faa186db7b0> but it shouldn't be. I don't have iPhone 6s plus so please test on the device first. It might be bug of simulator.Ryan
I have tried it on my iPhone 6 Plus and it gets the same result as the simulator. It seems to only effect devices that show more than 5 items when rotated.Josh Luongo
Have you found anything? I'm still working on this. So weird. It happens only iPhone 6+ and iPhone 6s+. I suspect size class but not sure.Ryan
Nothing as of yet. I also posted this over on the Apple Developer Forums. If I can't find a solution i will have to end up using one of my TSI's on this issue. When I find a solution I will update this thread.Josh Luongo
My best guess of the cause of this is the UITabBarController is pulling the view controllers out of the UINavigationController and placing them in the UIMoreNavigationController (Because UINavigationController's in UINavigationController's are a no no) and not returning them back to their original parent.Josh Luongo

2 Answers

2
votes

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

1
votes

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