Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I create a segue that will use a popover on iPad, and push onto the navigation stack on the iPhone?

In my app, there are certain view controllers where on iPad (or to be more specific, a regular horizontal size class) it makes sense to present them as popovers, but on iPhone (or a compact horizontal size class) it makes sense to push them onto the navigation stack. Is there an elegant way to support this? By default, if I use a "Present as Popover" segue, it will display modally on iPhone, which isn't what I want.

I've found a way to get the behavior I want, but it's ugly and seems error-prone. I choose between two different segues based on what size class I'm currently in. In order to support iOS 9 multitasking, I implement [UIViewController willTransitionToTraitCollection:withTransitionCoordinator] and manually move the view controller between a popover and the navigation controller (this part seems particularly error-prone).

It seems like there should be some simple way to implement either a custom segue to handle this, or some sort of custom adaptive presentation controller, but I haven't been able to wrap my head around it. Has anyone had success doing this?

like image 888
Chris Vasselli Avatar asked Aug 28 '15 12:08

Chris Vasselli


2 Answers

According to me this is simplest way,

Step 1: Create two segues from your one controller to another.
Step 2: Set one segue's segue property to push and popover of another
Step 3: Now call perform segue according to your requirements, i.e.iPad or iPhone

Here is a sample code

Sample code Note : Change bool condition to false to check another condition in didSelectRowAtIndexPath.

like image 108
Mohammad Zaid Pathan Avatar answered Oct 24 '22 22:10

Mohammad Zaid Pathan


Here's what I ended up building. I'm not super happy with it, which is why I haven't posted it until now. It won't support two segues going to view controllers with the same class, and it requires you to keep track of the source rect and source view for the popover yourself. But maybe it will be a good starting point for someone else.

PushPopoverSegue.swift

import UIKit

class PushPopoverSegue: UIStoryboardSegue {

    var sourceBarButtonItem: UIBarButtonItem!
    var permittedArrowDirections: UIPopoverArrowDirection = .Any

    override func perform() {
        assert( self.sourceViewController.navigationController != nil )
        assert( self.sourceBarButtonItem != nil )

        if self.sourceViewController.traitCollection.horizontalSizeClass == .Compact {
            self.sourceViewController.navigationController!.pushViewController(self.destinationViewController, animated: true)
        }
        else {
            let navigationController = UINavigationController(rootViewController: self.destinationViewController)
            let popover = UIPopoverController(contentViewController: navigationController)
            popover.presentPopoverFromBarButtonItem(self.sourceBarButtonItem, permittedArrowDirections: self.permittedArrowDirections, animated: true)
        }
    }

}

UIViewController+PushPopoverTransition.h

#import <UIKit/UIKit.h>

@interface UIViewController (PushPopoverTransition)

- (void) transitionPushPopoversToHorizontalSizeClass: (UIUserInterfaceSizeClass) sizeClass withMapping: (NSDictionary*) mapping;

@end

UIViewController+PushPopoverTransition.m

#import "UIViewController+PushPopoverTransition.h"

@implementation UIViewController (PushPopoverTransition)

- (void) transitionPushPopoversToHorizontalSizeClass: (UIUserInterfaceSizeClass) sizeClass withMapping: (NSDictionary*) mapping
{
    if ( sizeClass == UIUserInterfaceSizeClassCompact )
    {
        if ( self.presentedViewController == nil )
            return;

        NSParameterAssert( [self.presentedViewController isKindOfClass:[UINavigationController class]] );
        UINavigationController* navigationController = (UINavigationController*) self.presentedViewController;
        NSArray* viewControllers = navigationController.viewControllers;
        UIViewController* topOfStack = viewControllers[0];

        if ( [mapping.allKeys containsObject:NSStringFromClass( [topOfStack class] ) ] )
        {
            [self.presentedViewController dismissViewControllerAnimated:NO completion:^{
                for ( UIViewController* viewController in viewControllers )
                    [self.navigationController pushViewController:viewController animated:NO];
            }];
        }
    }
    else if ( sizeClass == UIUserInterfaceSizeClassRegular )
    {
        NSUInteger indexOfSelf = [self.navigationController.viewControllers indexOfObject:self];

        if ( indexOfSelf < self.navigationController.viewControllers.count  - 1 )
        {
            UIViewController* topOfStack = self.navigationController.viewControllers[indexOfSelf + 1];
            if ( [mapping.allKeys containsObject:NSStringFromClass( [topOfStack class] )] )
            {
                NSArray* poppedControllers = [self.navigationController popToViewController:self animated:NO];
                UINavigationController* navigationController = [[UINavigationController alloc] init];
                navigationController.modalPresentationStyle = UIModalPresentationPopover;
                navigationController.viewControllers = poppedControllers;

                id popoverSource = mapping[NSStringFromClass( [topOfStack class] )];
                if ( [popoverSource isKindOfClass:[UIBarButtonItem class]] )
                {
                    navigationController.popoverPresentationController.barButtonItem = popoverSource;
                }
                else if ( [popoverSource isKindOfClass:[NSArray class]] )
                {
                    NSArray* popoverSourceArray = (NSArray*) popoverSource;
                    NSParameterAssert(popoverSourceArray.count == 2);
                    UIView* sourceView = popoverSourceArray[0];
                    CGRect sourceRect = [(NSValue*) popoverSourceArray[1] CGRectValue];
                    navigationController.popoverPresentationController.sourceView = sourceView;
                    navigationController.popoverPresentationController.sourceRect = sourceRect;
                }

                [self presentViewController:navigationController animated:NO completion:nil];
            }
        }
    }
}

@end

Example Usage

Create a segue in interface builder, and set its "Kind" to Custom, and its "Class" to PushPopoverSegue.

ViewController.m

- (void) prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
    ((PushPopoverSegue*) segue).sourceView = /* source view */;
    ((PushPopoverSegue*) segue).sourceRect = /* source rect */;
}

-(void) willTransitionToTraitCollection:(UITraitCollection *)newCollection withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator
{
    if ( newCollection.horizontalSizeClass == UIUserInterfaceSizeClassUnspecified )
        return;

    [self transitionPushPopoversToHorizontalSizeClass:newCollection.horizontalSizeClass withMapping:@{
        @"MyDestinationViewController": @[ /* source view */,
                                       [NSValue valueWithCGRect:/* source rect*/] ]
    }];
}
like image 37
Chris Vasselli Avatar answered Oct 24 '22 23:10

Chris Vasselli