iT邦幫忙

第 11 屆 iThome 鐵人賽

DAY 17
1

當然我們建立一個表格,不是讓他在那裡躺分的。
tableView是靠資料驅動,但他並不會管理data,而是只處理dataSource送給他的data。
如果你提供tableView參照的data source,tableView就會依照你提供的資料去創造和配置cell,這就是為何我們剛剛要讓tableView啟用dataSource這個協定。

DataSource

dataSource會對table發出來與data相關的請求做出回應,直接管理table的data。其他dataSource負責的事件如下:

  • 告訴table總共有幾個section和row。
  • 為table的每列提供相對應的自定義cell。
  • 為section的抬頭(header)和尾標(footer)提供標題。
  • 對table的索引進行配置。
  • 回應使用者或table發出的更新要求。

你可以定義dataSource的方法以增加table的特性與功能。例如
tableView(_:commit:forRowAt:)可以讓row允許滑動刪除。

Rows / Sections

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
    }
}

Cell

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
        }
    }
}

Title of Header / Footer

而你可以使用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:)的設定為主。

Insertion / Delete

現在為了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。

Move

要讓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了。

Section Index Title

有時侯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並不是必須的。他的存在是為了提前操作需長時間運行的數據。

prefetchDataSource與dataSource連接,會在table呼叫dataSource的tableView(_:cellForRowAt:)之前就預先讀取資料,預先發送data需求的相關警告給table。

為你的table加入prefetch data source的步驟如下:

  • 建立tableView以及規定的dataSource。
  • 類別繼承UITableViewDataSourcePrefetching並定義tableView(_:prefetchRowsAt:)。
  • table載入時會預先讀取tableView(_:prefetchRowsAt:)。
  • 準備顯示欲操作數據的cell。
  • 當table通知你這份data不再被需要之後,使用tableView(_:cancelPrefetchingForRowsAt:)取消對data的預操作。

Prefetching Data

如同前面所說,定義tableView(:prefetchRowsAt:)並不是必要的,所以tableView(:cellForRowAt:)必須要能符合以下情況:

  • 資料已經應預取請求完成讀取,並隨時準備被顯示。
  • 資料已經被預取,但還無法使用。
  • 資料還沒被請求讀取。

有種幾乎可以處理上述所有情況的的方法就是使用Operation去讀取每一列的資料,建立一個Operation並加儲存進tableView(_:prefetchRowsAt:)。
如此一來如果你的data存在的話,這個function可以幫助你重新運行及回傳結果;如果data不存在,則可以幫你建立。

終於講完了DataSource,開始可以建立自己的table了!
下一回就來講講怎麼把資料放進table裡吧~


上一篇
Day 16: 來自深淵-UITableView(I)
下一篇
Day 18: 來自深淵-UITableView(III)
系列文
Hey! UIKit, 做個朋友吧~30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言