您可以捐助,支持我们的公益事业。

1元 10元 50元





认证码:  验证码,看不清楚?请点击刷新验证码 必填



  求知 文章 文库 Lib 视频 iPerson 课程 认证 咨询 工具 讲座 Model Center   Code  
会员   
   
 
     
   
 订阅
  捐助
使MVC 模式下的 iOS Tableview
 
来源:blog.coding.net 发布于: 2017-5-23
  1947  次浏览      20
 

如果你写过 iOS 项目的话,应该会了解到,iOS 里面最常用的一个控件就是 UITableView;即便没写过 iOS 项目,你应该也会在一些流行的 App 里面看到过它,比如:YouTube,Facebook,Twitter,Medium 等等。一般来讲,当你想要在一个页面上,展示一个数量动态变化的数据的时候,你应该会考虑使用 UITableView。

还有一个基础控件是 CollectionView,它相对来讲更灵活,所以我个人更喜欢用这个。稍后我还会写一篇文章来讲它。

所以,在你的项目里面,不可避免的会用到 UITableView。

比较常见的做法是使用 UITableViewController,它有一个内置的 UITableView;通过简单的设置就可以让它工作起来,你需要做的只是设置好数组数据和显示数据的 Cell。它使用起来很简单,而且也可以满足需求,但是它有一个缺点:这会让 UITableViewController 里面的代码变得超级长,而且这打破了 MVC 模式。关于 MVC 具体是什么,或者我们为什么要去了解它,你可以先看一下 这篇文章(译文),它很好的介绍了 iOS 里面所有的架构模式。

即便你不想去弄懂所有的这些模式,至少对于 UITableViewController 里面的那上千行代码,你总是想要重构划分一下的吧。

在我的上一篇文章里面,我提到了 从 Controller 向 Model 传递数据的三种方式。

在这篇文章里面,我要讲的是我处理 tableView 所有的方式,也就是在上篇文章里提到的 - 代理的方式。用这种处理方式,可以让代码看起来更整洁、模块化、易重用。

这次不适用 UITableViewController,而是把它划分成几个类:

DRHTableViewController:UIViewController 的子类,然后添加一个 UITableView 作为子视图

DRHTableViewCell:UITableViewCell 的子类

DRHTableViewDataModel:它有一个 API 方法:创建数据并用代理的方式返回数据给 DRHTableViewController

DRHTableViewDataModelItem:数据类:它包括了展示在 DRHTableViewCell 里面的所有数据

先从 UITableViewCell 开始吧。

一、TableViewCell

以单视图应用(Single View Application)为模板,创建一个新工程;然后删掉自带的 ViewController.swift 和 Main.storyboard 文件。稍后我们会一步步的创建所有用到的文件。

首先,创建一个 UITableViewCell 的子类。如果你想用 XIB,就勾选“Also create XIB file”这个选项。

在这里,我们想要做的是一个 Medium 主页的简化版,所以需要添加下面这些子视图:

用户头像

姓名标签

日期标签

文章标题

文章概要

约束条件(Autolayout)你可以随意加,这不是重点。给每个视图添加一个对应的属性,完了在你的 DRHTableViewCell.swift 文件里面,应该有类似下面的这部分代码:

class DRHTableViewCell: UITableViewCell {
@IBOutlet weak var avatarImageView: UIImageView?
@IBOutlet weak var authorNameLabel: UILabel?
@IBOutlet weak var postDateLabel: UILabel?
@IBOutlet weak var titleLabel: UILabel?
@IBOutlet weak var previewLabel: UILabel?
}

在这里,我把每个 @IBOutlet 默认的 “!” 改成了 “?”。当你从 InterfaceBuilder 里面拖拽 UILabel 到代码里的时候,它会自动强制解包开这个标签,然后在它后面加上 “!”。这里面有一部分原因是为了和 objective-C API 保持一致性,但是我个人总是喜欢避免强制解包,所以我这里用 optional 标识符做了替换。

接下来,还需要一个方法:用数据去填充上面的这些标签和图片。在数据这块,我们不是在 Cell 里创建很多的变量去表示它,而是为它创建一个新的类 DRHTableViewDataModelItem:

class DRHTableViewDataModelItem {
var avatarImageURL: String?
var authorName: String?
var date: String?
var title: String?
var previewText: String?
}

最好还是用 Date 类型去存储 date,但是这里为了方便,就把它存储成了 String 型。

所有的变量都是可选的(optional),所以不用去担心默认值的问题,稍后还会为它添加一个 Init() 方法。现在再回到 DRHTableViewCell.swift 文件,添加下面这些代码(用数据去填充 Cell 里面的标签和图片):

func configureWithItem (item: DRHTableViewDataModelItem) {
// setImageWithURL(url: item.avatarImageURL)
authorNameLabel?.text = item.authorName
postDateLabel?.text = item.date
titleLabel?.text = item.title
previewLabel?.text = item.previewText
}

setImageWithURL() 方法具体的实现,依赖于具体项目里面对图片缓存的处理;所以这里没有去管它。

现在我们已经有了 Cell,可以创建 TableView 了。

二、TableView

在这里,我们使用基于故事版的(storyboard-based)ViewController。你可以先看下 我的上一篇文章,了解下怎么更好的使用故事版。

首先,创建一个 UIViewController 的子类:

在这面,用 UIViewController 而不是 UITableViewController,这样可以有更多的控制。比如把 UITableView 创建成一个子视图,就可以根据自己的需要,用约束条件去设置它的位置。

接下来,创建一个故事版文件,用相同的名字给它命名:DRHTableViewController。从对象库里面拖拽出来一个 ViewController,并设置它为上面创建的类。

添加一个 UITableView,并让它跟 View 的四边对齐。

最后,在 DRHTableViewController 里面添加 tableView 属性。

class DRHTableViewController: UIViewController {
@IBOutlet weak var tableView: UITableView?
}

我们已经创建了 DRHTableViewDataModelItem 类,现在在 viewController 里面添加一个本地变量

fileprivate var dataArray = [DRHTableViewDataModelItem]()

这个变量用来存储将要展示在 tableView 上面的数据。

记住,我们不会在 ViewController 里面去创建数据:dataArray 只是一个空数组;而是在稍后用代理的方式给它填充数据。

现在在 viewDidLoad 方法里面设置 tableView 的一些基本属性。在这里颜色和样式都可以随意设置,但是唯一需要确认的是注册 nib 文件:

tableView?.register(nib: UINib?, forCellReuseIdentifier: String)

在调用这个方法之前(这个方法里面的 identifier 参数很难写),我们先不创建 nib 文件,而是在 DRHTableViewCell 里面添加两个方法:nib、identifier。

要尽量避免去重复写一些很难写的字符串;如果实在没有办法,可以创建一个 字符串变量,并用它来代替。

打开 DRHTableViewCell,在开头添加下面的代码:

class DRHMainTableViewCell: UITableViewCell {
class var identifier: String {
return String(describing: self)
}
class var nib: UINib {
return UINib(nibName: identifier, bundle: nil)
}
.....
}

保存这些修改,然后回到 DRHTableViewController,调用 registerNib 方法:

tableView?.register(DRHTableViewCell.nib, forCellReuseIdentifier: DRHTableViewCell.identifier)

不要忘了设置 tableViewDataSource 和 tableViewDelegate 为 self:

override func viewDidLoad() {
super.viewDidLoad()
tableView?.register (DRHTableViewCell.nib, forCellReuseIdentifier:
DRHTableViewCell.identifier)
tableView?.delegate = self
tableView?.dataSource = self
}

写完之后,编译器会报错:“Cannot assign value of type DRHTableViewController to type UITableViewDelegate”

当你使用 UITableViewController 子类的时候,tableView 的代理和数据源是已经设置好了的。但是如果你是在 UIViewController 中创建 UITableView 的话,就需要让 UIViewController 继承一下 UITableViewControllerDelegate 和 UITableViewControllerDataSource。

只要为 DRHTableViewController 添加两个扩展,就可以解决了:

extension DRHTableViewController: UITableViewDelegate {
}
extension DRHTableViewController: UITableViewDataSource {
}

又会报错:“type DRHTableViewController does not conform to protocol UITableViewDataSource”。这是因为有一些必须实现的方法,需要你在这个扩展里面实现它们:

extension DRHTableViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, cellForRowAt
indexPath: IndexPath) -> UITableViewCell {
}
func tableView(_ tableView: UITableView, numberOfRowsInSection
section: Int) -> Int {
}
}

UITableViewDelegate 所有的方法都是非必须的,所以即使你没有实现,这里也不报错。按住 Command 键,点击 UITableViewDelegate,可以看到它具体都有哪些方法。它最常用的方法是 选择/取消选择 某个 cell,设置 cell 高度,配置 tableView 的 header/footer 等。

上面两个方法都是需要返回值的,所以编译器又报错了:“Missing return type”。让我们来解决它。

首先,需要设置 section 里面 row 的数量:我们已经有了 dataArray,可以直接使用它的 count 就可以:

func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return dataArray.count
}

在这里,我没有重载另一个方法:numberOfSectionsInTableView。这个方法是非必须的,它默认是返回 1;而这个项目里面 tableView 只有一个 section,所以不需要去重载这个方法。

最后一步,配置 UITableViewDataSource 还需要在 cellForRowAtIndexPath 方法里面返回 cell:

func tableView (_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if let cell = tableView.dequeueReusableCell(withIdentifier:
DRHTableViewCell.identifier, for: indexPath) as? DRHTableViewCell
{
return cell
}
return UITableViewCell()
}

我们分行来看一下。

为了创建 cell,我们可以使用 DRHTableViewCell 的 identifier 作为参数去调用 dequeueReusableCell 方法。它会返回一个 UITableViewCell,所以我们需要用一个可选标识符把它从 UITableViewCell 转换成 DRHTableViewCell:

let cell = tableView.dequeueReusableCell(withIdentifier:
DRHTableViewCell.identifier, for: indexPath) as? DRHTableViewCell

然后安全解包它(safe-unwrap):如果成功,就返回这个自定义的 cell:

if let cell = tableView.dequeueReusableCell(withIdentifier:
DRHTableViewCell.identifier, for: indexPath) as? DRHTableViewCell
{
return cell
}

如果安全解包失败,就返回一个默认的 UITableViewCell:

if let cell = tableView.dequeueReusableCell (withIdentifier:
DRHTableViewCell.identifier, for: indexPath) as? DRHTableViewCell
{
return cell
}
return UITableViewCell()

我们是不是漏了什么?对,还需要用数据去配置 cell 视图:

func tableView (_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if let cell = tableView.dequeueReusableCell(withIdentifier:
DRHTableViewCell.identifier, for: indexPath) as? DRHTableViewCell
{
cell.configureWithItem(item: dataArray[indexPath.item])
return cell
}
return UITableViewCell()
}

我们已经为最后一部分做好准备了:创建 DataSource 并连接到 TableView。

三、DataModel

创建一个 DRHTableViewDataModel 类。

我们会在这个类里面获取数据,至于获取方式,可以是从一个 JSON 文件,或者是 HTTP 请求,或者是别的数据源,这不是本文的重点。我们假定已经有了一个 API 方法,它可以返回一个可选类型的数据对象和一个可选类型的错误信息:

class DRHTableViewDataModel {
func requestData() {
// code to request data from API or local JSON file will go
here
// this two vars were returned from wherever:
// var response: [AnyObject]?
// var error: Error?
if let error = error {
// handle error
} else if let response = response {
// parse response to [DRHTableViewDataModelItem]
setDataWithResponse(response: response)
}
}
}

在 setDataWithResponse 方法里面,我们需要用一个 AnyObject 类型的数组对象 response,构建出一个 DRHTableViewDataModelItem 类型的数组;所以,紧接着添加下面这些代码:

private func setDataWithResponse(response: [AnyObject]) {
var data = [DRHTableViewDataModelItem]()
for item in response {
// create DRHTableViewDataModelItem out of AnyObject
}
}

在这个方法里面,我们创建了一个 DRHTableViewDataModelItem 类型的空数组,我们需要用 response 数组去构建它。然后我们遍历 reponse 数组里面的每个 item;在这个遍历循环里面,我们需要根据 AnyObject 类型的 item 创建一个 DRHTableViewDataModelItem 类型的对象。

我们还没有给 DRHTableViewDataModel 创建初始化方法,所以回到 DRHTableViewDataModel 类,创建这个初始化方法。在这里,我们用一个 Dictionary [String: String]? 类型的对象作为参数,创建一个 Optional 类型的初始化方法(或者说是 可失败的初始化)。

init?(data: [String: String]?) {
if let data = data, let avatar = data[“avatarImageURL”], let name = data[“authorName”], let date = data[“date”], let title = data[“title”], let previewText = data[“previewText”] {
self.avatarImageURL = avatar
self.authorName = name
self.date = date
self.title = title
self.previewText = previewText
} else {
return nil
}
}

如果这个 Dictionary 里面,缺少了任意一个必需的 key 值,或者说这个字典本身就是一个 nil 的话,那么这次初始化就是失败的(返回 nil)。

有了这个可失败的初始化方法(Failable Init),就可以补全 DRHTableViewDataModel 类里面的 setDataWithResponse 方法了:

private func setDataWithResponse(response: [AnyObject]) {
var data = [DRHTableViewDataModelItem]()
for item in response {
if let drhTableViewDataModelItem =
DRHTableViewDataModelItem(data: item as? [String: String]) {
data.append(drhTableViewDataModelItem)
}
}
}

在 for 循环之后,我们得到了一个 DRHTableViewDataModelItem 类型的数组。那么我们怎么把这个数据传递给 TableView 呢?

四、Delegate

首先,在 DRHTableViewDataModel.swift 文件里面创建一个代理 协议 DRHTableViewDataModelDelegate,放在 DRHTableViewDataModel 类的正上方:

protocol DRHTableViewDataModelDelegate: class {
}

在这个协议里面,创建两个方法:

protocol DRHTableViewDataModelDelegate: class {
func didRecieveDataUpdate (data: [DRHTableViewDataModelItem])
func didFailDataUpdateWithError(error: Error)
}

Swift 协议中,class 这个关键字限定了该协议只接受 class 类型(不接受结构体或者枚举类型),从而可以对它使用弱引用(weak reference )。为了确保代理和委托对象之间不会有循环引用,在这里需要用到弱引用。

然后,在 DRHTableViewDataModel 里面添加一个可选的弱引用。

weak var delegate: DRHTableViewDataModelDelegate?

现在,需要在可能用到它的地方调用它。具体到这个例子,在请求失败的时候需要传递错误信息,在创建成功的时候需要传递数据。错误处理的方法可以放在 requestData 方法里面调用:

class DRHTableViewDataModel {
func requestData() {
// code to request data from API or local JSON file will go
here
// this two vars were returned from wherever:
// var response: [AnyObject]?
// var error: Error?
if let error = error {
delegate?.didFailDataUpdateWithError(error: error)
} else if let response = response {
// parse response to [DRHTableViewDataModelItem]
setDataWithResponse(response: response)
}
}
}

最后,在 setDataWithResponse 方法里面调用第二个代理方法:

private func setDataWithResponse(response: [AnyObject]) {
var data = [DRHTableViewDataModelItem]()
for item in response {
if let drhTableViewDataModelItem =
DRHTableViewDataModelItem(data: item as? [String: String]) {
data.append(drhTableViewDataModelItem)
}
}
delegate?.didRecieveDataUpdate(data: data)
}

五、显示数据

有了 DRHTableViewDataModel 就可以向 tableView 里面传递数据了。

首先,需要在 DRHTableViewController 里面创建 dataModel 的引用:

private let dataSource = DRHTableViewDataModel()

然后,还需要请求数据。我会在 ViewWillAppear 方法里面去做这个事情,这样每次视图出现的时候数据都会得到更新:

override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(true)
dataSource.requestData()
}

这是一个简单的例子,所以我在 viewWillAppear 方法里面请求数据。在真正的 app 里面,这需要根据很多因素视情况而定,比如缓存时间、API 的使用、App 自身的逻辑等等。

然后,在 viewDidLoad 方法里面,把它的代理赋值给 self:

dataSource.delegate = self

又报编译错误,这是因为 DRHTableViewController 还没有继承 DRHTableViewDataModelDelegate。在文件的末尾添加下面的代码就可以搞定:

extension DRHTableViewController: DRHTableViewDataModelDelegate {
func didFailDataUpdateWithError (error: Error) {
}
func didRecieveDataUpdate(data: [DRHTableViewDataModelItem]) {
}
}

最后,我们需要处理 didFailDataUpdateWithError 和 didRecieveDataUpdate 这两种情况:

extension DRHTableViewController: DRHTableViewDataModelDelegate {
func didFailDataUpdate WithError(error: Error) {
// handle error case appropriately (display alert, log an error, etc.)
}
func didRecieveDataUpdate(data: [DRHTableViewDataModelItem]) {
dataArray = data
}
}

给 dataArray 赋值就表示,其实我们是想要重新加载 tableView 的数据的。但是在这里我们并没有在 didRecieveDataUpdate 方法里去做这件事,而是用对 dataArray 添加 属性观察者(property observer)的方式来实现:

fileprivate var dataArray = [DRHTableViewDataModelItem]() {
didSet {
tableView?.reloadData()
}
}

设置属性观察者(Setter Property Observer)会在设置完成之后,运行它里面的这些代码。

就是这些!

现在,你有了一个 tableView 模板,它配置了自定义的数据源和自定义的 cell。

你不再需要那个把所有代码都搞在一起,弄了有上千行代码的 tableViewController 了。

你上面创建的每一个部分,在整个项目里都是可以重用的,当然这是做代码划分的另一个好处了。

   
1947 次浏览       20
相关文章

深度解析:清理烂代码
如何编写出拥抱变化的代码
重构-使代码更简洁优美
团队项目开发"编码规范"系列文章
相关文档

重构-改善既有代码的设计
软件重构v2
代码整洁之道
高质量编程规范
相关课程

基于HTML5客户端、Web端的应用开发
HTML 5+CSS 开发
嵌入式C高质量编程
C++高级编程