UITableViewCell的重覆使用機制


TableView中的Cell

UITableView僅只是個外框架,實際上我們會花更多的時間專注在TableView裡的Cell,也就是每列所呈現的樣子,當然所有的資料呈現也是在這個裡面。UITableViewCell並不是UITableView的子類,它是繼承自UIView的子類(註:UITableView→UIScrollerView→UIView)

重覆使用的機制

UITableViewDataSource中有一個要求實作的方法tableView: cellForRowAtIndexPath:。Swift語言(或Objective-C)的方法是訊息傳遞(message passing)的機制,在名稱的設計上是很特別的,如果你把所有叫tableView開頭的方法列出來,在UITableViewDataSource裡有9個,在UITableViewDelegate中有30多個,它的方法不是這樣看的,開頭大概只能說明它的"開頭"或針對的對象是哪一種實例,用途是看接下來的傳入參數與回傳值,方法的整個名稱應該是要包含接下來參數名稱在內。tableView:cellForRowAtIndexPath:說明文件:

方法說明

Asks the data source for a cell to insert in a particular location of the table view. (required)

回傳值(Return Value)

An object inheriting from UITableViewCell that the table view can use for the specified row. An assertion is raised if you return nil.

討論(Discussion)

The returned UITableViewCell object is frequently one that the application reuses for performance reasons. You should fetch a previously created cell object that is marked for reuse by sending a dequeueReusableCellWithIdentifier: message to tableView. Various attributes of a table cell are set automatically based on whether the cell is a separator and on information the data source provides, such as for accessory views and editing controls.

它的回傳值必須是一個UITableViewCell的物件。這裡已經有點明了要使用另一個名稱為dequeueReusableCellWithIdentifier:的方法,這是為了"重覆使用(reuse)"的效能理由。如果在TableView的資料數千、萬列非常多的情況下,我們不能對每一列的Cell都建立一個新物件給它,而是重覆使用同樣的物件,事實上雖然TableView中有幾千筆資料,我們能看到的只有螢幕大小的那個範圍。Cell使用的規則很簡單就是:

絕對不可以為每列建立一個新的cell物件,而是重覆使用同一個物件

上面的討論已經有指出要使用dequeueReusableCellWithIdentifier:這個屬於UITableView方法,這個方法的dequeue"to remove from a queue"(從隊列中移出)的意思,至於是指的是哪一個隊列?它的說明裡有解釋:

(Discussion) For performance reasons, a table view’€™s data source should generally reuse UITableViewCell objects when it assigns cells to rows in its tableView:cellForRowAtIndexPath: method. A table view maintains a queue or list of UITableViewCell objects that the data source has marked for reuse. Call this method from your data source object when asked to provide a new cell for the table view. This method dequeues an existing cell if one is available or creates a new one based on the class or nib file you previously registered.

(IMPORTANT) You must register a class or nib file using the registerNib:forCellReuseIdentifier: or registerClass:forCellReuseIdentifier: method before calling this method.

它說明的很清楚,這個"隊列(queue)"是由TableView維護的,當data source方法中要求TableView物件要提供cell物件時,由TableView決定是要把現有的cell物件,移出隊列給你,還是建立一個新的cell物件。說明中提到了,我們要"先"註冊cell的類或nib檔案給TableView,如果需要它會建立。註冊的方法是下面兩個registerNib:forCellReuseIdentifier:registerClass:forCellReuseIdentifier:

有些書或教學範例看不到這兩個方法,一種是在Storyboard上完全沒有UITableViewCell時,會直接呼叫初始化建立一個Cell實例(像下面這樣的程式碼,注意是用as?不是as!,說老實話,我實在不喜歡這個範例,要調整一下才會沒有錯誤,在一些入門書會看到。)。另一種是用了TableView中的"Prototype Cells"來自訂Cell,這應該是自動註冊了。

var cell = tableView.dequeueReusableCellWithIdentifier(simpleTableIdentifier) as? UITableViewCell

if cell == nil {
    cell = UITableViewCell(style: .Default, reuseIdentifier: simpleTableIdentifier)
 }

cell!.textLabel!.text = data[indexPath.row]

return cell!

當我們要使用這個重覆使用的機制時,我們會把Cell實例的初始化工作整個交接給TableView,唯一能作的我們會在自訂的Cell中的初始化方法中,加入針對所有Cell物件的一些屬性調整(外觀之類的)。

Cell是如何被重覆使用的

當TableView決定要重用Cell物件時,也就是透過dequeueReusableCellWithIdentifier方法後,它會呼叫Cell的prepareForReuse方法。大概就是通知Cell洗乾淨準備被重覆利用之類的(聽起來有點…)。在prepareForReuse的說明中有提及Cell是要怎麼被重覆使用的:

(Discussion)... For performance reasons, you should only reset attributes of the cell that are not related to content, for example, alpha, editing, and selection state. The table view'€™s delegate in tableView:cellForRowAtIndexPath: should always reset all content when reusing a cell.

這也就是說,當你要自訂Cell時,其中與"內容(Content)"有關的部份,絕對不能由你的程式中來變動,要交給TableView delegate的重用機制來作。不過像"alpha(透明度)、editing(編輯)、selection state(選中狀態)"這些是你一樣可以自行改變的。重覆使用時不會重置這些屬性。

Cell還有另一個必要的條件,就是要有一個唯一的identifier(識別子),這個identifier是一堆相關方法都需要有的參數之一,你看方法名稱的後面都有個identifier就知道了。這個字串需要在Storyboard中的Attributes Inspector填入。這是一個重要而且必要的字串,沒了這個識別子,上面說的這些方法都沒作用了。

dequeueReusableCellWithIdentifier的回傳值

這個方法實際上有兩個,一個是有forIndexPath參數,另一個是沒有。因為要配合在tableView:cellForRowAtIndexPath:裡面使用,會使用IndexPath參數的那個方法。定義如下:

func dequeueReusableCellWithIdentifier(_ identifier: String,
                          forIndexPath indexPath: NSIndexPath) -> AnyObject

這個方法最要注意的是它的回傳值,它會是個AnyObject(我覺得應該是個UITableViewCell的物件(或nil),組合為Optional值。不過為了對應在Objective-C中的id,它用了AnyObject。),文件說明如下:

(Return Value) A UITableViewCell object with the associated reuse identifier. This method always returns a valid cell.

因為要獲取UITableViewCell的物件,所以這個方法後面接的會是Type casting(類型轉換,我比較喜歡用"類型鑄模"的說法)的語法,而且是downcasting的強制轉換回UITableViewCell類型的物件。下面是範例程式碼:

func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {

    let cellIdentifier = "Cell"

    var cell = tableView.dequeueReusableCellWithIdentifier(cellIdentifier, forIndexPath: indexPath) as! UITableViewCell

    //...put data

    return cell
}

結論

  • Cell在Storyboard中一定要設定一個唯一的identifier(識別子)。也記得要在呼叫dequeueReusableCellWithIdentifier中註冊好identifier(識別子)。

  • 對於重覆使用的Cell,裡面的"內容(Content)"資料,會在cellForRowAtIndexPath中獲得的Cell物件指定,但Cell物件的初始化與是否要建立實體的工作是交給TableView物件去作的,其他的我們只能先準備好每個Cell共同的外觀之類的。以及在程式碼改變一些外觀屬性、編輯、選定狀態之類的。

心得:英文名詞在翻譯時,過於簡寫有時會造成誤用,多打幾個字會比較明確指出它的意思。"reuse"常常看到被翻成"重用","重用"當然在這裡是"重覆使用"的意思。但"重用"在中文裡有另一個意思,例如"x總統重用xxx"。

參考資料