Swift:第一时间获得正确的MVC

开发人员为什么倾向于在其视图控制器中填充视图? 在Swift中使用MVC的更好方法呢?

我遇到的大多数开发人员都倾向于做真正奇怪的事情。 他们中有些人避免洗衣服和发臭,像地狱一样。 其他人则喝加盐和胡椒粉的咖啡。 但是我看到的最常见的行为是,开发人员不使用视图,而是喜欢在视图控制器中执行所有与视图相关的操作,例如填充,创建动画等。

首先,假设我们有一些描述用户的结构:

其次,我们的任务是用该用户的数据填充一些演示文稿。 许多开发人员的代码,甚至是Apple在其教程中提供的代码,看起来都像下面的怪兽:

罪人,听我说,谁会说:“哦。 嘿。 那就是我通常写的代码。 实际上,这没有什么错。”实际上在几个层面上确实是错的:

  • 视图的内容取决于您何时设置模型。
  • 它不可扩展且不可维护;
  • 这是不可重用的。

而且,我们不应该忘记使@IBOutlets变弱,因为视图控制器的view属性可能会更改,并且我们将对视图持有强大的引用,对此我们不承担任何责任。

如果模型是在视图出现后设置的(例如,它是从Internet下载的),则不会在屏幕上显示,除非进一步进入导航层次结构然后返回。 有很多人在野外用viewDidLoad做到这一点,但更糟糕的是,更新内容的唯一方法是用新模型创建新的视图控制器。 即使这样,如果在将控制器推入导航控制器后设置模型,也不会显示该模型,因为在推入过程中已经调用了viewDidLoad

可扩展性和可维护性在这里也确实很痛苦。 您的视图控制器负责填充整个视图层次并为其设置动画。 目前,只有2个子视图,但是想象一下,您有20个甚至更多。 此外,您必须为它们设置动画或根据模型数据更改它们的外观。 viewWillAppear很快就会变成一团糟。

至于重用,请设想一下这种情况,当您希望在不同的视图控制器上呈现相同的视图和模型关系时,具有何时以及如何获取和处理模型的逻辑不同。 例如,在表格视图中将其显示为单元格,或者将其呈现给来自不同API端点的另一种用户。

在我看来,确实很奇怪,网络上没有那么多声音,他们提出了一种更好,更简单的选择。 在大多数情况下,开发人员不会反对编写任何东西:

让我们考虑一下。 UILabel具有一些复杂的绘制逻辑和字符串处理功能。 它的作用是显示字符串。 但是视图的字符串是什么? 这不是很明显吗? 这是模特!!!

那么,为什么我们的代码在使用自己的视图时却归结为我之前模拟的代码? 有些人会开始争论,那是做MVC的正确方法。 MVC是一种设计模式。 苹果公司在其代码中做到了这一点,我们应该承担义务。 你知道,这使我想起什么? 使用20个类和协议编写具有hello world的企业Java项目。 当然,Java开发人员可以简化事情,但是他们严格遵守GoF和其他主流设计模式,忘记了它们是建议,而不是严格遵循的规则。

让我们从我坚持的观点出发,对我们的代码和演示进行推理。 我们的每种观点都与某种模型紧密相关。 它的设计和呈现方式仅适用于一种模型。 证明? 您将无法使用用户展示屏幕展示考试数学问题。 它的设计不同,子视图也不同。 例如,同样适用于UITableView,但是在这种情况下,我们应该将其模型视为模型数组。 在那种情况下,它适合于1模型与1视图关系的相同方案。

同样适用于动画和其他内容。 View Controller不应在实现细节的底层上处理这些事情。 它应该调用适当的视图方法,负责表示。 因此,我们的视图成为一个独立的实体,只需要模型即可表示。 在这种情况下,我们可以拖动一对模型,然后在周围的任意位置查看并在应用程序周围展示它们,而无需编写其他代码。 这对您来说具有很高的可重用性。 唯一真正的问题是,您应该适当地分解视图和模型,以使实体自包含,而不会明显膨胀代码并使用可重用性。

另一重要的事情是,视图不应修改模型,而应适应模型。 而且模型应该对它们的视图一无所知,因为可以通过几种不同的方式来表示一个模型,但是任何视图都与其模型紧密相关。

此外,视图不应基于用户操作开始任何处理。 他们应该调用控制器@IBActions或调用视图控制器传递给视图的闭包。

因此,我的责任观念如下:

视图控制器

  1. 处理用户交互;
  2. 将模型传递给视图;
  3. 调用负责表示的不同方面的视图方法(例如,显示加载视图);
  4. 启动模型的数据处理逻辑;

查看

  1. 从模型中填充自身,并可能对模型更改做出反应;
  2. 提供隐藏演示的实现细节的界面(例如动画和内容或某些特定设置);
  3. 从笔尖接收其外观和布局;

型号

  1. 处理数据;
  2. 存储数据;
  3. 可能提供观察状态的方法。

是的,看起来像MVVM,但事实并非如此。 但是,如果需要,可以注入视图模型以进行更好的分解。 只需将它们传递给视图,而不是原始模型即可。

至于模型,我将在以后的博客文章中介绍。 现在,让我们看看我的想法在实践中的实现。 这是提供与模型一对一关系的抽象视图:

如果您想以某种方式观察模型状态,那么didSet可能包含更有趣的设置。 但这将对我们的情况有所帮助。 它是通用的,没有约束,因为模型可以是任何东西,我们不能做任何假设,因为我们不期望任何特定的模型行为。

然后,我们为视图控制器创建根视图类,并对其模型进行专用化。 我的命名约定是通过从控制器名称中删除“ Controller”一词来调用它:

请注意,我从@IBOutlets中删除了弱点 。 这是因为我们的视图负责对其层次结构的更改,并且完全可以将子视图移除或读取到该视图层次结构中。

UserView是自包含的。 您可以将其用作UIViewController根视图,也可以将其作为UITableViewCell上的子视图,它的工作原理相同。 您想更改其在单元格中的外观吗? 没问题,只需使用单元格创建一个笔尖并指定您喜欢的约束即可。 那就是您的可重用性。

接下来,让我们通过创建一个负责管理模型和复合视图的抽象类来解决UIViewController的问题:

这里没什么好看的。 通用类,由于与IDPView相同的原因,因为我们对模型一无所知,而仅了解视图,因此它应该是IDPView子类,并且其模型应该与模型具有相同的类型IDPViewController的

同时,我们解决了我前面提到的问题,即视图的内容将根据设置模型的时间而有所不同。 它是如何工作的? 简单,沃森。 如果尚未加载rootView (未调用viewDidLoad ),但是已经设置了模型,则在加载模型时将模型传递给视图。 否则,设置模型后,我们将立即执行此操作。

在这种情况下, UserViewController变得非常简单,因为它不必处理我在上面的责任列表中概述的内容:

IDPViewIDPViewController都是可重用的,它们都解决了我在本文开头概述的问题。 它们的子类在其核心上也很可重用。 如果您尝试为UIViewUIViewController的每个子类分别解决它们,则可能会导致一个blo肿且重复使用率低得多的解决方案。

当然,也有例外。 如果我们在没有实现细节的情况下采用可重用的高级视图,例如UITableViewUITableViewCell (我在说的是那些特定的类,而不是它们的子类),那么它们是通用的,并且没有与任何特定模型紧密耦合,因此我的解决方案不可行。为了适应它们,在特定情况下,我们将需要视图控制器在实现级别上与这些实体一起工作。 例如,我们可以对单元进行子类化和专用化,但是UITableView和其他集合的可重用性和自包含性无疑是另一回事了。

就是这样,伙计们。 祝您有美好的一天,无论您身在何处,都要保持干燥。