165

作为 Objective-c、cocoa 和 iPhone 开发的新手,我强烈希望充分利用语言和框架。

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

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

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

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

第 16/51 页:

如何不共享数据

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

好的。我对此很失望。不要盲目地将用于在 viewcontroller 之间进行通信的所有方法都扔到应用程序委托中,并在应用程序委托方法中引用 viewcontroller 实例。公平'nuff。

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

第 18/51 页:

数据流的最佳实践

  • 弄清楚需要传达的确切内容
  • 为您的视图控制器定义输入参数
  • 为了在层次结构上进行通信,请使用松散耦合
    • 为观察者定义一个通用接口(如委托)

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

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

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

好的,这就是组织的帮派。我知道我可以通过引用我的 appdelegate 中的多个 viewcontroller 实例轻松地在应用程序委托中执行我的通信方法,但我想正确地做这种事情

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

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

4 回答 4

225

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

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

其次,有关如何以编程方式将控制器推送到导航控制器上的示例,请参见斯坦福演示文稿的第 10 页。有关如何使用 Interface Builder 以“可视方式”执行此操作的示例,请查看本教程

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

如果您遵循依赖注入模式,您的控制器将是模块化和可重用的。如果您考虑一下斯坦福大学的演讲者来自哪里(即,作为 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 应该做什么?好吧,这不是它的工作。它应该将这项工作委托给其他对象,这意味着它依赖于另一个对象。所以我们可能会修改我们的 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
   ...
}
于 2009-02-22T19:17:34.873 回答
15

这种事情总是一个品味问题。

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

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

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

对于#1,在我早期的应用程序中,我在子控制器中有某种​​ displayModel:withNavigationController: 方法,可以设置并将控制器推送到堆栈上。但是随着我对 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];
}

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

于 2009-02-22T01:37:57.573 回答
0

看到你的问题了。。

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

MVC 具有三个部分.. 模型、视图和控制器.. 所述问题似乎无缘无故地将其中两个结合在一起。视图和控制器是独立的逻辑部分。

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

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

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

所以..确保您的视图只是这样做,对数据有一个很好的看法。让您的控制器决定如何处理数据以及使用哪个视图。

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

于 2009-02-22T08:21:54.513 回答
0

假设有两个类 A 和 B。

A类的实例是

一个实例;

A 类生成 B 类的实例,如

B b实例;

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

1) 错误的方式

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

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

如何?

当您将aInstance 传递给bInstance 时,我们将aInstance 的retaincount 增加了1。在释放bInstance 时,我们将阻塞内存,因为bInstance 永远无法将aInstance 变为0,因为bInstance 本身是aInstance 的对象。

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

2)正确的方式

通过将aInstance定义为bInstance的delegate,不会有aInstance的retaincount变化或内存纠缠。

bInstance 将能够自由调用位于 aInstance 中的委托方法。在bInstance的deallocation上,所有的变量都是自己创建并被释放的。在aInstance的deallocation上,由于bInstance中没有aInstance的纠缠,会被干净的释放。

于 2012-03-05T08:38:23.293 回答