原文连接:medium.cobeisfresh.com/dealing-wit… 求大佬们点个关注,会按期写原创和翻译国外最新文章,跟大佬们一块儿学习进步,有问题或者建议欢迎加微信ruiwendelll,拉你们进技术交流群,一块儿探讨学习,谢谢了!react
table view是iOS开发中最重要的布局组件之一。一般咱们最重要的一些页面是表格视图:Feed,设置,列表等。ios
每一个写过复杂table viewiOS开发人员都知道它能够很是快速地实现。它有大量的UITableViewDataSource方法和大量的if和switch语句。编程
我总结了一套原则,我暂时满意,这有助于我克服这些问题。这些技巧的好处在于它们不只适用于复杂的表视图,并且也适用于全部表视图。后端
下面是一个复杂table view的例子:数组
这是PokeBall,Pokémon的社交网络。与全部社交网络同样,它须要一个显与用户的不一样动态的Feed。这些动态包括按天分组的新照片和状态消息。所以,咱们有两个点须要担忧:表视图具备不一样的状态,以及多个cell和section。安全
我看到不少开发者将cell配置过程放在他们的cellForRowAt:
方法中。w咱们思考一下啊,该方法的目的是建立一个cell。 UITableViewDataSource
的目的是提供数据。dataSource不该该为按钮设置字体。bash
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(
withIdentifier: identifier,
for: indexPath) as! StatusTableViewCell
let status = statuses[indexPath.row]
cell.statusLabel.text = status.text
cell.usernameLabel.text = status.user.name
cell.statusLabel.font = .boldSystemFont(ofSize: 16)
return cell
}
复制代码
你应该把样式相关的的代码放在cell内部。若是它在cell的整个生命周期中都会出现,就像标label的字体同样,将它放在awakeFromNib
方法中。微信
class StatusTableViewCell: UITableViewCell {
@IBOutlet weak var statusLabel: UILabel!
@IBOutlet weak var usernameLabel: UILabel!
override func awakeFromNib() {
super.awakeFromNib()
statusLabel.font = .boldSystemFont(ofSize: 16)
}
}
复制代码
除此以外,你可使用属性观察器来设置cell的数据。网络
var status: Status! {
didSet {
statusLabel.text = status.text
usernameLabel.text = status.user.name
}
}
复制代码
这样你的cellForRow方法就清晰可靠多了。数据结构
func tableView(_ tableView: UITableView,
cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(
withIdentifier: identifier,
for: indexPath) as! StatusTableViewCell
cell.status = statuses[indexPath.row]
return cell
}
复制代码
更重要的是,特定于cell的逻辑如今位于一个单独的位置,而不是分散在cell和视图控制器之间。
一般,你使用从某种后端接口得到的模型对象数组来填充table view。而后,cell须要根据该模型对自身进行更改。
var status: Status! {
didSet {
statusLabel.text = status.text
usernameLabel.text = status.user.name
if status.comments.isEmpty {
commentIconImageView.image = UIImage(named: "no-comment")
} else {
commentIconImageView.image = UIImage(named: "comment-icon")
}
if status.isFavorite {
favoriteButton.setTitle("Unfavorite", for: .normal)
} else {
favoriteButton.setTitle("Favorite", for: .normal)
}
}
}
复制代码
您能够建立一个特定于单元格的模型,您将使用模型对象进行初始化,它将处理cell的标题,图像和其余属性。
class StatusCellModel {
let commentIcon: UIImage
let favoriteButtonTitle: String
let statusText: String
let usernameText: String
init(_ status: Status) {
statusText = status.text
usernameText = status.user.name
if status.comments.isEmpty {
commentIcon = UIImage(named: "no-comments-icon")!
} else {
commentIcon = UIImage(named: "comments-icon")!
}
favoriteButtonTitle = status.isFavorite ? "Unfavorite" : "Favorite"
}
}
复制代码
如今,您能够将大量cell的展现逻辑移动到model。而后,您能够单独实例化和单元测试model,而无需在单元测试中进行复杂的模拟和获取单元格。这也意味着您的cell代码很是简单易读。
var model: StatusCellModel! {
didSet {
statusLabel.text = model.statusText
usernameLabel.text = model.usernameText
commentIconImageView.image = model.commentIcon
favoriteButton.setTitle(model.favoriteButtonTitle, for: .normal)
}
}
复制代码
这是与MVVM相似的模式,但应用于单个table view cell。
分section的table view一般会早成代码很乱。你见过相似下面的代码吗:
func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
switch section {
case 0: return "Today"
case 1: return "Yesterday"
default: return nil
}
}
复制代码
这是不少代码,而且不少硬编码索引应该很是简单,易于更改和交换。这个问题有一个简单的解决方案:矩阵。
还记得矩阵吗?这是机器学习相关开发者和一年级CS专业学生使用的东西,但应用程序开发人员一般不这样作。然而,若是你想到一个分段的table view,你正在展现一个section列表。每一个section都是一个cell列表。这听起来像一个数组或矩阵。
这就是你应该对分段table view进行建模的方式。而不是一维数组,使用二维数组。这就是UITableViewDataSource方法的结构:你被要求返回第m个section的第n个cell,而不是table View自己的第n个cell。
var cells: [[Status]] = [[]]
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(
withIdentifier: identifier,
for: indexPath) as! StatusTableViewCell
cell.status = statuses[indexPath.section][indexPath.row]
return cell
}
复制代码
而后咱们能够经过定义Section容器类型来扩展这个概念。此类型不只会保存某个section的cell,还会保留secton标题。
struct Section {
let title: String
let cells: [Status]
}
var sections: [Section] = []
复制代码
如今咱们能够避免使用咱们的硬编码索引,而是能够定义一个section数组并直接返回它们的标题。
func tableView(_ tableView: UITableView,
titleForHeaderInSection section: Int) -> String? {
return sections[section].title
}
复制代码
这样,咱们的数据源方法中的代码就越少,所以越界错误的可能性就越小。代码也变得更具表现力和可读性。
使用多种cell类型可能很是棘手。考虑某种类型的feed,你必须显现不一样类型的cell,如照片和状态。为了保持清楚并避免奇怪的数组索引运算,你应该将它们存储在同一个数组中。
可是,数组是同质的,这意味着您不能拥有不一样类型的数组。想到的第一个解决方案是协议。毕竟,Swift是面向协议的!
你能够定义协议FeedItem,并确保咱们的cell的模型实现该协议。
protocol FeedItem {}
struct Status: FeedItem { ... }
struct Photo: FeedItem { ... }
复制代码
而后你能够定义一个FeedItem数组
var cells: [FeedItem] = []
复制代码
可是,在实现cellForRowAt时:使用此解决方案,咱们能够看到一个小问题。
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cellModel = cells[indexPath.row]
if let model = cellModel as? Status {
let cell = ...
return cell
} else if let model = cellModel as? Photo {
let cell = ...
return cell
} else {
fatalError()
}
}
复制代码
在将模型向上转换为协议时,您丢失了许多实际须要的信息。你已经抽出了cell,但实际上你须要具体的实例。所以,您最终必须检查是否能够转换为类型,而后根据该类型显示cell。
这会有效,但它并不漂亮。向下倾斜本质上是不安全的,并致使optional。你也不知道是否已涵盖全部 状况,由于无数种类型均可以实现你的协议。这就是为何你须要调用fatalError,为了防止你获得一个意外的类型。
当您尝试将协议的实例强制转换为具体类型时,一般会使代码出现问题。当你不须要特定信息时,可使用协议,但可使用原始数据的子集代替。
更好的方法是使用枚举。这样你能够打开它,若是你没有处理全部状况,代码将没法编译。
enum FeedItem {
case status(Status)
case photo(Photo)
}
复制代码
枚举也能够有关联的值,所以您能够将所需的数据放在特定的枚举值中。
你的数组定义保持不变,但你的cellForRowAt:方法如今看起来更清晰:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cellModel = cells[indexPath.row]
switch cellModel {
case .status(let status):
let cell = ...
return cell
case .photo(let photo):
let cell = ...
return cell
}
}
复制代码
这样,你没有强制转换,没有可选项和没有未处理的状况,因此咱们没有错误。
咱们的table view一般具备全部这些状态等等。管理它们可能会很痛苦
假设您有两种可能的状态:显示数据或无数据视图。一个naive的开发人员会隐藏table View并展现无数据视图就来表示“无数据”状态。
noDataView.isHidden = false
tableView.isHidden = true
复制代码
在这种状况下更改状态意味着您必须更改两个bool属性。在视图控制器的另外一部分中,您可能但愿将状态设置为其余部分,而且须要记住设置两个属性。
实际上,这两个bool属性应该始终保持同步。您不能拥有无数据视图而同时以显示一些数据。
考虑现实世界状态数与应用中可能的状态数之间的区别颇有用。两个布尔值有四种可能的组合。这意味着你有两个不想要的无效状态,你须要处理这些状态。
您能够经过定义一个包含屏幕可能处于的全部可能状态的状态枚举来解决此问题。
enum State {
case noData
case loaded
}
var state: State = .noData
复制代码
你还能够定义单个状态属性,这是更改屏幕状态的惟一方法。每次更改属性时,你都将更新屏幕以显示该状态。
var state: State = .noData {
didSet {
switch state {
case .noData:
noDataView.isHidden = false
tableView.isHidden = true
case .loaded:
noDataView.isHidden = false
tableView.isHidden = true
}
}
}
复制代码
若是你只经过此属性修改状态,则能够确保永远不会忘记更新属性,而且永远不会输入无效状态。如今改变状态很简单。
self.state = .noData
复制代码
你拥有的状态越多,此模式就越有用。
你甚至能够经过使用咱们的错误消息和项目的关联值来改善这一点。
enum State {
case noData
case loaded([Cell])
case error(String)
}
var state: State = .noData {
didSet {
switch state {
case .noData:
noDataView.isHidden = false
tableView.isHidden = true
errorView.isHidden = true
case .loaded(let cells):
self.cells = cells
noDataView.isHidden = true
tableView.isHidden = false
errorView.isHidden = true
case .error(let error):
errorView.errorLabel.text = error
noDataView.isHidden = true
tableView.isHidden = true
errorView.isHidden = false
}
}
}
复制代码
这样,你就定义了一个数据结构,它是table view controller的完整表示。它很容易测试(由于它是一个纯粹的Swift值),并为咱们的table view提供单点更新和单一事实来源。
下面是几个小贴士,很是有用:
确保table view始终展现数据源源数组的当前状态。使用属性观察器刷新table View,不要尝试手动保持它们同步。
任何人均可以实现协议!请记住,下次编写复杂的table view数据源或委托时。定一个惟一目的是table view的数据源的类型会更好 。这样能够保持视图控制器的干净,并将逻辑和职责分离到各自的对象中。
若是你发现你会确认某个indexPath是某个确切的index,经过switch语句到某个section,或者相似的操做。这是不对的。若是你有某个cell要放在肯定的位置,在你的源数组中展现它。不要在你的代码中隐藏这些cell。
总而言之,惟一的法则是在编程中,朋友只和它的朋友交谈,不要和朋友的朋友交谈。
换句话说,一个对象应该只访问它本身的属性。那些属性的属性应该保持不变。因此,UITableViewDataSource不该该为cell的label设置text属性。若是你在代码中看到两个点(例如cell.label.text=...)那就是不对的。
若是你不按照这个原则来,更改cell意味着你也不得不更改数据源。将cell和数据源解耦可让你更改或者重构一个cell而不用影响其余。
有时候,拥有多个相似的UITableViewCell类比使用一堆if语句的单个类更好。你不会知道他们之后会怎么出现问题,将它们抽象是一个陷阱。
我但愿这些技巧能够帮助你,我相信你下次写table view相关代码会用到这些建议。
求大佬们点个关注,会按期写原创和翻译国外最新文章,跟大佬们一块儿学习进步,有问题或者建议欢迎加微信ruiwendelll,拉你们进技术交流群,一块儿探讨学习,谢谢了!