UITableView 教程:动态 Table View Cell 高
旧翻译文章迁移自:简书 create by 2016.03.11 00:33
原文地址: 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 方法替换成如下代码:1
2
3
4
5
6
7func 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(在DeviantArtBrowserproject 下的DeviantArtBrowser文件夹Views分组里),你将会看到下面四个场景:
从左到右,它们是:
- 顶级的导航控制器。
*FeedViewController,标题是Deviant Browser。 - 还有两个都是DetailViewController的场景,标题分别是Deviant Article和Deviant Media,一个只用来显示文本,另一个文本和图片一起显示。
编译并运行,你将看到一些控制台上的输出日志和一个短暂出现的活动指示器,但是在 app 里并没有什么内容显示出来。
日志输出像下面一样:
1 |
|
app 发起一个网络请求并获得返回,但是并没有做任何事。
(旁白:如果请求失败,很可能是iOS9系统下不可用http协议,打开info.plist的源码,粘贴如下内容。)
1 | <key>NSAppTransportSecurity</key> |
现在,打开FeedViewController.swift(在Controllers文件夹的下面)。
看下parseForQuery这段代码:
1 | func parseForQuery(query: String?) { |
parser是一个RSSParser的实例,属于 MediaRSSParser 的一部分。
这是一个得到Deviant ArtRSS 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并添加如下属性:
1 | @IBOutlet var titleLabel: 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的titleLabeloutlet 上。
接下来,拖拽第二个 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 layoutconstraints,来布局 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 的高度,遇到这些问题需要你重新调整约束条件 直到满足条件为止。
现在, 选择subtitlelabel, 按住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()这个方法。
1 | func configureTableView() { |
确信 table view 是用 auto layout 来动态设置高的时候,将rowHeight设置为UITableViewAutomaticDimension。
确定这些之后,你还要提供一个estimatedRowHeight的值。小意思,160.0是一个随意的值也可以使用。在你的项目里,你可能也想根据数据类型设置一个更好的值。
实现 UITableView 的 Data Source
接下来,你需要实现UITableViewDataSource的协议方法。
首先,在FeedViewController里加上这个常量:
1 | let basicCellIdentifier = "BasicCell" |
这可以让你用这个标识在 storyboard 里取得BasicCell。
接下来,用tableView(_:numberOfRowsInSection:)返回从 Deviant Art 获取到数据个数:
1 | func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int { |
然后用,下面的代码替换tableView(_:cellForRowAtIndexPath:)这个方法。
1 | func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { |
这边发生什么:
- 在 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并且增加下面的属性:
1 | @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 的customImageViewoutlet 进行链接。
你需要一个ImageCell跳转到Deviant Media场景的链接(segue),
这样用户点击ImageCell就可以看到详情了。
像之前设置 basic cell 一样, 选择ImageCell,按住 control 并拖拽到Deviant Media场景,然后在Selection Segue选项中选择Push。
确认你的Accessory改回为None。
非常好,你的ImageCell已经设置完成了!现在你可以加一些代码让它显示出来了。
显示这些图片!
打开FeedViewController.swift在上面增加一个常量:
1 | let imageCellIdentifier = "ImageCell" |
接下来替换tableView(_:cellForRowAtIndexPath:)的代码:
1 | func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { |
像上面创建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():里的一行代码:
1 | tableView.estimatedRowHeight = 160.0 |
现在,在// MARK: UITextFieldDelegate组下增加下面的方法。(实际上,你可以在这个类的任何地方增加它们,但这样结构比较清楚易读。)
1 | // MARK: UITableViewDelegate |
这个预估 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以上的呢。)
如果你有一些问题或建议,请在下面留言。