First, here's a sample GitHub project of the solution: click. I wasn't sure whether you wanted to swap the views or simply push the 2nd view onto a proverbial stack, so I went with a push/pop scheme. If you want to swap the views instead, you should be able to do that fairly easily by just skipping the stack storage.
Essentially, we have our "host" NSViewController that holds a Container View (CV) inside of it. This host doesn't actually manually manage the view controller that the CV is showing at the moment. The way this is done is through, well, a sort of nested view controller that then manages all the other view controllers that you're going to show/hide/push/pop/swap/etc. (Note: you might be able to remove the layering a bit, but in iOS terms, I'm treating the 'Sub View Controller Manager' in the storyboard screenshot sort of like a UINavigationController
).
We also take advantage of some custom segues/segue animators in order to be able to do more work in the storyboard.
You just have to tell the content view manager view controller to manipulate its subviews in such a way that the old views that you want to pop "back" to are retained (in this case, using an NSMutableArray
) and such that the new views have the right frame
or have their constraints set up properly.
Here is a screenshot of the storyboard:
Each segue you see on the storyboard of a custom type (looks like this -> { } ) is of type SegueBetweenEmbedded
in the sample project. Buttons that push perform a segue, and buttons labeled 'Pop' perform dismissController:
on the NSViewController
(so that was done in the storyboard).
Here's some code (and there's a lot of it, so I suggest looking at the sample project instead):
ViewController.h
#import <Cocoa/Cocoa.h>
#import "ContentManagerViewController.h"
@class ContentManagerViewController;
@protocol ContentManagerViewControllerHolder <NSObject>
-(ContentManagerViewController*)retreiveContentManagerController;
@end
@interface ViewController : NSViewController <ContentManagerViewControllerHolder>
@end
ViewController.m
#import "ViewController.h"
#import "ContentManagerViewController.h"
#import "BackForwardViewController.h"
@interface ViewController ()
@property ContentManagerViewController *vcController;
-(IBAction)pushViewController:(id)sender;
-(IBAction)popViewController:(id)sender;
-(IBAction)popToRootViewController:(id)sender;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
}
-(void)prepareForSegue:(NSStoryboardSegue *)segue sender:(id)sender {
if ([[segue destinationController] class] == [ContentManagerViewController class]) {
self.vcController = segue.destinationController;
}
}
-(ContentManagerViewController*)retreiveContentManagerController {
return self.vcController;
}
-(IBAction)pushViewController:(id)sender {
// note: this works, but then pop is broken via dismissController: since it wasn't done with a segue.
// Better way is to rig up a manual segue and execute the segue.
//BackForwardViewController *viewController = [[NSStoryboard storyboardWithName:@"Main" bundle:nil] instantiateControllerWithIdentifier:@"BackForwardStoryboardID"];
//[self.vcController push:viewController];
[self performSegueWithIdentifier:@"CustomSegueToBackForward" sender:self];
}
-(IBAction)popViewController:(id)sender {
[self.vcController pop];
}
-(IBAction)popToRootViewController:(id)sender {
[self.vcController popToRoot];
}
@end
SegueBetweenEmbedded.h
#import <Cocoa/Cocoa.h>
@interface SegueBetweenEmbedded : NSStoryboardSegue
@end
SegueBetweenEmbedded.m (sorry not sorry for the nested class)
#import "SegueBetweenEmbedded.h"
#import "ContentManagerViewController.h"
#import "ViewController.h"
@interface SegueAnimator : NSObject <NSViewControllerPresentationAnimator>
- (void)animatePresentationOfViewController:(NSViewController *)viewController fromViewController:(NSViewController *)fromViewController;
- (void)animateDismissalOfViewController:(NSViewController *)viewController fromViewController:(NSViewController *)fromViewController;
@end
@implementation SegueAnimator
- (void)animatePresentationOfViewController:(NSViewController *)viewController fromViewController:(NSViewController *)fromViewController {
NSViewController *parent = [fromViewController parentViewController];
if (parent && [parent class] == [ContentManagerViewController class]) {
ContentManagerViewController *manager = (ContentManagerViewController*)parent;
[manager push:viewController];
}
else if ([fromViewController conformsToProtocol:@protocol(ContentManagerViewControllerHolder)]) {
id<ContentManagerViewControllerHolder> holder = (id<ContentManagerViewControllerHolder>)fromViewController;
[[holder retreiveContentManagerController] push:viewController];
}
}
- (void)animateDismissalOfViewController:(NSViewController *)viewController fromViewController:(NSViewController *)fromViewController {
NSViewController *parent = [viewController parentViewController];
if ([parent class] == [ContentManagerViewController class]) {
ContentManagerViewController *manager = (ContentManagerViewController*)parent;
[manager pop];
}
}
@end
@implementation SegueBetweenEmbedded
- (void)perform {
SegueAnimator *animator = [[SegueAnimator alloc] init];
[self.sourceController presentViewController:self.destinationController
animator:(id<NSViewControllerPresentationAnimator>)animator];
}
@end
ContentManagerViewController.h
#import <Cocoa/Cocoa.h>
@interface ContentManagerViewController : NSViewController
-(void)push:(NSViewController*)viewController;
-(void)pop;
-(void)popToRoot;
@end
ContentManagerViewController.m
#import "ContentManagerViewController.h"
#import "BackForwardViewController.h"
@interface ContentManagerViewController ()
@property (weak) IBOutlet NSView *subViewControllerManager;
@property NSViewController *currentViewController;
@property NSMutableArray<NSViewController*> *viewControllerStack;
@end
@implementation ContentManagerViewController
-(instancetype)init {
self = [super init];
self.viewControllerStack = [NSMutableArray array];
return self;
}
-(instancetype)initWithCoder:(NSCoder *)coder {
self = [super initWithCoder:coder];
self.viewControllerStack = [NSMutableArray array];
return self;
}
-(instancetype)initWithNibName:(NSNibName)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil {
self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
self.viewControllerStack = [NSMutableArray array];
return self;
}
- (void)viewDidLoad {
[super viewDidLoad];
}
-(void)showViewController:(NSViewController*)viewController {
[self addChildViewController:viewController];
viewController.view.frame = self.currentViewController.view.frame;
[self.view addSubview:viewController.view];
self.currentViewController = viewController;
}
-(void)removeCurrentViewControllerFromView {
[self.currentViewController.view removeFromSuperview];
[self.currentViewController removeFromParentViewController];
}
-(void)push:(NSViewController*)viewController {
[self removeCurrentViewControllerFromView];
[self.viewControllerStack addObject:viewController];
[self showViewController:viewController];
}
-(void)pop {
if (self.viewControllerStack.count > 1) {
[self removeCurrentViewControllerFromView];
[self.viewControllerStack removeLastObject];
NSViewController *viewController = [self.viewControllerStack lastObject];
[self showViewController:viewController];
}
}
-(void)popToRoot {
while (self.viewControllerStack.count > 1) {
[self pop];
}
}
-(void)prepareForSegue:(NSStoryboardSegue *)segue sender:(id)sender {
// this will be called on the initial embed to set up the first view controller
self.currentViewController = segue.destinationController;
[self.viewControllerStack addObject:segue.destinationController];
}
@end
BackForwardViewController.h
#import <Cocoa/Cocoa.h>
@interface BackForwardViewController : NSViewController
@end
BackForwardViewController.m
#import "BackForwardViewController.h"
@interface BackForwardViewController ()
@end
@implementation BackForwardViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do view setup here.
}
@end