在为苹果平台构建应用程序时,一个非常常见的问题是,在哪里放置许多不同视图控制器使用的公共Function。一方面,我们想要尽可能避免代码重复,另一方面,我们想要有一个良好的关注点分离,以避免出现可怕而大型的视图控制器ViewController
。
处理网络中的加载和错误的状态就是这种常见功能的一个例子。应用程序中的大多数视图控制器在某个时候都需要异步加载数据——这个操作会花费一些时间,且有可能失败。为了让用户知道发生了什么,我们通常希望在加载时显示某种形式的活动指示器(activity indicator ),并在操作失败时显示一个错误视图。
那么把这种功能放在哪里呢?很常见的解决方案是创建一个BaseViewController
的基类,然后让其他需要这些功能的视图控制器继承它:
class BaseViewController: UIViewController {
func showActivityIndicator() {
...
}
func hideActivityIndicator() {
...
}
func handle(_ error: Error) {
...
}
}
虽然做上面这样的事情可能看起来不错——因为它非常方便——但它通常也会导致一些棘手的架构问题。BaseViewController
很容易成为各种功能的集散地,导致其很难维护。
BaseViewController
方法的另一个问题是,它将所有视图控制器都锁定为从单个类继承。这通常不是一个好情况,因为它使您在为给定类的父类选择最合适的类时的灵活性更小。例如,如果我们想实现一个基于UITableView
的视图控制器,从UITableViewController
继承可能是一个更好的选择。
现在,让我们看看如何使用子视图控制器( child view controllers)作为“插件”,使我们能够轻松地混合和匹配通用功能,而不必求助于单个基类。
子视图控制器
自iOS 5以来,子视图控制器(Child view controllers)就已经出现了,但它仍然是一个经常被忽视的特性。这是一个简单的概念-就像你可以用子视图和父视图建立UIView
的层次结构一样,你可以用视图控制器做同样的事情。
使用子视图控制器的原因是,它们可以访问与父视图控制器完全相同的事件(如viewDidLoad
、viewWillAppear
等),而不必是它的子类。它们还可以负责自己的内部布局,并执行自己的控制器逻辑。这使我们能够将我们的代码构建得非常像一组模块化插件,可以根据需要添加和删除它们。
例如,我们实现一个子视图控制器,在加载数据时想要显示一个活动指示器(activity indicator ),我们就可以直接添加它:
lass LoadingViewController: UIViewController {
private lazy var activityIndicator = UIActivityIndicatorView(activityIndicatorStyle: .gray)
override func viewDidLoad() {
super.viewDidLoad()
activityIndicator.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(activityIndicator)
NSLayoutConstraint.activate([
activityIndicator.centerXAnchor.constraint(equalTo: view.centerXAnchor),
activityIndicator.centerYAnchor.constraint(equalTo: view.centerYAnchor)
])
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
// We use a 0.5 second delay to not show an activity indicator
// in case our data loads very quickly.
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in
self?.activityIndicator.startAnimating()
}
}
}
与使用BaseViewController
相比,上述方法的主要好处是,我们的应用程序中任何需要显示加载指示器的视图控制器都可以简单地将LoadingViewController
作为子控制器添加。它还让我们包含所有的逻辑,进入显示加载指标在一个地方,而不是让它和一些完全无关的功能聚在一起。
如何添加和移除子视图控制器
我们如何使用新的LoadingViewController
呢?UIViewController
有一个API允许我们添加子视图控制器:addChildViewController
,但事实证明,事情并不是简单调用方法那么简单。
为了添加子视图控制器,我们需要执行以下操作:
//将子视图控制器的视图移动到父视图
parent.view.addSubview(child.view)
//将视图控制器添加为子控制器
parent.addChild(child)
// 通知子控制器它已经被移动到了父控制器
child.didMove(toParent: parent)
然后,要删除子视图控制器,我们还需要执行3个不同的步骤:
// 通知子控制器它将要被移动到父控制器
child.willMove(toParent: nil)
// 从父控制器上移除子控制器
child.removeFromParent()
//从子控制器视图的父视图中移除自己
child.view.removeFromSuperview()
如果你想开始在你的应用中大量使用子视图控制器,每次都这样做会很快变得乏味。因为它符合对抽象方法的3个要求——它是重复的,无聊的和容易出错的——让我们来抽象它吧!
让我们在UIViewController
上做一个扩展,使得处理子视图控制器更简单:
extension UIViewController {
func add(_ child: UIViewController) {
addChild(child)
view.addSubview(child.view)
child.didMove(toParent: self)
}
func remove() {
guard parent != nil else {
return
}
willMove(toParent: nil)
view.removeFromSuperview()
removeFromParent()
}
}
在我们的app中,我们现在可以简单地调用add()
和remove()
方法来管理子视图控制器。
使用子视图控制器
牛逼的是,因为只是UIKit负责布局,和发送所有的标准UIViewController
事件给我们的子视图控制器,我们所要做的就是添加和移除它。下面是我们如何超级容易地添加支持显示和隐藏加载指示器在一个列表视图控制器:
class ListViewController: UITableViewController {
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
loadItems()
}
private func loadItems() {
let loadingViewController = LoadingViewController()
add(loadingViewController)
dataLoader.loadItems { [weak self] result in
loadingViewController.remove()
self?.handle(result)
}
}
}
很好!最好的部分是,我们所有的视图控制器现在可以利用这个功能,无论他们从那个超类继承的。
处理错误的显示
现在我们有了一个视图控制器我们可以想插件一样放入用于加载状态,错误状态的处理也是如此。与我们之前创建LoadingViewController
的方式类似,我们可以创建一个ErrorViewController
来显示一条错误消息。假设我们还包括一个Reload按钮在我们的UI中,每当Reload按钮被点击,我们就调用reloadHandler
闭包:
class ErrorViewController: UIViewController {
var reloadHandler: () -> Void = {}
}
就像我们可以简单地添加LoadingViewController
作为一个子控件一样,我们现在可以做同样的事情来显示一个错误视图:
private extension ListViewController {
func handle(_ error: Error) {
let errorViewController = ErrorViewController()
errorViewController.reloadHandler = { [weak self] in
self?.loadItems()
}
add(errorViewController)
}
}
总结
将您的代码结构化为模块化插件,而不是过多地依赖于子类化,可以使您的代码更容易扩展和维护。对于几乎所有的代码库,有一件事是真实的,那就是它们需要适应和更改新的特性或SDK的新版本,而将公共功能构建为独立的子视图控制器确实有助于尽可能轻松地做到这一点。
虽然我并不是建议您完全放弃继承,但根据需要设计可组合的api通常是一种更灵活的方法。
感谢你的阅读!
翻译自Using child view controllers as plugins in Swift