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


165

作为对Objective-C,可可和iPhone开发人员的新手,我强烈希望充分利用语言和框架。

我正在使用的资源之一是斯坦福大学的CS193P类笔记,它们已经留在网上了。它包括讲义,作业和示例代码,并且由于该课程是由Apple开发人员提供的,因此我绝对认为它是“从马口算起的”。

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

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

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

第16/51页:

如何不共享数据

  • 全局变量或单例
    • 这包括您的应用程序委托
  • 直接依赖关系使代码的可重用性降低
    • 而且更难调试和测试

好。我对此感到沮丧。不要盲目地将用于在viewcontroller之间进行通信的所有方法扔到您的应用程序委托中,并在应用程序委托方法中引用viewcontroller实例。公平的诺夫。

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

第18/51页:

数据流最佳实践

  • 弄清楚究竟需要传达什么
  • 为您的视图控制器定义输入参数
  • 为了在层次结构之间进行通讯,请使用松散耦合
    • 为观察者定义通用接口(例如委派)

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

好的,所以...我怕我的objc-fu不太牢固。我也对上面引用的最后一行感到困惑。我一直在尽职调查这个问题,发现似乎是一篇体面的文章,在谈论观察/通知技术的各种方法:http :
//cocoawithlove.com/2008/06/five-approaches-to -listening-observing.html

方法5甚至将委托表示为方法!除了...对象只能一次设置一个委托。因此,当我有多个ViewController通讯时,我该怎么办?

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

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

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

-一些,这是这篇文章从苹果解决developer.apple.com/library/ios/#featuredarticles/...
詹姆斯·穆尔

简要说明一下:Stanford CS193P类的视频现在可以通过iTunes U获得。最新的(2012-13)可以在itunes.apple.com/us/course/coding-together-developing/…中查看,我希望未来的视频和幻灯片将在cs193p.stanford.edu
Thomas Watson

Answers:


224

这些都是很好的问题,很高兴看到您正在进行这项研究,并且似乎在学习如何“正确地做到这一点”,而不是仅仅一起学习。

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

其次,请参阅Stanford演示文稿的第10页,以获取有关如何以编程方式将控制器推入导航控制器的示例。有关如何使用Interface Builder进行“可视化”操作的示例,请参阅本教程

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

如果遵循依赖项注入模式,则控制器将是模块化且可重复使用的。而且,如果您考虑到斯坦福演讲者来自何处(例如,作为Apple员工,他们的工作是建立易于重用的类),那么可重用性和模块化是当务之急。他们提到的共享数据的所有最佳实践都是依赖注入的一部分。

那就是我的回应要点。如果有帮助,我将在下面提供一个示例,该示例将依赖项注入模式与控制器一起使用。

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

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

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

不要这样做:

@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
   ...
}

19
当我看到如此精心制作的问题(和答案)时,我禁不住微笑。我们勇敢的发问者和您当之无愧的荣誉!同时,我想分享您在第二点中引用的便捷入侵代码的更新链接:入侵代码 .com / 2009/09 /… -再次感谢您分享您的见解和最佳实践,并提供示例支持!
乔·安德里亚

我同意。这个问题形成得很好,答案简直是太棒了。除了使用技术上的答案外,它还包括在使用DI实现方式/原因方面的一些心理原因。谢谢!+1。
凯文·埃利奥特

如果您还希望使用BookPickerController为愿望清单或几种可能的摘书原因之一挑选一本书,该怎么办。您是否仍将使用CheckoutController接口方法(也许重命名为BookSelectionController之类的方法)还是使用NSNotificationCenter?
Les

这仍然紧密耦合。从一个集中的地方引发和消耗事件将更加宽松。
Neil McGuigan 2012年

1
第2点中引用的链接似乎再次发生了变化-这是工作链接入侵
代码.com / blog / archives / 322

15

这种事情总是有品味的。

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

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

例如,如果TransactionController编辑该事务,它将在其事务对象中进行更改,然后调用其“保存”方法。“保存”发送一个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];
}

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


1
这应该按原样工作吗?我遇到了麻烦Editor.delegate。用我的viewDidLoad方法,我得到了Property 'delegate' not found...。我只是不确定我是否搞砸了其他东西。或者,为了简洁起见,将其删节。
杰夫

现在这是很旧的代码,以较旧的样式和较旧的约定编写。我不会将其直接复制并粘贴到您的项目中;我只是尝试从模式中学习。
布伦特皇家-戈登

知道了 那正是我想知道的。我对其进行了一些修改,但我有点担心它与逐字记录不匹配。
杰夫

0

我看到你的问题了。

发生的事情是,有人对MVC体系结构感到困惑。

MVC包含三个部分:模型,视图和控制器。所陈述的问题似乎没有充分的理由将其中的两个结合在一起。视图和控制器是分开的逻辑。

所以...您不想有多个视图控制器。

您想拥有多个视图,并需要一个在它们之间进行选择的控制器。(如果您有多个应用程序,则也可以有多个控制器)

视图不应做出决定。控制器应该这样做。因此,任务,逻辑和使生活更轻松的方法分开了。

因此..确保您的视图就可以做到,并显示出很好的数据效果。让您的控制器决定如何处理数据以及使用哪个视图。

(当我们谈论数据时,我们谈论的是模型……一种存储,访问,修改的不错的标准方法。另外一个独立的逻辑部分,我们可以将其分开并忘记)


0

假设有两个类A和B。

A类的实例是

一个实例;

A类和B类的实例

B bInstance;

按照B类的逻辑,您需要在某个地方交流或触发A类方法。

1)错误的方式

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

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

怎么样?

当您将aInstance传递给bInstance时,我们将aInstance的保留计数增加了1。取消分配bInstance时,由于aInstance永远不会被bInstance变为0保留计数,我们将阻塞内存,原因是bInstance本身是aInstance的对象。

此外,由于aInstance被卡住,bInstance的内存也将被卡住(泄漏)。因此,即使在稍后释放aInstance本身的时间之后,它的内存也将被阻塞,因为无法释放bInstance并且bInstance是aInstance的类变量。

2)正确的方法

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

bInstance将能够自由调用位于aInstance中的委托方法。在bInstance的解除分配上,所有变量将由其自己创建并释放。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.