顯示廣告
隱藏 ✕
看板 KnucklesNote
作者 Knuckles (站長 那克斯)
標題 [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]() 代表先建立空的陣列
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
            }
        }
    }
用來下載所有看板的列表,然後存在成員變數 boardAllList


設定 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 時,傳入 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()
    }


修改 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 數目為1

第二個函數設定 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 顯示高度為 20

第二個函數設定 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
    }

加上 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 按鈕後,
要執行的動作


加上 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()
    }
用來執行即時搜尋,每次輸入框中的文字改變時,就會執行這個函數

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()
        })
    }

想要點擊自訂的按鈕啟動輸入狀態的話
在新增的 @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()
        })
    }


預設隱藏輸入框

想要一進頁面時,輸入框是隱藏的,要往下拉才會顯示的話,
參考 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 {
            // 點擊了"作者"時要做的事
        }
    }

在點擊了「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 累積人氣: 509 
分享網址: 複製 已複製
r)回覆 e)編輯 d)刪除 M)收藏 ^x)轉錄 同主題: =)首篇 [)上篇 ])下篇