看板 KnucklesNote
作者 標題 [Xcode][Swift3] 加上搜尋輸入框 UISearchController
時間 2017-04-21 Fri. 00:51:16
在右下方 Object library 裡有個 UISearchDisplayController
但那是比較舊的方法
從 iOS 8 開始,可以使用新的 UISearchController
但 UISearchController 沒有放在 Object library 裡
不能在 storyboard 中拉出來,要用程式加上去
例如我們想要加上一個搜尋看板的頁面
預設是先顯示瀏覽過的看板
在搜尋輸入框中輸入看板名稱後改成顯示搜尋的結果
像這樣
瀏覽過的看板的功能先空著之後再作
在輸入框輸入「Te」後在下方的 TableView 列出含有 te 的板名
新增一個類別程式檔 BoardSearchViewController.swift
Subclass of: UITableViewController
在 storyboard 新增一個 Table View Controller
自訂類別選擇「BoardSearchViewController」
Table View 的 Prototype Cells 數量設為「2」
兩個 Cell 的 Style 設為 Basic
Identifier 分別設為 BoardSearchHeaderCell, BoardSearchCell
第一個 BoardSearchHeaderCell 設定 Height: 20, BackgroundColor: #333333
裡面的 Label 設定 Color: Light Gray Color, Font: System 18.0
修改 BoardSearchViewController.swift
新增成員變數
var boardAllList = [Any]()
var boardSearchResult = [Any]()
var shouldShowSearchResult = false
var searchController: UISearchController!
boardAllList 用來儲存所有看板的列表,使用 [Any]() 代表先建立空的陣列var boardSearchResult = [Any]()
var shouldShowSearchResult = false
var searchController: UISearchController!
boardSearchResult 用來儲存搜尋的結果
shouldShowSearchResult 用來判斷是否要顯示搜尋結果
searchController 用來建立一個 UISearchController
從網路下載所有看板的列表
安裝 Alamofire 的方法參考這篇
[Xcode][Swift3] 使用 Alamofire 存取網站資料 - KnucklesNote板 - Disp BBS
先在前面加上
import Alamofire
新增成員函數 loadBoardAllList()
func loadBoardAllList() {
let urlString = "https://disp.cc/api/get.php?act=bSearchList"
Alamofire.request(urlString).responseJSON { response in
guard let JSON = response.result.value as? [String: Any] else {
return
}
if let list = JSON["list"] as? [Any] {
self.boardAllList = list
}
}
}
用來下載所有看板的列表,然後存在成員變數 boardAllListlet urlString = "https://disp.cc/api/get.php?act=bSearchList"
Alamofire.request(urlString).responseJSON { response in
guard let JSON = response.result.value as? [String: Any] else {
return
}
if let list = JSON["list"] as? [Any] {
self.boardAllList = list
}
}
}
設定 UISearchController
繼承兩個類別 UISearchResultsUpdating, UISearchBarDelegate
class BoardSearchViewController: UITableViewController, UISearchResultsUpdating, UISearchBarDelegate {
新增成員函數 initSearchController()
func initSearchController() {
searchController = UISearchController(searchResultsController: nil)
searchController.searchResultsUpdater = self
searchController.dimsBackgroundDuringPresentation = false
searchController.hidesNavigationBarDuringPresentation = false
let searchBar = searchController.searchBar
searchBar.delegate = self
searchBar.placeholder = "請輸入看板名稱"
searchBar.setValue("取消", forKey:"_cancelButtonText")
searchBar.sizeToFit()
tableView.tableHeaderView = searchBar
tableView.backgroundView = UIView()
self.definesPresentationContext = true
}
UISearchController(searchResultsController: nil)searchController = UISearchController(searchResultsController: nil)
searchController.searchResultsUpdater = self
searchController.dimsBackgroundDuringPresentation = false
searchController.hidesNavigationBarDuringPresentation = false
let searchBar = searchController.searchBar
searchBar.delegate = self
searchBar.placeholder = "請輸入看板名稱"
searchBar.setValue("取消", forKey:"_cancelButtonText")
searchBar.sizeToFit()
tableView.tableHeaderView = searchBar
tableView.backgroundView = UIView()
self.definesPresentationContext = true
}
建立 SearchController 時,傳入 nil 代表不另外開一個 TableView 來顯示
而是與本來的資料共用一個 TableView 來顯示
searchResultsUpdater = self
設定在這個類別使用 Updater 的代理函數
dimsBackgroundDuringPresentation = false
設定 false 代表不要在輸入搜尋框時,將下面的 TableView 變深色
hidesNavigationBarDuringPresentation = false
設定 false 代表不要在輸入搜尋框時,將搜尋框移到頁面最上方
searchBar.delegate = self
設定在這個類別使用 searchBar 的代理函數
searchBar.placeholder = "請輸入看板名稱"
輸入框中要顯示的輸入提示文字
searchBar.sizeToFit()
設定輸入框的寬度符合顯示的位置
tableView.tableHeaderView = searchBar
將輸入框放在 tableView 的 HeaderView
tableView.backgroundView = UIView
用來移除 tableView 的白色背景,避免下拉時顯示出來
self.definesPresentationContext = true
要設定一下 ViewController 的這個屬性
避免離開這個頁面時,搜尋輸入框沒有消失並蓋住了其他頁面
設定這個也會在離開頁面再回來時能保持搜尋狀態
(若搜尋頁是放在 ContainerView 中的子頁面,
那這行要寫在主頁面的 ViewDidLoad() 才行)
在 viewDidLoad() 執行上面兩個新增的成員函數
override func viewDidLoad() {
super.viewDidLoad()
loadBoardAllList()
initSearchController()
}
super.viewDidLoad()
loadBoardAllList()
initSearchController()
}
修改 TableView 的 data source 函數
override func numberOfSections(in tableview: UITableView) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if shouldShowSearchResult {
return boardSearchResult.count
} else {
return 0
}
}
第一個函數設定 section 數目為1return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if shouldShowSearchResult {
return boardSearchResult.count
} else {
return 0
}
}
第二個函數設定 row 的數目
使用 shouldShowSearchResult 來決定是否要顯示搜尋結果
要顯示搜尋結果時,row 的數目設為陣列 boardSearchResult 的大小
新增兩個 tableView 的函數
override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
return 20
}
override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let cell = tableView.dequeueReusableCell(withIdentifier: "BoardSearchHeaderCell")
if shouldShowSearchResult {
cell?.textLabel?.text = "搜尋結果"
} else {
cell?.textLabel?.text = "瀏覽過的看板"
}
return cell
}
第一個函數設定 section 的 Header 顯示高度為 20return 20
}
override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let cell = tableView.dequeueReusableCell(withIdentifier: "BoardSearchHeaderCell")
if shouldShowSearchResult {
cell?.textLabel?.text = "搜尋結果"
} else {
cell?.textLabel?.text = "瀏覽過的看板"
}
return cell
}
第二個函數設定 section 的 Header 使用之前在 storyboard 加上的 BoardSearchHeaderCell
使用 shouldShowSearchResult 判斷 Header 中要顯示什麼文字
設定每個 row 要顯示的內容
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "BoardSearchCell", for: indexPath)
if shouldShowSearchResult {
let board = boardSearchResult[indexPath.row] as! [String: Any]
cell.textLabel?.text = board["name"] as? String
} else {
cell.textLabel?.text = ""
}
return cell
}
let cell = tableView.dequeueReusableCell(withIdentifier: "BoardSearchCell", for: indexPath)
if shouldShowSearchResult {
let board = boardSearchResult[indexPath.row] as! [String: Any]
cell.textLabel?.text = board["name"] as? String
} else {
cell.textLabel?.text = ""
}
return cell
}
加上 SearchBar 的代理函數
// MARK: - UISearchBarDelegte
// 輸入框取得focus時
func searchBarTextDidBeginEditing(_ searchBar: UISearchBar) {
}
// 輸入框失去focus時
func searchBarTextDidEndEditing(_ searchBar: UISearchBar) {
shouldShowSearchResult = true
tableView.reloadData()
}
// 點擊輸入框的 Cancel 按鈕
func searchBarCancelButtonClicked(_ searchBar: UISearchBar) {
shouldShowSearchResult = false
tableView.reloadData()
}
// 點擊鍵盤上的 Search 按鈕
func searchBarSearchButtonClicked(_ searchBar: UISearchBar) {
shouldShowSearchResult = true
tableView.reloadData()
searchController.searchBar.resignFirstResponder()
}
用來在輸入框輸入文字前後、點擊 Cancel 按鈕後、點擊鍵盤上的 Search 按鈕後,// 輸入框取得focus時
func searchBarTextDidBeginEditing(_ searchBar: UISearchBar) {
}
// 輸入框失去focus時
func searchBarTextDidEndEditing(_ searchBar: UISearchBar) {
shouldShowSearchResult = true
tableView.reloadData()
}
// 點擊輸入框的 Cancel 按鈕
func searchBarCancelButtonClicked(_ searchBar: UISearchBar) {
shouldShowSearchResult = false
tableView.reloadData()
}
// 點擊鍵盤上的 Search 按鈕
func searchBarSearchButtonClicked(_ searchBar: UISearchBar) {
shouldShowSearchResult = true
tableView.reloadData()
searchController.searchBar.resignFirstResponder()
}
要執行的動作
加上 UISearchResultsUpdating 的代理函數
// MARK: - UISearchResultsUpdating
func updateSearchResults(for searchController: UISearchController) {
guard let searchString = searchController.searchBar.text else {
return
}
// 輸入框沒有輸入文字時
if searchString.characters.count == 0 {
shouldShowSearchResult = false
tableView.reloadData()
return
}
boardSearchResult = boardAllList.filter({ obj -> Bool in
let board = obj as! [String: Any]
let boardName = board["name"] as! String
// 只輸入一個字元時,只尋找板名開頭為這個字元的板
if searchString.characters.count == 1 {
if boardName.lowercased().characters.first == searchString.lowercased().characters.first {
return true
}
} else if boardName.range(of: searchString, options: NSString.CompareOptions.caseInsensitive) != nil {
return true
}
return false
})
shouldShowSearchResult = true
tableView.reloadData()
}
用來執行即時搜尋,每次輸入框中的文字改變時,就會執行這個函數func updateSearchResults(for searchController: UISearchController) {
guard let searchString = searchController.searchBar.text else {
return
}
// 輸入框沒有輸入文字時
if searchString.characters.count == 0 {
shouldShowSearchResult = false
tableView.reloadData()
return
}
boardSearchResult = boardAllList.filter({ obj -> Bool in
let board = obj as! [String: Any]
let boardName = board["name"] as! String
// 只輸入一個字元時,只尋找板名開頭為這個字元的板
if searchString.characters.count == 1 {
if boardName.lowercased().characters.first == searchString.lowercased().characters.first {
return true
}
} else if boardName.range(of: searchString, options: NSString.CompareOptions.caseInsensitive) != nil {
return true
}
return false
})
shouldShowSearchResult = true
tableView.reloadData()
}
boardSearchResult = boardAllList.filter({ obj -> Bool in … })
使用陣列的 .filter() 將所有看板過濾為要尋找的看板,
然後存在另一個陣列 boardSearchResult
.filter() 的輸入參數為一個 callback function
陣列中的每個值會依序輸入這個 callback function
若 return true 則保留,return false 則去除
使用程式將輸入框設為輸入狀態
想要一進頁面,不用點輸入框,就直接跳出鍵盤進入輸入狀態的話
在 viewDidAppear() 加上
override func viewDidAppear(_ animated: Bool) {
searchController.isActive = true
DispatchQueue.main.async(execute: {
self.searchController.searchBar.becomeFirstResponder()
})
}
searchController.isActive = true
DispatchQueue.main.async(execute: {
self.searchController.searchBar.becomeFirstResponder()
})
}
想要點擊自訂的按鈕啟動輸入狀態的話
在新增的 @IBAction 加上
@IBAction func search(_ sender: Any) {
//要先捲動到最頂,避免 searchBar 位置錯誤
self.tableView.setContentOffset(CGPoint(x: 0.0, y: -self.tableView.contentInset.top), animated: false)
searchController.isActive = true
DispatchQueue.main.async(execute: {
self.searchController.searchBar.becomeFirstResponder()
})
}
//要先捲動到最頂,避免 searchBar 位置錯誤
self.tableView.setContentOffset(CGPoint(x: 0.0, y: -self.tableView.contentInset.top), animated: false)
searchController.isActive = true
DispatchQueue.main.async(execute: {
self.searchController.searchBar.becomeFirstResponder()
})
}
預設隱藏輸入框
想要一進頁面時,輸入框是隱藏的,要往下拉才會顯示的話,
參考 StackOverflow 在 viewDidLoad() 加上
tableView.contentOffset = CGPoint(x: 0.0, y: 44.0)
加上搜尋範圍的按鈕
要在輸入框下方加上搜尋範圍的按鈕,例如可選擇要搜尋的是「標題」還是「作者」
在 initSearchController() 裡加上
searchBar.scopeButtonTitles = ["標題", "作者"]
在輸入狀態時就會變成像這樣
加上點擊按鈕後會執行的代理函數
func searchBar(_ searchBar: UISearchBar, selectedScopeButtonIndexDidChange selectedScope: Int) {
// 顯示點擊按鈕的名稱
print("click " + searchBar.scopeButtonTitles![selectedScope])
if selectedScope == 0 {
// 點擊了"標題"時要做的事
} else if selectedScope == 1 {
// 點擊了"作者"時要做的事
}
}
// 顯示點擊按鈕的名稱
print("click " + searchBar.scopeButtonTitles![selectedScope])
if selectedScope == 0 {
// 點擊了"標題"時要做的事
} else if selectedScope == 1 {
// 點擊了"作者"時要做的事
}
}
在點擊了「Search」按鈕的代理函數 searchBarSearchButtonClicked() 中
要取得搜尋範圍按鈕是選擇了哪一個,可使用
if searchBar.selectedScopeButtonIndex == 0 {
//顯示搜尋"標題"的搜尋結果
}
//顯示搜尋"標題"的搜尋結果
}
□ 問題解決記錄
SearchBar 為輸入狀態時,位置莫名的下移了一段
解決方法參考 StackOverflow
在 storyboard 中,TableViewController 的屬性設定裡,將「Under Opaque Bars」勾選即可
搜尋框為輸入狀態時,捲動列表,再按搜尋框的取消時,出現 index out of range 的錯誤並閃退
但若是有先執行搜尋後,再捲動列表後按取消就沒事
解決方法,在 searchBarCancelButtonClicked() 的代理函數中
要先檢查是否有執行過搜尋,有的話才能執行 refresh 重整列表
參考
AppCoda 如何利用UISearchController添加搜尋功能並打造客製化搜尋列
RayWenderlich UISearchController Tutorial: Getting Started
--
※ 作者: Knuckles 時間: 2017-04-21 00:51:16
※ 編輯: Knuckles 時間: 2017-07-26 04:47:01
※ 看板: KnucklesNote 文章推薦值: 0 目前人氣: 0 累積人氣: 518
回列表(←)
分享