为什么我爱Apple MVC

声明:我仍在学习iOS编程,因此请将此作为学习笔记而不是教程。 干杯!

MVC摇滚。 不严重,这是我见过的最干净的架构。 让我们看一下这张图:

现在告诉我这种三组分结构如何变得更清洁。 也许MVVM?

它也仅由3个组件组成。 但是,等等,现在我们有了另一层称为binding ,而不是普通的事件驱动的MVC体系结构 。 是的,绑定有其优点,但是恕我直言,它不必要地使事件流难以理解。 因此,它是比Apple MVC更复杂的体系结构。

我认为人们之所以如此讨厌Apple MVC,是因为用它编写大型视图控制器很容易。 但是首先, 大规模视图控制器有什么不好? 我的意思是,如果视图控制器职责明确且结构合理,因此非常易于维护,那么编写大型视图控制器是一种不好的做法? 对我来说,更少的代码并不等于更好的代码。 易于理解对代码而言更为重要。 可以在一种方法中配置表视图(可能是viewDidLoad() ),并带有一些UIKit closurising扩展,并使代码比将经典的委托和数据源协议应用于视图控制器本身要少得多,但实际上,维护该视图非常容易具有协议扩展的代码,而不是单一方法中的一堆多层代码。

想一想。 委托扩展是这样的:

 extension MyViewController: UITableViewDelegate { 
// Protocol implementations
}

它基本上意味着MyViewController在说“我可以管理UITableView ,这就是我要管理的方式”。 换句话说, 所有与 UITableView 相关的代码都在此extension中 。 这真太了不起了。 即使您的视图控制器具有数千行代码,您也可以轻松地在此扩展中查找任何表视图问题。

对我来说,如果视图控制器过于复杂,可能是因为

  1. 一个视图控制器的职责过多。
  2. 该方法设计不良。
  3. 视图控制器中存储的属性过多。

那么如何改进呢?

确定何时使用视图控制器(而不是仅使用视图)

通常,人们认为视图控制器代表一个场景 ,因此仅在出现新场景时才应使用视图控制器。 这不是真的。 每当场景过于复杂时,都应将其分解为几个子场景-或子视图控制器。 像UINavigationController一样。 它的职责仅是管理其子视图控制器的转换 。 对于UITableViewController ,工作就是管理表视图 。 因此,如果您的视图控制器中有一个表视图,并且视图控制器不是UITableViewController ,则IMO可能是您做错了什么。 为什么?

如果您不使用UITableViewController的原因是要使表格视图成为根视图的子视图(以便您可以在表格视图周围移动并向场景中添加其他控件和视图),请考虑添加一个UITableViewController子视图控制器用于管理表视图,以便您可以将所有表视图特定的代码移出父视图控制器。 父视图控制器不需要了解UITableViewDelegateUITableViewDataSource以及所有其他相关协议(如UITableViewDataSourcePrefetchingUITableViewDropDelegateUITableViewDragDelegate的实现细节。 它只需要将数据传递给子视图控制器,并从中接收必要的事件或更新的数据,并且这些任务可以封装到子视图控制器的一个非常干净的委托协议中。

或者可以这样认为:视图控制器基本上是其托管视图的委托。 查看以下方法:

 func viewDidLoad() 
func viewWillAppear(Bool)
func viewDidAppear(Bool)
func viewWillDisappear(Bool)
func viewDidDisappear(Bool)
func viewSafeAreaInsetsDidChange()
func viewLayoutMarginsDidChange()
func viewWillLayoutSubviews()
func viewDidLayoutSubviews()

如果有UIViewDelegate协议,它将看起来像这样。 因此, 如果需要通过观察特定生命周期或任何布局事件来管理特定视图,请考虑将其设为子视图控制器的根视图。 当然,如果您需要选择另一个场景,请为该场景创建一个新的视图控制器。

综上所述,在以下情况下应使用新的视图控制器

  1. 父视图控制器太复杂了。
  2. 您需要观察视图的生命周期。
  3. 您需要从场景中选择场景。

设计简单的方法

从语义上讲,有几种方法:

  1. Getter方法 :包括工厂方法和一些委托方法,例如带有应当和可以前缀的那些方法。 基本上任何具有返回值或返回对象的人。
  2. 通知方法 :在发生某种情况时调用。 viewDidLoad()是一个很好的例子。 所做的一切都将是重要的。 一些getter方法也可以同时通知方法,例如textViewShouldEndEditing(UITextView)
  3. 动作方法 :它们描述它们的所有者对象可以做什么。 它们的目的是做某事或更改某事,因此其中大多数都有其名称中描述的特定副作用。 例如, loadView()加载拥有的UIViewController实例的视图。 对于此方法,其中应仅包含视图加载代码。 如果需要在操作方法之前或之后执行某些操作,请尽可能将代码放入通知方法中。

视图控制器之所以容易失控的原因之一是,它很想在通知方法(例如viewDidLoad()编写所有内容。 由于通知方法不会告诉您它们在名称中的作用 ,因此很难找到一段代码。 因此,请帮自己一个忙,将复杂的通知方法分解为命名的动作函数或动作方法。 如果它们不应该成为对象API的一部分,请不要忘记将它们设为private

理想情况下,这是一种通知方法:

 func viewDidLoad() { 
// Some single-line configuration code.
self.dataFetcher.delegate = self
self.title = "MyViewController"

// Some action methods.
self.dataFetcher.fetchData()
self.showAlert()
}

在这种情况下,我使用一个拥有的对象来获取数据,而不是用这种方法编写所有的数据获取代码和数据处理代码。 当然,您可以这样做,但是当数据获取代码比其他配置代码更多时,该方法就会被这些数据获取代码所劫持。 而且,如果您需要解决某些数据获取问题,则只需查看它们的名称就应该能够在某些相关方法中找到它们,因此viewDidLoad()显然不是放置代码的好地方。

定义尽可能少的存储属性

因为他们使人感到困惑和疲惫。 对于每个存储的属性,您都必须考虑为什么要更新它,何时更新,何时更新,应该做什么以及何时对其进行了不适当的更新,等等。 我的意思是,如果您可以从其他地方检索相同的信息,请考虑不要将其作为存储的属性。

例如,如果您的视图控制器中有一个文本视图,是否真的有必要在视图控制器中定义一个单独的text属性? 如果应该始终自动同步它们,为什么不将计算属性作为textView.text的简写呢? 这样可以避免很多不必要的问题。

就我自己而言,我什至让视图控制器使用唯一的UUID从数据源实时检索其相应的模型值。 这样,我在视图控制器的API中唯一需要存储的属性是id字符串和弱数据源引用。 当视图控制器的状态更新时(例如textView.text),我使用一种数据源方法来立即反映更新。 通过这种方式,我不必太担心自己状态的管理。 视图控制器仅成为数据流的中介者,而无需管理数据本身。

唯一需要具有存储属性的时间是缓存。 换句话说,当从太昂贵甚至不可能的地方检索数据时。 在这些情况下,请使用存储的属性。 否则,请使用函数(方法和计算变量)来检索数据。

结论:面向对象的编程真正意味着什么

有各种各样的应用程序架构声称它们比Apple MVC等OOP架构要好。 但是他们尝试解决的许多问题实际上不是OOP的问题,而是糟糕的实现问题。 以OOP方式进行设计时,应按照职责的分离和指定方式以及对象API的设计方式, 将每个对象都视为艺术品

同样,使用OOP进行设计并不意味着它就不会起作用或反应迟钝。 通过消除存储的属性并在对象之间建立数据流,它可以同时具有很好的功能和反应性,同时仍然是OOP代码。 设置委托和数据源也具有约束力。 唯一的区别是它是对象级绑定而不是事件级绑定。 尽管Apple MVC方式需要至少一个对象(控制器)处于绑定周期,这与不需要进行对象的反应式绑定不同,但实际上维护起来更容易,因为在视觉上,委托实现比反应式要简单。绑定代码,因为它具有较少的块级别

考虑一下。 下次尝试将更多时间花在思考如何设计对象上,而不是学习如何实现一些新颖的体系结构。 仅使用MVC听起来可能会更容易,但实际上设计一个简单干净的MVC应用程序确实很困难。 但是,如果操作正确,它将像艺术品一样美丽,并且易于维护,因此您无需任何特定原因就可以对其进行访问。