[译] 重写 loadView() 方法使 Swift 视图代码更加简洁

12,288 阅读5分钟

究竟选择使用 Storyboards 还是纯代码书写 view 是非常主观的事情。在对两种方式都进行了尝试之后,我个人支持使用纯代码书写 view 来完成项目,这样能够允许多人编辑相同的类而不产生讨厌的冲突,也更方便进行代码审查。

在最开始练习纯代码写 view 的时候,人们普遍遇到的一个问题是最开始不知道将代码放在哪里。如果你采用普通 storyboard 的方式,将所有相关代码都放进你的 ViewController 之中,这样很容易会最终产生一个巨大的上帝类:

final class MyViewController: UIViewController {
    private let myButton: UIButton = {
    	//
    }()
  
  	private let myView: UIView = {
    	//
    }()
  
  	// 其他 10 个左右的 view
  
  	override func viewDidLoad() {
        super.viewDidLoad()
      	setupViews()
    }
  
  	private func setupViews() {
    	setupMyButton()
      	setupMyView()
      	// 设置其他的 view
    }
  
  	private func setupMyButton() {
  	    view.addSubview(myButton)
    	// 十行约束代码
    }
  
    private func setupMyView() {
  	    view.addSubview(myView)
    	// 十行约束代码
    }
  
  	// 所有其他的设置
  
  	// 所有 ViewModel 的逻辑
  
  	// 所有 Button 的点击逻辑等东西...
}

你可以通过把 view 移动到不同的文件并添加引用到原来的 ViewController 之中来改善这样的情况,但是你仍然需要用本不应该在 ViewController 中的内容填满 ViewController,就比如约束代码和其他设置 view 的代码 — 更不用说你现在有两个 view 属性(myView 和原生 view)在 ViewController 之中,而这没有任何好处。

final class MyViewController: UIViewController {
    
	let myView = MyView()
  
  	override func viewDidLoad() {
        super.viewDidLoad()
      	setupMyView()
    }
  
  	private func setupMyView() {
  	    view.addSubview(myView)
    	// 10 行左右的约束代码
    	myView.delegate = self
    	// 现在我们同时有了 view 和 MyView...
    }
}

臃肿的 ViewController 以及逻辑过多的 ViewController 都非常难以管理和维护。在像 MVVM 这样的架构下,ViewController 应该主要作为自身的 View 以及 ViewModel 之间的路由器 -- 设置并且约束 View 并不是它们的职责,ViewController 只应该起到前后传递信息的路由作用

在一个大部分代码都是关于自身 View 的视图代码项目中,能够清晰地拆分你的架构中各部分的职责,对于一个便于维护的项目来说非常重要。你要让你真正构建视图部分的代码完全和你的 ViewController 分离 -- 幸运的是有一个简单的方法,就是重写 UIViewController 中原生的 View 属性。这样做允许你在分离的文件中管理你的多个 View,同时也仍能保证你的 ViewController 不用去设置任何 View。

loadView()

loadView()UIViewController 中并不常见的一个方法,但它是 ViewController 的生命周期中非常重要的一部分,因为它承担着最开始加载出 view 属性的责任。当使用 Storyboard 的时候,它会加载出 nib 并将其附加给 view,但当手动初始化 ViewController 时,这个方法所做的一切就是创建出一个空的 UIView。你可以重写这个方法并改变它的行为,并且在 ViewController 的 view 上添加任何类型的 view。

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

    override func viewDidLoad() {
        super.viewDidLoad()
		print(view) // 一个 MyView 的实例
	}
}

注意 view 会自动的约束自己到 ViewController 的边界,所以并不需要为 myView 设置外部约束!

现在,view 成为了我自定义的 view(在本例中为 MyView)的一个引用。你可以在这个 view 独立的文件内部构建其所有功能,并且 ViewController 对此毫无权限。太棒了!

为了获取 MyView 中的内容,你可以将 View 强制转换为你自己的类型:

var myView: MyView {
    return view as! MyView
}

这样看起来有点奇怪,但这是因为 view 将仍然被定义为 UIView 类型,而不是你为它定义的类型。

为了避免我的 ViewController 中重复出现这样的代码,我喜欢创建一个 CustomView 协议,并在其中定义包含关联类型的行为:

/// HasCustomView 协议为 UIViewController 定义了一个 customView 属性,它是为了去代替普通的 view 属性。
/// 为了实现这些,你必须在 loadView() 方法时为你的 UIViewController 提供一个自定义的 View。
public protocol HasCustomView {
    associatedtype CustomView: UIView
}

extension HasCustomView where Self: UIViewController {
    /// UIViewController 的自定义 view。
    public var customView: CustomView {
        guard let customView = view as? CustomView else {
            fatalError("Expected view to be of type \(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() // 一些 MyView 的方法
	}
}

如果每次都定义这个 CustomView 类型别名会让你有点烦,那么你可以进一步在泛型类中定义这些行为:

class CustomViewController<CustomView: UIView>: UIViewController {
    var customView: CustomView {
        return view as! CustomView // 因为我们正在重写 view,所以永远不会解析失败。
    }

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

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

我个人不太喜欢泛型的方式,因为编译器并不允许泛型类具有的 @objc 方法的扩展,这会禁止你在扩展中拥有 UITableViewDataSource 之类的协议。但是,除非你需要做一些特殊的事情(比如设置委托),它会允许你跳过重写 loadView() 这一步,从而能保持 ViewController 的整洁。

结论

重写 loadView() 是一个让你的视图代码项目更加易于理解、易于维护的好方法,并且我已经使用 HasCustomView 方法获得了非常良好的效果,特别是在最近几个项目中。编写视图部分的代码也许不是你的选择,但是它带来了很多显而易见的好处。尝试一下吧,看看它是不是更适合你。

如果你有更好的定义 view 并且不需要 storyboard 的方法,或者你可能有一些疑问、意见或者反馈,请让我知道。

参考文献和推荐阅读

苹果官方文档:loadView()

如果发现译文存在错误或其他需要改进的地方,欢迎到 掘金翻译计划 对译文进行修改并 PR,也可获得相应奖励积分。文章开头的 本文永久链接 即为本文在 GitHub 上的 MarkDown 链接。


掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 AndroidiOS前端后端区块链产品设计人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划官方微博知乎专栏