原文链接: http://www.raywenderlich.com/113772/uisearchcontroller-tutorial
为便于理解本文有做一些删减和结构上的改动。
如果你的 app 需要展示大量的数据,滚屏将会造成卡顿甚至崩溃。在这种情况下,允许用户搜索指定的特定的项目显得十分重要。幸运的是,UIKit 包含的 UISearchBar 完美结合了 UITableView 使你可以快速地筛选过滤内容。
在这个教程中你将会创建一个基于 tableView,可根据种类搜索糖果的 app。得益于 iOS 8 带来的 UISearchController,你将为tableView 加入搜索功能,包括动态地筛选内容,以及添加可选范围的选项(optional scope bar)。
在 这里 下载已经初步配置好的项目,构建并运行你将会看到一个空白的列表:
返回 Xcode,Candy.swfit 包含了一个储存着所需要展示信息的类,其包含了两个属性,分别是糖果的 category 和 name。
当用户在这个 app 中搜索糖果时,你将引用到 name 这个属性与之匹配,后面用到 Scope Bar 时将会用到 category 这个属性。
打开 MasterViewController.swift
,变量 candies 将会用来存储所有的 Candy 对象可供用户搜索,让我们来创建一些糖果!
在这个教程中只要创建少量的数据让我们了解 search bar 是如何工作的,而一个产品很有可能会有成千上万的数据可供搜索,但原理是相同的。
在 viewDidLoad() 中添加如下代码创建 Candy 对象来填充数组:
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")
]
因为项目已经配置好了 tableView 的 delegate 和 datasource 方法,所以现在编译运行程序就能看到数据展示在列表上了:
同时,选中某一行将会展示相应糖果的 detailView:
如果你看过 UISearchController 的官方文档会发现这个类「有点懒」,它并不提供搜索的具体实现,它与 delegate 通过协议来获悉用户的动作,你需要自己写所有实现数据匹配的功能。
尽管这看起来有点吓人,但是自定义实现搜索功能使我们能够更好的掌控搜索结果的返回从而提高搜索性能,提升用户体验。
UISearchDisplayController 类从 iOS8 开始被废除,转而使用 UISearchController。遗憾的是,目前 Interface Builder 还不支持 UISearchController,所以你需要用代码实现 UI。
在 MasterViewController.swift 加入一个新属性:
let searchController = UISearchController(searchResultsController: nil)
初始化 UISearchController 时传入 nil 意味着我们希望在同一个视图显示搜索结果,如果传入另一个 viewController 则是在那展示结果。
接下来,你需要要为 searchController 对象设置几个参数,在MasterViewController.swfit 的 viewDidLoad() 中添加如下代码:
searchController.searchResultsUpdater = self
searchController.dimsBackgroundDuringPresentation = false
definesPresentationContext = true
tableView.tableHeaderView = searchController.searchBar
概述以上代码:
searchResultsUpdater 是 UISearchController 的一个属性,遵循UISearchResultsUpdating 协议,这样就能够监听 UISearchBar 内文本的改变。
默认下 UISearchController 会使视图变暗(不可操作),如果要使用其他视图来作为 searchResultController 可以将 dimsBackgroundDuringPresentation 这个值设为 true(变暗)。在本例中,我们需要当前视图作为 searchReslultsUpdater,所以将这个值设为 false,这样就能在当前视图看到搜索结果并响应点击。
将 definesPresentationContext 设为 true ,意味着当用户导航到其他视图控制器时正在输入的 searchBar 将会消失而非保留在画面上。
最后,将 searchBar 加入到 tableView 的 tableHeaderView 中。
接下来,在 MasterViewController 中创建一个变量:
var filteredCandies = [Candy]()
这个属性用来保存筛选后的数据,接着添加如下方法:
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 参数,后面会用到。filter() 带有一个闭包(candy: Candy) -> Bool ,该方法会遍历数组的所有对象,闭包中调用 containsString 判断文本是否有包含用户输入的内容,并返回一个 bool 值。在这里,我们将文本都通过 lowercaseString 转为小写字母来对比。
注:大多时候用户并不在意搜索结果的大小写区分,所以在这里统一转为小写对比。
想让视图控制器响应 searchBar 就需要遵循 UISearchResultsUpdating,为MasterViewController.swift 添加如下类扩展:
extension MasterViewController: UISearchResultsUpdating {
func updateSearchResultsForSearchController(searchController: UISearchController) {
filterContentForSearchText(searchController.searchBar.text!)
}
}
该方法是这个协议中唯一一个必须实现的。
(译者注:这里作者将协议的遵循和方法实现写在类扩展里应该是风格问题,让类的结构更简洁明了,可以参考 这篇文章 。)
现在,当用户在 searchBar 输入时文本的增减都会通知 MasterViewController ,方法中调用了我们刚刚实现的方法(传入searchBar中输入的文本并筛选结果)。
现在编译运行程序,你会看到 tableView 上方有一个 searchBar,但是现在还无法响应搜索匹配的结果,因为还没将匹配后的结果 filteredCandies 呈现给 tableView。
在 tableView(:numberOfRowsInSection:) 方法中修改如下:
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if searchController.active && searchController.searchBar.text != "" {
return filteredCandies.count
}
return candies.count
}
判断用户是否在搜索和 searchBar 是否有内容输入,返回不同的值。
同时,修改 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
}
同理,判断并使用不同的数据展示在 tableView 中。search controller 会自动处理搜索结果的显示与隐藏,我们要做的仅仅是提供我们需要展示的数据。
现在运行 App ,已经可以显示筛选后的数据了!但是现在还有个问题,当你选择某一行结果时,detailView 显示的糖果有可能并不匹配,下面我们来修复它。
当把内容传递给 detail ViewController 的时候必须判断是搜索结果数组还是原数组,这样才能完美匹配,在 prepareForSegue 方法中修改代码:
let candy = candies[indexPath.row]
将这句代码替换为:
let candy: Candy
if searchController.active && searchController.searchBar.text != "" {
candy = filteredCandies[indexPath.row]
} else {
candy = candies[indexPath.row]
}
现在运行 app 已经可以正确地传递数据至 detailView。
你可以为 search bar 添加一个 scope bar 供用户选择并筛选内容,本例将根据糖果的类型进行筛选,在实例化 Candy 对象时我们已经为它们赋上了相应的 category : Chocolate,Hard and Other 。
首先要在创建一个 scope bar(一个 segmented control 控件)我们需要实现另一个协议方法,在 MasterViewController.swift 添加类扩展:
extension MasterViewController: UISearchBarDelegate {
func searchBar(searchBar: UISearchBar, selectedScopeButtonIndexDidChange selectedScope: Int) {
filterContentForSearchText(searchBar.text!, scope: searchBar.scopeButtonTitles![selectedScope])
}
}
当用户切换 scope 时,会调用该方法,这时候我们要重新筛选结果,所以这时候调用之前定义过的 filterContentForSearchText 方法,并传入 scope 参数。
修改 filterContentForSearchText() 方法为:
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()
}
这里我们遍历 candies 内的 每一个candy 对象,通过「逻辑或符号」判断 scope 是否被设为「All」或者 candy 的种类是否与所选的 scope 匹配,赋值 categoryMatch,接着通过「逻辑与符号」判断这个 candy 是否应该被加入filteredCandies 数组。
修改 updateSearchResultsForSearchController 方法,传入 scope:
func updateSearchResultsForSearchController(searchController: UISearchController) {
let searchBar = searchController.searchBar
let scope = searchBar.scopeButtonTitles![searchBar.selectedScopeButtonIndex]
filterContentForSearchText(searchController.searchBar.text!, scope: scope)
}
最后,还需要在 ViewDidLoad() 中实现添加以下代码,添加 scope bar:
searchController.searchBar.scopeButtonTitles = ["All", "Chocolate", "Hard", "Other"]
searchController.searchBar.delegate = self
现在运行 app,可以通过 scope 筛选内容了, 点这里 可以下载完整的项目源码。TableView 在 app 中广泛使用,而添加搜索选项无疑增加了易用性与用户体验。