當然我們建立一個表格,不是讓他在那裡躺分的。
tableView是靠資料驅動,但他並不會管理data,而是只處理dataSource送給他的data。
如果你提供tableView參照的data source,tableView就會依照你提供的資料去創造和配置cell,這就是為何我們剛剛要讓tableView啟用dataSource這個協定。
dataSource會對table發出來與data相關的請求做出回應,直接管理table的data。其他dataSource負責的事件如下:
你可以定義dataSource的方法以增加table的特性與功能。例如
tableView(_:commit:forRowAt:)可以讓row允許滑動刪除。
tableView使用row和section的模式呈現資料給使用者,索引值都是zero based。而你藉由dataSource給定row與section的值來規劃你的table。
(圖片來源:UITableViewDataSource)
使用numberOfSections(in:)和tableView(_:numberOfRowsInSection:)回傳section及row的數目:
func numberOfSections(in tableView: UITableView) -> Int {
return 2
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
switch section {
case 0: return 1
case 1: return 3
default: return 0
}
}
swift提供了很多種內建和自定義格式的cell,而你設計好cell之後各別給定他們identifier,dataSource就會使用tableView(_:cellForRowAt:)這個function,依據identifier告訴你的table哪個row要應用哪個cell格式:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
switch indexPath.section {
case 0:
let cell = tableView.dequeueReusableCell(withIdentifier: "cell1", for: indexPath)
return cell
default:
switch indexPath.row {
case 0:
let cell = tableView.dequeueReusableCell(withIdentifier: "cell2", for: indexPath)
return cell
case 1:
let cell = tableView.dequeueReusableCell(withIdentifier: "cell3", for: indexPath)
return cell
default:
let cell = tableView.dequeueReusableCell(withIdentifier: "cell4", for: indexPath)
return cell
}
}
}
而你可以使用tableView(:titleForHeaderInSection:)及tableView(:titleForFooterInSection:)為你的section加上title與尾標:
func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
switch section {
case 0: return "section 0"
default: return "section \(section)"
}
}
func tableView(_ tableView: UITableView, titleForFooterInSection section: Int) -> String? {
switch section {
case 0: return "footer 0"
default: return "footer \(section)"
}
}
不難發現我的header title雖然打的是小寫,但範例卻被轉成大寫了,這是因為dataSource的header跟footer是固定格式的。
如果想要客製化header跟footer,可以使用tableView(:viewForHeaderInSection:)跟tableView(:viewForFooterInSection:)兩個function,如果tableView(:titleForHeaderInSection:)和tableView(:viewForHeaderInSection:)同時存在的話,會以tableView(_:viewForHeaderInSection:)的設定為主。
現在為了demo範例,我們把table變得簡單一點,並加入data。
首先建立一個裝data的list:
var data = ["Data 1", "Data 2", "Data 3"]
並且讓numberOfRow參照陣列長度並統一格式:
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return data.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
cell.textLabel?.text = data[indexPath.row]
return cell
}
當用戶點擊插入或刪除的按鈕時,tableView會發送editingStyle的訊息向dataSource請求變更,搭配tableView的insertRows(at:with:)和deleteRows(at:with:)的function。
定義如果收到的editingStyle為delete的情況下,會將table的row及相對應的資料一併刪除,就可以執行向左滑動刪除的動作了~
func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
if editingStyle == .delete {
data.remove(at: indexPath.row)
tableView.deleteRows(at: [indexPath], with: .fade)
}
}
另外要注意的是,如果你使用了delegate裡的setEditing(:animated:),tableView(:commit:forRowAt:)就會失效。如果你非兩個一起用不可的話,需要使用perform(_:with:afterDelay:)設定一個delay讓兩個的的執行緒錯開。
另外可以使用tableView(_:canEditRowAt:)使row無法被編輯:
func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
switch indexPath.row {
case 1: return false
default: return true
}
}
如範例所示,row == 1的傢伙就被我們排擠了,無法執行刪除動作,不是我故意不滑他喔。
預設的情況下都是true。
要讓row可以被移動,首先要讓tableView轉為可以被編輯的狀態。
建立一個button,並指定他的target-action為啟用/取消tableView的編輯:
@objc func editAction(_ sender: UIButton) {
if editButton.titleLabel?.text == "Edit" {
newTableView.isEditing = true
editButton.setTitle("Finish", for: .normal)
} else if editButton.titleLabel?.text == "Finish" {
newTableView.isEditing = false
editButton.setTitle("Edit", for: .normal)
}
}
接下來使用tableView(_:moveRowAt:to:),讓row在移動的同時,data裡的資料也會隨著row的index改變而改變:
func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {
let readyToMove = data[sourceIndexPath.row]
data.remove(at: sourceIndexPath.row)
data.insert(readyToMove, at: destinationIndexPath.row)
}
同樣也可以用tableView(_:canMoveRowAt:)來排擠某列讓他不可被移動,就不額外demo了。
有時侯table旁邊會有一排Section的標題索引(下圖左),這又是怎麼做到的呢?
其實只要使用sectonIndexTitles(for:)這個function,回傳一個section title的字串陣列即可。
為了demo這個範例,我們先加爆資料:
func numberOfSections(in tableView: UITableView) -> Int {
return 3
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 10
並且建立一個section title的陣列:
var sectionTitle = ["A", "B", "C"]
接下來把這個陣列指定給tableView(:titleForHeaderInSection:)和sectonIndexTitles(for:)即可:
func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
switch section {
case 0 ..< sectionTitle.count: return sectionTitle[section]
default: return ""
}
}
func sectionIndexTitles(for tableView: UITableView) -> [String]? {
return sectionTitle
}
如此一來,table的右邊就會出現section標題的索引,點擊相對應的title之後,table也會跳轉到該section。
prefetchDataSource並不是必須的。他的存在是為了提前操作需長時間運行的數據。
prefetchDataSource與dataSource連接,會在table呼叫dataSource的tableView(_:cellForRowAt:)之前就預先讀取資料,預先發送data需求的相關警告給table。
為你的table加入prefetch data source的步驟如下:
如同前面所說,定義tableView(:prefetchRowsAt:)並不是必要的,所以tableView(:cellForRowAt:)必須要能符合以下情況:
有種幾乎可以處理上述所有情況的的方法就是使用Operation去讀取每一列的資料,建立一個Operation並加儲存進tableView(_:prefetchRowsAt:)。
如此一來如果你的data存在的話,這個function可以幫助你重新運行及回傳結果;如果data不存在,則可以幫你建立。
終於講完了DataSource,開始可以建立自己的table了!
下一回就來講講怎麼把資料放進table裡吧~