原创作者:Paul Hudson
原文链接:How to move view code out of your view controllers
这是解决 “臃肿的ViewController” 这个问题系列教程中的第三部分:
- 如何在 iOS app 中使用协调模式
- 如何把数据源和代理从你的 ViewController 抽离出来
- 如何把关于视图构建的代码搬离 ViewController
想要用代码而不是使用 interface Builder 来编写你的用户界面的原因有很多: 你可能会发现使用源代码管理更容易,也可能会发现它是一个更具表现力的环境,用于编写复杂的自动布局约束,或者你可能更喜欢通过组合函数来创建界面。
但是,这样做有正确的方法也有错误的方法,太多的应用程序会做出错误的决定。你会看到,即使你将视图控制器视为视图层的一部分,而不是控制器层(MVC中的V而不是C),它们仍然应该将实际的视图代码留给 UIView
及其许多子类。
尽管应该由 UIView
或其子类来处理视图代码是无需争论的,但是你经常会在视图控制器的 viewDidLoad()
方法内部看到各种视图的创建和配置,这几乎肯定是错误的。 在视图加载完成时调用该方法,而不是在开始创建视图时调用该方法。
将此类代码移动到自定义 UIView
子类中,不仅使视图控制器更加简单,而且还允许你根据需要在不同的地方重用视图代码。 更好的是,一旦将视图控制器变得更小,更简单,您就可以开始更多地依赖于视图控制器的封闭性以使其也可重复使用,最终得到的是松散耦合的更灵活的代码。
与其抽象地讨论所有这些,不如看一个具体的例子来说明人们如何混合视图和视图控制器。 将您的视线投向这种代码:
backgroundColor = UIColor(white: 0.9, alpha: 1)
let stackView = UIStackView()
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.spacing = 10
view.addSubview(stackView)
stackView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor).isActive = true
stackView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor).isActive = true
stackView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor).isActive = true
stackView.axis = .vertical
let notice = UILabel()
notice.numberOfLines = 0
notice.text = "Your child has attempted to share the following photo from the camera:"
stackView.addArrangedSubview(notice)
let imageView = UIImageView(image: shareImage)
stackView.addArrangedSubview(imageView)
let prompt = UILabel()
prompt.numberOfLines = 0
prompt.text = "What do you want to do?"
stackView.addArrangedSubview(prompt)
for option in ["Always Allow", "Allow Once", "Deny", "Manage Settings"] {
let button = UIButton(type: .system)
button.setTitle(option, for: .normal)
stackView.addArrangedSubview(button)
}
那甚至不是一个复杂的用户界面,但这是你会在 viewDidLoad()
中看到的那种东西,尽管那是放置它的可怕地方。
上面的所有代码都是视图代码,因此需要这样对待。 它不是控制器代码,即使使用 Apple 的混乱定义,它也不是视图控制器代码。 它是 View 的代码,属于 UIView
的子类。
进行此更改很容易:复制所有代码,将其粘贴到 UIView
的新子类 SharePromptView
中,然后将视图控制器视图的类更改为新的子类。
最终的 SharePromptView
类应如下所示:
class SharePromptView: UIView {
override init(frame: CGRect) {
super.init(frame: frame)
createSubviews()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
createSubviews()
}
func createSubviews() {
// all the layout code from above
}
}
所有的 UIView
子类必须实现 init(coder:)
,但是当你在代码中创建你的 UI 时,你还需要添加 init(frame:)
,createSubviews()
方法来构建视图。
由于有了自定义的 UIView
子类,你现在可以从视图控制器中提取大量代码:
class ViewController: UIViewController {
var shareView = SharePromptView()
override func loadView() {
view = shareView
}
}
loadView()
方法是以编程方式加载视图的正确位置。
注意:拥有专用的 shareView
属性可让您访问在 SharePromptView
中声明的任何属性,而不必强制转换视图类型。
那么,视图控制器应该是什么?
我经常问这个问题。 我已经讨论过如何使用协调器完成导航,以及如何从视图控制器中分离出委托和源代码,所以视图控制器应该做什么?
在我自己的代码中,我努力使视图控制器尽可能简单,因为我亲眼目睹了失去控制会发生什么。 这意味着他们:
- 负责视图生命周期事件,例如
viewDidLoad()
,viewWillAppear()
和traitCollectionDidChange()
。 - 有一些
@IBOutlets
和@IBActions
,尽管这些动作实际上应该只是在其他地方运行一个方法。 请记住,你可以根据需要在视图中添加出口。 - 在模型和视图之间穿梭数据。 这不会增加诸如格式化数据之类的代码,它只是将值绑定到他们的视图并将更改从用户发回。
他们还可以根据我在做什么来处理模型的获取和存储; 我觉得非常实用。
正如 Dave DeLong 所说的那样,“ 300行视图控制器或崩溃”。当你编写301行时,不会发生任何不好的事情,当然我不会为了让 SwiftLint
通过检测脱身而将视图控制器重构为扩展。 但这确确实实表示你正在使视图控制器承担过多的责任。
网友评论