iosobjective-cuinavigationcontrolleruisplitviewcontrolleriphone-6-plus

UISplitViewController with nav stacks in both master and detail - how to remove a VC from one of the stacks?


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.


Solution

  • 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;
    }