簡介
UISearchController
是iOS 8新增的一個類卤唉,用于實現(xiàn)搜索欄的默認行為啦逆,這也是官方推薦的搜索欄的實現(xiàn)方法。
行為
在UITableViewController
上長這個樣子
-
初始狀態(tài)下搜索欄是隱藏的
-
往下拉時安皱,搜索欄顯示并占據導航欄的位置
-
點擊搜索欄進入搜索狀態(tài)
-
輸入時進行實時搜索
用法1:使用當前View Controller顯示搜索結果
(1) 建立工程诗茎,Storyboard如下:
(2) 在MainViewController
中設置搜索欄
class MainViewController: UITableViewController {
private let items = [
"Jon Snow",
"Bran",
"Theon",
"Tarly",
"Tyrion",
"Tywin",
"Jaime",
"Cersei",
"Ned",
"Robb",
]
private var searchResults = [String]()
override func viewDidLoad() {
super.viewDidLoad()
let searchController = UISearchController(searchResultsController: nil)
searchController.obscuresBackgroundDuringPresentation = false
searchController.searchBar.placeholder = "Search name..."
searchController.searchBar.autocapitalizationType = .none
searchController.searchResultsUpdater = self
navigationItem.searchController = searchController
}
}
- 用
items
和searchResults
來分別保存全部數據和搜索結果尿背。默認狀態(tài)下顯示全部數據默赂,搜索狀態(tài)下顯示搜索結果捻激。 - 創(chuàng)建一個
UISearchController
實例钧排。參數searchResultsController
為nil
即表示沒有單獨的顯示搜索結果的界面敦腔,也就是使用當前界面來顯示。 -
obscuresBackgroundDuringPresentation
表示搜索欄進入搜索狀態(tài)時恨溜,要不要在當前界面上顯示一層半透明的遮罩(效果可以在手機的設置界面上感受一下符衔,或者參考上面的第3張圖)找前。該屬性的默認值為true
,但是如果沒有要把當前界面作為搜索結果顯示界面判族,最好設成false
躺盛。否則,點擊搜索結果時就會點在遮罩上形帮,造成搜索欄退出搜索狀態(tài)槽惫,重新顯示全部數據。 - 對
searchBar
進行一些外觀上的設置 - 設置
searchResultsUpdater
以監(jiān)聽搜索事件辩撑。如果不需要實時搜索界斜,則不需要這一步。 - 最后把實例放到
navigationItem
上合冀。
現(xiàn)在運行代碼各薇,下拉可以顯示搜索欄,只是還沒有實現(xiàn)搜索功能水慨。
(3) 根據狀態(tài)顯示數據
// MARK: - helpers
extension MainViewController {
var isSearching: Bool {
return navigationItem.searchController?.isActive ?? false
}
func item(at indexPath: IndexPath) -> String {
return isSearching ? searchResults[indexPath.row] : items[indexPath.row]
}
var itemCount: Int {
return isSearching ? searchResults.count : items.count
}
}
// MARK: - UITableViewDataSource
extension MainViewController {
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return itemCount
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
cell.textLabel?.text = item(at: indexPath)
return cell
}
}
- 用
isActive
屬性來判斷當前是否在搜索狀態(tài)得糜。只有當搜索欄顯示并獲得焦點時,isActive
才為true
晰洒。 - 根據狀態(tài)切換顯示內容
(4) 監(jiān)聽搜索事件
添加search
方法朝抖,更新searchResults
并刷新界面。
// MARK: - helpers
extension MainViewController {
...
func search(_ text: String?) {
if let text = text, !text.isEmpty {
searchResults = items.flatMap { $0.range(of: text, options: .caseInsensitive) == nil ? nil : $0 }
}
else {
searchResults = items
}
tableView.reloadData()
}
}
實現(xiàn)UISearchResultsUpdating
協(xié)議
// MARK: - UISearchResultsUpdating
extension MainViewController : UISearchResultsUpdating {
func updateSearchResults(for searchController: UISearchController) {
search(searchController.searchBar.text)
}
}
運行代碼谍珊,就可以在搜索欄進行實時搜索了治宣。
(5) 如果不想用實時搜索,而只在輸入完成并按return
時搜索砌滞。就把這行:
searchController.searchResultsUpdater = self
改成
searchController.searchBar.delegate = self
searchController.delegate = self
并實現(xiàn)這兩個協(xié)議
// MARK: - UISearchBarDelegate
extension MainViewController : UISearchBarDelegate {
func searchBarSearchButtonClicked(_ searchBar: UISearchBar) {
search(searchBar.text)
}
}
// MARK: - UISearchControllerDelegate
extension MainViewController : UISearchControllerDelegate {
func didDismissSearchController(_ searchController: UISearchController) {
tableView.reloadData()
}
}
- 實現(xiàn)
didDismissSearchController
方法是為了在點擊Cancel
按鈕退出搜索狀態(tài)時可以重新顯示所有數據侮邀。其實UISearchBarDelegate
也有個searchBarCancelButtonClicked
方法可以監(jiān)聽Cancel
按鈕事件,但這個事件是發(fā)生在Cancel
按鈕被點擊之時贝润,而不是在那之后绊茧。也就是當時searchController
的isActive
屬性還是true
,此時界面刷新也仍會顯示searchResults
的內容打掘。
用法2:使用其它View Controller顯示搜索結果
多數時候华畏,我們都需要定制搜索界面(參考微信的搜索界面)。
(1) 以上面的代碼為基礎尊蚁,在Storyboard中新增一個UITableViewController
亡笑,類名為SearchResultsViewController
class SearchResultsViewController: UITableViewController {
private var results = [NSAttributedString]() {
didSet {
tableView.reloadData()
}
}
func search(_ text: String, in items: [String]) {
results = items.flatMap { item in
let matchedRange = (item as NSString).range(of: text, options: .caseInsensitive)
guard matchedRange.location != NSNotFound else {
return nil
}
return highlighted(string: item, in: matchedRange)
}
}
private func highlighted(string s: String, in range: NSRange) -> NSAttributedString {
let s = NSMutableAttributedString(string: s)
s.beginEditing()
let highlightAttributes: [NSAttributedStringKey : Any] = [
.foregroundColor : UIColor.blue,
.font : UIFont.boldSystemFont(ofSize: 17),
]
s.addAttributes(highlightAttributes, range: range)
s.endEditing()
return s
}
}
extension SearchResultsViewController {
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return results.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
cell.textLabel?.attributedText = results[indexPath.row]
return cell
}
}
-
search
方法跟前面差不多,只是多了一步横朋,把匹配的部分高亮顯示仑乌。為此,要使用NSAttributedString
數組來保存搜索結果。
接下來可以把MainViewController
里搜索相關的代碼都去掉晰甚,變成:
class MainViewController: UITableViewController {
private let items = ...
override func viewDidLoad() {
super.viewDidLoad()
let searchResultsController = storyboard?.instantiateViewController(withIdentifier: "searchResults")
let searchController = UISearchController(searchResultsController: searchResultsController)
searchController.searchBar.placeholder = "Search name..."
searchController.searchBar.autocapitalizationType = .none
searchController.searchBar.delegate = self
searchController.delegate = self
navigationItem.searchController = searchController
definesPresentationContext = true
}
private var searchResultsController: SearchResultsViewController? {
return navigationItem.searchController?.searchResultsController as? SearchResultsViewController
}
}
// MARK: - helpers
extension MainViewController {
func search(_ text: String?) {
guard let text = text, !text.isEmpty else {
return
}
searchResultsController?.search(text, in: items)
}
}
- 刪除
searchResults
- 創(chuàng)建
SearchResultsViewController
對象衙传,并傳給UISearchController
對象。后面點擊搜索欄的時候就會通過present
方法來顯示這個界面厕九。 - 下面是
definesPresentationContext
屬性的官方說明
When a context-based presentation occurs, UIKit starts at the presenting view controller and walks up the view controller hierarchy. If it finds a view controller whose value for this property is true, it asks that view controller to present the new view controller. If no view controller defines the presentation context, UIKit asks the window’s root view controller to handle the presentation.
也就是說粪牲,把它設為true
的話,presentation就會以這一層為root view來展示止剖。如果保持默認值false
腺阳,則會以window
為root view,將導致SearchResultsViewController
遮住整個屏幕(包括搜索欄)穿香。
-
obscuresBackgroundDuringPresentation
可以使用默認值true
來顯示半透明遮罩了亭引。把原先的賦值語句刪掉就好了。 - 原來的
search
方法改為直接調用searchResultsController
上的search
方法皮获。
剩下的部分簡化成:
// MARK: - UITableViewDataSource
extension MainViewController {
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return items.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
cell.textLabel?.text = items[indexPath.row]
return cell
}
}
// MARK: - UISearchBarDelegate
extension MainViewController : UISearchBarDelegate {
func searchBarSearchButtonClicked(_ searchBar: UISearchBar) {
search(searchBar.text)
}
}
// MARK: - UISearchControllerDelegate
extension MainViewController : UISearchControllerDelegate {
func didDismissSearchController(_ searchController: UISearchController) {
tableView.reloadData()
}
}
現(xiàn)在運行代碼焙蚓,輸入文字并點return
,可以顯示搜索結果洒宝,并把匹配部分高亮顯示购公。看起來不錯雁歌。
此時點Cancel
宏浩,再點搜索欄,輸入文字靠瞎。這時發(fā)現(xiàn)searchResultsController
還顯示著上次的結果比庄。這是因為每次present用的是同一個SearchResultsViewController
的實例。
個人覺得API的設計還不如改成在
UISearchControllerDelegate
里提供一個接口乏盐,用來返回一個UIViewController
實例佳窑,作為顯示結果的界面。這樣可以在需要顯示時才去創(chuàng)建這個view controller父能。
解決這個問題神凑,只要在present的時候把搜索結果重置一下就好了。
在SearchResultsViewController
中添加一個reset
何吝,負責重置狀態(tài)溉委。
func reset() {
results.removeAll()
}
然后在MainViewController
里響應willPresentSearchController
事件
extension MainViewController : UISearchControllerDelegate {
func willPresentSearchController(_ searchController: UISearchController) {
searchResultsController?.reset()
}
}
運行,妥妥的岔霸。