原文地址: RAYWENDERLICH
说明:英文水平有限,主要是为了巩固学到的知识,也能帮别人快速上手,节约时间,有任何破绽,尤其是技术上的,请您一定要告诉我。
(旁白:其实这张图几乎说明了所有问题,设置好
constraints
,其它都不是问题。补充一下:这篇教程实际上就是 auto layout教程。)
你应该在使用 table view cells 的时候经常会写很多代码,来手动计算label,image view,text field 和 cell 相关的每一个控件的高。
坦白讲,这种方法很容易出错且容易让人迷糊看不懂。
在这篇教程里,你将学会怎样创建自定义 cell 并且根据内容动态调整 cell 的高度,你可能会想,"这得需要多少工作量...!"
Nope! * (旁白:you are right ~*)
你很幸运,Apple 可以让你在 iOS8 里很容易的做到这些。你将从写适配代码中解脱出来。但是还是要实现 table view 的数据源和代理方法。
让我们开始
iOS6出来几天之后,Apple 介绍了一个超赞的技术:auto layout。程序猿们开始庆祝,街头聚会,写赞歌...(旁白:要不要这么夸张啊)
好吧,也许有些问题,但它毕竟是一次大飞跃(旁白:是这个意思吧,哈哈)
在给了很多开发者希望的同时,auto layout 还是很难用的。尤其是手写 auto layout 代码。Interface Builder 在设置 constraints 时也并不理想。
很快到现在,伴随着对 Interface Builder 的所有改进和 iOS8 的到来,我们现在可以很容易的动态设置 table view cells 的高度了。
你需要不得不做的事情有:
1,在创建 table view cells 时使用 auto layout。
2,设置 table view 的** rowHeight 等于 UITableViewAutomaticDimension 。
3,设置 estimatedRowHeight ** 的值或者实现预估高度的代理方法。
这是你需要知道的几个要点,现在开始下载代码,搞起项目了。
(旁白:确实很重要,虽然说了不少废话,但是并不是浪费时间的。)
教程 App 概览
设想一下你的老大来到你面前,对你说:“我们的用户在为看** Deviant Artists 的方法而大声抗议”。
我会问:"什么是 Deviant Artists "。
你的头解释说:“那是一个艺术家们用来分享自己作品的社交平台。你可以通过 Deviant Art website 和 Media RSS endpoint 来了解艺术家的公告和动态。”
老大:“我们开始做这个 App 吧,但是要怎么样把内容显示到表格上呢?你能做到吧?”
你突然受到了感召,走进最近的电话亭,换上了披风成了super Dev..
但你不需要弄骗人的把戏做你老大的英雄,用你的编程技术就可以做到了。
(旁白:你怎么不去做导演啊...)
首先,现在客户端代码(项目的起始程序)这里。
(旁白:语法有一些过时,打开项目会自动让你转换到最新的语法,转换完之后会报一个错,将 ** FeedViewController.swift 里的 deselectAllRows **方法替换成如下代码:
func deselectAllRows() {
if let selectedRows = tableView.indexPathsForSelectedRows{
for indexPath in selectedRows {
tableView.deselectRowAtIndexPath(indexPath, animated: false)
}
}
}
)
这个项目使用的** CocoaPods ,因此打开 DeviantArtBrowser.xcworkspace ** (不是** xcodeproj 这个文件),pods 已经包含到 zip 包里了,所以不用在重新 pod install **。
注意:如果你不清楚什么是 CocoaPods ,可以看下这个教程。
打开** Main.storyboard (在 DeviantArtBrowser ** project 下的** DeviantArtBrowser 文件夹 Views **分组里),你将会看到下面四个场景:
从左到右,它们是:
- 顶级的导航控制器。
- **FeedViewController **,标题是 ** Deviant Browser **。
- 还有两个都是** DetailViewController 的场景,标题分别是Deviant Article** 和 Deviant Media ,一个只用来显示文本,另一个文本和图片一起显示。
编译并运行,你将看到一些控制台上的输出日志和一个短暂出现的活动指示器,但是在 app 里并没有什么内容显示出来。
日志输出像下面一样:
2014-11-08 14:30:02.746 DeviantArtBrowser[70847:829282] GET 'http://backend.deviantart.com/rss.xml?q=boost%3Apopular'
2014-11-08 14:30:03.297 DeviantArtBrowser[70847:829282] 200 'http://backend.deviantart.com/rss.xml?q=boost%3Apopular' [0.5506 s]
app 发起一个网络请求并获得返回,但是并没有做任何事。
(旁白:如果请求失败,很可能是iOS9系统下不可用http协议,打开info.plist的源码,粘贴如下内容。)
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
现在,打开** FeedViewController.swift(在 Controllers **文件夹的下面)。
看下 ** parseForQuery **这段代码:
func parseForQuery(query: String?) {
showProgressHUD()
parser.parseRSSFeed(deviantArtBaseUrlString,
parameters: parametersForQuery(query),
success: {(let channel: RSSChannel!) -> Void in
self.convertItemPropertiesToPlainText(channel.items as! [RSSItem])
self.items = (channel.items as! [RSSItem])
self.hideProgressHUD()
self.reloadTableViewContent()
}, failure: {(let error:NSError!) -> Void in
self.hideProgressHUD()
println("Error: \(error)")
})
}
** parser 是一个 RSSParser 的实例,属于 MediaRSSParser 的一部分。
这是一个得到 Deviant Art ** RSS feed 的网络请求,它会在成功的 block 里返回一个** RSSChannel 实例。然后解析数据将 HTML 转成普通文本, channel.items 和控制器里的 items **属性都是数组。
** channel.items 数组包含 RSSItem **对象,每一个对象元素都是一个 RSS feed。(现在你该知道要将什么显示到表格里了吧,正是 ** items **数组!)
最后,项目里会有一些** //TODO:Write this... **的注释,是为了告诉我们需要实现些什么。
开始创建自定义 Cell
查看源代码之后,你现在知道这个 app 有了不错的数据,但是什么都没有显示出来,要显示它们,你需要创建一个自定义的 table view cell。
- 添加一个新类到** DeviantArtBrowser**项目里。
- 名字是 ** BasicCell 并且继承自 UITableViewCell **。
- 确信** Also create xib file **没有被勾选。
- 语言选择** Swift 。
打开 BasicCell.swift ** 并添加如下属性:
@IBOutlet var titleLabel: UILabel!
@IBOutlet var subtitleLabel: UILabel!
接下来,打开** Main.storyboard ,拖拽一个UITableViewCell到** FeedViewController 的 table view上。
设置 BasicCell 的 Custom Class 。
设置 BasicCell 的 Identifier (Reuse Identifier) 。
设置 cell 的 Row Height 为 83 。
拖拽一个 UILabel 到 cell 上,设置 text为 Title 。
设置 label 的 Lines 为 0,就是没有上限。(行数可以设置很多)
接下来像下面截图一样设置 label 的尺寸和位置。
连接 cell 的 label 到 BasicCell 的 titleLabel ** outlet 上。
接下来,拖拽第二个 UILabel 到 cell 上,像第一个 label 一样,并且设置 text 为** Subtitle 。
像第一个 label 一样,按照下面的截图来设置 Subtitle 的尺寸和位置。
设置 subtitle label 的 Color ** 为 * Light Gray Color ;字体大小为 * 15.0 ;并且 Lines **为 0 ;
将 cell 的 subtitle label 连接到 BasicCell 上的 subtitleLabel outlet上。
接下来,你将给** BasicCell **添加 auto layout ** constraints ** ,来布局 cell 。
注意:如果你对 auto layout 还不太熟悉,不清楚怎么设置 auto layout constraints ,可以看下 这个教程。
选择 title label 且设置它的 top,trailing,leading 距离父视图(也就是content view)20个点。确信你没有勾选Constrain to margins。(旁白:这个属性就是系统会为你默认两边留白)
确信 cell 的 title label 一直是:
- 向上距离20个点。
- 相对于 content view 的整体宽度,左右两边空出20个点。
现在,选择 subtitle label 设置它的 leading,trailing,和** bottom 距离父视图20个点。再次确认,没有勾选Constrain to margins。
像 title label 一样,确定在 subtitle label 上的 constraints ,是按照底部距离 content view 20个点,左右距离 content view 也是20个点。
(旁白:还是看图更直观一些)
技巧: 使用 auto layout 布局 UITableViewCell 时,要确定这些约束都布局到了每一个 subview 的四边,也就是说,每一个 subview 都应该有 leading,top,trailing 和 bottom 约束。
除此之外,** contentView 的顶部到底部都要有清晰的约束条件, 你要能确定,这些子视图的约束可以正确的指定出 contentView **的高度。
另一部分技巧是,interface Builder 经常在你缺失一些约束的情况下,没有警告提示。在运行项目时,auto layout 没有返回正确的高度,比如会返回 0 的高度,遇到这些问题需要你重新调整约束条件 直到满足条件为止。
现在, 选择 subtitle label, 按住 Control 并拖拽到 title label。选择 Vertical Spacing 连接** subtitle label 的顶部和 title label **的底部。
在 title label 上,设置 ** Horizontal** 和 ** Vertical** 的 Content Hugging Priority 和 Content Compression Resistance Priority为 751。
在 subtitle label 上,设置 ** Horizontal** 和 ** Vertical** 的 Content Hugging Priority 和 Content Compression Resistance Priority为 750。
(旁白:解释一下,这两个优先级的意思,**Content Hugging Priority **就是级别越高,越不会被拉开,抻开。 **Content Compression Resistance Priority **就是级别越高,越不会被压缩,挤掉。这个还是要看具体的例子来理解的,有一点绕。)
这就是告诉 auto layout 怎样去适配 labels 的文本-区分 title label 和 subtitle label 之间约束的优先级。在这个例子里,这些约束基本满足了条件。
检查: 上面的约束条件满足情况了吗?
1,每一个子视图的所有侧边都有约束吗?Yes。
2,**contentView 从上到下都有约束吗?Yes(旁白:只有这样 contentView 才能确定出自己的高度,像 UIScrollView 一样,才能知道自己的 contentSize,否则运行起来都不能滑动。)
**titleLabel **距顶部有20个点,它和 **subtitleLabel **之间的距离是4个点,并且 ** subtitleLabel **和底部有19.5个点。
所以,现在 auto layout 已经可以动态设置 cell 的高度了。
接下来,你要创建一个 ** BasiceCell ** 跳转到 **Deviant Article **场景的链接(segue)。
选择你的 BasiceCell,按住 control 拖拽到 **Deviant Article 场景,从 Selection Segue **选项中选择 **Push **。
Interface Builder 将自动更改 Accessory 属性为 Disclosure Indicator,这是为了指示你从 cell 导航到详情里去。然而,这并不符合程序的设计,选择 **BasiceCell ** ,更改 ** Accessory **为 None。
现在,用户在任何时候点击 BasicCell 都会跳转到 **DetailViewController **里了。
哇塞,你的** BasicCell 已经设置完了!如果你编译运行 app 的话,还是毛都看不到,为啥呢...(旁白:自言自语..)
还记得哪些** TODO **的注释吗?,对了,这就是问题所在了。你需要到哪些 TODOs 里写一些代码。
配置 Table View
首先,你需要配置下这个 table view。
打开** FeedViewController.swift ** ,用下面的代码替换**configureTableView() **这个方法。
func configureTableView() {
tableView.rowHeight = UITableViewAutomaticDimension
tableView.estimatedRowHeight = 160.0
}
确信 table view 是用 auto layout 来动态设置高的时候,将 **rowHeight 设置为 UITableViewAutomaticDimension 。
确定这些之后,你还要提供一个 ** estimatedRowHeight 的值。小意思,160.0是一个随意的值也可以使用。在你的项目里,你可能也想根据数据类型设置一个更好的值。
实现 UITableView 的 Data Source
接下来,你需要实现** UITableViewDataSource 的协议方法。
首先,在 FeedViewController **里加上这个常量:
let basicCellIdentifier = "BasicCell"
这可以让你用这个标识在 storyboard 里取得 ** BasicCell 。
接下来,用 tableView(_:numberOfRowsInSection:) **返回从 Deviant Art 获取到数据个数:
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return items.count
}
然后用,下面的代码替换** tableView(_:cellForRowAtIndexPath:) **这个方法。
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
return basicCellAtIndexPath(indexPath)
}
func basicCellAtIndexPath(indexPath:NSIndexPath) -> BasicCell {
let cell = tableView.dequeueReusableCellWithIdentifier(basicCellIdentifier) as! BasicCell
setTitleForCell(cell, indexPath: indexPath)
setSubtitleForCell(cell, indexPath: indexPath)
return cell
}
func setTitleForCell(cell:BasicCell, indexPath:NSIndexPath) {
let item = items[indexPath.row] as RSSItem
cell.titleLabel.text = item.title ?? "[No Title]"
}
func setSubtitleForCell(cell:BasicCell, indexPath:NSIndexPath) {
let item = items[indexPath.row] as RSSItem
var subtitle: NSString? = item.mediaText ?? item.mediaDescription
if let subtitle = subtitle {
// Some subtitles are really long, so only display the first 200 characters
if subtitle.length > 200 {
cell.subtitleLabel.text = "\(subtitle.substringToIndex(200))..."
} else {
cell.subtitleLabel.text = subtitle as String
}
} else {
cell.subtitleLabel.text = ""
}
}
这边发生什么:
- 在 tableView(:cellForRowAtIndexPath:) 里,调用basicCellAtIndexPath(:) 方法获取一个** BasicCell **。
- 在basicCellAtIndexPath(:)里获取一个** BasicCell **,使用setTitleForCell(:indexPath:) 方法设置 title label 的text,使用** setSubtitleForCell(_:indexPath:) **方法设置 subtitle label 的text,然后return 这个 cell。
现在,你需要实现... 等等,搞定了!就这么简单?
编译运行,你将看到这个表格:
图片在哪呢?
这个app看起来还不错,但是好像感觉缺了点什么?
噢,艺术在哪呢?
Deviant Art 上都是图片,但这个 app 没有显示它们,你需要去修复下这个问题,不然,你的老大会让你失去理智!
有个方法是在你的** BasicCell **上加一个 image view。
但是在 Deviant Art 上带图片的信息和纯文字的信息都有,所以更好的做法是新建一个自定义 cell。
增加一个继承自** BasicCell **的新类到项目里,名字叫 ImageCell,原因是你的新 cell 也需要 titleLabel 和 subtitleLabel。因此,有必要在基类里已经存在一些方法的时候再做所有的事吗?
打开** ImageCell.swift ** 并且增加下面的属性:
@IBOutlet var customImageView: UIImageView!
这个属性的名字是** customImageView,因为在 UITableViewCell 里已经有了一个叫 ImageView **的属性了。
打开** Main.storyboard ,选择 basic cell 使用 ⌘C ,或者从菜单里选择 Edit > Copy **。
选择这个 table view 并且 按下 **⌘V ,或者 Edit > Paste **,去创建一个 cell 的新 copy 。
注意:如果你操作有误,没有得到想要的结果,记得使用** ⌘Z 或者 Edit > Undo **来撤销操作。
选择新 cell ,更改它的Custom Class为** ImageCell **。同样的,更改它的 Reuse Identifier ** 为 ImageCell **。
在** ImageCell 上选择 title label,更改它的位置 x 为128**,且宽度为 172。subtitle label 也是一样。
Interface Builder 将会有一些警告,因为这些 labels 摆放的位置和约束设置的不一样。
正确的做法是,选择** ImageCell 上的 title label 删掉 leading 这条约束,subtitle label 也是一样删掉 leading **约束。
现在选择** ImageCell 的 title label,按照下面的截图改变它的 Intrinsic Size 的 Placeholder 的值。同样的,改变 subtitle label 的 Intrinsic Size 。
(这是用来告诉 Interface Builder 去更新当前 view 的 frame 的占位符,Interface Builder 将不会显示警告了。)
你需要在 cell 上增加一个 image view。但是现在的高度有一点小,所以选择 ImageCell 修改它的 Row Height 为 141**。
现在,拖拽一个 image view 到** ImageCell **上,按照下面的截图设置这个新 view 的位置和尺寸。
接下来,选择这个 image view 做如下布局:
- 设置 leading,top 和 bottom 为20。
- 设置** width 和 height 为 100**。
- 确认 **Constrain to margins **没有被勾选
-
最后点击** Add 5 constrain **的按钮。
选择** image view ** 显示它的所有约束,然后选择它的 bottom 约束来编辑它。在属性编辑器里,改变** Relation 为 Greater Than or Equal 它的 Priority 为 999。
同样的,选择 subtitle label 去显示它的所有约束条件,然后选择它的 bottom ,在属性编辑器里,改变 Relation 为 Greater Than or Equal 它的 Priority**为 1000。
这是告诉 auto layout 在** imageView 和 subtitleLabel 向下的约束都为20个点的时候,打破 imageView 的,遵循 subtitleLabel 的。(旁白:因为它的优先级高,这样避免约束冲突。)
然后将 image view 的** height 和 width 约束的优先级 Priority 设置成 999 。
这是因为自定义的约束有时候会和系统定义的约束之间会产生冲突,这时就是告诉 auto layout ,“如果一定要这么做,就打破这些自定义的约束吧”
(旁白:系统定义的优先级高,1000。)
在大部分情况下,auto layout 都会满足这些约束。在极少数的情况下它会打破这些约束,比如在改变设备方向时,但通常都是1-2的像素偏差,不明显。
提示: 尤其在 table view cell上, auto layout 不总是很明显的提示这些约束。
如果你在控制台上看到这些 auto layout 不得不打破一个约束的警告信息,你就要试着去调整下你的约束条件的** priorities **了。
最后,选择 ImageCell上的 title label 使用 Pin Button 来设置下 leading 约束为 8。subtitle label 也是一样的。
现在你的** ImageCell **上的约束看起来是这个样子的:
你需要选择** ImageCell 和 image view 的 customImageView ** outlet 进行链接。
你需要一个** ImageCell 跳转到 Deviant Media 场景的链接(segue),
这样用户点击 ImageCell **就可以看到详情了。
像之前设置 basic cell 一样, 选择** ImageCell ,按住 control 并拖拽到 Deviant Media 场景,然后在 Selection Segue 选项中选择 Push **。
确认你的** Accessory 改回为 None **。
非常好,你的** ImageCell **已经设置完成了!现在你可以加一些代码让它显示出来了。
显示这些图片!
打开** FeedViewController.swift ** 在上面增加一个常量:
let imageCellIdentifier = "ImageCell"
接下来替换** tableView(_:cellForRowAtIndexPath:) **的代码:
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
if hasImageAtIndexPath(indexPath) {
return imageCellAtIndexPath(indexPath)
} else {
return basicCellAtIndexPath(indexPath)
}
}
func hasImageAtIndexPath(indexPath:NSIndexPath) -> Bool {
let item = items[indexPath.row]
let mediaThumbnailArray = item.mediaThumbnails as! [RSSMediaThumbnail]
for mediaThumbnail in mediaThumbnailArray {
if mediaThumbnail.url != nil {
return true
}
}
return false
}
func imageCellAtIndexPath(indexPath:NSIndexPath) -> ImageCell {
let cell = self.tableView.dequeueReusableCellWithIdentifier(imageCellIdentifier) as! ImageCell
setImageForCell(cell, indexPath: indexPath)
setTitleForCell(cell, indexPath: indexPath)
setSubtitleForCell(cell, indexPath: indexPath)
return cell
}
func setImageForCell(cell:ImageCell, indexPath:NSIndexPath) {
let item: RSSItem = items[indexPath.row]
// mediaThumbnails are generally ordered by size,
// so get the second mediaThumbnail, which is a
// "medium" sized image
var mediaThumbnail: RSSMediaThumbnail?
if item.mediaThumbnails.count >= 2 {
mediaThumbnail = item.mediaThumbnails[1] as? RSSMediaThumbnail
} else {
mediaThumbnail = (item.mediaThumbnails as NSArray).firstObject as? RSSMediaThumbnail
}
cell.customImageView.image = nil
if let url = mediaThumbnail?.url {
cell.customImageView.setImageWithURL(url)
}
}
像上面创建** BasicCell **一样,但是有一点不同,有一些新的代码:
- hasImageAtIndexPath(_:) 检查IndexPath下的item的** mediaThumbnail的 url 不为空。如果不为空的要使用 ImageCell **来展示数据。
- **imageCellAtIndexPath(:) 和 basicCellAtIndexPath(:) 一样,但是它要用 setImageForCell(_:indexPath:) **来设置下图片。
- ** setImageForCell(:indexPath:) ** 尝试获取第二个 media thumbnail。使用 AFNetworking提供的** setImageWithURL(:) **方法来获取图片。
编译运行,你将看到漂亮的艺术图片!默认,app 检索 “popular“ 分类的数据,但你也能按照艺术家搜索。
试着输入** by:CheshireCatAr t** 并搜索。这是我的一个朋友,Devin Kraft,他是一个杰出的画家。(查看他的website)。
这是我很喜欢的一位,他在 Deviant Art 上很活跃,发过作品和博客。因此,他的账号是个很好的测试账号,可以测试带图片的 cell 和不带图片的 cell。
这个 app 已经看起来很漂亮了,但是你还可以让你的老大对你的技能信心提升一到两个级别。
优化表格
还记得很早之前设置** estimatedRowHeight 为 160.0 的时候吗?这个属性是在 BasicCell **的竖屏方向上工作的。但是这个值非常的不准。
你可以使用** UITableViewDelegate **提供的,在运行时给 cell 估算高度的方法替换这个值。
改之前,你需要删除** configureTableView(): **里的一行代码:
tableView.estimatedRowHeight = 160.0
现在,在** // MARK: UITextFieldDelegate **组下增加下面的方法。(实际上,你可以在这个类的任何地方增加它们,但这样结构比较清楚易读。)
// MARK: UITableViewDelegate
func tableView(tableView: UITableView, estimatedHeightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
if isLandscapeOrientation() {
return hasImageAtIndexPath(indexPath) ? 140.0 : 120.0
} else {
return hasImageAtIndexPath(indexPath) ? 235.0 : 155.0
}
}
func isLandscapeOrientation() -> Bool {
return UIInterfaceOrientationIsLandscape(UIApplication.sharedApplication().statusBarOrientation)
}
这个预估 cell 高的方法很简单。检查当前方向是否是横屏并且如果当前 index path 有图片的话,返回一个预先裁定的值。
提示:不论你是实现的代理方法,还是简单的设置了一个** estimatedRowHeight 固定的值。
这个 table view 会在代理方法和 estimatedRowHeight 的值之中选择一个。它会影响到滑动的指示条和滑动的性能。(是否卡顿)
如果你的 cell 预估的高度不太正确,那么在滑动的时候就会卡顿,滑动指示条不太准,内容也会混乱。
如果你的预估是准确的,计算就会慢,table view 滚动也会变慢。
这个成功的关键就在于在准确和不准确之间找一个平衡,减少不必要的计算成本。
(旁白:难道自己慢慢调试这个数值吗?)
你还是使用固定的值来估算的高度,但是现在可以根据 cell 的类型和设备的方向来设置更合理的高度了。你可以设置自己感兴趣的值,但是记住诀窍就是能让它计算的更快就行。
编译并运行,你应该能看到 table view 滑动的很流畅,看起来很棒。
这就是方法的最后实现,这个 app 现在完成了!
从这去哪呢
你可以去下载完成项目,在这
(旁白:上面的路径下载下来的项目编译会报错,因为它是用Xcode6.3和Swift1.2做的,转换到最新语法之后,在像上面已经提到过的修改一个方法的代码,添加http白名单。你也可以下载这里已经修改过的代码)
Table views 可能是iOS里面组织数据视图中最常用的了。你的 apps 会很复杂,你可能要使用各种类型的自定义 cell 来布局。幸运的是 iOS8 和 auto layout 能很容易的做到这些。
(旁白:不幸的是,有多少 app 是只支持iOS8以上的呢。)
如果你有一些问题或建议,请在下面留言。