4

I'm trying to link two different view controllers in an Xcode 6 storyboard with a NSContainerView so that they can be switched conditionally. Unfortunately this tutorial here isn't of any help since things seem to have changed since in Xcode.

So I have two different view controllers and one of them is loaded into the container view by default but I want to be able to load the second view controller into the container view programmatically. Xcode 6 only allows to create embed Segues when I drag from one to the other so that's not much of a help.

Can somebody tell me how this is achieved with Xcode 6?

BadmintonCat
  • 9,416
  • 14
  • 78
  • 129
  • I don't know if you need ContainerView for other reasons, but if you only want the dual ViewController functionality, you could approach it differently. Create your first 'ViewController's View in the Storyboard; assign it as the Custom Class; size whatever components you need, leaving space for the second ViewController. Then in the first VC's code, load the second VC in the space you've left. I can provide code snippets if this approach would help? – Slowburner May 12 '15 at 10:33

1 Answers1

6

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: enter image description here 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
Deadpikle
  • 356
  • 6
  • 22
  • 1
    I found it useful, thank you, however needed something similar in swift. If somebody needs it - here you go: https://github.com/Ruzard/ContainerSequencedView If you have offers how to improve it using swift techniques - I will be grateful. Pretty much rewrote line-by-line due to lack of time to optimize – Ruzard Mar 31 '17 at 13:47
  • I've checked your screenshot about storyboard. How does it possible to deal with zooming in storyboard of macOS project? I found that zooming/scaling is unavailable. I think it's bug. Is it? – devshok Apr 13 '18 at 08:43
  • @mcsh0k You can't zoom in a storyboard for a macOS project. I took a screenshot on my Mac of that portion of the storyboard and just posted it here. I don't quite understand if your comment is posting an issue with the proposed solution; could you please help me understand what might be a bug? – Deadpikle Apr 13 '18 at 13:05