iOS Table View 加入Search功能(使用Core Data當資料來源,並客製化Table View Cell)

    其實如何加入Search功能至Table View的做法是有很多人討論的,只要google一下就可以找到許多。目前大部份人都說要使用search display controller,也就是在XCode裡右下角的元件庫裡看到的Search Bar and Search Display Controller,如下圖。只要把它拉到你的Table View裡面,然後再加入一些程式碼就可以了。但大部份都是很簡單的利用NSArray來當做Table View的資料來源。例如[1], 是一個很好教學文件,它也告訴我一個重要的觀念:search display controller會自帶一個Table View。但是它用NSArray來做Table View的資料來源。就連我手上談論iOS程式的書(1)都有提到這個議題,但同樣地,都是使用Array來當資料來源。

    所以當我的專案是使用Core Data當資料來源時,這變成了一個問題。不過,這就是程式開發者的工作,不是嗎?當然,我們有許多選擇。例如,可以把Core Data的資料先轉成NSArray啦。不過,這是笨方法。因為當初使用Core Data,就是有一個好用的NSFetchedResultsController可以使用,它不但協助我們處理掉讀取Core Data的細節,還考量到效能的問題。實在沒有理由,要棄之不用。
    另一個問題是,我的專案裡的Table View Cell是客製化的,也就是說不是預設的只有顯示一個字串,而是我自己拉Label,拉UIImage...。這會有什麼問題嗎?問題出在search display controller會自帶結果Table View,不知道要如何去客製化它的Cell。總不能讓Search結果的Table View Cell長的樣子和原來的不一樣吧!
    最後一個問題是,現在iOS8剛出來,有了一些改變。原本這個議題,大家的作法都是使用search display controller來做,有趣的是,居然文件裡講,在iOS8中,請不要再用這個類別了。不過,若要相容於iOS7,那還是用search display controller。

以下,分別來討論解決的方法和概念。
Core Data當資料來源,沒問題!Stackoverflow上就有人問了這個問題,Brent Priddy[2]很好心的把程式寫法都放上去了。所以,參考他的程式就可以了,許多人都在Stackoverflow上讚揚他程式寫的很優美啊。我一邊忙著複製貼上他寫的程式,一邊閱讀程式的邏輯。其實概念上不是太難!和Stanford CS193P課程Paul Hegarty教授所教的CoreDataTableViewController這個類別(據他課堂上所說,他只是由Apple文件中的範例改一些貼下來而已)基本上是一樣的。只是CoreDataTableViewController只考量一個Table View的情況,而現在我們要考量兩個Table View(因為多了一個Search結果的Table View)。而Brent Priddy針對這個問題,為每一個Table View都設置一個專用的NSFetchedResultsController。而實際上,search display controller預設把其結果Table View的資料來源用在你所拉上去的那個View,也就是說你的view controller原本只負責提供資料給自己的Table View,現在則要同時提供資料給search結果Table View。記得我們提供資料的方法是使用delegate method嗎?以UITableViewDataSource Protocol的-numberOfSectionInTableView:這個delegate method來說。意思是Table View會問我們在表格中有幾個區段呢?大部份範例程式都直接回一個區段,這是因為要簡化問題,讓你好入手。在CoreDataTableViewController中則是利用fetchedResultsController本身的能力來回答這個問題。同樣地,要在search display controller中回答這個問題,也是一樣利用fetchedResultsController來回答,只要我們可以區分的出來是誰(那一個Table View)在問就可以了。這是很容易做到的,因為-numberrOfSectionInTableView:這個delegate method的輸入參數就是那個Table View。所以利用這個輸入參數來判斷要用那個fetchedResultsController,後面的程式就一樣了。其他的delegate method也都是如此,利用輸入參數來決定是要提供那一個Table View的資料,如此將原本用於服務一個Table View的delegate method擴展成可以服務兩個Table View。

接下來要解決客製化Table View Cell的問題,還記得前面提到search display controller預設會把你的view controller當做資料來源。所以
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    static NSString *identifier = @"Cell";
    StepsRecordTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:identifier];
    
    // Configure the cell...
    if (cell == nil) {
        cell = [[StepsRecordTableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:identifier];
    }
    
    [self fetchedResultsController:[self fetchedResultsControllerForTableView:tableView] configureCell:cell atIndexPath:indexPath];
    return cell;
}
如果你沒特別處理,預設的行為,在把search display controller拉到table view後,就試著執行程式,你會想也許我先讓search結果和沒有search前一樣,先看到跑出全部的資料後,再來加入search的真正功能。你會發現程式居然當掉了!當掉的地方就在這個-tableView:cellForRowAtIndexPath:方法的dequeueReuseableCellWithIdentifier:這行程式。因為原本的預設的程式是dequeueReusableCellWithIdentifier:forIndexPath:這個方法,它在找不到對應的Cell時是會直接當掉的。為什麼會找不到呢?因為search display controller自帶的Table View我們沒有去設他的Cell的Identifier,自然會找不到啦。
解法有兩個:
1. 就和上面的程式片斷一樣,使用dequeueReuseableCellWithIdentifier:這個方法,不要用後面有forIndexPath:的方法。這樣即使找不到這個Identifier的Cell,他也不會當掉,他會回傳nil而已。接下來我們再處理cell為nil的情況就好了。
2. 雖然沒有UI可以用來設定那個自帶Table View Cell的Identifier,但是可以使用程式碼來做這件事,所以你可以在-viewDidLoad裡加入以下程式:
[self.searchDisplayController.searchResultsTableView registerClass:[XyzTableViewCell class] forCellReuseIdentifier:@"Cell"];
這樣就相當於我們在storyboard裡為Table View Cell填入其Identifier一樣,如下圖:

同樣地,客製化的Table View Cell,也是可以用相同的手法來做。terry lewis[3]在stack overflow就問了這個問題,而sienna給了一些答案後,terry lewis最後建了一個xib檔,然後將原本客製化的Cell複製貼上到xib檔去。再到viewDidLoad加入以下程式:
[self.searchDisplayController.searchResultsTableView registerNib:[UINib nibWithNibName:@"searchTableViewCell" bundle:[NSBundle mainBundle]] forCellReuseIdentifier:@"Cell"];
這裡searchTableViewCell是xib檔的檔名,就這樣神奇的事就發生了,search結果的表格跑出來的結果就是我們客製化的樣子。
等等,為什麼我找不到方法去加入xib檔呢?聽說好像之前XCode的版本是可以在檔案->新增的地方找到加入xib檔的方法,但後來XCode已經把它拿掉了。所以我是在新增類別時,勾選產生xib檔的選項來產生這個檔案的。


參考網址:
[1] simon ng, search-bar-tutorial-ios7, Feb 2014
[2] Brent Priddy, how-to-filter-nsfetchedresultscontroller-coredata-with-uisearchdisplaycontroll
[3] terry lewis & svena, uisearchdisplaycontroller-not-correctly-displaying-custom-cells

參考書籍:
(1) 朱克剛, iOS7程式設計實戰, 碁峰資訊, 2013

留言

這個網誌中的熱門文章

D-BUS學習筆記

Cisco Switch學習筆記: EtherChannel

Cisco Switch學習筆記: interface的封包錯誤統計