优雅地定制 UITableViewController

UITableViewController 是专门设计用来应付UITableView的视图控制器,讲道理用它是比用普通的ViewController + Data Source & Delegate 要方便很多的,它有很多便利的功能能加速我们开发TableViewController。

  • clearSelectionOnViewWillAppear
    若是在UIViewController上自己实现倒也是很简单。其实这个功能的名字几乎已经完全说明了他是怎么实现的。

  • 键盘监听与输入框防遮挡
    这个功能可以说是UITableViewController一大非常便利的功能。它提供了在列表视图中非常完美的输入框键盘防遮挡功能。只要你聚焦于其内部的某个输入框,他一定会以一种十分恰当的方式让聚焦框露出来。
    但我们自己在UIViewController上实现的类似功能是有些费劲的。

  • Refresh Control
    其内部的一个好处就是,在目前主流兼容的版本中,你不需要再做版本适配,因为它是从iOS 6起兼容的。而UIScrollView中的相同api是从iOS 10 起。如果你的项目是iOS 10以上起步的话,就忽略这个优点吧。

问题

UITableViewController的主控视图(UIViewController.view)是UITableView,而且他是继承了UIScrollView,所以当你直接往上面添加一个子视图(比如Toast、浮动按钮)的时候,你会发现这些子视图会跟着你页面一起上下滚动。
emmmm… 我当时在给公司项目接入toast的时候就是遇到了这个问题,所以我一怒之下抛弃了UITableViewController

UIViewController + Data Source & Delegate

如果我们直接用UIViewController,并使tableView作为其view的子视图的话,toast的问题就可以解决,因为toast所坐落的父视图并不是一个滚动视图。
就酱,我以为从此我找到了究极解决方案,直到有一天在我写Terrace这个库的时候,一个UITableViewCell内置输入框键盘防遮挡的效果迟迟不能实现成我满意的样子。就在这个时候,我想起了曾经被我抛弃的UITableViewController处理起键盘防遮挡如丝顺滑。我决定再试一次UITableViewController

Customizing

关键问题是要给tableView加个座垫儿。

作为子控制器

从iOS 5开始,苹果为UIViewController增加了子视图控制器概念,但类似的概念并不算苹果第一次提出,UINavigationController不就是导航栏爸爸管着一群视图控制器崽子们吗?

UITableViewController 做为子控制器:

1
2
3
4
5
6
7
8
9
10
11
12
13
class WrappedTableViewController : UITableViewController {
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
// 被父控制器添加为子视图后配置约束
if let superview = self.parent?.view {
self.tableView.translatesAutoresizingMaskIntoConstraints = false
self.tableView.topAnchor.constraint(equalTo: superview.topAnchor).isActive = true
self.tableView.bottomAnchor.constraint(equalTo: superview.bottomAnchor).isActive = true
self.tableView.leftAnchor.constraint(equalTo: superview.leftAnchor).isActive = true
self.tableView.rightAnchor.constraint(equalTo: superview.rightAnchor).isActive = true
}
}
}

UIViewController 父控制器:

1
2
3
4
5
6
7
8
class ViewController: UIViewController {
private lazy var tableViewController = WrappedTableViewController(style: .plain)
override func viewDidLoad() {
super.viewDidLoad()
self.addChild(self.tableViewController)
self.view.addSubview(self.tableViewController.tableView)
}
}

这种方式可能会显得仅仅起包裹作用的父控制器没什么事做,当一个座垫儿是不是有点大材小用了。

改变控制的tableView

1
var tableView: UITableView! { get set }

Returns the table view managed by the controller object.

既然它的目的是返回由控制器控制的table view object,我们可以通过override来达到我们的目的,告诉它谁才是真正的主角!

这次作为Container的就可以是一个UIView,而不是用UIViewController。

1
2
3
4
class ContainerView : UIView {
var tableView = UITableView()
// ... view configurations
}

敲黑板敲黑板!注意啦注意啦!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class TableViewController : UITableViewController {
let containerView = ContainerView()

// 控制我们自己的TableView
override var tableView: UITableView! {
get { return self.containerView.tableView }
set {
self.containerView.tableView = newValue
newValue.delegate = self
newValue.dataSource = self
}
}

override func loadView() {
self.view = self.containerView // 将主视图替换为自定义基底视图
self.tableView.delegate = self
self.tableView.dataSource = self
}
}

首先在loadView配置本控制器显示的主视图不再是默认的tableView,而是我们准备的containerView。然后让tableViewgettersetter都指向我们自己的tableView别忘了指定dataSourcedelegate

写在最后

当时我在tableView上解决toast问题和输入框键盘遮挡问题,第一次使用UITableViewController的时候也是我第一次用UITableView的时候。当时还并没有深入了解视图控制器对视图的加载时机和自定义方式,直到后来学会了利用loadView方法。但那个时候我已经在UIViewController + DataSource & Delegate 的方式中走了很久,再直到后来一次在这个方式基础之上处理输入框键盘遮挡问题时觉得很麻烦,才在一次将注意力移回到UITableViewController上。