I use storyboard in a OS X cocoa application project with a SplitView controller and 2 others view controller LeftViewController and RightViewController.
In the LeftViewController i have a tableView that display an array of name. The datasource and delegate of the tableview is the LeftViewController.
In the RightViewController i just have a centered label that display the select name. I want to display in the right view the name selected in the left view.
To configure the communication between the 2 views controllers i use the AppDelegate and i define 2 property for each controller in AppDelegate.h
The 2 property are initialized in the viewDidLoad of view controller using the NSInvocation bellow :
@implementation RightViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
id delg = [[NSApplication sharedApplication] delegate];
SEL sel1 = NSSelectorFromString(@"setRightViewController:");
NSMethodSignature * mySignature1 = [delg methodSignatureForSelector:sel1];
NSInvocation * myInvocation1 = [NSInvocation
invocationWithMethodSignature:mySignature1];
id me = self;
[myInvocation1 setTarget:delg];
[myInvocation1 setSelector:sel1];
[myInvocation1 setArgument:&me atIndex:2];
[myInvocation1 invoke];
}
I have the same in LeftViewController.
Then if i click on a name in the table view, i send a message to the delegate with the name in parameter and the delegate update the label of the RightViewController with the given name. It works fine but according to apple best practice it’s not good.
Is there another way to communicate between 2 view controller inside a storyboard ?
I've already read a lot of post but found nothing for OS X.
You can download the simple project here : http://we.tl/4rAl9HHIf1
This is more advanced topic of app architecture (how to pass data).
- Dirty quick solution: post
NSNotification
together with forgotten representedObject
:
All NSViewControllers have a nice property of type id called representedObject
. This is one of the ways how to pass data onto NSViewController
. Bind your label to this property. For this simple example we will set representedObject
some NSString instance
. You can use complex object structure as well. Someone can explain in comments why storyboards stopped to show representedObject
(Type safety in swift?)
Next we add notification observer and set represented object in handler.
@implementation RightViewController
- (void)viewDidLoad
{
[super viewDidLoad];
[[NSNotificationCenter defaultCenter] addObserverForName:@"SelectionDidChange" object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification * _Nonnull note) {
//[note object] contains our NSString instance
[self setRepresentedObject:[note object]];
}];
}
@end
Left view controller and its table:
Once selection changes we post a notification with our string.
@interface RightViewController () <NSTableViewDelegate, NSTableViewDataSource>
@end
@implementation RightViewController
- (NSInteger)numberOfRowsInTableView:(NSTableView *)tableView
{
return [[self names] count];
}
- (nullable id)tableView:(NSTableView *)tableView objectValueForTableColumn:(nullable NSTableColumn *)tableColumn row:(NSInteger)row
{
return [self names][row];
}
- (NSArray<NSString *>*)names
{
return @[@"Cony", @"Brown", @"James", @"Mark", @"Kris"];
}
- (void)tableViewSelectionDidChange:(NSNotification *)notification
{
NSTableView *tableView = [notification object];
NSInteger selectedRow = [tableView selectedRow];
if (selectedRow >= 0) {
NSString *name = [self names][selectedRow];
if (name) {
[[NSNotificationCenter defaultCenter] postNotificationName:@"SelectionDidChange" object:name];
}
}
}
PS: don't forget to hook tableview datasource and delegate in storyboard
Why is this solution dirty? Because once your app grows you will end up in notification hell. Also view controller as data owner? I prefer window controller/appdelegate to be Model owner.
Result:
- AppDelegate as Model owner.
Our left view controller will get it's data from AppDelegate
. It is important that AppDelegate
controls the data flow and sets the data (not the view controller asking AppDelegate
it's table content cause you will end up in data synchronization mess). We can do this again using representedObject
. Once it's set we reload our table (there are more advanced solutions like NSArrayController and bindings). Don't forget to hook tableView in storyboard. We also modify tableview's delegate methos the tableViewSelectionDidChange
to modify our model object (AppDelegate.selectedName)
#import "LeftViewController.h"
#import "AppDelegate.h"
@interface LeftViewController () <NSTableViewDelegate, NSTableViewDataSource>
@property (weak) IBOutlet NSTableView *tableView;
@end
@implementation LeftViewController
- (NSInteger)numberOfRowsInTableView:(NSTableView *)tableView
{
return [[self representedObject] count];
}
- (nullable id)tableView:(NSTableView *)tableView objectValueForTableColumn:(nullable NSTableColumn *)tableColumn row:(NSInteger)row
{
return [self representedObject][row];
}
- (void)setRepresentedObject:(id)representedObject
{
[super setRepresentedObject:representedObject];
//we need to reload table contents once
[[self tableView] reloadData];
}
- (void)tableViewSelectionDidChange:(NSNotification *)notification
{
NSTableView *tableView = [notification object];
NSInteger selectedRow = [tableView selectedRow];
if (selectedRow >= 0) {
NSString *name = [self representedObject][selectedRow];
[(AppDelegate *)[NSApp delegate] setSelectedName:name];
} else {
[(AppDelegate *)[NSApp delegate] setSelectedName:nil];
}
}
In RightViewController
we delete all code. Why? Cause we will use binding AppDelegate.selectedName <-->
RightViewController.representedObject
@implementation RightViewController
@end
Finally AppDelegate
. It needs to expose some properties. What is interesting is how do I get my hands on all my controllers? One way (best) is to instantiate our own window controller and remember it as property. The other way is to ask NSApp for it's windows (be careful here with multiwindow app). From there we just ask contentViewController and loop through childViewControllers. Once we have our controllers we just set/bind represented objects.
@interface AppDelegate : NSObject <NSApplicationDelegate>
@property (nonatomic) NSString *selectedName;
@property (nonatomic) NSMutableArray <NSString *>*names;
@end
#import "AppDelegate.h"
#import "RightViewController.h"
#import "LeftViewController.h"
@interface AppDelegate () {
}
@property (weak, nonatomic) RightViewController *rightSplitViewController;
@property (weak, nonatomic) LeftViewController *leftSplitViewController;
@property (strong, nonatomic) NSWindowController *windowController;
@end
@implementation AppDelegate
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
_names = [@[@"Cony", @"Brown", @"James", @"Mark", @"Kris"] mutableCopy];
_selectedName = nil;
NSStoryboard *storyboard = [NSStoryboard storyboardWithName:@"Main"
bundle:[NSBundle mainBundle]];
NSWindowController *windowController = [storyboard instantiateControllerWithIdentifier:@"windowWC"];
[self setWindowController:windowController];
[[self windowController] showWindow:nil];
[[self leftSplitViewController] setRepresentedObject:[self names]];
[[self rightSplitViewController] bind:@"representedObject" toObject:self withKeyPath:@"selectedName" options:nil];
}
- (RightViewController *)rightSplitViewController
{
if (!_rightSplitViewController) {
NSArray<NSViewController *>*vcs = [[[self window] contentViewController] childViewControllers];
for (NSViewController *vc in vcs) {
if ([vc isKindOfClass:[RightViewController class]]) {
_rightSplitViewController = (RightViewController *)vc;
break;
}
}
}
return _rightSplitViewController;
}
- (LeftViewController *)leftSplitViewController
{
if (!_leftSplitViewController) {
NSArray<NSViewController *>*vcs = [[[self window] contentViewController] childViewControllers];
for (NSViewController *vc in vcs) {
if ([vc isKindOfClass:[LeftViewController class]]) {
_leftSplitViewController = (LeftViewController *)vc;
break;
}
}
}
return _leftSplitViewController;
}
- (NSWindow *)window
{
return [[self windowController] window];
}
//VALID SOLUTION IF YOU DON'T INSTANTIATE STORYBOARD
//- (NSWindow *)window
//{
// return [[NSApp windows] firstObject];
//}
@end
Result: works exactly the same
PS: If you instantiate own window Controller don't forget to delete initial controller from Storyboard
Why is this better? Cause all changes goes to model and models sends triggers to redraw views. Also you will end up in smaller view controllers.
What can be done more? NSObjectController
is the best glue between your model objects and views. It also prevents retain cycle that sometimes can happen with bindings (more advanced topic). NSArrayController
and so on...
Caveats: not a solution for XIBs
I managed to get what i want by adding the following code in AppDelegate.m :
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
// Insert code here to initialize your application
//
NSStoryboard *storyboard = [NSStoryboard storyboardWithName:@"Main"
bundle:[NSBundle mainBundle]];
self.windowController = [storyboard instantiateControllerWithIdentifier:@"windowController"];
self.window = self.windowController.window;
self.splitViewController = (NSSplitViewController*)self.windowController.contentViewController;
NSSplitViewItem *item0 = [self.splitViewController.splitViewItems objectAtIndex:0];
NSSplitViewItem *item1 = [self.splitViewController.splitViewItems objectAtIndex:1];
self.leftViewController = (OMNLeftViewController*)item0.viewController;
self.rightViewController = (OMNRightViewController*)item1.viewController;
[self.window makeKeyAndOrderFront:self];
[self.windowController showWindow:nil];
}
We also need to edit the storyboard NSWindowController object as follow :
Uncheck the checkbox 'Is initial controller' because we add it programmatically in AppDelegate.m.
Now the left and right view can communicate. Just define a property named rightView in OMNLeftViewController.h :
self.leftViewController.rightView = self.rightViewController;