NSContainerView with multiple child view controllers in Xcode 6
I am trying to link two different view controllers in an Xcode 6 storyboard NSContainerView
so that they can switch conditionally. Unfortunately this tutorial won't help here as things seem to have changed in Xcode.
So, I have two different view controllers and one of them is loaded into the container's view by default, but I want the program to be able to load the second view controller into the container. Xcode 6 allows embeddable Segues to be created when I drag and drop them from one to the other so it doesn't help.
Can someone tell me how this is achieved with Xcode 6?
source to share
First, here's a sample GitHub project solution: click . I wasn't sure if you wanted to swap views or just push the 2nd view onto the proverbial stack, so I went with a push / pop program. If you want to swap places instead, you can do it quite easily by simply skipping the stack store.
Essentially, we have our NSViewController "host" which contains a Container View (CV) inside it. This host does not actually manually manage the view controller that is showing the CV at the moment. The way it is done is something like a nested view controller that then manages all the other view controllers that you are going to show / hide / push / pop / swap / etc. (Note: you might be able to remove the layers a bit, but in iOS terms, I treat the "Sub View Dispatcher" in the storyboard screenshot as a UINavigationController
).
We are also using some custom segues / segue animators to be able to do more work in the storyboard.
You just need to tell the content view manager's view manager to manage its children in such a way that the old views you want to put back are preserved (in this case, using NSMutableArray
) and such that the new views are eligible frame
or their constraints are set correctly.
Here is a screenshot of the storyboard: the segue you see on the custom type storyboard (looks like this:> {}) is of the type SegueBetweenEmbedded
in the sample project. The buttons that press the execute button and the buttons labeled "Pop" execute dismissController:
on NSViewController
(as was done in the storyboard).
Here's some code (and there's a lot of it out there, so I suggest looking at an example 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, won't regret 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
source to share