简体   繁体   English

Mac OSX故事板:NSViewController之间进行通信

[英]Mac OSX Storyboard : communicate between NSViewController

I use storyboard in a OS X cocoa application project with a SplitView controller and 2 others view controller LeftViewController and RightViewController. 我在带有SplitView控制器和其他2个视图控制器LeftViewController和RightViewController的OS X可可应用程序项目中使用情节提要。

In the LeftViewController i have a tableView that display an array of name. 在LeftViewController中,我有一个tableView显示一个名称数组。 The datasource and delegate of the tableview is the LeftViewController. tableview的数据源和委托是LeftViewController。

In the RightViewController i just have a centered label that display the select name. 在RightViewController中,我只有一个居中的标签,用于显示选择名称。 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 : 为了配置2个视图控制器之间的通信,我使用AppDelegate,并在AppDelegate.h中为每个控制器定义2个属性。2个属性是使用NSInvocation波纹管在视图控制器的viewDidLoad中初始化的:

@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. 我在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. 然后,如果我在表视图中单击一个名称,则会向该代理发送一条消息,并在参数中带有该名称,然后该代理使用给定名称更新RightViewController的标签。 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 ? 在故事板中的2视图控制器之间还有另一种通信方式吗?

I've already read a lot of post but found nothing for OS X. 我已经阅读了很多文章,但对于OS X却一无所获。

You can download the simple project here : http://we.tl/4rAl9HHIf1 您可以在此处下载简单的项目: http : //we.tl/4rAl9HHIf1

在视图控制器之间通信

This is more advanced topic of app architecture (how to pass data). 这是应用程序体系结构(如何传递数据)的更高级主题。

  1. Dirty quick solution: post NSNotification together with forgotten representedObject : 肮脏的快速解决方案:交NSNotification与遗忘一起representedObject

All NSViewControllers have a nice property of type id called representedObject . 所有NSViewControllers有一个名为ID类型的一个很好的属性representedObject This is one of the ways how to pass data onto NSViewController . 这是将数据传递到NSViewController的方法NSViewController Bind your label to this property. 将标签绑定到该属性。 For this simple example we will set representedObject some NSString instance . 对于这个简单的例子中,我们将设置representedObject一些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?) 有人可以在评论中解释为什么情节提要板停下来显示representedObject (快速输入安全性?)

绑定

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 PS:别忘了钩住tableview数据源并在情节提要中委派

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. 我更喜欢窗口控制器/ appdelegate成为模型所有者。

Result: 结果:

结果

  1. AppDelegate as Model owner. AppDelegate作为模型所有者。

Our left view controller will get it's data from AppDelegate . 我们的左视图控制器将从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). AppDelegate控制数据流并设置数据非常重要(不是视图控制器询问AppDelegate它的表内容,因为这将导致数据同步混乱)。 We can do this again using representedObject . 我们可以使用representedObject再次执行此操作。 Once it's set we reload our table (there are more advanced solutions like NSArrayController and bindings). 设置好后,我们重新加载表(还有更高级的解决方案,例如NSArrayController和绑定)。 Don't forget to hook tableView in storyboard. 不要忘记将tableView钩在情节提要中。 We also modify tableview's delegate methos the tableViewSelectionDidChange to modify our model object (AppDelegate.selectedName) 我们还修改了tableview的委托方法the tableViewSelectionDidChange来修改我们的模型对象(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. RightViewController我们删除所有代码。 Why? 为什么? Cause we will use binding AppDelegate.selectedName <--> RightViewController.representedObject 因为我们将使用绑定AppDelegate.selectedName <--> RightViewController.representedObject

@implementation RightViewController

@end 

Finally AppDelegate . 最后是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). 另一种方法是向NSApp询问其Windows(在此处使用多窗口应用程序要小心)。 From there we just ask contentViewController and loop through childViewControllers. 从那里,我们只要求contentViewController并遍历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 结果:完全相同 RESULT2

PS: If you instantiate own window Controller don't forget to delete initial controller from Storyboard PS:如果您实例化自己的窗口控制器,请不要忘记从情节提要中删除初始控制器

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. NSObjectController是模型对象和视图之间的最佳NSObjectController It also prevents retain cycle that sometimes can happen with bindings (more advanced topic). 它还可以防止绑定有时会发生的保留周期(更高级的主题)。 NSArrayController and so on... NSArrayController等...

Caveats: not a solution for XIBs 注意事项:不是XIB的解决方案

I managed to get what i want by adding the following code in AppDelegate.m : 我设法通过在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 : 我们还需要如下编辑故事板NSWindowController对象: 为NSWindowController定义一个ID

Uncheck the checkbox 'Is initial controller' because we add it programmatically in AppDelegate.m. 取消选中“是初始控制器”复选框,因为我们以编程方式将其添加到AppDelegate.m中。

取消选中“是初始控制器”复选框

Now the left and right view can communicate. 现在,左视图和右视图可以进行通信。 Just define a property named rightView in OMNLeftViewController.h : 只需在OMNLeftViewController.h中定义一个名为rightView的属性即可:

    self.leftViewController.rightView = self.rightViewController;

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM