第九章 用原型单元格(prototype cell)自定义table view

QQ20151211-4@2x.png

我认为这是最好的建议:不断的思考你怎样才能把事情做得更好并且不断的质疑自己。
-Elon Musk,Tesla Motors

在前面一章,我们创建了一个简单的基于表格的app来用基本的单元格形式演示餐厅的清单。这一章,我们将自定义表格单元格然后让它看起来更时髦。我们将作出一些变化和增强功能:

  • 用UITableViewController代替UITableView来重构app。
  • 给每个餐厅显示独特的图片代替显示同样的缩略图。
  • 设计自定义的table view单元格代替基本样式的table view单元格。
    你可能想知道为什么我们要重构同样的app。做事情永远不止一种办法。以前,我们用UITableview来创建table view。在这章,我们将用UITableViewController来在Xcode里创建table view app。它更简单吗?是的。回忆一下,我们需要采用UITableViewDataSource和UITableViewDelegate协议,UITableViewController已经采用了这些协议并且为我们建立联系。除此之外,它有所有必须的布局约束。

Quick note: 我们正在构建一个真正的app,所以我们给它一个更好的名字。

QQ20151211-2@2x.png

创建一个Xcode工程,选择Main.storyboard然后跳到界面编辑器。Xcode会生成默认的视图控制器。这次,我们不用默认的控制器。选择视图控制器,按delete键来删除它。这个视图编辑器是与ViewController.swift关联的。我们也不需要它。在工程导航里,选择文件然后点击delete按钮。选择”Move to Trash”。这将完全的删除文件。
回到界面编辑器,从对象库里拖曳一个 Table View Controller(如 UITableViewController),然后放在 storyboard里。你必须指明这个控制器作为初始视图控制器。这会告诉 iOS 这个table view 控制器是第一个被读取的视图控制器。你需要做的是选择 Attributes inspector,然后检查 Is Initial View Controller 选项。你会看到一个箭头指向table view控制器。


QQ20151211-3@2x.png

我们还没有插入任何数据到表格里。所以如果你编译运行 app,你将会看到一个空白的表格。
默认情况下,table view 控制器与 UITableViewController 类关联在一起。回到工程导航栏然后右击 FoodPin文件夹。选择“New Files”来创建一个新的文件。

QQ20151211-4@2x.png

选择 Source(在 iOS分类下)> Cocoa Touch Class作为模板,然后点击 Next。命名为新类 RestaurantTableViewController。因为我们用一个 table view 编辑器工作,改变“Subclass of”值到 UITableViewController。保持其他的值不变,点击“Next”然后把它保存到工程文件夹。你应该能看到 RestaurantTableViewController.swift文件在工程导航栏。


QQ20151211-5@2x.png

超类和子类

如果你是个编程新手,你可能想知道子类是什么。Swift 是一个面向对象(OOP)语言。在 OOP 里,一个类会被继承到另一个类。在这个实例中,RestaurantTableViewController类继承自 UITableViewController 类。它继承 UITableViewController 类所有的状态和方法。RestaurantTableViewController 类被称作 UITableViewController 的子类。换句话说,UITableViewController 类作为超类(或者父类(parent class))被 RestaurantTableViewController 继承。

storyboard 里的table view controller不知道 RestaurantTableViewController 类。所有我们必须分配给 table view controller新的自定义类。进到界面编辑器然后选择 table view controller。在 Identity inspector 里,设置自定义类为 RestaurantTableViewController。现在我们给storyboard 里的 table view controller和新类建立了联系。


QQ20151212-0@2x.png

还要做一件事来给 table view 进行设置。选择原型单元格。在 Attributes inspector 里,改变格式为 Basic 然后设定 identifier 为 Cell。这和我们前一章做的很像。
OKay,用户界面准备好了。我们去码字。在工程导航栏里选择 RestaurantTableViewController.swift文件然后声明一个实例变量,用来放置表格内容。

var restaurantNames = [“Cafe Deadend", "Homei", "Teakha", "Cafe Loisl", "Petite Oyster", "For Kee Restauran”,“ "Po's Atelier", "Bourke Street Bakery", "Haigh's Chocolate", "Palomino Espresso", "Upstate", "Traif", "Graham Avenue Meats", "Waffle & Wolf", "Five Leaves", "Cafe Lore", "Confessional", "Barrafina", "Donostia", "Royal Oak", "Thai Cafe”]

像之前说的,UITableViewController 类已经添加了 UITableViewDataSource 和 UITableViewDelegate 协议。作为 UITableViewController 的子集,RestaurantTableViewController 也添加了这些协议。
如果你没忘记,我们需要实现下面 UITableViewDataSource 协议要求的方法来提供表格内容:

  • tableView(_:numberOfRowsInSection:)
  • tableView(_:cellForRowAtIndexPath:)

UITableViewController 类为这两个方法提供了一个默认的执行。通常,这些默认的方法在我们的 app 里并不合适。我们需要重写默认的方法然后提供我们自己的执行。在 RestaurantTableViewController.swift 里插入以下的代码:

override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell{
let cellIdentifier = “Cell”
let cell = tableView.dequeueReusableCellWithIdentifier(cellIdentifier, forIndexPath: indexPath)
//配置单元格
cell.textLabel?.text = restaurantNames[indexPath.row]
cell.imageView?.image = UIImage(named: “restaurant.jpg”)
return cell
}

在 Swift 里,为了重写一个超类的方法,我们在方法的开头简单的增加 override 关键词。以上的代码和前一章一样,我不再复述细节了。
下一步,在 RestaurantTableViewController里改变下面的代码片段。这些代码是当生成了类文件时 Xcode 自动创建的。默认情况下,章节数和章节行数都返回0值。换句话说,它告诉 table view说 table view 里面没有数据。这不是我们想要的,所以我们必须把代码改成这样:

override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return 1
}
override func tableView(tableView: UITableView, numberOfRowsInSection section:Int) ->Int {
return restaurantNames.count
}

这里我们告诉table view这里只有一章然后在数组里保存返回餐厅的数量。作为附注,numberOfSectionsInTableView 方法是可选的。如果你移动它,table view 仍然会工作,因为章节数默认设置成1。
最后,下载图片然后拖曳所有图片到 Assets.xcassets文件夹。现在,点击“Run”按钮然后你的 FoodPin app 看起来和我们之前构建的一样。


QQ20151212-3@2x.png

现在你应该理解怎样在一个 table view 里显示数据。我已经给你展示了两个办法:
1、用 UITableView 和 View Controller。
2、用 UITableViewController。
你可能想知道你应该用哪个办法。大体来说,2#办法更好。UITableViewController 为你配置了所有事情。你可以简单的重写一些方法来提供表格数据。但是这样失去了灵活性。嵌入在 UITableViewController 里的 table view 是固定的,你不能改变它。如果你想要用 table view布局一个更复杂的 UI,1#方法将会更合适。

演示不同的缩略图

你做了前面章节的练习吗?我希望你已经用功了。有很多方法来演示不同的缩略图对应每个表格行。我会给你展示最直白的方法。首先,下载图片包。添加所有的图片到 Assets.xcasset。如果你喜欢,你可以使用你自己的图片。


QQ20151212-1@2x.png

在我们改变代码之前,让我们重温在表格行里显示缩略行的代码。

cell.imageView?.image = UIImage(named: “restaurant.jpg”)

上面那行代码tableView(_:cellForRowAtIndexPath:) 方法指导 UITableView 来在每个单元格里演示 restaurant.jpg。为了显示不同的图片,我们需要更改代码行。
像之前解释的那样,iOS 每次调用这个方法,特殊的表格行将会显示出来。正确的行数被嵌入到 indexPath 参数。你可以简单的使用 indexPath.rou 来找出哪一行正在处理。
因此,为了在每行显示不同的图片,我们需要做的是添加一个新的数组来储存缩略图的文件名。我们把这个数组命名为 restaurantImages。插入下面这行代码到 RestaurantTableViewController 类:

var restaurantImages = ["cafedeadend.jpg", "homei.jpg", "teakha.jpg", "cafeloisl.jpg", "petiteoyster.jpg", "forkeerestaurant.jpg", "posatelier.jpg", "bourkestreetbakery.jpg", "haighschocolate.jpg", "palominoespresso.jpg", "upstate.jpg", "traif.jpg", "grahamavenuemeats.jpg", "wafflewolf.jpg", "fiveleaves.jpg", "cafelore.jpg", "confessional.jpg", "barrafina.jpg", "donostia.jpg", "royaloak.jpg", "thaicafe.jpg”]

在上面的代码里,我们用一个图片文件名清单初始化了 restaurantImages 数组。图片的顺序与 restaurantNames 保持一致。
为了读取相应的餐厅图片,把 tableview(_:cellForRowAtIndexPath:)方法的代码行改成:
cell.imageView?.image = UIImage(named:restaurantImages[indexPath.row])
保存所有改变以后,尝试再次运行你的 app。每个餐厅有它自己图片。


QQ20151213-0@2x.png

自定义 table view 单元格

app 是不是看起来更好了?我们用自定单元格来让它变得更好。到目前为止我们利用的是默认的 table view 单位格。缩略图的位置和大小都是固定的。如果你想要:

  • 改变单元格的高度。
  • 把缩略图变大一点。
  • 显示更多餐厅的信息,比如位置和类型。
  • 改变字体类型和大小。
  • 显示圆形的图片来代替方形的图片。

为了让你更好的理解单元格是怎样自定义的,看下图,单元格看起来很酷,对吧?

QQ20151213-1@2x.png

在界面编辑器里设计原型单元格(prototype cell)

prototype cell的美妙之处在于,它允许开发者在 table view controller 里来自定义单元格。
我们先来改变单元格的样式。当它设置成 Basic 样式的适合你不能自定义单元格。在 Attributes inspector 里选择原型单元格然后把样式从 Basic 变成 Custom。


QQ20151213-2@2x.png

为了容纳更大的缩略图,我们必须把单元格也变大一点。你需要改变table view 和原型单元格的行高。选择 table view 然后把行高变成80。

QQ20151213-3@2x.png

然后选择prototype cell,进入 Size inspector。检查 custom 复选框然后把行高变成80。

QQ20151213-4@2x.png

变更行高之后,从对象库里拖一个 Image View 到prototype cell。选择 image view,点击“Size”inspector,改变“X”,“Y”,“Width”和“height”的属性。

QQ20151213-5@2x.png

接下来,我们添加三个标签到prototype cell:

  • Name - 餐厅名字
  • Lcation - 餐厅地址(如:纽约)
  • Type - 餐厅形式(如:茶馆)

为了添加一个标签,从对象库里拖曳一个 Label 对象到单元格。给第一个标签命名为“Name”。我们用一个文本样式(text style)来代替标签的系统字体。下一章我会给你解释固定字体和文本样式的区别。现在,进入 Attributes inspector,把字体改成 Txet Style - Headline。

QQ20151213-6@2x.png

拖曳另一个标签到单元格然后把它命名为 Location。把字体样式改成 Light 然后设置字体大小为14。同样的设置字体颜色为 Dark Gray。最后,创造另一个标签命名为 Type。同样的,把字体形式改成 Light 然后设置字体大小为13。
我已经在第六章介绍过 stack views。你不仅仅可以在 view controller 里用 stack view,你同样在 prototype cell 里使用 stack views 来布局约束。因此,我们将用 stack views 来管理标签和图片视图,而不是定义布局约束。首先,按住 command 键,选择三个标签。点击布局栏里的 stack 按钮来把他们嵌入到垂直的 stack view 里。进入到 Attributes inspector,把 stack view 的间距从0改成1。这会在标签之间添加一个约束。

QQ20151213-7@2x.png

现在选择我们刚创建的 image view 和 stack view。点击 Stack 按钮,界面编辑器将把它们嵌入到一个水平 stack view。默认情况下,image view 和“Label”stack view 之间没有间距。你可以到 Attributes inspector 里把 spcing 选项从0改成10。很酷,对吗?你可以嵌套堆栈视图来创建一些复杂的布局。


QQ20151213-8@2x.png

但是,这不意味着你不需要用 auto layout。我们仍然需要为 stack view 来定义布局约束。下图描述了单元格的布局要求。

QQ20151213-9@2x.png

简而言之,我们希望单元格内容(如 stack view)受限于单元格的可视区域。这是为什么我们要为每边的 stack view定义间距约束。除此之外,image view 的大小应该固定在60X60。
现在选择 stack view,点击布局栏里的 Pin 按钮。分别设置上,左,下,右的值为2,6,1.5和0


QQ20151213-10@2x.png

一旦你添加了4个约束,stack view 会自动调整大小。下一步,在文件大纲里,水平拖曳 image view。在弹出的菜单里,按住 shift 键选择“width”和“height”选项。这个内嵌视图的大小就固定了。

QQ20151213-11@2x.png

Cool!你已经完成了 prototype cell 的布局。我们继续写一些代码。

为自定义单元格创造一个类

到目前为止,我们设计了单元格。但是我们怎样改变标签的 prototype cell 值?这些值应该是动态的。默认情况下,prototype cell 应该是和 UITableViewCell 类相关联的。为了更新单元格数据,我们将为 prototype cell创建一个新的类,这个类继承自UITableViewCell。这个类代表了自定义单元格的底层数据模型。像往常一样,在工程导航栏右击“FoodPin”文件夹然后选择“New File…”。
在选择这个选项之后,Xcode 提示你来选择一个模型。我们准备为自定义 table view 单元格创建一个新的类,选择“Cocoa Touch Class”然后点击“Next”。填入 RestaurantTableViewCell 作为类名然后设置“Subclass of”的值为 UITableViewCell。

QQ20151213-12@2x.png

点击“Next”然后把文件保存到 FoodPin 工程文件里。Xcode 应该在工程导航栏里创建一个文件叫做 RestaurantTableViewCell.swift。
下一步,在 RestaurantTableViewCell 类里声明下列 outlet 变量:

@IBOutlet var nameLabel: UILabel!
@IBOutlet var locationLabel: UILabel!
@IBOutlet var typelabel:UILabel!
@IBOutlet var thumnailImageView: UIImageView!

RestaurantTableViewCell 类为自定义单位各的数据模型服务。在单元格里,我们有4个属性是可变的:

  • 缩略图图片视图
  • 名字标签
  • 位置标签
  • 类型标签
    数据模型储存和提供单元格的值来显示。它们都是必须的,用来在界面编辑器里连接响应的用户界面对象。通过 UI 对象来连接源代码,我们可以改变 UI 对象的动态值。
    这是在 iOS 编程里很重要的一个概念。在 storyboard 里,你的 UI 和代码是分开的。你在界面编辑器里创建 UI,然后在 Swift 里写你的代码。如果你要改变数值或者 UI 元素(如 label)的属性,你必须给它们建立联系,这样你代码里的对象可以获得一个引用到storyboard 里定义的对象。在 Swift 里,你用@IBOutlet关键词来表明一个类的属性,那样可以解除到界面构建器(Interface Builder)。为了给 IBOutlet 关键字注释属性,我们叫它 outlets。
    所以在上面的代码里,我们声明了4个 outlets。每个 outlet都将联系与它相关的 UI 对象。下图描述了它们之间的关系。
QQ20151213-13@2x.png

@IBAction vs @IBOutlet

在 HelloWorld app里我们曾经用@IBAction来表明动作的方法。@IBAction和@IBOutlet之间有什么区别?@IBOutlet 用来表示一个属性,这个属性与 storyboard 里的视图对象连接。例如,如果 outlet 与一个按钮相连,你可以用 outlet 来改变按钮的颜色或者标题。另一方面,@IBAction 用来表明一个动作的方法,这个方法可以被确定的事件所引发。例如,当用户点击按钮,它可以引发一个动作的方法来做一些事。

在我们建立CustomTableViewCell 类的 outlets 和界面构建器里的 prototype cell 之间的联系之前,我们必须先设置 custom class。

QQ20151213-14@2x.png

默认情况下,prototype cell 与默认的 UITableViewCell 类相连。为了用 custom class 分配 prototype,选择 storyboard 里的单元格。在 Identifier inspector 里,设置 custom class 为 CustomTableViewCell。

建立联系

接下来,我们建立 outlets 和在 prototype cell 里的 UI 对象之间的联系。在界面构建器里,右击文档大纲视图里的 cell 来弹出 Outlets inspector。拖曳圆环(thumdnailImageView边上)到prototype 单元格 UIImageView 对象(看下图)。当你松开按钮时,Xcode 会自动建立联系。

QQ20151214-0@2x.png

为下面的 outlets重复上面的步骤:

  • locationLabel - 连接地址标签单元格
  • nameLabel - 连接名字标签单元格
  • typeLabel - 连接类型标签单元格
    在你做完所有的连接之后,UI 应该看起来像下图一样。
QQ20151214-1@2x.png

写 table view controller 的代码

最后,我们来到改变的最后一部分。在 RestaurantTableViewController 类里,我们依然使用 UITableViewCell(如默认单元格)来显示内容。我们需要修改一行代码来使用自定义单元格。如果你观察 tableView(_:cellForRowAtIndexPath:)方法现在的执行。第二行代码是:

let cell = tableView.dequeueReusableCellWithIdentifier(cellIdentifier, for IndexPath: indexPath)

我已经在之前的章节解释过 dequeueReusableCellWithIdentifier 方法的意思。它足够灵活从队列里返回任何单元格格式。默认情况下,它返回一个 UITableViewCell 类型的泛型单元格。为了使用 RestaurantTableViewCell 类,“转换”dequeueReusableCellWithIdentfier 返回的对象到 RestaurantTableViewCell 是我们的责任。这个转换过程被称作向下类型转换。在 Swift 里,我们使用 as!关键词来执行强制转换。因此,把上面的代码行改变为:

let cell = tableView.dequeueReusableCellWithIdentifier(cellIdentifier, forIndexPath: indexPath) as! RestaurantTableViewCell

as!和as?

向下类型转换允许你将一个类的值转换到它的派生类。例如,RestaurantTableViewCell是一个 UITableViewCell 的子类。dequeueReusableCellWithIdentifier 方法总是返回一个 UITableViewCell 对象。如果用自定义单元格,这个对象可以被转换成特定的单元格形式(如 RestaurantTableViewCell)。Swift1.2 之前,你仅仅能够用“as”向下类型运算符。然而,有时对象不能转换成制定的类型。因此,从 Swift1.2之后,Apple 介绍了另外2个运算符:as! 和 as?。如果你想当确定向下类型转化能够正确的执行,使用“as!”来执行转换。一旦你不确定一种类型的值能转换成另一种,使用“as?”来执行一个可选的向下类型转换。你需要执行额外的检查来看向下类型转化是否成功。
我知道你已经等不及来测试 app 了,但是我们需要改变更多的一些代码行。下面这些代码行设置餐厅名字和图片的值:

// 配置单元格...
cell.textLabl?.text = restaurantNames[indexPath.row]
cell.imageView?.image = UIImage(named: restaurantImages[indexPath.row])

textLabel 和 imageView 都是 UITableViewCell 类的默认属性。因为我们现在使用自己的 RestaurantTableViewCell,我们需要使用自定义类的属性。把上面的代码行改成这样:

//配置单元格...
cell.namelabel.text = restaurantNames[indexPath.row]
cell.thumbnailImageView.image = UIImage(named: restaurantImages[indexPath.row])

现在你已经准备好了。点击 Run 按钮来测试 app。你的 app 应该看起来像下图显示的。试着旋转模拟器。app在横向情况下同样可以工作。

QQ20151214-2@2x.png

与前一版的 app 相比这是一个巨大的进步。我们继续把缩略图变成圆形,让它看起来更好。

圆形图片

自从iOS7发布后,相比于方形图片,iOS 更喜欢圆形图片。你可以在内置 app里找到圆形图标或者图片,如联系人和电话。把所有的餐厅图片变成圆形会更好吗?你不需要 PS 来调整图片。你需要的仅仅是两行代码。CALayer 类的实例支持UIKit 里的每个视图(如UIView,UIImageView)。设计层(layer)对象来管理支持存储视图和处理与视图相关的动画。
layer 对象提供各种属性,可以设置成控制图像的可视化内容例如:

  • 背景色
  • 边界和边界宽度
  • 阴影颜色,宽度,等等
  • 不透明度
  • 圆角半径
    圆角半径是我们用来画圆角的属性。Xcode 提供两个方法来编辑 layer 属性。你可以直接通过代码更新它的属性。这行代码用来改变image view 的角度:

cell.thumbnailImageView.layer.cornerRadius = 30.0
cell.thumbnailImageView.clipsToBounds = true

一个更简单的方法是通过界面构建器来改变。首先,在 stack view 里选择 image view。进入 Identifier inspector,点击User Defined Runtime Attributes 左下角的 Add 按钮(+),一个新的运行时属性编辑器出现在编辑器中。双击在 Key Path 文件下的新属性来为属性编辑新的路径。给 layer.cornerRadius 设置新的值然后点击 Return 来确定。点击 Type 属性然后选择 Number。最后,把值设置成30.把方形的图片变成圆形的图片,半径设置成图片高度的一半。这里,方形图片的宽度是60,所以角度半径设置成30.

QQ20151214-3@2x.png

当初始化 image view 时,运行时属性将自动读取成圆角。在圆形图片正确工作之前还有一件事你必须配置。选择 image view 然后进入 Attributes inspector。在 Drawing 部分里,使用 Clip Subview 选项。这会省略内容成圆角。

QQ20151214-4@2x.png

现在编译运行 app。UI 看起来更棒了对吗?没有写一行代码,我们把方形图片变成了圆形。

QQ20151214-5@2x.png

你可以随意改变角度值。试着把corner radius 变成10然后看看你会得到什么。

推荐阅读更多精彩内容