内容字号:默认大号超大号

段落设置:段首缩进取消段首缩进

字体设置:切换到微软雅黑切换到宋体

抛弃视图控制器 做自己的控制器

2018-05-15 17:00 出处:清屏网 人气: 评论(0

苹果官方文档中提到过 MVC 的软件设计模式。不过,苹果介绍的并不是本质意义上的 MVC 。 关于这一点我之前也有写过一篇文章 ,MVC 是为 Smalltalk 语言设计的一种设计模式。在 Smalltalk 语言里,MVC 的三个组件:模型(model)、视图(view)和控制器(controller)之间都可以相互通信。这就意味着要么视图知道自己持有的模型是如何实现的,要么模型知道自己是如何在视图中被应用的。

我们写 iOS 软件的时候,通常会把可以直接通信的视图和模型称之为“反模式”(PS:不推荐这么做)。我们所谓的 MVC 更准确的说其实是“模型-视图-适配器(Model - View - Adapter)”。我们说的“视图控制器”其实只是模型和视图中间沟通的一个桥梁。笼统来讲,我觉得这是对正统MVC一个不错的改良——不是把视图和模型绑在一起,而是通过一个适配器把模型和视图联系起来,这个方法不错。然而,不得不说,在我工作中涉及到的大部分系统中,模型和视图都是分开的。

以上就是为什么 iOS 开发里会有视图控制器的原因:用来连接模型和视图。但是这种模式的编码会产生一些问题:有一些代码看起来既不属于模型,也不属于视图,所以我们就把这些代码放到了视图控制器里,最后视图控制器变得超级臃肿。关于这个问题,我在博客里面讨论过 很多次 ,但这次我想说的不是这个问题。

我私下里听到过很多关于 UIViewController 的谈论。我认为 UIViewController 这个基本类写的不是很好,这一点你用 UIKit 用得越久就越能感觉到。听说 UIViewController 这个基本类型有 1 万到 2 万行代码(那是几年前了,现在可能已经超过两万行了)。

当我们需要把 UIView 和一个模型联系起来的时候,我们通常会把视图控制器分成一个个小的视图控制器组件,然后再组装到一起。

但是,这样做太小题大做了。一个小地方没处理好,就会出现很多 bug,而且这样的 bug 很难修复,也没有什么提示。然后,当你终于找到 bug 的时候,发现通常都是 didMove 或者 willMove 的调用顺序不对导致的。其实,出现 didMovewillMove 的时候就已经说明这些组件一些内部状态需要清理了。

这样的情况我自己就遇到过两次。第一次是我把视图控制器放在了 tableViewcell 里。出现的 bug 就是,table view 里面的一些内容总会莫名其妙地消失。然后过了好几个月,我才意识到我对 table view cells 的生命周期理解有误。

在我改正了一些对 -addChildViewController 的调用之后,程序就正常运行了。

这件事让我看到了一个很大的问题:视图控制器的视图并不是一个普普通通的视图,而是一个视图控制器的视图。它有自己一些特性。

回过头去看,一切都很明显。 UIViewController 怎么知道什么时候该去调用 viewDidLayoutSubviews ? 肯定是 view 向它发送了请求,这就意味着视图控制器对视图是有一些依赖的。

第二次是最近碰到的,这次的问题出现在我把一个视图控制器的视图作为 text fieldinputAccessoryView 时。当时我在实现一个通信软件(类似 iMessage)里 textField 贴在屏幕底部的功能,整个过程十分挫败。我花了整整一天时间都没搞定,最后还是把这个视图转换成了一个普通的视图。

所以,我们通常想 UIViewController 应该做的是哪些事情呢?

  • 承载视图
  • 把模型和视图联系起来

UIViewController 还做了哪些我们并不十分在意的事呢?

  • 为子视图控制器提供存储
  • 把外观和过渡动画推送给子视图控制器
  • 可以在类似 UINavigationController 的容器中显示
  • 内存过低通知
  • 处理状态栏
  • 保存状态、恢复状态

知道了这些,在一些特殊情况下,需要我们做一个替代视图控制器的东西时,我们就知道了哪些东西是我们并不需要的。我喜欢这样,因为这样可以快速地解决问题,同时也符合我“自己的事情自己做”的性格。

还有一个问题,这个东西怎么命名呢?我觉得命名成一个视图控制器不太好,很容易被误解为一个 UIViewController 的子类。或者,我们就叫它 Controller ?我觉得可以( 尽管我之前可能有其他观点 ),因为它的作用就是 iOS MVC 设计框架中控制器的作用(把视图和模型联系起来),但是还有其他一些备选: Binder (粘合), Binding (捆绑), Pair (配对), Mediator (中介), Concierge (前台)。

这个做法还有一个好处是, 特别好写

class DestinationTextFieldController {

    var destination: Destination?

    weak var delegate: DestinationTextFieldControllerDelegate?

    let textField = UITextField().configure({
        $0.autocorrectionType = .no
        $0.clearButtonMode = .always
    })
    
}

虽然可能不用 UIViewController 的子类,然后写这样一个东西,会有人喊,“异教徒!烧死他!”。但是,当 UIViewController 没有把自己该做的事情做好的时候,我们就应该抛弃它。

现在大家已经知道怎么给自己的新对象加新功能了。在我的这个例子中,控制器成了 textField 的代理,文字变化时发出事件(以及域元数据 domain metadta),同时提供更新视图(这个例子中是 textField )的接口。

extension DestinationTextFieldController {
	var isActive: Bool {
		return self.textField.isFirstResponder
	}

	func update(with destination: Destination) {
		self.destination = destination
		configureView()
	}
	
	private func configureView() {
		textField.text = destination.descriptionForDisplay
	}
}

使用这种新的控制器你还需要做其他几件事:

  • 你得新建一个实例变量来存储数据
  • 你得负责一些触发事件——因为它不是一个视图控制器,没有 -viewDidAppear
  • 因为新的控制器已经不在 UIKit 框架里面了,所以其他一些 UIKit 的特性( UITraitCollection , safe area insets ,或者 UIResponder )都不能再使用了。而需要你自己实现。

使用这个对象不是很难,不过你还是需要写明它的数据存储方式, 防止它的内存被回收。

class MyViewController: UIViewController, DestinationTextFieldControllerDelegate {


	let destinationViewController = DestinationTextFieldController()
	
	override func viewDidLoad() {
		super.viewDidLoad()
		destinationViewController.delegate = self
		view.addSubview(destinationViewController.view)
	}
	
	//handle any delegate methods

}

话说回来,即使你使用我说的这个方法,其他大部分的视图应该还会是视图控制器和 UIViewController 的子类。不过,在某些特殊的情况下,整合一个视图控制器会耗费你过多的精力,这时候采用这种方法,就可以避免再次被 UIKit 折磨。

分享给小伙伴们:
本文标签: 控制器

相关文章

发表评论愿您的每句评论,都能给大家的生活添色彩,带来共鸣,带来思索,带来快乐。

CopyRight © 2015-2016 QingPingShan.com , All Rights Reserved.

清屏网 版权所有 豫ICP备15026204号