视图控制器之间通信的最佳方式是什么?

作为 Objective-c、可可和 iPhone 开发人员的新手,我有一个强烈的愿望,希望从语言和框架中获得最大的收益。

我正在使用的资源之一是斯坦福大学 CS193P 课堂笔记,它们已经留在了网上。它包括课堂讲稿、作业和示例代码,而且由于这门课程是由苹果开发人员授课的,因此我绝对认为它是“从马嘴里说出来的”。

班级网页:
Http://www.stanford.edu/class/cs193p/cgi-bin/index.php

课程08涉及到构建一个基于 UINavigationController 的应用程序的任务,该应用程序将多个 UIViewController 推送到 UINavigationController 堆栈上。这就是 UINavigationController 的工作原理。这是合乎逻辑的。但是,幻灯片中有一些关于 UIViewController 之间通信的严重警告。

我要引用这一系列幻灯片中的话:
Http://cs193p.stanford.edu/downloads/08-navigationtabbarcontrollers.pdf

第16/51页:

如何不共享数据

  • 全局变量或单例
    • 这包括你的 申请委托人
  • 直接依赖性使代码的可重用性降低
    • 而且更难调试和测试

好吧。我同意。不要盲目地将所有用于在视图控制器之间进行通信的方法抛入应用程序委托中,并引用应用程序委托方法中的视图控制器实例。好吧。

再往前一点,我们看到这张幻灯片告诉我们 应该是做什么的。

第18/51页:

数据流的最佳实践

  • 找出需要沟通的 没错
  • 为视图控制器定义输入参数
  • 对于向上层次结构的通信,< strong > 使用松散耦合
    • 为观察者定义一个通用接口(比如委托)

这个幻灯片之后是一个占位幻灯片,演讲者显然使用 UIImagePickerController 的示例演示了最佳实践。我希望视频可以看到!:(

好吧,那么... 恐怕我的目的性没那么强。我对上面引用的最后一行也有点困惑。我一直在用谷歌搜索这个问题,发现了一篇看起来不错的文章,谈到了观察/通知技术的各种方法:
Http://cocoawithlove.com/2008/06/five-approaches-to-listening-observing.html

方法 # 5甚至将委托表示为一个方法!除了..。对象一次只能设置一个委托。因此,当我有多个视图控制器通信,我该怎么办?

好了,这就是设计好的帮派。我知道我可以很容易地做我的通信方法在应用程序委托通过引用的多个视图控制器实例在我的应用程序委托,但我想做这种事情的 的方式。

请通过回答以下问题来帮助我“做正确的事情”:

  1. 当我试图在 UINavigationController 堆栈上推送一个新的 viewcontroller 时,应该执行这个推送操作。哪个类/文件在我的代码中是正确的位置吗?
  2. 当我使用 与众不同 UIViewController 时,如果我想在我的 UIViewController 中影响一些数据(iVar 的值) ,什么是“正确”的方法?
  3. 假设我们一次只能在一个对象中设置一个委托,那么当讲师说 “为观察者定义一个通用接口(如委托)”时,实现会是什么样子。如果可能的话,这里的伪代码示例将非常有帮助。
38477 次浏览

这种事情永远是品味的问题。

话虽如此,我还是更喜欢通过模型对象来进行协调(# 2)。顶级视图控制器加载或创建它所需的模型,每个视图控制器在其子控制器中设置属性,以告诉它们需要使用哪些模型对象。大多数更改通过使用 NSNotificationCenter 在层次结构中进行通信; 触发通知通常内置在模型本身中。

例如,假设我有一个包含帐户和事务的应用程序。我还有一个 AccountListController、一个 AccountController (它显示一个带有“ show all actions”按钮的帐户摘要)、一个 TransactionListController 和一个 TransactionController。AccountListController 加载所有帐户的列表并显示它们。单击列表项时,它将设置。AccountController 的 account 属性,并将 AccountController 推送到堆栈上。当您点击“ show all actions”按钮时,AccountController 加载事务列表,并将其放入 TransactionListController 的事务列表中。属性,并将 TransactionListController 推送到堆栈上,依此类推。

例如,如果 TransactionController 编辑事务,它将在其事务对象中进行更改,然后调用其“ save”方法。‘ save’发送一个 TransactionChangedNotification。在事务更改时需要刷新自身的任何其他控制器都将观察通知并更新自身。TransactionListController 可能会; AccountController 和 AccountListController 可能会,这取决于它们试图做什么。

第一,在我早期的应用程序中,我在子控制器中有一些 displayModel: withNavigationController: 方法,它可以设置一些东西并将控制器推到堆栈上。但是随着我对 SDK 的使用越来越熟悉,我已经渐渐远离了 SDK,现在我通常让父母来推动孩子。

对于 # 3,考虑这个例子。这里我们使用两个控制器 AmountEditor 和 TextEditor 来编辑事务的两个属性。实际上,编辑器不应该保存正在编辑的事务,因为用户可以决定放弃该事务。因此,它们都将其父控制器作为一个委托,并在其上调用一个方法,说明是否已经更改了任何内容。

@class Editor;
@protocol EditorDelegate
// called when you're finished.  updated = YES for 'save' button, NO for 'cancel'
- (void)editor:(Editor*)editor finishedEditingModel:(id)model updated:(BOOL)updated;
@end


// this is an abstract class
@interface Editor : UIViewController {
id model;
id <EditorDelegate> delegate;
}
@property (retain) Model * model;
@property (assign) id <EditorDelegate> delegate;


...define methods here...
@end


@interface AmountEditor : Editor
...define interface here...
@end


@interface TextEditor : Editor
...define interface here...
@end


// TransactionController shows the transaction's details in a table view
@interface TransactionController : UITableViewController <EditorDelegate> {
AmountEditor * amountEditor;
TextEditor * textEditor;
Transaction * transaction;
}
...properties and methods here...
@end

现在来看看 TransactionController 的一些方法:

- (void)viewDidLoad {
amountEditor.delegate = self;
textEditor.delegate = self;
}


- (void)editAmount {
amountEditor.model = self.transaction;
[self.navigationController pushViewController:amountEditor animated:YES];
}


- (void)editNote {
textEditor.model = self.transaction;
[self.navigationController pushViewController:textEditor animated:YES];
}


- (void)editor:(Editor*)editor finishedEditingModel:(id)model updated:(BOOL)updated {
if(updated) {
[self.tableView reloadData];
}


[self.navigationController popViewControllerAnimated:YES];
}

需要注意的是,我们已经定义了一个通用协议,编辑器可以使用该协议与自己的控制器进行通信。通过这样做,我们可以在应用程序的其他部分重用 Editor。(或许会计部也可以有记录。)当然,EditorVeneate 协议可以包含多个方法; 在本例中,这是唯一必要的方法。

我知道你的问题了。

发生的事情是有人混淆了 MVC 架构的概念。

MVC 有三个部分。.模型、视图和控制器。.所陈述的问题似乎毫无理由地将两者结合在一起。视图和控制器是独立的逻辑部分。

所以... 你不希望有多个视图控制器. 。

您希望拥有多个视图,以及在它们之间进行选择的控制器。(如果有多个应用程序,也可以有多个控制器)

意见不应该作出决定。控制器应该这样做。因此,分离的任务,逻辑,和方法,使您的生活更容易。

那么。.确保你的视图只是这样做,提出了一个很好的数据视图。让控制器决定如何处理数据,以及使用哪个视图。

(当我们谈论数据时,我们谈论的是模型... 一种存储、访问和修改的很好的标准方法。.另一个独立的逻辑片段,我们可以打包和忘记)

这些都是很好的问题,很高兴看到你正在做这项研究,并且似乎很关心学习如何“正确地做”,而不是仅仅把它们拼凑在一起。

首先 ,我同意前面的答案,即在适当的时候(根据 MVC 设计模式)将数据放入模型对象的重要性。通常,您希望避免将状态信息放入控制器中,除非它是严格的“表示”数据。

第二个 ,请参阅 Stanford 演示文稿的第10页,以获得如何通过编程将控制器推送到导航控制器上的示例。要想知道如何使用 Interface Builder 来“直观地”做到这一点,可以看看 本教程

第三点,也许是最重要的一点,注意斯坦福演讲中提到的“最佳实践”,如果你把它们放在“依赖注入”设计模式的背景下来考虑,就会更容易理解。简而言之,这意味着您的控制器不应该“查找”它需要执行其工作的对象(例如,引用一个全局变量)。相反,您应该始终将这些依赖项“注入”到控制器中(即,通过方法传递它需要的对象)。

如果你遵循依赖注入模式,你的控制器将是模块化和可重用的。如果你想想斯坦福大学的演讲者是从哪里来的(比如,作为苹果的员工,他们的工作就是构建可以轻松重用的类) ,可重用性和模块化是最重要的。他们提到的所有共享数据的最佳实践都是依赖注入的一部分。

这就是我回答的要点。我将在下面给出一个使用依赖注入模式和控制器的例子,以备不时之需。

在视图控制器中使用依赖注入的例子

假设您正在构建一个屏幕,其中列出了几本书。用户可以选择他/她想要购买的书籍,然后点击“结帐”按钮进入结帐屏幕。

为此,可以创建一个 BookPickerViewController 类来控制和显示 GUI/view 对象。它将从哪里获得所有的图书数据?我们假设它取决于 BookWarehouse 对象。因此,现在您的控制器基本上是在模型对象(BookWarehouse)和 GUI/view 对象之间代理数据。换句话说,BookPickerViewController 依赖于 BookWarehouse 对象。

别这样:

@implementation BookPickerViewController


-(void) doSomething {
// I need to do something with the BookWarehouse so I'm going to look it up
// using the BookWarehouse class method (comparable to a global variable)
BookWarehouse *warehouse = [BookWarehouse getSingleton];
...
}

相反,依赖项应该像这样注入:

@implementation BookPickerViewController


-(void) initWithWarehouse: (BookWarehouse*)warehouse {
// myBookWarehouse is an instance variable
myBookWarehouse = warehouse;
[myBookWarehouse retain];
}


-(void) doSomething {
// I need to do something with the BookWarehouse object which was
// injected for me
[myBookWarehouse listBooks];
...
}

当苹果公司的人谈论使用委托模式来“向上层交流”时,他们仍然在谈论依赖注入。在这个例子中,一旦用户选择了他/她的书并准备签出,BookPickerViewController 应该做什么?那不是它的工作。它应该委派工作到其他对象,这意味着它依赖于另一个对象。因此,我们可以像下面这样修改 BookPickerViewController init 方法:

@implementation BookPickerViewController


-(void) initWithWarehouse:    (BookWarehouse*)warehouse
andCheckoutController:(CheckoutController*)checkoutController
{
myBookWarehouse = warehouse;
myCheckoutController = checkoutController;
}


-(void) handleCheckout {
// We've collected the user's book picks in a "bookPicks" variable
[myCheckoutController handleCheckout: bookPicks];
...
}

所有这些的最终结果是,你可以给我你的 BookPickerViewController 类(和相关的 GUI/view 对象) ,我可以很容易地在我自己的应用程序中使用它,假设 BookWarehouse 和 CheckoutController 是我可以实现的通用接口(即协议) :

@interface MyBookWarehouse : NSObject <BookWarehouse> { ... } @end
@implementation MyBookWarehouse { ... } @end


@interface MyCheckoutController : NSObject <CheckoutController> { ... } @end
@implementation MyCheckoutController { ... } @end


...


-(void) applicationDidFinishLoading {
MyBookWarehouse *myWarehouse = [[MyBookWarehouse alloc]init];
MyCheckoutController *myCheckout = [[MyCheckoutController alloc]init];


BookPickerViewController *bookPicker = [[BookPickerViewController alloc]
initWithWarehouse:myWarehouse
andCheckoutController:myCheckout];
...
[window addSubview:[bookPicker view]];
[window makeKeyAndVisible];
}

最后,BookPickerController 不仅可以重用,而且更容易测试。

-(void) testBookPickerController {
MockBookWarehouse *myWarehouse = [[MockBookWarehouse alloc]init];
MockCheckoutController *myCheckout = [[MockCheckoutController alloc]init];


BookPickerViewController *bookPicker = [[BookPickerViewController alloc] initWithWarehouse:myWarehouse andCheckoutController:myCheckout];
...
[bookPicker handleCheckout];


// Do stuff to verify that BookPickerViewController correctly called
// MockCheckoutController's handleCheckout: method and passed it a valid
// list of books
...
}

假设有两个类 A 和 B。

类别 A 的实例是

一宗个案;

A 类构成和 B 类的实例,如

实例;

在类 B 的逻辑中,需要在某个地方通信或触发类 A 的方法。

1)走错了

您可以将 aInstance 传递给 bInstance。 现在从 bInstance 中的所需位置调用所需的方法[ aInstance methodname ]。

这将达到您的目的,但是释放将导致内存被锁定而没有释放。

怎么做到的?

当您将 aInstance 传递给 bInstance 时,我们将 aInstance 的保留计数增加了1。 在释放 bInstance 时,我们会阻塞内存,因为 bInstance 本身是一个实例的对象,所以 bInstance 永远不会被带到0的保留计数。

此外,由于一个实例被卡住,bInstance 的内存也会被卡住(泄漏)。 因此,即使后来解除了 aInstance 本身的分配,它的内存也会被阻塞,因为 bInstance 不能被释放,而且 bInstance 是 aInstance 的类变量。

2)正确的方法

通过将 aInstance 定义为 bInstance 的委托,就不会有 aInstance 的保留计数更改或内存纠缠。

BInstance 将能够自由地调用位于 aInstance 中的委托方法。 在 bInstance 的释放操作中,所有的变量都将被自己创建并释放 在 aInstance 的释放时,由于 bInstance 中没有 aInstance 的纠缠,所以它将被干净地释放。