使用loadView()编写更清晰的Swift View代码

最初发布于 swiftrocks.com

在使用情节提要和以编程方式编写视图之间的选择非常隐晦。 过去已经处理过这两个问题,我个人保证完全通过视图代码编写项目,因为它允许多个人在同一个类中工作,而不会造成棘手的冲突,并且代码审查更加轻松。

从以编程方式编写视图的实践开始,人们面临的一个普遍问题就是将代码放在首位。 如果您遵循将所有与视图相关的内容放入视图控制器的常规情节提要板方法,那么最终获得一个巨大的神类非常容易:

final class MyViewController: UIViewController { 
private let myButton: UIButton = {
//
}()

private let myView: UIView = {
//
}()

//Other 10 views or so

override func viewDidLoad() {
super.viewDidLoad()
setupViews()
}

private func setupViews() {
setupMyButton()
setupMyView()
//setup for all the other views
}

private func setupMyButton() {
view.addSubview(myButton)
//10 lines of constraints
}

private func setupMyView() {
view.addSubview(myView)
//10 lines of constraints
}

//All other setups

//All ViewModel logic

//All the button clicking logic and stuff...
}

您可以通过将视图移至其他文件并向该视图控制器添加引用来使此效果更好,但是您仍然必须在视图控制器中填充不应包含在其中的内容,例如约束代码和其他形式的视图设置-更不用说您现在有两个不同的视图属性( myView和native view ),没有充分的理由:

 final class MyViewController: UIViewController { 

let myView = MyView()

override func viewDidLoad() {
super.viewDidLoad()
setupMyView()
}

private func setupMyView() {
view.addSubview(myView)
//10 lines of constraints
myView.delegate = self
}
}

知道太多的 Giant View Controller和View Controller很难维护和扩展。 在像MVVM这样的体系结构中,视图控制器应主要充当视图本身与视图模型之间的路由器-知道如何设置视图或约束视图不是它的工作,它应该只是来回路由信息。

在大多数代码都是视图本身的View Code项目中,为了拥有可维护的项目,在体系结构的各个方面之间明确划分职责非常重要。 您希望实际的视图代码与View Controller完全分开-幸运的是,有一种非常简单的方法可以覆盖UIViewController的原始view属性,从而使您可以为视图维护单独的文件,同时仍然确保View Controller不必进行任何类型的视图设置。

loadView()

loadView()是一个UIViewController方法,您不会经常看到它,但是它对于视图控制器的生命周期非常重要,因为它首先负责使view属性存在。 使用Storyboards时,此方法将加载您的笔尖并将其附加到view ,但是当手动实例化视图控制器时,此方法所做的只是创建一个空的UIView 。 您可以覆盖它以更改此行为,并将任何类型的视图添加到视图控制器的view属性。

 final class MyViewController: UIViewController { 
override func loadView() {
let myView = MyView()
myView.delegate = self
view = myView
}

override func viewDidLoad() {
super.viewDidLoad()
print(view) // a MyView instance
}
}

注意, view将自动将其自身约束到视图控制器的边缘,因此外部myView不需要约束!

现在, view是对我的自定义视图(在本例中为MyView )的引用。 您可以在其自己的单独文件中构建视图的全部功能,而无需View Controller知道任何有关它的信息。 真好!

要访问MyView的内容,可以将view MyView为自定义类型:

 var myView: MyView { 
return view as! MyView
}

这看起来有些怪异,但这是因为无论您更改view的类型, view仍将定义为UIView

为了避免在我的View Controller之间重复此代码,我喜欢在CustomView协议中定义此行为并具有相关的类型要求:

 public protocol HasCustomView { 
associatedtype CustomView: UIView
}

extension HasCustomView where Self: UIViewController {
public var customView: CustomView {
guard let customView = view as? CustomView else {
fatalError("Expected \(CustomView.self) but got \(type(of: view)) instead")
}
return customView
}
}

结果是:

 final class MyViewController: UIViewController, HasCustomView { 
typealias CustomView = MyView

override func loadView() {
let customView = CustomView()
customView.delegate = self
view = customView
}

override func viewDidLoad() {
super.viewDidLoad()
customView.render() //some MyView method
}
}

如果每次都定义CustomViewCustomViewCustomView困扰,则可以更进一步,并在通用类中定义此行为:

 class CustomViewController: UIViewController { 
var customView: CustomView {
return view as! CustomView //Will never fail as we're overriding 'view'
}

override func loadView() {
view = CustomView()
}
}

final class MyViewController: CustomViewController {
override func loadView() {
super.loadView()
customView.delegate = self
}
}

我个人不喜欢通用方法,因为编译器不允许通用类具有@objc方法的扩展,这禁止您在扩展中使用类似UITableViewDataSource协议。 但是,它允许您跳过覆盖的loadView()除非需要执行一些特殊的操作(例如设置委托),这确实有助于保持View Controller的整洁。

结论

重写loadView()是使View Code项目更易于阅读和维护的好方法,并且我在最近的几个项目中一直特别使用HasCustomView ,因此效果很好。 视图编码可能不是您的事,但是它为表带来了很多优点。 尝试一下,看看哪种方法更适合您。

让我知道您是否有其他方法可以在没有情节提要的情况下定义项目中的视图,以及可能存在的任何其他问题,评论或反馈。

参考文献和优秀读物

Apple文件:loadView()