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*/] ]
}];
}