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

时间:2009-02-20 15:20:46

标签: objective-c iphone cocoa-touch delegates key-value-observing

一般来说,对于Objective-c,cocoa和iPhone开发人员来说,我非常希望能够充分利用语言和框架。

我正在使用的资源之一是斯坦福大学的CS193P课程,他们已经留在网上了。它包括讲义,作业和示例代码,由于课程由Apple开发者提供,我绝对认为它是“从马的嘴里”。

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

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

我将引用这个严肃的幻灯片:
http://cs193p.stanford.edu/downloads/08-NavigationTabBarControllers.pdf

第16/51页:

  

如何不共享数据

     
      
  • 全球变量或单身人士      
        
    • 这包括应用程序代理
    •   
  •   
  • 直接依赖关系使您的代码不再可重用      
        
    • 更难以调试&测试
    •   
  •   

确定。我很失望。不要盲目地将所有用于在viewcontroller之间进行通信的方法扔到app委托中,并引用app delegate方法中的viewcontroller实例。公平的'nuff。

再进一步说,我们得到这张幻灯片告诉我们应该做什么。

第18/51页:

  

数据流的最佳实践

     
      
  • 弄清楚完全需要传达的内容
  •   
  • 为视图控制器定义输入参数
  •   
  • 为了备份层次结构,使用松散耦合      
        
    • 为观察者定义通用接口(如委托)
    •   
  •   

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

好的,所以......我担心我的objc-fu不是那么强大。我也对上面引用的最后一行感到困惑。我一直在谷歌搜索这个,我发现似乎是一篇体面的文章谈论观察/通知技术的各种方法:
http://cocoawithlove.com/2008/06/five-approaches-to-listening-observing.html

方法#5甚至表示委托作为一种方法!除了....对象一次只能设置一个委托。所以,当我有多个viewcontroller通信时,我该怎么办?

好的,那是设置帮派。我知道我可以通过引用在我的appdelegate中的多个viewcontroller实例轻松地在app委托中执行我的通信方法,但我想以正确的方式执行此类操作。

请回答以下问题,帮助我“做正确的事”:

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

4 个答案:

答案 0 :(得分:224)

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

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

第二次 ,请参阅斯坦福大学演示文稿的第10页,了解如何以编程方式将控制器推送到导航控制器的示例。有关如何使用Interface Builder“直观地”执行此操作的示例,请查看this tutorial

第三 ,也许最重要的是,请注意,如果您在上下文中考虑它们,斯坦福大学演示文稿中提到的“最佳实践”会更容易理解。 “依赖注入”设计模式。简而言之,这意味着您的控制器不应“查找”完成其工作所需的对象(例如,引用全局变量)。相反,您应该始终将这些依赖项“注入”到控制器中(即,通过方法传入它需要的对象)。

如果您遵循依赖注入模式,您的控制器将是模块化的并且可重复使用。如果您考虑斯坦福大学主持人的来源(即,作为Apple员工,他们的工作是构建可以轻松重用的课程),可重用性和模块化是高优先级。他们提到的用于共享数据的所有最佳实践都是依赖注入的一部分。

这是我的回应的要点。我将在下面提供一个使用依赖注入模式和控制器的示例,以防它有用。

使用视图控制器进行依赖注入的示例

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

要构建它,您可以创建一个BookPickerViewController类来控制和显示GUI /视图对象。它会在哪里获得所有书籍数据?让我们说它依赖于BookWarehouse对象。所以现在你的控制器基本上是在模型对象(BookWarehouse)和GUI /视图对象之间进行数据代理。换句话说,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应该做什么?嗯,这不是真正的工作。它应该将该工作委托给其他对象,这意味着它在另一个对象上进行DEPENDS。所以我们可以修改我们的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 /视图对象),我可以在我自己的应用程序中轻松使用它,假设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
   ...
}

答案 1 :(得分:15)

这种事总是有品味的。

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

例如,假设我有一个带有帐户和交易的应用程序。我还有一个AccountListController,一个AccountController(显示一个带有“show all tr​​ansactions”按钮的帐户摘要),一个TransactionListController和一个TransactionController。 AccountListController加载所有帐户的列表并显示它们。当您点击列表项时,它会设置其AccountController的.account属性并将AccountController压入堆栈。当您点击“显示所有交易”按钮时,AccountController会加载事务列表,将其放入TransactionListController的.transactions属性中,并将TransactionListController压入堆栈,依此类推。

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

对于#1,在我早期的应用程序中,我在子控制器中有一些displayModel:withNavigationController:方法,用于设置并将控制器推入堆栈。但随着我对SDK的熟悉程度越来越高,我已经远离了这一点,现在我通常让父母推动孩子。

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

@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];
}

需要注意的是,我们已经定义了一个通用协议,编辑可以使用该协议与他们自己的控制器进行通信。通过这样做,我们可以在应用程序的另一部分重用编辑器。 (也许Account也可以有注释。)当然,EditorDelegate协议可以包含多个方法;在这种情况下,这是唯一必要的。

答案 2 :(得分:0)

我看到了你的问题..

发生的事情是有人混淆了MVC架构的想法。

MVC有三个部分..模型,视图和控制器..所述问题似乎已经将两个部分组合在一起,没有充分的理由。视图和控制器是独立的逻辑。

所以...你不想拥有多个视图控制器..

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

观点不应该做出决定。控制器应该这样做。因此,分离任务,逻辑以及让你的生活更轻松的方法。

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

(当我们谈论数据时,我们正在谈论模型......一种很好的标准方式,即被存储,访问,修改......另一个独立的逻辑,我们可以包裹并忘记)

答案 3 :(得分:0)

假设有两个A和B类。

A类的实例是

a aInstance;

类A和类B的实例,如

B bInstance;

在你的B类逻辑中,你需要在某处沟通或触发A类方法。

1)错误的方式

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

这可以达到您的目的,但是释放会导致内存被锁定而不会被释放。

如何?

当你将aInstance传递给bInstance时,我们将aInstance的零售额增加了1。 当解除分配bInstance时,我们将阻塞内存,因为bInstance原因是bInstance本身是aInstance的一个对象,因此aInstance永远不会被带到0 retaountount。

此外,由于实例被卡住,bInstance的内存也会被卡住(泄漏)。 因此,即使在稍后时间释放aInstance本身之后,它的内存也会被阻塞,因为bInstance不能被释放而bInstance是aInstance的类变量。

2)正确的方式

通过将aInstance定义为bInstance的委托,aInstance不会有零售额变更或内存纠缠。

bInstance将能够自由调用位于aInstance中的委托方法。 在bInstance的释放上,所有变量都将自己创建并将被释放 在一个实例的释放中,由于bInstance中没有一个实例的纠缠,它将被彻底释放。