自适应Table View Cells
注意:这篇教程支持最新的Xcode 7.3,iOS 9和Swift 2.2。
如果你曾经创建过自定义table view cells,那么很可能你写了一堆适应大小的代码。你可能会对于计算每个label、image view、text field和其它一切cell里的东西高度很熟悉,而且还是手动的。
讲道理,这种方式实在令人难以接受、极易出错、性价比很低。
这篇自适应Table View Cells教程里,你会学习到如何创建自定义table view cells,并且动态改变他们到适合他们内容的大小。你可能在想,“那一定需要许多工作……!”
并不。:] 很幸运,苹果在iOS 8里让这一切都变得非常简单。
注意::这篇教程需要Xcode 7.3或更新的版本才能兼容最新的Swift语法。
这篇教程还假定你已经基本熟悉Auto Layout、UITableView和Swift开发。如果你是iOS或Swift开发的完全新手,你应该先看看我其他的教程。
上车
穿越到iOS 6时代,苹果发布了一个非常好的新技术:Auto Layout。为庆祝这一壮举,全国的开发者举国欢庆,夜夜笙歌,红旗招展,人山人海……
好吧,可能有一点点夸张,但真的是件大事儿。
虽然它鼓舞了无数的开发者,但Auto Layout还是很笨重。手动写Auto Layout代码曾经是、现在也是iOS开发的啰嗦的绝佳例证。Interface Builder一开始的时候在设置constraints的时候也完全在帮倒忙。
回到现在。伴随着对Interface Builder的所有提升以及iOS 8的发布,使用Auto Layout来创建自适应table view cells终于简单了!
除了一点点额外的工作,你要做的所有事情就是:
- 创建table view cells的时候使用Auto Layout。
- 设置table view的rowHeight为UITableViewAutomaticDimension。
- 设置estimatedRowHeight或实现height estimation delegate方法。
但你不想现在马上就挖掘理论,不是吗?你已经准备好写代码了,所以让我们直接用项目开始吧。
教程App预览
想象一下你最大的客户跑过来和你说,“我想要一个app可以显示曾经的最著名艺术家以及他们最著名的作品!”
“我们开始做App了,但我们被如何在table view里显示内容给难倒了,”你的客户承认。“你能帮个忙吗?”
你突然有一种强烈欲望想跑进最近的电话亭然后换上穿上披风。
但你并不需要这些小花招来让你成为客户的大英雄——你的编程技能点已经够了!
首先,从这里下载“客户的代码”—艺术-起始(提取码:cc3a)-这个教程的起始项目,基于自适应table view cells。解压zip文件然后在Xcode里打开项目。
打开Main.storyboard(在Artistry项目下的Views分组。)你会看见三个scenes:
从左到右,他们是:
- 一个顶级导航控制器
- ArtistListViewController显示艺术家列表
- ArtistDetailViewController显示艺术家的作品和每个作品的信息
Build and run。你会看到ArtistListViewController显示了艺术家的列表。选择第一个艺术家(Pablo Picasso),app会segue到ArtistDetailViewController,显示了选中的艺术家的作品列表:
不仅app没有每个艺术家和每件作品的图片,你想显示的信息都被裁掉了!每一条信息和图片都会是不同的尺寸,所以不能只是增加table view cell高度,然后就收工了!你的cell高度需要时动态的,基于每个cell的内容。
你会从ArtistListViewController开始实现动态cell高度。
自适应Table View Cells
要让动态cell高度能正常工作,你需要创建一个自定义table view cell然后设置它为正确的Auto Layout constraints。
在project navigator里选择Views分组,然后按Command-N来在这个分组里创建一个新文件。创建一个新的Cocoa Touch Class叫做ArtistTableViewCell,让它是UITableViewCell的子类
打开ArtistTableViewCell.swift,删除两个自动生成的方法,然后添加下面的property:
@IBOutlet var bioLabel: UILabel!
下一步,打开 Main.storyboard,选择ArtistListViewController里的table view里的cell。在Identity Inspector把Class改为ArtistTableViewCell:
拖一个新的UILabel到cell上,设置text为“简历”。在Identity Inspector里设置新的label的Lines property(label可以拥有的最大行数)为 0。看起来应该像这样:
设置行数对于动态大小cells非常重要。一个设置行数为0的label会根据它显示的文本数量来增长。设置为任何其他数字的行数label会缩短他们的文字,一旦超出了可用的行数的话。
链接ArtistTableViewCell的bioLabel outlet到cell上的label。一个快速方法是右击Document Outline里的Cell,然后按住从弹出的菜单的Outlets列表的bioLabel右侧的空白圆圈到拖到你排放的label:
让Auto Layout在 UITableViewCell 上工作的秘诀就是确保每个subview所有的边上都有constraints来把它们固定住——这就是,每个subview都要有leading、top、trailing和bottom constraints。然后,subviews的实际高度会被用来决定每个cell的高度。你马上就会这么做。
注意:如果你并不熟悉Auto Layout,或者希望复习一下如何设置Auto Layout constraints,可以看看其他教程。
选择bioLabel然后按storyboard底部的Pin按钮。在这个菜单里,简单的选择菜单最上方的4条虚线,改变leading和trailing值为8,然后点击Add Constraints。看起来会像这样
这确保不论cell是大还是小,bio label总是:
- 上下外边距都是0点
- 左右外边距都是8点
回顾:这满足之前的Auto Layout标准吗?
- 是否每个子视图每个边都有constraints固定?是的。
- constraints是从contentView的顶部到底部吗?是的
bioLabel 用0点连接了上下外边距
所以,Auto Layout现在可以确定cell的高度了!
酷,你的ArtistTableViewCell设置好啦!如果你现在build and run一下app,你会看到...
什么都没变。什么鬼?!不要担心,在cells成为动态前只需要再写一点点代码。
配置 Table View
首先,你需要配置 table view 来正确使用你的自定义cell。
打开 ArtistListViewController.swift 然后用下面的替换 tableView(_:cellForRowAtIndexPath:):
func tableView(tableView: UITableView,
cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("Cell",
forIndexPath: indexPath) as! ArtistTableViewCell
let artist = artists[indexPath.row]
cell.bioLabel.text = artist.bio
cell.bioLabel.textColor = UIColor(red: 114 / 255,
green: 114 / 255,
blue: 114 / 255,
alpha: 1.0)
return cell
}
上面的代码很直接:让一个 cell 出列(dequeue),设置它的信息和文字颜色,然后返回 cell。
再次运行app,它会看起来还是没什么改变。你现在用的是 bioLabel,但每个cell只显示一行文本。即使行数设置为 0并且你的 constraints 被正确配置了,所以你的 bioLabel 占据了整个 cell,这说明 table views 需要被告知让 Auto Layout 来驱动每个 cell 的高度。
回到 ArtistListViewController.swift 把这两行代码加到 viewDidLoad() 方法的底部:
tableView.rowHeight = UITableViewAutomaticDimension
tableView.estimatedRowHeight = 140
当你设置 rowHeight 为 UITableViewAutomaticDimension 的时候,table view 被告知使用 Auto Layout constraints 和 cells 的内容来决定每个 cell 的高度。
为了让 table view 这么做,你必须要也提供一个 ** estimatedRowHeight**。这个例子里,140.0 只是一个随意值,在这个特定实例里很合适。对于你自己的项目,你应该选择一个更符合你会显示的数据类型的值。
Build and run,你现在应该能看到每个艺术家的简介啦 :]
添加图片
现在你能看到每个艺术家的整个简介了,这很好,但还有更多需要需要显示。每个艺术家有一个图片和名字。使用这些额外数据块来让 app 看起来更棒。
你需要添加一个 image view 给 ArtistTableViewCell,还有另一个 label 给艺术家的名字。
打开 ArtistTableViewCell.swift 然后添加下面的 properties:
@IBOutlet var nameLabel: UILabel!
@IBOutlet var artistImageView: UIImageView!
image view 的变量名叫做 artistImageView 而不是 imageView 因为 UITableViewCell 已经有一个 imageView property 了。
打开 Main.storyboard,选择 cell 然后在 Size Inspector 改变 Row Height 为 140;给你更多空间来使用:
现在选择 Bio Label 的 leading constraint。你可以在 Document Outline 里找到它,在 Content View 里的 Constraints 下面:
按 Delete 删除这个 constraint。现在忽略任何 Auto Layout 警告。用你的光标抓住 bio label 左侧边缘,然后把它拉到右边让 bio label 现在只占据 cell 一半的宽度。左半边会用来给 image view 和 艺术家名字的 label:
拖一个新 label,放到 cell 的底部,在你新开辟的区域水平居中。设置 label 的 text 为 “名字”:
现在拖出一个 image view,放到 name label 上面:
最后,为新的 image view 和新的 label 都连接 outlets,使用你对 bio label 所用的同样的技术:
现在是时候设置更多 constraints 了。从 name label 开始向上去,使用 Pin 菜单来:
- 固定 name label 的底部边距为 0 点,从 content view 的底部外边距。
- 固定 name label 的上边距为 8 点,从 image view 的底部。
- 固定 image view 的上边距为 0 点,从 content view 的上边距。
- 固定 image view 的 leading 边距为 0 点,从 content view 的 leading 边。
- 固定 image view 的 trailing 边距为 16 点,从 bio label 的 leading 边距。
选中 image view,按住 Control 拖到 cell 的 content view。Let go,然后选择菜单里的 Equal Widths:
从 Document Outline 里选择这个新的宽度 constraint,然后设置 multiplier 为 0.5:
这让 image view 的宽度会等于准确的 cell 的一半宽度。
只有一对 constraints 需要添加:
- 按住 Shift 点击 image view 和 name label 然后从 Pin 菜单里选择 Equal Width
- 按住 Shift 点击 image view 和 name label 然后从 Align 菜单里选择 Horizontal Centers
带有这些全新的 constraints,Auto Layout 大概已经抛出一些警告让你知道一些框架过期了。要修复好它,选择 Document Outline 里 cell 的 Content View 然后点击 Resolve Auto Layout Issues 菜单然后选择 All Views 下面的 Update Frames:
storyboard 现在就这样了。打开 ArtistListViewController.swift 然后添加下面两行代码到 tableView(_:cellForRowAtIndexPath:),在你设置好 bioLabel 的文字之后:
cell.artistImageView.image = artist.image
cell.nameLabel.text = artist.name
然后在设置好 textColor 之后添加这些行:
cell.nameLabel.backgroundColor = UIColor(red: 255 / 255, green: 152 / 255, blue: 1 / 255, alpha: 1.0)
cell.nameLabel.textColor = UIColor.whiteColor()
cell.nameLabel.textAlignment = .Center
cell.selectionStyle = .None
Build and run 这个 app。这一屏看起来更好了,但向下滑到 Georgia O’Keeffe 然后你会注意到一些奇怪的事情:
根据 constraints name label 被撑大了(上面到 image view 底部 8 点,底部外边距到 cell 的 content view 为 0 点)。
可以调整两个 constraints 来修复它。在 Main.storyboard 选择 Name label 然后从它的底端创建另一个 constraint 到 cell 的 底部外边距(margin)。现在从 Document Outline 选择那个 constraint 然后改变它的 Relation 为 Greater Than or Equal:
现在选择 name label 的旧的底部 constraint 然后设置它的 priority 为 250:
这样的话,Auto Layout 会在需要的时候打破老的 constraint,因为它的 priority(优先级)比带有 >=0 relation 的底部 constraint 要低。再次运行 app 然后所有东西现在看起来应该都很棒。
展示艺术!
如果你回顾一下开头,选择一个艺术家显示一个 view controller,它显示选中艺术家的作品。table view 里的 cells 会需要有动态高度,因为每个作品有不同尺寸的图片和伴随数据。
第一步,就像之前一样,创建另一个 UITableViewCell 子类。
在 project navigator 里选择 Views 组然后按 Command-N 在这个组里创建一个新文件。创建一个新的 Cocoa Touch Class 叫做 WorkTableViewCell 然后让它成为 UITableViewCell 的子类。
打开 WorkTableViewCell.swift 然后,像从前一样,删除两个 WorkTableViewCell 自动生成的方法然后添加这些 properties:
@IBOutlet weak var workImageView: UIImageView!
@IBOutlet weak var workTitleLabel: UILabel!
@IBOutlet weak var moreInfoTextView: UITextView!
打开 Main.storyboard 然后选择 Artist Detail View Controller 场景里的 table view 里的 cell。设置 cell 的 Custom Class 为 WorkTableViewCell,然后改变 row height 为 200 来给你自己大量空间去操作。
现在拖出一个 image view,一个 label,和一个 text view,像下面的图片那样放置他们(text view 在最下面):
改变 text view 的 text 为“点击查看更多信息 >”,并且 label 改为 “名字”。把 image view 的 mode 改为 Aspect Fit。选择 text view,在 Attribute Inspector 里,改变 alignment 为居中并且禁用 scrolling:
禁用 scrolling 和设置 label 为 0 lines 相似的重要。scrolling 禁用的话,text view 知道增长它的尺寸来满足它的全部内容,因为用户不能滑动来浏览文本呀。
再远一点,回到你禁用 scrolling 的地方,移除 User Interaction Enabled 的钩钩,这会允许触摸传递给 text view 并且除法 cell 本身的选中状态。
连接三个元素到对应的 outlets 上,就像你对第一个 cell 做的那样。
现在你要添加 constraints。从 text view 开始然后向上走:
- 固定 text view 的底边到 content view 的底部 margin 为 0 点。
- 固定 text view 的 leading 和 trailing 边到 content view 的 leading 和 trailing margins 为 8 点。
- 固定 text view 的上边到 label 的底部为 8 点。
- 固定 label 的上边到 image view 的下边为 8 点。
- 居中 label,通过在 Align 菜单选择 Horizontally in Container。
- 同时选择名字 label 和 image view(按住 Shift 点击)然后从 Pin 菜单选择 Equal Widths。
- 固定 image view 的上边到 content view 的顶部 margin 为 0 点。
- 固定 image view 的 leading 和 trailing 边到 content view 的 leading 和 trailing margins 为 8 点。
更新 frames 就像之前 Auto Layout 显示任何警告的时候那样做。现在 storyboard 都已经搞完了。就像你要对之前的 view controller 要做的,动态 cell 高度也要用一点代码来做。
打开 ArtistDetailViewController.swift 然后替换 tableView(_:cellForRowAtIndexPath:) 为如下代码:
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath) as! WorkTableViewCell
let work = selectedArtist.works[indexPath.row]
cell.workTitleLabel.text = work.title
cell.workImageView.image = work.image
cell.workTitleLabel.backgroundColor = UIColor(red: 204 / 255, green: 204 / 255, blue: 204 / 255, alpha: 1.0)
cell.workTitleLabel.textAlignment = .Center
cell.moreInfoTextView.textColor = UIColor(red: 114 / 255, green: 114 / 255, blue: 114 / 255, alpha: 1.0)
cell.selectionStyle = .None
return cell
}
这个现在看起来应该非常熟悉了。你在让你的 cell 出列,然后打造他们,获得连接到你要显示的模型结构的关系,然后在返回 cell 之前配置好它。
现在这个类的 viewDidLoad() 里,添加如下代码到方法的结尾:
tableView.rowHeight = UITableViewAutomaticDimension
tableView.estimatedRowHeight = 300
这是同样的代码,你在前一个 view controller 里也用了。运行 app,选择 Picasso,然后你会看到现在可以遍历艺术家的作品了:
哎呦不错哦,但让我们到更高级别吧,添加扩展 cells 来展现有关每个作品更多的信息。你的客户会爱上这个的!
扩展 Cells
因为你的 cell 高度由 Auto Layout constraints 和每个界面元素的内容来驱动,用户点击 cell 的时候扩展 cells 应该和添加更多 text 到 text view 一样简单。
打开 ArtistDetailViewController.swift 然后添加如下extension:
extension ArtistDetailViewController: UITableViewDelegate {
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
// 1
guard let cell = tableView.cellForRowAtIndexPath(indexPath) as? WorkTableViewCell else { return }
var work = selectedArtist.works[indexPath.row]
// 2
work.isExpanded = !work.isExpanded
selectedArtist.works[indexPath.row] = work
// 3
cell.moreInfoTextView.text = work.isExpanded ? work.info : moreInfoText
cell.moreInfoTextView.textAlignment = work.isExpanded ? .Left : .Center
// 4
UIView.animateWithDuration(0.3) {
cell.contentView.layoutIfNeeded()
}
// 5
tableView.beginUpdates()
tableView.endUpdates()
// 6
tableView.scrollToRowAtIndexPath(indexPath, atScrollPosition: UITableViewScrollPosition.Top, animated: true)
}
}
这是发生的事情:
- 你问 tableview 要了到 cell 的关系,到那个与 selected index path 相符的 cell,然后获得了相应的 Work
- 改变 Work 的 isExpanded 状态,然后再把它放回数组(很必要,因为结构体是通过拷贝传递的)。
- 下一步,改变 cell 的 text view,基于 work 是不是被扩展了:如果是,设置 text view 显示作品的 info property,然后改变 text alignment 为 Left。如果没被扩展,把 text 设置回 “点击查看更多信息 >”以及 alignment 设置回 Center。
- 现在 text view 的内容已经改变了,cell 的 constraints 需要被刷新。在动画 block 中调用 layoutIfNeeded(),会显示这些 constraint 改变的动画。
- 除了 constraint 改变,table view 现在需要刷新 cell 高度。调用 beginUpdates() 和 endUpdates() 会强制 table view 用动画的方式刷新高度。
- 最后,告诉 table view 把选中的行滑动到 table view 的顶端,用动画的方式。
现在在 tableView(_:cellForRowAtIndexPath:) 里,添加下面两行到末尾,在你返回 cell 之前:
cell.moreInfoTextView.text = work.isExpanded ? work.info : moreInfoText
cell.moreInfoTextView.textAlignment = work.isExpanded ? NSTextAlignment.Left : NSTextAlignment.Center
这个代码会让正在被复用的 cell 正确的记住之前是否在被扩展状态。
Build and run app。当你点击一个作品 cell,你会看到它扩展到容纳了全部文本。但图片动画有一点诡异。
这不难修复!打开 Main.storyboard 然后在你的 WorkTableViewCell 里选择 image view,然后打开 size inspector。改变 Content Hugging Priority 和 Content Compression Resistance Priority 为下面图片里的值:
设置 Vertical Content Hugging Priority 为 252 会帮助 image view 紧挨着它的内容,并且在动画过程中不会被撑大。设置 Vertical Compression Resistance Priority 为 749 让图片可以被压缩,如果其它界面元素在它旁边增长的话。但这只会有助于让 cell 扩大的动画更平滑。图片根本不会被压缩,因为如果 cell 里面的东西增长了 cell 的高度也会增长。
Build and run app。选择一个艺术家,然后点击作品。你会看到一些非常平滑的 cell 扩展,展示有关每个艺术品的信息:
万岁!
动态类型
你已经向你的客户展示了你的进步,他们都爱上它了!但他们还有最后一个请求。他们想要 app 支持 更大字体(Larger Text)辅助功能。app 需要调整到顾客偏好的阅读尺寸。
在 iOS 7 中发布的,动态类型(Dynamic Type)让这变得简单。动态类型给开发者能力来为不同的文本块(例如大标题或正文)指定不同的文本样式,当用户在设备的设置里改变偏好尺寸的时候文本就能自动调整。
在 ArtistListViewController.swift,添加这两行代码到 tableView(_:cellForRowAtIndexPath:) 的最后,就在返回 cell 之前:
cell.nameLabel.font = UIFont.preferredFontForTextStyle(UIFontTextStyleHeadline)
cell.bioLabel.font = UIFont.preferredFontForTextStyle(UIFontTextStyleBody)
这是使用动态类型设置一个基于文本的界面元素的方式。preferredFontForTextStyle(style:) 只有一个参数,就是你希望这个文本元素使用的样式。有 10 个不同的常量供你使用,参考苹果的文档上 preferredFontForTextStyle(style:) 来了解更多相关内容。
现在你只需要确保在用户改变他们的偏好尺寸的时候 table view 会刷新自己。要实现它,添加下面的方法到 ArtistListViewController 里,就在 viewDidLoad() 正下方:
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
NSNotificationCenter.defaultCenter().addObserverForName(UIContentSizeCategoryDidChangeNotification, object: nil, queue: NSOperationQueue.mainQueue()) { [weak self] _ in self?.tableView.reloadData()
}
}
}
这里你为 onContentSizeCategoryChange: 通知添加了 observer,只要用户改变了偏爱的文本大小就会触发。
observer 使用闭包告诉 table view 来重载自己。这会导致屏幕上的所有 cells 都调用 tableView(_:cellForRowAtIndexPath:),会执行我们刚刚添加的 preferredFontForTextStyle(style:)。现在通知一旦接收字体就总是会更新。
注意:从 iOS 9 开始,移除通知中心 observers 不再是必要的了。如果你的 app 的开发目标是 iOS 8,那么你还是需要这么做!
为 ArtistDetailViewController 添加动态类型支持也几乎相同。打开 ArtistDetailViewController.swift 然后添加这两行代码到 tableView(_:cellForRowAtIndexPath:) 的末尾:
cell.workTitleLabel.font = UIFont.preferredFontForTextStyle(UIFontTextStyleHeadline)
cell.moreInfoTextView.font = UIFont.preferredFontForTextStyle(UIFontTextStyleFootnote)
然后添加和之前的 view controller 完全相同的 viewDidAppear(_:) 实现。
目前用 iOS 9.3 模拟器测试不会工作,所以你要在你的设备上构建来测试它。在你的设备上启动 app,然后回到主屏。打开设置 app,然后点击通用 > 辅助功能 > 更大字体,然后拖动滑块到右边,来增大文本大小到更大的设置:
然后回到艺术 app,你的文本现在应该显示的更大了。并且由于你对动态尺寸 cells 所做的工作,table view 现在看起来棒极了:
接下来去哪儿
恭喜完成了自适应 table view cells的教程!:]
你可以从这里下载完整项目,https://yunpan.cn/cBI579k4YhgkJ (提取码:cde6)。
Table views 可能是 iOS 里最基本的结构化数据视图了。在你的 apps 变复杂的时候,您可能会使用各种自定义 table view cell 布局。但幸运的是,Auto Layout 和 iOS 8 让这个任务变得无比简单。
如果你有任何评论或问题,就写在下面吧!