带有MVC的iOS Tableview

如何使其清晰并享受您的代码

如果您构建iOS项目,您已经知道这个事实:最常用的组件之一是UITableView 。 如果您尚未构建任何项目,您仍然可以在许多流行的iOS应用中看到UITableView :YouTube,Facebook,Twitter,Medium,大多数Messenger应用等。基本上,每次需要显示动态数量的在同一视图上的数据对象,请使用UITableView

另一个基本组件是CollectionView,我个人更喜欢使用它,因为它更灵活。 稍后我将发表另一篇有关CollectionView的文章。

因此,您想将UITableView添加到您的项目。

一种明显的方法是使用具有内置UITableView的UITableViewController 。 它可以通过简单的设置工作,您只需要添加数据数组并创建一个单元格即可。 它看起来很简单,并且可以按照我们需要的方式工作,除了以下几点: UITableViewController代码变得超长。 并且它打破了MVC模式。 什么是MVC?为什么我们需要考虑一下? 您可以查看这篇文章,其中对所有iOS体系结构模式都有很好的解释。

您不想处理所有这些模式吗? 无论如何,您可能仍想拆分您的一千行长的UITableViewController。

在上一篇文章中,我描述了将数据从模型传递到控制器的三种方法。

在本文中,我将使用委派方法向您介绍处理tableViews的方法。 这种方法使代码看起来非常整洁,模块化且可重用。

我们将不使用一个UITableViewController ,而是将其拆分为多个类:

  • DRHTableViewController :我们将其设为 UIViewController的子类,并添加UITableView作为子视图
  • DRHTableViewCellUITableViewCell的子类
  • DRHTableViewDataModel :它将使用委托进行API调用,创建数据并将数据返回给DRHTableViewController
  • DRHTableViewDataModelModelItem :一个简单的类,将保存所有数据,这些数据将显示在DRHTableViewCell中

让我们从UITableViewCell开始。

第1部分:TableViewCell

从一个名为“单视图应用程序”的新项目开始,并删除默认的ViewController.swiftMain.storyboard文件。 我们将逐步创建所有需要的文件。

首先,创建一个UITableViewCell子类。 如果要使用XIB ,请选中“也创建XIB文件”。

在此示例中,我们将复制中型主页的简化版本。 因此,我们将需要添加以下子视图:

  1. 头像图片
  2. 名称标签
  3. 日期标签
  4. 文章标题
  5. 文章预览

以所需的方式应用“自动布局”,因为单元格设计不会影响我们在本教程中所做的任何事情。 为每个子视图创建一个出口。 在您的DRHTableViewCell.swift中,您将具有类似以下内容:

 类DRHTableViewCell:UITableViewCell { 
  @IBOutlet弱var avatarImageView:UIImageView? 
@IBOutlet弱var authorNameLabel:UILabel?
@IBOutlet弱var postDateLabel:UILabel?
@IBOutlet弱var titleLabel:UILabel?
@IBOutlet弱var PreviewLabel:UILabel?
  } 

如您所见,我将每个@IBOutlet的默认“!”标记更改为“?”。 当您将UILabel从InterfaceBuilder拖到代码中时,它将自动强制展开并在末尾添加“!”。 这背后有Objective-C API兼容性的原因,但我总是更喜欢避免强制展开,因此我改用Optionals。

接下来,我们需要添加一种方法来设置所有带有数据的标签和图像视图。 我们将创建一个DRHTableViewDataModelModelItem ,而不是对每个数据使用单独的变量:

 类DRHTableViewDataModelItem { 
  var avatarImageURL:字符串? 
var authorName:字符串?
var date:字符串?
var标题:字符串?
var PreviewText:字符串?
  } 

最好将日期存储为Date,但在此示例中,为了使其更简单,我们将其存储为String

所有变量都是可选的,因此我们不必担心默认值。 稍后我们将提供一个Init(),因此现在返回DRHTableViewCell.swift并添加以下代码,该代码将为所有单元格标签和imageView配置自定义数据:

  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方法将取决于您在项目中使用图像缓存的方式,因此在本教程中不会介绍它。

现在,准备好单元格,我们可以创建TableView。

第2部分:TableView

在此示例中,我们将使用基于情节提要的viewController。 您可以参考我以前的文章,以找到用Storyboard实例化viewController的最佳方法。 首先,创建一个UIViewController子类:

在此项目中,我使用UIViewController而不是UITableViewController,因此我们拥有更多控制权。 同样,将UITableView作为子视图将使您可以使用自动布局将其定位为所需的方式。

接下来,创建一个情节提要文件并使用相同的方式命名: DRHTableViewController 。 从对象库中拖动ViewController并为其分配一个自定义类:

添加一个UITableView并将其固定到所有四个视图边缘:

最后,向DRHTableViewController添加一个tableView出口:

 类DRHTableViewController:UIViewController { 
  @IBOutlet弱var tableView:UITableView? 
  } 

我们已经创建了DRHTableViewDataModelModelItem类,因此在viewController中添加一个局部变量:

  fileprivate var dataArray = [DRHTableViewDataModelItem]() 

此变量存储将在tableView中显示的数据

注意,我们不在ViewController类中创建此数据:dataArray是一个空数组。 稍后,我们将使用Delegate向其提供数据。

现在,在viewDidLoad方法中设置所有基本的tableView属性。 您可以按照自己的方式调整颜色和样式。 这个例子中我们唯一需要的属性是registerNib:

  tableView?.register(笔尖:UINib ?, forCellReuseIdentifier:字符串) 

与其在调用此方法和对单元格标识符进行硬键入之前立即创建nib,我们将在DRHTableViewCell中创建NibReuseIdentifier作为类属性

尝试避免硬键入项目中的任何字符串标识符。 如果没有其他方法,则可以创建字符串文字并使用它。

打开DRHTableViewCell并在类的开头添加以下代码:

 类DRHMainTableViewCell:UITableViewCell { 
 类var标识符:字符串{ 
返回字符串(描述:自我)
}
  class var nib:UINib { 
返回UINib(nibName:标识符,bundle:nil)
}
  ..... 
  } 

保存更改并返回到DRHTableViewController。 registerNib方法 将变得简单

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

不要忘记将tableViewDataSourceTableViewDelegate分配给self:

 覆盖func viewDidLoad(){ 
  super.viewDidLoad() 
  tableView?.register(DRHTableViewCell.nib,forCellReuseIdentifier: 
DRHTableViewCell.identifier)
  tableView?.delegate =自我 
tableView?.dataSource =自我
  } 

完成此操作后,编译器将向您抛出错误: “无法将DRHTableViewController类型的值分配给UITableViewDelegate类型”。

当您使用UITableViewController子类时,tableView委托和数据源已经内置。 如果创建UITableView作为UIViewController的子视图,则需要使UIViewController子类继承自UITableViewControllerDelegate和UITableViewControllerDataSource。

要消除该错误,只需向DRHTableViewController类添加两个扩展

 扩展DRHTableViewController:UITableViewDelegate { 
  } 
 扩展DRHTableViewController:UITableViewDataSource { 
  } 

您仍然会看到另一个错误: “类型DRHTableViewController不符合协议UITableViewDataSource” 。 发生这种情况是因为您需要在此扩展中实现一些必需的方法:

 扩展DRHTableViewController:UITableViewDataSource { 
  func tableView(_ tableView:UITableView,cellForRowAt 
indexPath:IndexPath)-> UITableViewCell {
  } 
  func tableView(_ tableView:UITableView,numberOfRowsInSection 
部分:Int)-> Int {
  } 
}

UITableViewDelegate中的所有方法都是可选的,因此,如果不覆盖它们,则不会出现错误。 右键单击UITableViewDelegate以查看可用的方法。 最常用的方法是选择/取消选择单元格,设置单元格高度以及配置tableView的页眉/页脚的方法

如您所见,以上两个方法都有一个返回类型,因此您再次看到编译器错误:“ Missing return type ”。 让我们修复它。

首先,我们在section中设置行数:我们已经设置了dataArray,因此我们只使用其计数:

  func tableView(_ tableView:UITableView,numberOfRowsInSection部分:Int)-> Int { 
 返回dataArray.count 
  } 

可能有人注意到,我没有覆盖另一个方法:numberOfSectionsInTableView,通常与UITableViewController一起使用。 此方法是可选的,它返回默认值1。 在此示例中,我们只有一个tableView部分,因此我们不需要重写此方法

配置UITableViewDataSource的最后一步是在cellForRowAtIndexPath方法中返回我们的自定义单元格:

  func tableView(_ tableView:UITableView,cellForRowAt indexPath:IndexPath)-> UITableViewCell { 
 如果让单元格= tableView.dequeueReusableCell(withIdentifier: 
DRHTableViewCell.identifier,用于:indexPath)作为? DRHTableViewCell
{
返回单元
}
 返回UITableViewCell() 
}

让我们逐行浏览。

为了创建一个单元格,我们使用带有DRHTableViewCell标识符的dequeueReusableCell方法。 这将返回一个UITableViewCell ,因此我们使用从UITableViewCellDRHTableViewCell的可选向下转换:

 让单元格= tableView.dequeueReusableCell(withIdentifier: 
DRHTableViewCell.identifier,用于:indexPath)作为? DRHTableViewCell

然后我们对其进行安全解包:如果成功,则返回自定义单元:

 如果让单元格= tableView.dequeueReusableCell(withIdentifier: 
DRHTableViewCell.identifier,用于:indexPath)作为? DRHTableViewCell
{
返回单元
}

如果安全展开失败,我们将返回默认的UITableViewCell

 如果让单元格= tableView.dequeueReusableCell(withIdentifier: 
DRHTableViewCell.identifier,用于:indexPath)作为? DRHTableViewCell
{
返回单元
}
返回UITableViewCell()

我们忘记了什么吗? 是的,我们需要使用数据项配置单元格:

  func tableView(_ tableView:UITableView,cellForRowAt indexPath:IndexPath)-> UITableViewCell { 
 如果让单元格= tableView.dequeueReusableCell(withIdentifier: 
DRHTableViewCell.identifier,用于:indexPath)作为? DRHTableViewCell
{
  cell.configureWithItem(item:dataArray [indexPath.item]) 
 返回单元 
  } 
 返回UITableViewCell() 
  } 

我们准备好完成最后一部分:创建并将数据源连接到我们的TableView。

第3部分:数据模型

创建一个DRHDataModel类。

在此类中,我们将从JSON文件或使用HTTP请求,或者仅从另一个本地dataSource文件请求数据。 这不是本文的一部分,因此我假设我们已经进行了API调用,并且它返回了AnyObject的可选数组和可选的Error:

 类DRHTableViewDataModel { 
  func requestData(){ 
//从API或本地JSON文件请求数据的代码将
这里
//这两个变量是从任何地方返回的:
// var回应:[AnyObject]?
// var错误:错误?
 如果让错误=错误{ 
//处理错误
  } if let response = response { 
//解析对[DRHTableViewDataModelItem]的响应
setDataWithResponse(响应:响应)
}
}
}

setDataWithResponse方法中,我们将使用响应数组构建一个DRHTableViewDataModelItem数组。 在requestData下面添加以下代码:

 私有函数setDataWithResponse(response:[AnyObject]){ 
  var data = [DRHTableViewDataModelModelItem]() 
 对于响应项目{ 
//从AnyObject创建DRHTableViewDataModelItem
}
}

在此方法中,我们创建了一个DRHTableViewDataModelItem空数组,需要在响应中进行设置。 接下来,我们遍历响应数组中的每个项目。 在此循环中,我们需要从AnyObject中创建DRHTableViewDataModelItem。

您还记得,我们还没有为DRHTableViewDataModel创建任何初始化程序。 因此,返回到DRHTableViewDataModel类并添加init方法。 在这种情况下,我们将使用带有字典[String:String]?的可选Init(或Failable Init)。

 初始化?(数据:[字符串:字符串]?){ 
 如果让数据=数据,让头像=数据[“ avatarImageURL”],让名字=数据[“ authorName”],让日期=数据[“日期”],让标题=数据[“标题”],让PreviewText =数据[“ previewText”] { 
  self.avatarImageURL =头像 
self.authorName =名称
self.date =日期
self.title =标题
self.previewText =预览文本
  }其他{ 
返回零
}
  } 

如果Dictionary中不存在任何必需的路径,或者Dictionary本身为nil,则初始化将失败( 返回nil )。

使用此故障初始化程序,我们可以在DRHTableViewDataModel类中完成setDataWithResponse方法:

 私有函数setDataWithResponse(response:[AnyObject]){ 
  var data = [DRHTableViewDataModelModelItem]() 
 对于响应项目{ 
 如果让drhTableViewDataModelItem = 
DRHTableViewDataModelItemItem(data:item as?[String:String]){
  data.append(drhTableViewDataModelItem) 
}
}
}

在for循环结束时,我们将准备使用DRHTableViewDataModelModelItem数组。 我们如何将这个数组传递给TableView?

第四部分:代表

首先,在DRHTableViewDataModel.swift文件内的DRHTableViewDataModel类上方创建委托协议DRHTableViewDataModelDelegate

 协议DRHTableViewDataModelDelegate:类{ 
  } 

在此协议内,我们将创建两个方法:

 协议DRHTableViewDataModelDelegate:类{ 
  func didRecieveDataUpdate(数据:[DRHTableViewDataModelItemItem)) 
func didFailDataUpdateWithError(错误:错误)
}

Swift协议定义中的class关键字将协议采用限制为类类型(而不是结构或枚举)。 如果我们要使用对委托的弱引用,这一点很重要。 我们需要确保不要在委托和委托对象之间创建保留周期,因此我们使用弱引用(请参见下文)。

接下来,在DRHTableViewDataModel类内添加一个可选的weak属性

 弱var委托:DRHTableViewDataDataModelDelegate? 

现在,我们需要在要使用的地方添加一个委托方法。 在此示例中,如果数据请求失败,则需要传递错误,并且在创建数据数组时需要传递数据。 错误处理程序方法位于requestData方法内部:

 类DRHTableViewDataModel { 
  func requestData(){ 
//从API或本地JSON文件请求数据的代码将
这里
//这两个变量是从任何地方返回的:
// var回应:[AnyObject]?
// var错误:错误?
 如果让错误=错误{ 
委托?.didFailDataUpdateWithError(错误:错误)
  } if let response = response { 
//解析对[DRHTableViewDataModelItem]的响应
setDataWithResponse(响应:响应)
}
}
}

最后,在setDataWithResponse方法的末尾添加第二个委托方法:

 私有函数setDataWithResponse(response:[AnyObject]){ 
  var data = [DRHTableViewDataModelModelItem]() 
 对于响应项目{ 
如果让drhTableViewDataModelItem =
DRHTableViewDataModelItemItem(data:item as?[String:String]){
  data.append(drhTableViewDataModelItem) 
}
}
委托?.didRecieveDataUpdate(数据:数据)
}

现在,我们准备将这些数据传递到我们的tableView中。

第5部分:显示数据

使用DRHTableViewDataModel我们可以为tableView提供数据。 首先,我们需要在DRHTableViewController中创建对此dataModel的引用

 私人let dataSource = DRHTableViewDataModel() 

接下来,我们需要请求数据。 我将在ViewWillAppear中执行此操作,因此每次打开此视图时数据都会更新:

 覆盖func viewWillAppear(_动画:布尔){ 
  super.viewWillAppear(真) 
dataSource.requestData()
}

这是一个简单的示例,因此我在viewWillAppear上请求数据。 在实际应用中,这将取决于多个因素,例如缓存时间,API使用情况和某些应用逻辑。

接下来,在ViewDidLoad中将其委托分配给self:

  dataSource.delegate =自我 

同样,您将看到编译器错误,因为DRHTableViewController尚未从DRHTableViewDataModelDelegate继承。 通过在文件末尾添加以下代码来修复此问题:

 扩展DRHTableViewController:DRHTableViewDataModelDelegate { 
  func didFailDataUpdateWithError(错误:错误){ 
  } 
  func didRecieveDataUpdate(data:[DRHTableViewDataModelModelItem]){ 
  } 
  } 

最后,我们需要处理didFailDataUpdateWithErrordidRecieveDataUpdate情况

 扩展DRHTableViewController:DRHTableViewDataModelDelegate { 
  func didFailDataUpdateWithError(错误:错误){ 
//适当处理错误情况(显示警报,记录错误等)
  } 
  func didRecieveDataUpdate(data:[DRHTableViewDataModelModelItem]){ 
dataArray =数据
}
}

将数据分配给本地dataArray变量后,就可以重新加载tableView了。 但是,我们将在属性数组中使用属性观察器来代替在didRecieveDataUpdate方法中执行此操作:

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

Setter Property Observer会在恰好需要时在值DidSet之后立即在内部运行代码。

而已! 现在,您有了带有自定义数据源和自定义单元格的基本tableView设置的工作原型。 而且您没有那种包含所有代码的千行长tableViewController类。 您刚刚创建的每个组件都可以在整个项目中重复使用,这为您带来了另一个优势。

供您参考,请使用完整的源代码检查我的github存储库。

Stan-Ost / DRHTableViewTutorial
通过在GitHub上创建一个帐户为DRHTableViewTutorial开发做出贡献。 github.com

有疑问或意见吗? 不要犹豫与我联系!