UISearchController使用
当一个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。