What's the best way to communicate between view controllers?
作为Objective-C、Cocoa和iPhoneDev的新手,我强烈希望充分利用语言和框架。
我使用的资源之一是斯坦福大学的CS193P课堂笔记,它们已经留在了网上。它包括讲稿、作业和示例代码,而且由于这门课程是由AppleDev's提供的,所以我肯定认为它是"从马口中"。
班级网站: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页:
How Not To Share Data
- Global Variables or singletons
- This includes your application delegate
- Direct dependencies make your code less reusable
- And more difficult to debug & test
好啊。我很失望。不要盲目地将用于在ViewController之间通信的所有方法抛到应用程序委托中,并引用应用程序委托方法中的ViewController实例。公平的"Nuff."
再往前一点,我们看到这张幻灯片,告诉我们应该做什么。
第18/51页:
Best Practices for Data Flow
- Figure out exactly what needs to be communicated
- Define input parameters for your view controller
- For communicating back up the hierarchy, use loose coupling
- Define a generic interface for observers (like delegation)
然后,这张幻灯片后面是一张看起来像是占位符幻灯片的幻灯片,在该幻灯片中,演讲者显然使用uiImagePickerController的一个示例演示了最佳实践。我希望这些视频都能用上!:(
好的,所以…恐怕我的目标不是很强。我也有点被上面引用的最后一行搞糊涂了。我在谷歌上对此做了相当多的尝试,我发现了一篇看起来不错的文章,讨论了观察/通知技术的各种方法:http://cocoawithlove.com/2008/06/five-approaches-to-listening-observation.html
方法5甚至将委托表示为方法!除了…对象一次只能设置一个委托。所以当我有多个视图控制器通信时,我该怎么做?
好吧,那是我们的帮派。我知道,通过引用AppDelegate中的多个ViewController实例,我可以轻松地在应用程序委托中进行通信,但我希望以正确的方式进行这类工作。
请回答以下问题,帮助我"做正确的事情":
这些是很好的问题,很高兴看到你正在做这项研究,并且似乎在关注如何"正确地做",而不是仅仅把它拼凑在一起。
首先,我同意前面的答案,这些答案关注在适当的时候将数据放入模型对象中的重要性(根据MVC设计模式)。通常您希望避免将状态信息放入控制器中,除非它是严格的"表示"数据。
第二,请参见斯坦福演示文稿第10页,了解如何以编程方式将控制器推送到导航控制器上的示例。有关如何使用InterfaceBuilder进行"可视化"操作的示例,请参阅本教程。
第三,也许最重要的是,请注意,如果您在"依赖注入"设计模式的上下文中考虑它们,那么斯坦福演示文稿中提到的"最佳实践"更容易理解。简而言之,这意味着您的控制器不应该"查找"它需要执行其工作的对象(例如,引用全局变量)。相反,您应该始终将这些依赖项"注入"到控制器中(即,通过方法传入它需要的对象)。
如果您遵循依赖注入模式,您的控制器将是模块化的,并且可以重用。如果你考虑斯坦福大学的演讲人来自何方(即,作为苹果的员工,他们的工作是构建易于重用的类),那么重用性和模块性是很重要的。他们提到的共享数据的所有最佳实践都是依赖注入的一部分。
这就是我回答的要点。我将在下面提供一个使用依赖注入模式和控制器的示例,以防有用。
将依赖注入与视图控制器一起使用的示例
假设您正在构建一个屏幕,其中列出了几本书。用户可以选择他/她想买的书,然后点击"结帐"按钮转到结帐屏幕。
要构建这个,您可以创建一个BookPickerView控制器类来控制和显示图形用户界面/视图对象。在哪里可以得到所有的图书数据?假设它取决于BookWarehouse对象。所以现在控制器基本上是在模型对象(BookWarehouse)和GUI/View对象之间代理数据。换句话说,BookPickerView控制器依赖于BookWarehouse对象。
不要这样做:
1 2 3 4 5 6 7 8 | @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]; ... } |
相反,应该像这样注入依赖项:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | @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]; ... } |
当苹果人谈论使用委托模式"在层次结构上进行通信"时,他们仍然在谈论依赖注入。在本例中,一旦用户选择了自己的书籍并准备好签出,BookPickerView控制器应该做什么?嗯,那不是它真正的工作。它应该将该工作委托给其他某个对象,这意味着它依赖于其他对象。所以我们可以修改bookpickerviewcontroller init方法如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | @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]; ... } |
所有这些的最终结果是,您可以为我提供BookPickerView控制器类(以及相关的GUI/View对象),我可以在自己的应用程序中轻松地使用它,假设BookWarehouse和CheckoutController是我可以实现的通用接口(即协议):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | @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不仅可以重用,而且更容易测试。
1 2 3 4 5 6 7 8 9 10 11 12 13 | -(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 ... } |
这类事情总是有品位的。
尽管如此,我还是喜欢通过模型对象进行协调(2)。顶级视图控制器加载或创建它需要的模型,并且每个视图控制器在其子控制器中设置属性,以告诉它们需要使用哪些模型对象。大多数更改通过使用nsnotificationCenter在层次结构中进行通信;触发通知通常内置于模型本身。
例如,假设我有一个包含帐户和交易的应用程序。我还有一个accountlistcontroller、一个accountcontroller(它用"显示所有事务"按钮显示帐户摘要)、一个transactionlistcontroller和一个transactioncontroller。AccountListController加载所有帐户的列表并显示它们。当您点击一个列表项时,它会设置其accountController的.account属性,并将accountController推到堆栈上。当您点击"显示所有事务"按钮时,AccountController加载事务列表,将其放入TransactionListController的.Transactions属性中,并将TransactionListController推送到堆栈上,依此类推。
例如,如果TransactionController编辑事务,它会在其事务对象中进行更改,然后调用其"保存"方法。save'发送TransactionChangedNotification。当事务更改时需要刷新自身的任何其他控制器都将观察通知并更新自身。TransactionListController可能会;AccountController和AccountListController可能会,这取决于他们试图做什么。
对于1,在我早期的应用程序中,我有一些显示模型:在子控制器中有navigationcontroller:method,它可以设置内容并将控制器推到堆栈上。但是随着我对软件开发工具包的使用越来越熟悉,我已经不再使用它了,现在我通常让父母来推孩子。
对于3,考虑这个例子。这里我们使用两个控制器amounteditor和textEditor来编辑事务的两个属性。编辑器不应该实际保存正在编辑的事务,因为用户可以决定放弃该事务。因此,它们都将其父控制器作为委托,并对其调用一个方法,表示是否更改了任何内容。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | @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提供了一些方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | - (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协议可以包含多个方法;在这种情况下,这是唯一必要的方法。
假设有两个等级A和B。
类的实例是
一种态度;
A类制造和B类实例,如
B bInstance;
在类B的逻辑中,需要在某个地方通信或触发类A的方法。
1)错误的方式
你可以把姿态传给宾斯坦斯。现在,从binstance中所需的位置调用所需的方法[ainstance methodname]。
这将满足您的目的,但释放会导致内存被锁定,而不是释放。
怎么用?
当您将ainstance传递给binstance时,我们将ainstance的retaincount增加了1。解除绑定binstance时,由于binstance本身是一个ainstance对象,所以无法将ainstance设置为0 retaincount,所以内存会被阻塞。
此外,由于内存卡滞,还将卡滞(泄漏)内存。因此,即使在稍后释放ainstance本身之后,它的内存也会被阻塞,因为binstance不能被释放,而binstance是ainstance的一个类变量。
2)正确的方式
通过将ainstance定义为binstance的委托,将不存在ainstance的重计数更改或内存纠缠。
binstance将能够自由调用位于ainstance中的委托方法。在binstance的deallocation上,所有变量都将自己创建并释放。在AINSTANCE的释放位置上,由于BINSTANCE中没有AINSTANCE的纠缠,它将被完全释放。
我看到你的问题了……
发生的事情是有人混淆了MVC体系结构的概念。
MVC由三部分组成。模型、视图和控制器。所说的问题似乎没有充分的理由把他们两个结合起来了。视图和控制器是分开的逻辑部分。
所以…您不希望有多个视图控制器。
您希望有多个视图,以及在它们之间进行选择的控制器。(如果有多个应用程序,也可以有多个控制器)
观点不应该做决定。控制器应该这样做。因此,任务和逻辑的分离,以及使你的生活更容易的方法。
所以…确保你的视图能做到这一点,输出一个很好的数据流。让您的控制器决定如何处理数据以及使用哪个视图。
(当我们谈论数据时,我们谈论的是模型……存储、访问、修改的标准方法。另一个单独的逻辑部分,我们可以将其打包并忘记)