當一個app要顯示大量的數據,滑動列表並不會讓人愉悅。所以允許用戶搜索指定的內容變得刻不容緩。
好消息是,UIKit已經將UISearchBar和UITableView無縫結合在一起了。
在本教程中,你將用標准的table view創建一個可以搜索糖果的app。
使用iOS8的新特性UISearchController,賦予table view搜索的功能,包含動態過濾,
還要添加一個可供選擇的scope bar。
最后,學會如何讓app更加友好,滿足用戶的需求。
開始
點擊這里下載初始項目並打開它,
這個項目已經有了一個帶樣式的navigation controller。運行它,你將看到一個空的列表:
回到Xcode,文件Candy.swift中有一個類用來保存每一個糖果的信息,
這個類有兩個屬性,分別對應糖果的名稱和種類。
當用戶使用你的app搜索糖果時,你將根據用戶輸入的文字定位到對應的那一項。
在教程的最后你要實現一個Scope Bar,到時你就明白種類字符串有多重要。
創建Table View
打開 MasterViewController.swift,candies屬性用來管理所有不同的Candy對象.
說到這,是時候創建一些Candy了。
在本教程中,你只需要少量的數據來演示search bar是如何工作的;
在正式的項目中,你也許有幾千個對象要被搜索。
不論是幾千條還是幾條數據,這個方法都同樣適用。
創建candies數據,將下面的代碼添加到viewDidLoad()方法中,然后call super.viewDidLoad()
candies = [
Candy(category:"Chocolate", name:"Chocolate Bar"), Candy(category:"Chocolate", name:"Chocolate Chip"), Candy(category:"Chocolate", name:"Dark Chocolate"), Candy(category:"Hard", name:"Lollipop"), Candy(category:"Hard", name:"Candy Cane"), Candy(category:"Hard", name:"Jaw Breaker"), Candy(category:"Other", name:"Caramel"), Candy(category:"Other", name:"Sour Chew"), Candy(category:"Other", name:"Gummi Bear") ]
再運行一次你的項目,table view的delegate和datasource方法已經實現了,
你將看到一個有數據的table view:
選擇一行后會展示相應的糖果詳細:
糖果太多了,需要一些時間才能找到想要到!你需要一個 UISearchBar。
引入 UISearchController
如果你看過UISearchController的文檔,你會發現它很懶。它沒有做任何關於搜索的工作。
這個類簡單地提供一個用戶期望的標准接口。
UISearchController通過委托告訴app用戶正在做什么。
你需要自己編寫所有的字符串匹配函數。
雖然看起來有點嚇人,編寫自定義搜索函數對返回的數據進行嚴格的控制,
你的用戶也會感到搜索非常智能和快速。
如果你使用過iOS的table view搜索,你也許很熟悉UISearchDisplayController。
從iOS8開始,這個類被UISearchController替代了,並簡化了搜索過程。
不幸的是,在撰寫本文時,Interface Builder不支持UISearchController,所以要用代碼來制作UI。
在MasterViewController.swift中添加一個屬性:
let searchController = UISearchController(searchResultsController: nil)
初始化UISearchController時並沒有設置searchResultsController,
你告訴search controller要使用默認的視圖用來展示搜索結果。
如果你指定一個不同的viewController,那么它將被用來展示結果。
下一步,需要給searchController設置一些參數。
依然在MasterViewController.swift中,添加下面的代碼到viewDidLoad():
searchController.searchResultsUpdater = self searchController.dimsBackgroundDuringPresentation = false definesPresentationContext = true tableView.tableHeaderView = searchController.searchBar
下面是代碼的說明:
- searchResultsUpdater是UISearchController中的一個屬性,遵循了協議UISearchResultsUpdating。
這個協議允許類接收UISearchBar文本變化的通知。過一會就要使用這個協議。 - 默認情況下,UISearchController會將presented視圖變暗。
當你使用另一個viewController作為searchResultsController會非常有用,
在現在的實例中,你已經設置了當前的view來展示結果,所以不需要讓它變暗。 - 通過設置definesPresentationContext為true,能夠確保UISearchController被激活時,
用戶跳轉到另一個viewController,而search bar依然保留在屏幕上。 - 最后,將searchBar添加到table view的tableHeaderView。
記住,Interface Builder還不兼容UISearchController,這一步是必須的。
UISearchResultsUpdating和Filtering
設置了search controller后,還需要寫一些代碼讓它工作起來。
首先,將下面的屬性添加到MasterViewController的頂部:
var filteredCandies = [Candy]()
這個屬性將持有用戶正在搜索的糖果對象。
下一步,將這個方法添加到MasterViewController:
func filterContentForSearchText(searchText: String, scope: String = "All") { filteredCandies = candies.filter { candy in return candy.name.lowercaseString.containsString(searchText.lowercaseString) } tableView.reloadData() }
這個方法會根據searchText過濾candies,並將結果添加到filteredCandies。
不要擔心scope這個參數,下一節就會用到它。
為了讓MasterViewController響應search bar,必須實現UISearchResultsUpdating。
打開MasterViewController.swift,添加下面的類擴展,在MasterViewController類外面:
extension MasterViewController: UISearchResultsUpdating { func updateSearchResultsForSearchController(searchController: UISearchController) { filterContentForSearchText(searchController.searchBar.text!) } }
updateSearchResultsForSearchController(_:)是UISearchResultsUpdating協議中唯一一個而且是必須實現的方法。
現在,無論用戶怎樣修改search bar的文本,UISearchController都會通過這個方法告訴MasterViewController。
這個方法簡單的調用了助手方法,並將search bar當前的文本作為參數。
filter()用到了(candy: Candy) -> Bool類型的閉包。它會循環數組中的每一個元素,然后當前的元素發給閉包。
使用它來確定一個糖果是否作為搜索結果來呈現給用戶。
返回true將當前的糖果添加到filtered數組中,否則返回false。
為了判斷結果,containsString(_:)用來檢查candy的name是否包含了searchText。
但在比較之前,使用lowercaseString方法將字符串轉換成小寫。
注意:大多數時候,用戶不會去在乎輸入的大小寫問題,
如果大小寫不匹配的話,依然不會返回結果。
現在,你輸入"Chocolate"或者"chocolate"都會返回匹配的結果。這太有用了!!
再運行一次,你會發現table上面有一個search bar。
然而,輸入任何文本都不會呈現過濾的結果。什么鬼?
這只是因為寫的代碼還沒有告訴table何時使用過濾后的數據。
回到MasterViewController.swift,將tableView(_:numberOfRowsInSection:)替換成下面的代碼:
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int { if searchController.active && searchController.searchBar.text != "" { return filteredCandies.count } return candies.count }
沒有太多的修改,僅僅檢查了一下用戶是否正在輸入,
並使用過濾后或者正常的數據給table。
接下來,將tableView(_:cellForRowAtIndexPath:)替換成下面的代碼:
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath) let candy: Candy if searchController.active && searchController.searchBar.text != "" { candy = filteredCandies[indexPath.row] } else { candy = candies[indexPath.row] } cell.textLabel?.text = candy.name cell.detailTextLabel?.text = candy.category return cell }
這兩個方法都使用了searchController的active屬性來決定呈現哪個數組。
當用戶點擊Search Bar的文本框,active會自動設置為true。
如果search controller處在激活狀態,會看到用戶已經輸入了一些文字。
如果已經存在了,便會返回filteredCandies的數據,否則返回完成列表的數據。
回想一下,search controller會自動顯示或隱藏table,所有代碼要做的就是根據用戶的輸入提供正確的數據。
編譯運行一下,你有一個Search Bar來過濾數據。
試試這個app,已經可以搜索到各種糖果了。
這里還有一個問題,選中一個搜索結果,會發現詳情界面顯示了錯誤的糖果!來修復它。
發送數據給Detail View
在將數據發送給詳細視圖時,需要確保控制器需要知道哪個上下文正在使用:是完整的列表還是搜索結果。
還是在MasterViewController.swift,在prepareForSegue(_:sender:)中找到下面的代碼:
let candy = candies[indexPath.row]
替換成:
let candy: Candy if searchController.active && searchController.searchBar.text != "" { candy = filteredCandies[indexPath.row] } else { candy = candies[indexPath.row] }
這里執行了tableView(_:numberOfRowsInSection:)和tableView(_:cellForRowAtIndexPath:)相同的檢查,
但現在提供了正確的糖果對象給詳細視圖。
再次運行一遍,看看是不是正確的。
使用Scope Bar來篩選數據
還有另一種方法過濾數據,添加一個Scope Bar根據糖果的類別來過濾。
類別就是創建Candy對象時添加的,如Chocolate,Hard,Other。
先在MasterViewController里面添加一個scope bar。
scope bar是一個分段控件,用來縮小搜索的范圍。
范圍就是最初定義的。在這個項目中,范圍就是糖果的種類,但也可以是其他的。
先來實現scope bar的代理方法。
在MasterViewController.swift中,添加另一個擴展UISearchBarDelegate,
將下面的代碼添加到UISearchResultsUpdating的后面:
extension MasterViewController: UISearchBarDelegate { func searchBar(searchBar: UISearchBar, selectedScopeButtonIndexDidChange selectedScope: Int) { filterContentForSearchText(searchBar.text!, scope: searchBar.scopeButtonTitles![selectedScope]) } }
這個代理方法會在用戶切換scope bar的時候通知viewController,
當它觸發時,之行了搜索方法filterContentForSearchText(_:scope:)。
修改filterContentForSearchText(_:scope:)方法支持范圍的選擇:
func filterContentForSearchText(searchText: String, scope: String = "All") { filteredCandies = candies.filter { candy in let categoryMatch = (scope == "All") || (candy.category == scope) return categoryMatch && candy.name.lowercaseString.containsString(searchText.lowercaseString) } tableView.reloadData() }
只有當參數scope等於“ALL”或者等於糖果對象的種類屬性才將candy添加到filteredCandies數組。
已經快完成了,但范圍過濾還沒有生效。
需要修改方法updateSearchResultsForSearchController(_:):
func updateSearchResultsForSearchController(searchController: UISearchController) { let searchBar = searchController.searchBar let scope = searchBar.scopeButtonTitles![searchBar.selectedScopeButtonIndex] filterContentForSearchText(searchController.searchBar.text!, scope: scope) }
現在唯一的問題就是還沒有scope bar控件!
選擇文件MasterViewController.swift,在viewDidLoad()中添加下面的代碼:
searchController.searchBar.scopeButtonTitles = ["All", "Chocolate", "Hard", "Other"] searchController.searchBar.delegate = self
這會給搜索欄添加一個分段控件,還有和candy的categories相對應的標題。
還包括一個名為“ALL”的分類,它將忽略種類的過濾。
何去何從
恭喜你,已經有了一個可以搜索的table view。
點擊這里下載完整的項目代碼。
越來越多的app都使用了表格視圖,搜索功能成了標配。
沒理由不使用UISearechBar和UISearchController。
