如何避免iOS上笨拙的UITableViewController?


36

在iOS上实施MVC模式时遇到问题。我已经搜索了Internet,但似乎找不到解决此问题的任何好方法。

许多UITableViewController实现似乎很大。我见过的大多数示例都允许UITableViewController工具<UITableViewDelegate><UITableViewDataSource>。这些实现UITableViewController是变大的一个重要原因。一种解决方案是创建实现<UITableViewDelegate>和的单独类<UITableViewDataSource>。当然,这些类必须参考UITableViewController。使用此解决方案是否有任何弊端?通常,我认为您应该使用委托模式将功能委托给其他“ Helper”类或类似类。是否有解决此问题的完善方法?

我不希望模型包含太多功能,也不希望包含视图。我认为逻辑应该真正属于控制器类,因为这是MVC模式的基石之一。但是最大的问题是:

您应该如何将MVC实现的控制器划分为较小的可管理部分?(在这种情况下,适用于iOS中的MVC)

尽管我正在特别寻找iOS的解决方案,但可能会有解决此问题的一般模式。请举例说明解决此问题的好方法。请提供一个论点,为什么您的解决方案很棒。


1
“这也是为什么这种解决方案很棒的争论。” :)
occulus 2012年

1
这有点不重要,但是UITableViewController机制对我来说似乎很奇怪,所以我可以解决这个问题。实际上,我很高兴我的使用MonoTouch,因为MonoTouch.Dialog特别是使得它很大的工作更容易在iOS上的表。同时,我很好奇其他更有知识的人在这里可能会建议...
PatrykĆwiek2012年

Answers:


43

我避免使用UITableViewController,因为它将很多责任归于一个对象。因此,我将UIViewController子类与数据源和委托分开。视图控制器的职责是准备表视图,创建包含数据的数据源,并将这些东西连接在一起。无需更改视图控制器即可更改表视图的表示方式,实际上,同一视图控制器可用于所有遵循此模式的多个数据源。同样,更改应用程序工作流程意味着更改视图控制器,而不必担心表发生了什么。

我曾尝试将UITableViewDataSourceUITableViewDelegate协议分隔为不同的对象,但是通常最终会导致错误的拆分,因为委托上的几乎每个方法都需要挖掘数据源(例如,在选择时,委托需要知道对象所代表的对象)选定的行)。因此,我最终得到了一个既是数据源又是委托的对象。这个对象总是提供一种-(id)tableView: (UITableView *)tableView representedObjectAtIndexPath: (NSIndexPath *)indexPath数据源和委托方面都需要知道它们正在做什么的方法。

那是我的“ 0级”关注点分离。如果我必须在同一张表视图中表示不同种类的对象,则使用1级。例如,假设您必须编写“联系人”应用程序-对于单个联系人,可能有代表电话号码的行,代表地址的其他行,代表电子邮件地址的其他行等等。我想避免这种方法:

- (UITableViewCell *)tableView: (UITableView *)tableView cellForRowAtIndexPath: (NSIndexPath *)indexPath {
  id object = [self tableView: tableView representedObjectAtIndexPath: indexPath];
  if ([object isKindOfClass: [PhoneNumber class]]) {
    //configure phone number cell
  }
  else if …
}

到目前为止,已经提出了两种解决方案。一种是动态构造选择器:

- (UITableViewCell *)tableView: (UITableView *)tableView cellForRowAtIndexPath: (NSIndexPath *)indexPath {
  id object = [self tableView: tableView representedObjectAtIndexPath: indexPath];
  NSString *cellSelectorName = [NSString stringWithFormat: @"tableView:cellFor%@AtIndexPath:", [object class]];
  SEL cellSelector = NSSelectorFromString(cellSelectorName);
  return [self performSelector: cellSelector withObject: tableView withObject: object];
}

- (UITableViewCell *)tableView: (UITableView *)tableView cellForPhoneNumberAtIndexPath: (NSIndexPath *)indexPath {
  // configure phone number cell
}

在这种方法中,您无需编辑史诗if()树即可支持新类型-只需添加支持新类的方法即可。如果此表视图是唯一需要表示这些对象或需要以特殊方式呈现的对象,则这是一种很好的方法。如果相同的对象将在具有不同数据源的不同表中表示,则此方法将失效,因为单元格创建方法需要跨数据源共享—您可以定义提供这些方法的公共超类,或者可以执行以下操作:

@interface PhoneNumber (TableViewRepresentation)

- (UITableViewCell *)tableView: (UITableView *)tableView representationAsCellForRowAtIndexPath: (NSIndexPath *)indexPath;

@end

@interface Address (TableViewRepresentation)

//more of the same…

@end

然后在您的数据源类中:

- (UITableViewCell *)tableView: (UITableView *)tableView cellForRowAtIndexPath: (NSIndexPath *)indexPath {
  id object = [self tableView: tableView representedObjectAtIndexPath: indexPath];
  return [object tableView: tableView representationAsCellForRowAtIndexPath: indexPath];
}

这意味着任何需要显示电话号码,地址等的数据源都可以询问表视图单元格所代表的对象是什么。数据源本身不再需要有关所显示对象的任何信息。

“但是等等,”我听到一个假设的对话者的感言,“这不会破坏 MVC吗?您不是将视图细节放入模型类中吗?”

不,它不会破坏MVC。在这种情况下,您可以将类别视为Decorator的实现;所以PhoneNumber是一个模型类,但是PhoneNumber(TableViewRepresentation)是一个视图类。数据源(控制器对象)在模型和视图之间进行中介,因此MVC架构仍然适用。

您也可以在Apple的框架中看到使用类别作为装饰。NSAttributedString是一个模型类,其中包含一些文本和属性。AppKit提供了NSAttributedString(AppKitAdditions),UIKit提供了NSAttributedString(NSStringDrawing),装饰器类别,可将绘图行为添加到这些模型类中。


用作数据源和表视图委托的类的好名字是什么?
约翰·卡尔森

1
@JohanKarlsson我经常将其称为数据源。也许这有点草率,但是我经常将两者结合起来,以至于我的“数据源”是对Apple更严格的定义的改编。

1
本文:objc.io/issue-1/table-views.html提出了一种处理多种单元格类型的cellForPhotoAtIndexPath方法,您可以在数据源的方法中计算出单元格类,然后调用适当的工厂方法。当然,只有当特定的类可预测地占据特定的行时,那才是可能的。我认为,您的模型生成视图分类系统在实践中要优雅得多,尽管这可能是MVC的非常规方法!:)
Benji XVI

1
我已经尝试在github.com/yonglam/TableViewPattern演示这种模式。希望对某人有用。
2014年

1
我会投票表决最终没有用于动态选择方法。这是非常危险的,因为这些问题仅在运行时出现。没有自动的方法来确保给定的选择器存在并且正确键入,并且这种方法最终会崩溃并且难以维护。但是,另一种方法非常聪明。
mkko 2014年

3

人们确实倾向于将大量内容打包到UIViewController / UITableViewController中。

委托给另一个类(而不是视图控制器)通常效果很好。委托不一定需要返回到视图控制器的引用,因为所有委托方法都传递了对的引用UITableView,但是他们将需要以某种方式访问​​要委托的数据。

重组以减少篇幅的一些想法:

  • 如果要在代码中构建表格视图单元格,请考虑从nib文件或情节提要中加载它们。情节提要允许原型和静态表格单元-如果您不熟悉这些功能,请查看这些功能

  • 如果您的委托方法包含很多'if'语句(或switch语句),那是可以进行一些重构的经典标志

我一直感到有点可笑,因为它UITableViewDataSource负责获取正确的数据位配置视图以显示它。一个很好的重构点可能是更改您的cellForRowAtIndexPath视图以获取需要显示在单元格中的数据的句柄,然后将单元格视图的创建委派给另一个委派(例如,制作一个CellViewDelegate或类似的事物),并在适当的数据项中传递该委派。


这是一个很好的答案。但是,我的脑海中出现了两个问题。为什么您会发现很多if语句(或switch语句)设计不正确?您实际上是说很多嵌套的if和switch语句吗?您如何重构以避免if或switch语句?
约翰·卡尔森

@JohanKarlsson的一种技术是通过多态性。如果您需要对一种类型的对象做一件事情,而对另一种类型的对象做别的事情,则使这些对象成为不同的类,然后让他们为您选择工作。

@GrahamLee是的,我知道多态性;-)但是我不确定如何在这种情况下应用它。请对此进行详细说明。
约翰·卡尔森

@JohanKarlsson完成了;)

2

遇到类似问题时,我现在大致正在执行以下操作:

  • 将与数据相关的操作移至XXXDataSource类(该类继承自BaseDataSource:NSObject)。BaseDataSource提供了一些便捷的方法,例如- (NSUInteger)rowsInSection:(NSUInteger)sectionNum;,子类覆盖了数据加载方法(因为应用程序通常具有某种非常规的缓存加载方法,- (void)loadDataWithUpdateBlock:(LoadProgressBlock)dataLoadBlock completion:(LoadCompletionBlock)completionBlock;因此我们可以在从网络更新信息以及完成块时,使用LoadProgressBlock中接收的缓存数据更新UI。我们会使用新数据刷新用户界面,并删除进度指示器(如果有)。这些类不符合UITableViewDataSource协议。

  • 在BaseTableViewController(符合UITableViewDataSourceUITableViewDelegate协议)中,我引用了在控制器初始化期间创建的BaseDataSource。在UITableViewDataSource控制器的一部分中,我只是从dataSource(如- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { return [self.tableViewDataSource sectionsCount]; })返回值 。

这是我在基类中的cellForRow(无需在子类中重写):

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    NSString *cellIdentifier = [NSString stringWithFormat:@"%@%@", NSStringFromClass([self class]), @"TableViewCell"];
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier];
    if (!cell) {
        cell = [self createCellForIndexPath:indexPath withCellIdentifier:cellIdentifier];
    }
    [self configureCell:cell atIndexPath:indexPath];
    return cell;
}

必须由子类覆盖configureCell,并且createCell返回UITableViewCell,因此,如果要自定义单元格,也要覆盖它。

  • 在配置完基础事物之后(实际上,在使用该方案的第一个项目中,此部分可以重复使用之后),剩下的BaseTableViewController子类是:

    • 覆盖configureCell(这通常会转换为向dataSource询问对象以获取索引路径,并将其馈送到单元格的configureWithXXX:方法或获取对象的UITableViewCell表示形式,如在user4051的答案中)

    • 覆盖didSelectRowAtIndexPath :(显然)

    • 编写BaseDataSource子类,该子类负责处理Model的必要部分(假设有2个类AccountLanguage,因此子类将是AccountDataSource和LanguageDataSource)。

这就是表格视图的全部内容。如果需要,我可以将一些代码发布到GitHub。

编辑:一些建议可以在http://www.objc.io/issue-1/lighter-view-controllers.html(具有此问题的链接)和有关tableviewcontrollers的配套文章中找到。


2

我对此的看法是,模型需要提供一个封装在cellConfigurator中的对象数组,称为ViewModel或viewData。CellConfigurator拥有将其序列化和配置单元所需的CellInfo。它为单元提供一些数据,以便单元可以配置其自身。如果您添加一些保存CellConfigurators的SectionConfigurator对象,这对section也适用。我刚开始使用此功能,最初只是给单元格一个viewData,然后让ViewController处理单元格的出队。但我读了一篇指向此gitHub存储库的文章。

https://github.com/fastred/ConfigurableTableViewController

这可能会改变您处理此问题的方式。



1

遵循SOLID原则将解决此类问题。

如果你想你的类有只是一个单一的责任,你应该定义单独DataSourceDelegate类和简单的注入他们的tableView所有者(可能是UITableViewControllerUIViewController或其他任何东西)。这就是您克服关注点分离的方式

但是,如果您只是想拥有清晰易读的代码,并且想要摆脱那个庞大的viewController 文件,并且您在Swif中,则可以使用extensions。单个类的扩展可以写在不同的文件中,并且它们都可以相互访问。但这确实可以解决我所提到的SoC问题。

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.