自適應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 讓這個任務變得無比簡單。
如果你有任何評論或問題,就寫在下面吧!