Swift 程式語言

如何利用UISearchController添加搜尋功能並打造客製化搜尋列

如何利用UISearchController添加搜尋功能並打造客製化搜尋列
如何利用UISearchController添加搜尋功能並打造客製化搜尋列
In: Swift 程式語言

隨著iOS 8的到來, 有些事情變得不一樣. 首先, UISearchDisplayController 已經被棄用了,雖然在Xcode內的Interface Builder’s中的控制器元件庫內(controls collection),UISearchDisplayController裡面還是一個能可用的控制器元件。一個名為 UISearchController的新控制器已經出現並取代這個位子。儘管這一切朝向這樣的變化,但我們可以看到在Interface Builder的控制器元件庫內(controls collection)卻不存在所對應的虛擬控制器(visual control)。很顯然的,這個元件需要被初始化(initialized)跟做一些程式化的設定(configured programmatically),這是非常簡單的工作,你等等就會看見。

custom-search-bar-featured

除了上面的改變,另外一個有趣的觀點是對於 tableview datasource 的搜尋。 iOS SDK 提供一個預先定義好的搜尋列(search bar),而這個搜尋列適用於多種狀況。然而,隨者App的UI因為設計的需求發展而需要高度的客製化。這個預設的搜尋列的外觀跟格式也許不再適合App的整體UI設計跟需要。在這種狀況下, 搜尋列必須要能夠被適當的客製化,能讓整體的功能與設計能成融入為app的生態系統(ecosystem)。


所以,我們落落長的談論了這麼多, 是時候用一個簡短的教學來呈現出我們剛剛討論的這些東西。在此之前有兩件事情我必須要事先說明:我的第一個目標是展示如何使用新的 UISearchController 並呈現在iOS 8中,能夠使用預設的 iOS 搜尋列 (iOS search bar)搜尋並過濾資料. 透過簡單的範例程式,也將會看見要撰寫UISearchController配置程序是一件很簡單的事情,儘管虛擬化控制器在Interface Builder 並不存在。

我的第二個目標是向你展示如何覆蓋(override)預設的搜尋列的外觀並且客製化外觀使得搜尋列符合App的整體設計跟感覺。正如你所看到的下一格,我們要繼承 UISearchControllerUISearchBar 兩個類別,但是所添到它們身上加的程式代碼卻十分簡單。 事實上,我們將在以下使用一個相當普遍的方式進行子類的實現, 這樣你就能在你得專案裡面重複使用。

是時候結讓我們這個簡介部分的章節進入尾聲囉,我們開始使進入第一個並探索整個demo app的第一部份的細節,然後我們將賦予它新的生命.

Demo App概述

下面兩張圖片說明了我們在本教學課程的主要目標:

search-bar-demo-app

在第一張圖片中,你可以看到使用UISearchController 這元件的預設搜尋列,UISearchController在iOS 8中被發表。 在第二張圖片中, 客製化得搜尋列已經被制定完成。 在你可以在畫面的上半部中看到客製化的搜尋列得兩個部分已經被實現完成。 但首先,請容許我們先對這個專案略作說明。

在一般的情況下, 你可以下載一個starter project 以便開始進行。 當你下載完畢, 在Xcode開啟它,且您自己可以大概瀏覽一下。由於最後的App很簡單, 你不會看到太多東西; 在storyboard中也只有一個簡單的場景(scene) 並包含著一個tableview, 這個tableview將用來顯示我們的資料, 搜尋列和搜尋之後的結果。除此之外, 在 ViewController 的類別裡面你將找到的最小化的程式碼,來實現並滿足UITableView delegate 和 datasource 的導入。

這個Demo專案的資料很簡單,他是來自世界各地的國家名單。在本專案中你將發現一個名為 countries.txt 得文字檔案已經涵蓋在本專案的下載檔案內。 我們必須這樣做是因為等等我們會將這檔案導入成為陣列並在專案的代碼內使用。 這些國家名單由這個網站所建立。

在接下來我們即將進行的工作我們將它分解為三個步驟:


  1. 一開始,我們將載入範例資料到app中, 已便我們接下來能隨時地使用它,在我們需要的任合時候。.
  2. 我們將實現預設的搜尋功能,使用全新的UISearchController元件。
  3. 我們將繼承並自定義搜尋控制器(search controller)和搜尋列(search bar),以便讓我們能改變預設的外觀。

所以, 話說回來,接這我們得來看看,每個單一步驟的細節。

載入並顯示範例資料

我們將由加載countries.txt來開始我們專案的第一步,我們讀入這個文字檔案進入一個陣列,以作為我們tableview的起始數據來源(datasource)。此外,我們將顯示所有的國家到整個列表。一旦我們這樣做,我們就可以完全的專注於實現我們的搜尋功能,並使用兩種方式實現它。

在把內容載入到陣列之前有幾個初始化的步驟要做,我們需要在ViewController類別內宣告幾個屬性。然後我們可以直接使用它們進行工作。在ViewController.swift物件的上方加入這三欄:

var dataArray = [String]()

var filteredArray = [String]()

var shouldShowSearchResults = false

現在讓我來解釋一下。 dataArray這個陣列將包含檔案文件的內容, 這意味個所有的國家名單將顯示在tableview中. 請注意,這個陣列也將用來為作為數據來源(datasource) 但僅在搜尋沒有被啟動的時候。 一旦使用者開始搜尋, 另一個 filteredArray 陣列將取而代之變成數據來源(datasource), 同時陣列的內容也會僅剩下符合搜尋條件的國家。 至於哪個陣列要被tableview 來使用作為數據來源(datasource),則是透過shouldShowSearchResults 這個屬性的值來決定。當它設定為true, filteredArray 陣列的內容將會被使用; 當它設為false, 則dataArray 陣列的內容將會被使用。

現在,我們從countries.txt文字檔案載入資料進入到dataArray陣列中,然後我們就可以在tableview裡面使用它。在專案開始時,我們就已經初始化並導入的tableview, 但是我們要繼續的豐富它。我們要開始建立一個客製化的函數並命名為 loadListOfCountries()。接下來我們將會執行面的事情: 首先我們將讀入整個文件的內容並轉換成為字串,然後我們將使用斷行符號(enter character)來切開所有的資料並導入到陣列中。 由此產生的陣列經作為tabaleview的原始數據來源。讓我們來看看吧:

func loadListOfCountries() {
    // Specify the path to the countries list file.
    let pathToFile = NSBundle.mainBundle().pathForResource("countries", ofType: "txt")

    if let path = pathToFile {
        // Load the file contents as a string.
        let countriesString = String(contentsOfFile: path, encoding: NSUTF8StringEncoding, error: nil)!

        // Append the countries from the string to the dataArray array by breaking them using the line change character.
        dataArray = countriesString.componentsSeparatedByString("\n")

        // Reload the tableview.
        tblSearchResults.reloadData()
    }
}

上面的函數,很明顯的將在viewDidLoad(...) 中被呼叫:

override func viewDidLoad() {
    ...

    loadListOfCountries()
}

我們現在已經準備來更新一些tableview的方法. 首先我們需要決定tableview行數的回傳值,它將依據我們目前所使用的陣列來決定:

func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    if shouldShowSearchResults {
        return filteredArray.count
    }
    else {
        return dataArray.count
    }
}

接著,我們來指定實際cell 內的內容:

func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
    var cell = tableView.dequeueReusableCellWithIdentifier("idCell", forIndexPath: indexPath)

    if shouldShowSearchResults {
        cell.textLabel?.text = filteredArray[indexPath.row]
    }
    else {
        cell.textLabel?.text = dataArray[indexPath.row]
    }

    return cell
}

現在我們可以第一次執行app了,並且看到了國家的名單出現在tableview上.到目前為止我們做的完成的既不新奇也不困難,所以我們接著進行搜尋控制器(search controller)的配置以及顯示預設的搜尋列.

t41_3_countries_list

配置UISearchController

為了使用UISearchController 以及能夠實現搜尋tableview的資料。有需要事先宣告這個屬性。因此, 我們在ViewController 物件的最上方只要新增下面這一行:

var searchController: UISearchController!

為了實現我們的目的,我們需要導入一格新的客製化函數 我們將它命名為configureSearchController(). 在這個函數內, 我們在上面直接宣告一些屬性並初始化, 然後我們會在做一些屬性的設置。實際上, 搜尋控制器(search controller)就是一個附加了一些特殊屬性的視覺控制器(view controller)。

其中一個屬性是searchBar 這代表顯示於tableview頂部上方的搜尋列(search bar)的編程控制器。有一些屬性我們將在接下來的步驟中把他們實現完成。搜尋列(search bar)並不會自動出現在tableview中; 我們需要手動的配置並且接下來你將會看到一部分。 在搜尋控制器(search controller)跟搜尋列(search bar)中都有許多的參數可以被配置,所以們儘可能的使用預設的配置來完成預設的控制器controls。更多得訊息你可以在Apple的開發者文件中的這裡這裡被發現。

我們已經開始了一個新功能,如所見我們將會一對一的來完成所有的配置。第一步, 我們來初始化searchController:

func configureSearchController() {
    searchController = UISearchController(searchResultsController: nil)
}

這邊有個重要的細節: 在我們的展示的應用程序中tableview顯示搜尋結果存在同樣的視圖控制器(view controller) 與搜尋控制器(search controller )內。然而在這個狀況下view controller 顯示的搜尋結果是在搜尋在另外一個搜尋開始執行前的結果,但是搜尋控制器並不知道這一件事情。唯一的辦法是透過上面的初始化。當我們nil 作為參數來傳遞,搜尋控制器(search controller)將會知道已經存在的視覺控制器(view controller) 也要去處理和顯示搜尋結果。在任何其他情況下視圖控制器的結果都是不同的。

有兩種方法我們可以用來更新tableview,當我們在搜尋條件上輸入搜尋條件時。第一種方式是使用搜尋列(search bar)的委派方法 (delegate method),第二種方式是委派 ViewController 類別中的 searchResultsUpdater 屬性給搜尋控制器(search controller)。 第一種方法你將會在等等看到如何實現它,當我們開始撰寫我們的客製化搜尋控制器時(custom search controller),現在讓我們先探討第二種方法吧。 為了實現第二種方法, 我們必須使 ViewController 符合 UISearchResultsUpdating 所指定的協議(protocol)跟命名(named)。 這個協議(protocol)第一次在iOS 8上出現,我將確切的描述它在做什麼: 他將會我們輸入的關鍵字的基礎上來更新搜尋解結果。這裏只有一個委派方法 (delegate method)我們必須要去實現它,但這部分我們稍後會完成。現在,我們先引入協議(protocols) 在我們已經建立好的類別裡面:

class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource, UISearchResultsUpdating

再一次回到我們的剛剛建立的新功能configureSearchController,並加入下面這一行,這將使得拼圖多拼好一塊:

func configureSearchController() {
    ...

    searchController.searchResultsUpdater = self
}

深入來說,你還可以更進一步的添加任何你需要的屬性。舉例, 要讓我們在輸入搜尋關鍵字的時候讓整個view的背景變得黯淡我們可以添加truedimsBackgroundDuringPresentation 屬性內:

func configureSearchController() {
    ...

    searchController.dimsBackgroundDuringPresentation = true
}

一般而言,我們沒有理由把view變得黯淡,當搜尋控制器與results tableview存在於同一視圖控制器之下。所以我們把直設定為false 來避免狀況發生。

我們現在來對搜尋列(search bar)做一些小變化, 我們來指定出現在placeholder 內的文字:

func configureSearchController() {
    ...

    searchController.searchBar.placeholder = "Search here..."
}

此外,我們也得讓我們主類別中的委派(delegate) 與搜尋列內的委派(delegate)一致,以便我們之後可以使用同樣的委派方法。

func configureSearchController() {
    ...

    searchController.searchBar.delegate = self
}

最後,我們提供一個小訣竅讓搜尋列(search bar)的尺寸跟tableview所顯示的尺寸一致:

func configureSearchController() {
    ...

    searchController.searchBar.sizeToFit()
}

要將搜尋列顯示在tableview中還需要下面的一行:

func configureSearchController() {
    ...

    tblSearchResults.tableHeaderView = searchController.searchBar
}

下面是configureSearchController方法完整全貌:

func configureSearchController() {
    // Initialize and perform a minimum configuration to the search controller.
    searchController = UISearchController(searchResultsController: nil)
    searchController.searchResultsUpdater = self
    searchController.dimsBackgroundDuringPresentation = false
    searchController.searchBar.placeholder = "Search here..."
    searchController.searchBar.delegate = self
    searchController.searchBar.sizeToFit()

    // Place the search bar view to the tableview headerview.
    tblSearchResults.tableHeaderView = searchController.searchBar
}

現在我們回到主要類別 ViewController 的頂部並且來引入UISearchBarDelegate 協定:

class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource, UISearchResultsUpdating, UISearchBarDelegate

最後, 在viewDidLoad() 中我們呼叫configureSearchController():

override func viewDidLoad() {
    ...

    configureSearchController()
}

以上就是你所需要的,你可以將一個搜尋search bar顯示在tableview中並使用全新的UISearchController類別。現在,我們可以繼續進行和處理搜尋結果。現在, 先不要在乎Xcode裡面顯示的錯誤訊息, 一旦我們撰寫完目前缺的少委派函數(delegate functions)他就會消失了。

執行搜尋

我們現在接著來處理搜尋解果的顯示。這個步驟有兩個狀況需要被處理。第一的狀況是, 當搜尋search bar有輸入搜尋條件的時候搜尋結果可以立即的被返回,第二個狀況是, 當鍵盤內的search按紐被使用者點擊的時候。

在第一種狀況下我們會依照我們輸入得內容來篩選顯示的國家列表. 此外,這種情況下,並不總是強制性的實現,如果你希望在打字的時候忽略立即的回傳搜尋結果,你就可以這樣做。 但是在鍵盤上的Search按鈕被點擊的時候就必須要顯示搜尋結果了,要不然在tableview中出現搜尋列就一點意義也沒有。在這兩種情況下filteredArray 這個陣列必須要成為tableview的資料來源datasource, 但是當沒有在執行搜尋的時候dataArray 陣列是我們一開始賦予的預設資料來源(default datasource)。

我們已經使用shouldShowSearchResults 的屬性來定義tableview實際的資料來源 datasource,但到目前為止,我們還沒有寫過一行代碼來改變它的值。現在,是時候這樣做了, shouldShowSearchResults 的值將取決於現在搜尋程序是否正在進行中,或者沒有。

考慮到這一點,讓我們首先定義了以下兩個的UISearchBarDelegate的委託方法(delegate methods)。正如你所看到的,我們在適當的時候改變上述 shouldShowSearchResults 屬性的值,並且重新讀取tableview。

func searchBarTextDidBeginEditing(searchBar: UISearchBar) {
    shouldShowSearchResults = true
    tblSearchResults.reloadData()
}


func searchBarCancelButtonClicked(searchBar: UISearchBar) {
    shouldShowSearchResults = false
    tblSearchResults.reloadData()
}

第一段程式使得我們開始搜尋時將 filteredArray 指定為 tableview的資料來源(datasource)。另一方面,第二個函數讓Cancel 按紐被點擊時將 dataArray指定為 tableview的資料來源。

我們要進行的下一個步驟,是導入另一個委派方法(delegate method),當鍵盤上的Search按鈕被點擊時,顯示搜尋結果並重新使用重行指派搜尋欄位的first responder來隱藏鍵盤.

注意if條件式,在我們按下搜尋按鈕之後,我們會禁用即時搜功能(real-time searching)尋並只顯示搜尋結果。

func searchBarSearchButtonClicked(searchBar: UISearchBar) {
    if !shouldShowSearchResults {
        shouldShowSearchResults = true
        tblSearchResults.reloadData()
    }

    searchController.searchBar.resignFirstResponder()
}

當你完成了上面的所有的委派方法(delegate methods), 通過設置適當的值給shouldShowSearchResults 指標並且重新讀取tableview, 每一次我們都管理將正確的資料顯示到我們的視途控制器view controller.

到目前為止我們都做得很好,但是我們還有成堆的工作尚未完成。 到現在為止我們還針對 filteredArray這個陣列進行處理, 所以上面的委派方法還不會達到我們預期的小果。. 在前面的部分,我們使用的 UISearchResultsUpdating的protocol,並且在實現那部份的過程中我曾經到有一個委派方法我們必須實現。在這個委派方法裡面你可以看到,在我們輸入關鍵字詞語的時候,我們會過濾原始的數據,並且將符合結果存儲在 filteredArray的陣列裡面。他的程式碼如下:

func updateSearchResultsForSearchController(searchController: UISearchController) {
    guard let searchString = searchController.searchBar.text else {
        return
    }
    
    // Filter the data array and get only those countries that match the search text.
    filteredArray = dataArray.filter({ (country) -> Bool in
        let countryText:NSString = country
        
        return (countryText.rangeOfString(searchString, options: NSStringCompareOptions.CaseInsensitiveSearch).location) != NSNotFound
    })
    
    // Reload the tableview.
    tblSearchResults.reloadData()
}

讓我們來深入了解這裡究竟是怎麼回事。一開始我們存取使用者所輸入的關鍵字,並暫時存放在一個名為searchString的局部變數上。我們本來可以避免這樣做,但它使我們的工作更加方便。

接下來我們進到這個部份的核心部分,我們使用到dataArray 陣列的filter(...)方法。如名稱所述 ,他將會依照我們要求過濾這個封閉陣列裡面的資料。並且將符合匹配的元素儲存在filteredArray 陣列內。在原始的資料陣列裡面每個國家的字串是存放在 country這個封閉的變數裡面。這個字串將會轉換成命名為
countryTextNSString物件型態,因為我們等等要使用到NSString物件裡面提供的rangeOfString(...) 方法。這個方法將會
檢查搜尋關鍵字(存放在searchString裡面)是否存在於目前國家名單內,如果有則回傳在國家名單內的範圍(range) (NSRange)。反之,如果不存在於目前國家名單內,則回傳 NSNotFound這個數值。

在這個封閉函式裡面Bool 值將會被回傳,我們要做的是只是回傳 rangeOfString(...)NSNotFound 執行之後的比較結果。

最後,我們刷新tableview的資料,所以它顯示已過濾的國家。有一點你必須要明白,我呈現的作法只是許多眾多方法中的其中一種,很顯然的,你可以依照每次實際開發時所遇到的需求,來需要重新的定義或修改上面的做法。

現在我們可以執行我們的應用程式來測次我們新增加的功能。當你開始輸入國家的名字,你將會看到tableview 會即時的更新。 使用search 和cancel 按鈕,並查看應用程序的反應。 最後,我們強烈建議你可以依照你的需求嘗試的做一些修改跟變化,不管是什麼都可以,畢竟我們都走到這個地步了。這也能幫助你更加了解跟學習到我們討論的內容。

到這邊,我們本教程的第一部分已經結束。在剩下的部分中,您將看到如何可以覆蓋(override)搜尋控制器和搜索欄的預設行為,使其適合於應用程序的用戶界面。

default search bar

自定義搜尋欄位

自定義搜索控制器和搜索欄並沒不是一件很艱難的工作,你全部要做的工作,只是依照你的需求來繼承並撰寫每個的功能的客製化或邏輯。實際上,這需要處理所有你想要覆蓋的iOS SDK類別裡面的預設行為,所以不管接下來要面臨的是什麼,一定是我們過去已經做過並完成的事情,所以請你不用擔心。

我們將開始這個客製化的工程,我們會先從繼承第一個搜尋列 (UISearchBar)開始。接著我們會在搜尋控制器內使用這個客製化之後的搜尋列,在最後我們會在ViewController類別裡面使用他們兩個。

我們將在Xcode裡面建立一個新的文件,因此我們點選File > New > File… 選單. 在template selection裡面我們點選Cocoa Touch Class 並注意 Source 的類別是iOS。下一個步驟, 選擇UISearchBar 類別在 Subclass of: 欄位, 並且輸入CustomSearchBar名稱為新的類別名稱,圖面如下所示:

t41_4_custom_search_bar_class

當新文件建立完成之後,再把它打開。我們將開始撰寫客製化的initializer程式碼。在此我們必須要傳遞搜尋列(search bar)跟搜尋文字框(search field)的desired frame、字型跟文字顏色參數,並且在稍後我們將針對這些參數進行客製化。在此之前,我們先來宣布兩個重要的屬性:

var preferredFont: UIFont!

var preferredTextColor: UIColor!

現在,讓我們看看custom initializer:

init(frame: CGRect, font: UIFont, textColor: UIColor) {
    super.init(frame: frame)

    self.frame = frame
    preferredFont = font
    preferredTextColor = textColor

}

如你所看到的,我們設定search bar的frame的參數,接著我們儲存字型跟文字顏色的參數以供等等我們來使用。

我們的下一個任務是改變預設搜索Bar的風格(style),我們使用下面的程式碼:

searchBarStyle = UISearchBarStyle.Prominent

這個命令的將會使搜尋列(search bar)變成translucent(半透明)背景並且搜尋文字框(search field)變的不透明。然而,這是不夠的,因為我們最後的目的是希望這搜尋列(search bar)和搜索文字框(search field)都是不透明的。為了實現這個可能,為了讓兩個顏色看起來成為一體。因此,我們將使用下一個命令來實現這個需求:

translucent = false

再增加了兩個additions,之後我們再來改寫一下初始化的部分:

init(frame: CGRect, font: UIFont, textColor: UIColor) {
    super.init(frame: frame)

    self.frame = frame
    preferredFont = font
    preferredTextColor = textColor

    searchBarStyle = UISearchBarStyle.Prominent
    translucent = false
}
注意: 搜尋列(search bar)不是一個由textfield (搜尋文字框: search field)分支而成的單一的控制元件。相反的, Bar(search bar)擁有一個UIView view 作為subview, 並且這個 view 包含了其他兩個重要的 subviews: 搜尋文字框(實際上有 UITextField 類別構成的子類別), 和搜尋文字框的背景視圖(background view)。

為了更清楚的理解,如果你輸入了下面的custom function:

print(subviews[0].subviews)

… 你將會在console裡面得到下面的訊息:

t41_5_search_bar_subviews

此外在custom initializer, 我們還需要新增一段程式:

required init(coder aDecoder: NSCoder) {
    super.init(coder: aDecoder)
}

根據上述說明,並且考慮到我們需要存取到”整個”搜尋列search field (包含 textfield和search bar),讓我們來寫了一個輔助函數來返回我們在搜索欄的子視圖(subview)它的index指標:

func indexOfSearchFieldInSubviews() -> Int! {
    var index: Int!
    let searchBarView = subviews[0]

    for var i=0; i

這很簡單吧,而且他呼應了我之前解釋的部分。因此,我們能通過重新處理物件裡面drawRect(...)的方法來達成實際的客製化。所以我們將處理兩個不同的任務: 我們將存取搜尋文字框(search field) 並且修改它的外觀讓他變成我們希望的樣子,接著我們會在搜索Bar(search bar)的底部並且在底部畫一條客製化的直線。

針對搜尋文字框(search field)的修改, 我們將做下面的修改:

  1. 我們會改變它的外框架,我們將使它有點略小於搜尋列(search bar)
  2. 我們將會設定自定義的字型
  3. 我們將會設定自定義的文字顏色
  4. 我們將會改變背景顏色,這將會符合搜索Bar的tint color 並且我們將會在下一部分來指定他。我們將會設法讓搜尋列(search bar)和搜尋文字框( search field)看起來像同一個控制元件(可以參考 app overview的章節並且看看我們的最終目的)。

我們來看一下完整的程式碼:

override func drawRect(rect: CGRect) {
    // Find the index of the search field in the search bar subviews.
    if let index = indexOfSearchFieldInSubviews() {
        // Access the search field
        let searchField: UITextField = subviews[0].subviews[index] as! UITextField

        // Set its frame.
        searchField.frame = CGRectMake(5.0, 5.0, frame.size.width - 10.0, frame.size.height - 10.0)

        // Set the font and text color of the search field.
        searchField.font = preferredFont
        searchField.textColor = preferredTextColor

        // Set the background color of the search field.
        searchField.backgroundColor = barTintColor
    }

    super.drawRect(rect)
}

如你所看到的,我們也完成了我們前面實現的所有的自定義funcion。

最後,讓我們來在搜尋列(search bar)的下方劃一條線。實際上我們做了兩件事情:第一件事情,我們建立了一個新的 bezier path 跟我們要繪畫的直線。第二件事,我們 建立一個CAShapeLayer 圖曾來設定bezier path, 並且用來指定直線的顏色跟寬度。最後,這一條直線將會被加入到搜尋列(search bar’s)圖層的子圖層(sublayer)內。讓我們開始吧:

override func drawRect(rect: CGRect) {
    ...

    let startPoint = CGPointMake(0.0, frame.size.height)
    let endPoint = CGPointMake(frame.size.width, frame.size.height)
    let path = UIBezierPath()
    path.moveToPoint(startPoint)
    path.addLineToPoint(endPoint)

    let shapeLayer = CAShapeLayer()
    shapeLayer.path = path.CGPath
    shapeLayer.strokeColor = preferredTextColor.CGColor
    shapeLayer.lineWidth = 2.5

    layer.addSublayer(shapeLayer)


    super.drawRect(rect)
}

當然,你可以根據自己的口味和需求,同時改變線的顏色和寬度。在任何情況下,很明顯的你已經知道如何處理它們。

現在,自定義搜索 bar已準備就緒。暫時我們無法測試它,因為我們仍然缺少自定義搜索控制器(custom search controller),但我們會在接下來的部分實作完畢。這部分最重要的是,在這部分的所有討論,我們展示了自定義搜索欄的正確方法,使之適合您的應用程序的外觀和感覺。

客製化的搜尋控制器

我們的客製化搜尋控制器將會是 UISearchController 類別的子類別,所以首先建立一個新檔案。將子類別設定為 UISearchController ,並將類別命名為 CustomSearchController

t41_6_custom_search_controller_class

當我們把檔案建立完畢之後, 在 Project Navigator 裡面開啟。在物件的最上放,我們設定搜尋列的屬性:

var customSearchBar: CustomSearchBar!

接著,我們也要建立自定義的 initializer。 我們將提供下面的五個參數:

  1. 搜尋結果的視圖控制器(view controller)
  2. 搜尋(search bar)所需要的frame
  3. 搜尋文字框(search field)的字型
  4. 搜尋文字框(search field)的文字顏色
  5. 搜尋列的 tint color.

程式碼如下:

init(searchResultsController: UIViewController!, searchBarFrame: CGRect, searchBarFont: UIFont, searchBarTextColor: UIColor, searchBarTintColor: UIColor) {
    super.init(searchResultsController: searchResultsController)

    configureSearchBar(searchBarFrame, font: searchBarFont, textColor: searchBarTextColor, bgColor: searchBarTintColor)
}

configureSearchBar(...)是一個客製化的function我們將使用正確的方法來實踐它。如同function的名稱一樣,我們將會透過他來針對搜尋列(search bar)做正確的配置。

然而,在我們看到這個function之前,我們還要新增兩個必要的初始化函數:

override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: NSBundle?) {
    super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
}

required init(coder aDecoder: NSCoder) {
    super.init(coder: aDecoder)
}

現在,我們開始配置搜尋列(search bar)吧。我們要做的很簡單, 我們來看看裡面的內容 :

func configureSearchBar(frame: CGRect, font: UIFont, textColor: UIColor, bgColor: UIColor) {
    customSearchBar = CustomSearchBar(frame: frame, font: font , textColor: textColor)

    customSearchBar.barTintColor = bgColor
    customSearchBar.tintColor = textColor
    customSearchBar.showsBookmarkButton = false
    customSearchBar.showsCancelButton = true
}

如你所看到的,在第一行的下方, 我們利用自定義的initializer來建立我們客自化的搜尋列(search bar)。其他的部分很簡單:我們設定定了搜尋列(Bar)中的search bar (barTintColor) 和他的元件內的 (tintColor)的顏色,我們"告訴"它我們不要顯示Bookmark按鈕, 最後我們啟用了cancel button的外觀。當然,你可以按照你自己的需求來改變或新增相對應的屬性。

現在,客製化控制器已經準備好了,儘管我們還沒採用 UISearchBarDelegate 來處立 搜尋列(search bar)的委派方法。我們將在下一個章節處理它。然而,我們能夠放置兩個客製化的物件到我們的行為裡面Action。.

回到一開始我們提到的ViewController物件,我們開始宣告一個屬性在類別在最上方:

var customSearchController: CustomSearchController!

接下來,讓我們定義一個非常簡單的函數來初始化客自化搜尋控制器,並指定為框架和字體和顏色的實際值。讓我們來看城市如下:

func configureCustomSearchController() {
    customSearchController = CustomSearchController(searchResultsController: self, searchBarFrame: CGRectMake(0.0, 0.0, tblSearchResults.frame.size.width, 50.0), searchBarFont: UIFont(name: "Futura", size: 16.0)!, searchBarTextColor: UIColor.orangeColor(), searchBarTintColor: UIColor.blackColor())

    customSearchController.customSearchBar.placeholder = "Search in this awesome bar..."
    tblSearchResults.tableHeaderView = customSearchController.customSearchBar
}

除了設定字體和顏色,我們也將欄位的預設值指定為 "Search in this awesome bar..."。最後,我們將 customSearchBar 加進 tableview header

現在在viewDidLoad() 行為內,我們需要做兩件搜尋: 需要呼叫上面的函數和避免預設的搜索控制器出現:

override func viewDidLoad() {
    ...

    // configureSearchController()

    configureCustomSearchController()
}

在這一刻我們自定義的控制器將不會到達我們達到我們預期的效果,這代表的意思是:當你真的打上關鍵字,但搜尋行為卻不會被執行。別擔心,我們將在下一個部分來修正它。然而,我們現在能夠看到我們客自化搜尋列的外觀了,我們可以執行App來看一下成果。

Custom Search Bar

透過客自化的搜尋控制器(Custom Search Controller)來執行搜尋

客製化搜尋控制器的類別將會變成客製化搜尋列的委派,這意味著我們將以CustomSearchController 類別來控制所有搜尋相關的功能。接著,透過實體化
客製protocol 和採用委派模式(delegation pattern),我們將透過客製化的委派方法處理 ViewController 類別的搜尋結果者真我們之前提的標準搜尋控制器的方法一樣。

所以,我們來為它注入新生命吧,首先我們開啟CustomSearchController.swift 檔案。 接著進入configureSearchBar(...) function內,並添加下面的程式碼:

func configureSearchBar(frame: CGRect, font: UIFont, textColor: UIColor, bgColor: UIColor) {
    ...

    customSearchBar.delegate = self
}

這將會使得我們客製化搜尋控Bar的類別委派變成CustomSearchController , 我們進到類別立面來處理UISearchBarDelegate:

class CustomSearchController: UISearchController, UISearchBarDelegate

別忘了加上UISearchBarDelegate協定定義到在類別的開頭:

class CustomSearchController: UISearchController, UISearchBarDelegate

現在,讓我們建立客製化的 protocol 透過下面的委派函式 (新增在 CustomSearchController 類別的 開始):

protocol CustomSearchControllerDelegate {
    func didStartSearching()

    func didTapOnSearchButton()

    func didTapOnCancelButton()

    func didChangeSearchText(searchText: String)
}

我認為來解釋每一行的功能有點多餘,因為光看function的命名就知道了,這非常得淺顯易見。在 CustomSearchController類別的最上方我們來宣告類別的屬性,如下所示:

var customDelegate: CustomSearchControllerDelegate!

到目前為止,我們可以來新增我們缺少的搜尋列(search bar)委派函式,並且呼叫每次呼叫 CustomSearchControllerDelegate必要的委派函式。讓我們開始編輯搜尋欄位開始編輯的事件呼叫吧:

func searchBarTextDidBeginEditing(searchBar: UISearchBar) {
    customDelegate.didStartSearching()
}

然後,在鍵盤上的搜尋按鈕被點擊時我們做如下處理:

func searchBarSearchButtonClicked(searchBar: UISearchBar) {
    customSearchBar.resignFirstResponder()
    customDelegate.didTapOnSearchButton()
}

然後,在取消搜尋按鈕被點擊時我們做如下處理:

func searchBarCancelButtonClicked(searchBar: UISearchBar) {
    customSearchBar.resignFirstResponder()
    customDelegate.didTapOnCancelButton()
}

最後,當搜尋文字被變更時我們做下面的處理:

func searchBar(searchBar: UISearchBar, textDidChange searchText: String) {
    customDelegate.didChangeSearchText(searchText)
}

所有上面的的函式將會讓ViewController類別在搜尋開始、搜尋結束、搜尋文字變更時被執行 。現在,我們開啟 ViewController.swift 檔案,並且直接跳到configureCustomSearchController() 函式內,並且新稱下面幾行:

func configureCustomSearchController() {
    ...

    customSearchController.customDelegate = self
}

在類別的開頭,採用了CustomSearchControllerDelegate 協定所以我們可以實體化委派方法:

class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource, UISearchResultsUpdating, UISearchBarDelegate, CustomSearchControllerDelegate

最後,我們看看實體化的部分。在下一個函式,將採取跟標準的搜尋控制器( normal search controller)一樣的行為。我們來進行這部分:

當搜尋開始時:

func didStartSearching() {
    shouldShowSearchResults = true
    tblSearchResults.reloadData()
}

當搜尋按鈕(search button)被點擊時:

func didTapOnSearchButton() {
    if !shouldShowSearchResults {
        shouldShowSearchResults = true
        tblSearchResults.reloadData()
    }
}

當取消按鈕(cancel button)被點擊時:

func didTapOnCancelButton() {
    shouldShowSearchResults = false
    tblSearchResults.reloadData()
}

當搜尋文字變更時:

func didChangeSearchText(searchText: String) {
    // Filter the data array and get only those countries that match the search text.
    filteredArray = dataArray.filter({ (country) -> Bool in
        let countryText: NSString = country

        return (countryText.rangeOfString(searchText, options: NSStringCompareOptions.CaseInsensitiveSearch).location) != NSNotFound
    })

    // Reload the tableview.
    tblSearchResults.reloadData()
}

這個App個功能應該十分的健全了並且使用我們的客製化搜尋控制器(custom search controller)和搜尋列。你可以運程程式來做最後的嘗試。

custom_search_bar

結語

我們快速的回顧一下在整個教學課程裡面我們先前已經完成的部分。我們我們很輕易理解如不管是使用iOs8所提供的UISearchController , 或者通過客製化的搜尋列和搜尋控制器,結果都是一樣的,我們都能夠完成搜尋的功能。如果你App的UI畫面並沒有需要客製化的修改,我們會建議你使用UISearchController擁有預設的搜尋列跟預設的外觀,且可以很快速的添加搜尋功能。如果你希望你的App裡面需要提供更多客製化的觸動,你也可以做這樣的變更,且到了這個階段你也知道了達成方法。

希望本教程將在未來給你的指導當你正在時做這樣的功能的時候,現在是我們要說再見的時候了,祝你有個快樂的搜尋體驗。

作為參考,你可以 下載完整的 Xcode 專案

譯者簡介:黃凱揚-系統分析與資深程式設計師,畢業南台科技大學電子系,曾在人力資源相關領域進行系統開發,13年的系統開發整合經驗,近年來協助客戶行 App 軟體以及 WebBase系統開發與系統分析與整合。

原文How To Create a Custom Search Bar Using UISearchController

作者
Gabriel Theodoropoulos
資深軟體開發員,從事相關工作超過二十年,專門在不同的平台和各種程式語言去解決軟體開發問題。自2010年中,Gabriel專注在iOS程式的開發,利用教程與世界上每個角落的人分享知識。可以在Google+或推特關注 Gabriel。
評論
更多來自 AppCoda 中文版
很好! 你已成功註冊。
歡迎回來! 你已成功登入。
你已成功訂閱 AppCoda 中文版 電子報。
你的連結已失效。
成功! 請檢查你的電子郵件以獲取用於登入的連結。
好! 你的付費資料已更新。
你的付費方式並未更新。