本文翻译自:http://www.raywenderlich.com/113772/uisearchcontroller-tutorial。
教程使用iOS 9 SDK和Swift 2进行开发。
如果你的app展示了很多条数据,并且需要在庞大的列表里滚动来查阅,那么这种情况下,比较友好的处理方式便是允许用户进行搜索。幸运的是,UIKit包括UISearchBar
能很好地集成进UITableView
并且能很快速地进行过滤和响应。
在这篇UISearchController
教程中,我们会编译一个含有搜索功能的Candy app,它建立于一个标准的table view。我们会在table view中增加搜索的功能,包括了动态过滤以及增加一个可选的范围选择,这所有的都是iOS 8新加入的UISearchController
的优势。
开始
可以从这儿下载初始项目,这个demo已经建立了一个navigation controller,启动它,我们会看到一个空的列表:
返回到Xcode,Candy.swift这个文件包含了会被显示的candy的所有信息,它实际上只有两个属性:
name
和category
。当用户搜索candy的时候,我们可以根据
name
进行搜索,但是在教程最后我们也会通过category
进行范围过滤。
填充Table View
打开 MasterViewController.swift,candies
属性就是我们用来展示的供搜索的数据源,说到这,我们先创建一些candy!
在本教程中,我们只需要创建有限个数据保证能完成搜索功能就好,在真正的生产app当中,我们可能会需要成千上万条数据来进行搜索。即便数据有那么多时,我们的搜索方式还是一样的。
在viewDidLoad()
中,在super.viewDidLoad()
后填充candies
数组:
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")
]
再次编译运行app,因为table view的代理和数据源方法已经实现好,我们会看到数据正常显示:
点击一行cell并且能够进入到详情页:
加入UISearchController
如果我们查阅UISearchController
文档,会发现文档精简得令人发指,没有进行任何的搜索例子,这个类只是简单的提供了一些标准接口来供开发者自己实现。
UISearchController
用代理的模式来和app进行沟通,从而知道用户在做些什么,我们必须自己写一些函数来过滤字符串。
尽管这一开始显得不是那么的友好,但是自定义搜索方法给了我们对app更深的控制权,我们的用户会喜欢我们的搜索功能---因为优雅和快速。
如果大家曾经开发过table view的搜索功能,可能会发现这和UISearchDisplayController
很像,但是自从iOS 8之后,这个类就被UISearchController
取代了,因为其简化了整个搜索流程。
不幸的是,在写这个教程的时候,Interface Builder并不支持UISearchController,所以我们需要通过代码来创建我们的UI。
在 MasterViewController.swift中,增加一个新的属性:
let searchController = UISearchController(searchResultsController: nil)
通过参数searchResultsController
传nil来初始化UISearchController
,意思是我们告诉search controller我们会用相同的view来展示我们的搜索结果,如果我们想要指定一个不同的view controller,那就会被替代为显示搜索结果。
下一步,我们需要为我们的searchController设置一些参数。仍然在MasterViewController.swift中,在viewDidLoad()
中加入以下代码:
searchController.searchResultUpdater = self
searchController.dimsBackgroundDuringPresentation = false
definesPresentationContext = true
tableView.tableHeaderView = searchController.searchBar
-
searchResultUpdater
是UISearchController
的一个属性,它的值必须实现UISearchResultsUpdating
协议,这个协议让我们的类在UISearchBar
文字改变时被通知到,我们之后会实现这个协议。 - 默认情况下,
UISearchController
暗化前一个view,这在我们使用另一个view controller来显示结果时非常有用,但当前情况我们并不想暗化当前view。 - 设置
definesPresentationContext
为true
,我们保证在UISearchController
在激活状态下用户push到下一个view controller之后search bar不会仍留在界面上。 - 最后,我们增加
searchBar
到我们的tableHeaderView
中。
UISearchResultsUpdating and Filtering
在设置完search controller后,我们需要再加些代码来让它工作,首先,加入如下属性到MasterViewController
:
var filterCandies = [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
参数,之后我们会用到的。
为了让MasterViewController
响应这个search bar,我们需要实现UISearchResultsUpdating
,打开** MasterViewController.swift**,在主MasterViewController
类之外再加上如下的extension:
extension MasterViewController: UISearchResultsUpdating {
func updateSearchResultsForSearchController(searchController: UISearchController) {
filterContentForSearchText(searchController.searchBar.text!)
}
}
这个updateSearchResultsForSearchController(_:)
方法是UISearchResultsUpdating
中唯一一个我们必须实现的方法。
现在不管用户输入还是删除search bar的text,UISearchController
都会被通知到并执行上述方法。
filter()
带了一个(candy: Candy) -> Bool
类型的闭包,这个方法遍历数组里的所有数据,然后调用这个闭包,传入当前的数组值。
我们用这个方法来判断数据是否需要被搜索到展示给用户,如果需要我们返回true
,否则返回false
。
我们达到比较友好的体验,我们把searchTet以及原数据都小写之后在进行过滤。
编译运行程序,我们发现现在已经有一个搜索框在列表上方了。
但不管怎么输入,我们的搜索功能似乎不起作用,这仅仅是因为我们没有对过滤后的数据进行显示。
回到** MasterViewController.swift**,替换tableView(_:numberOfRowsInSection:)
如下:
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if sesarchController.active && searchController.searchBar.text != "" {
return filteredCandies.count
}
return candies.count
}
接着替换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是active的,我们用看到用户进行搜索的结果。
编译运行程序,我们看到搜索成功了!
测试一段时间后,我们发现详情页有时会和点击的不一致,我们来修复它。
向详情页传数据
在** MasterViewController.swift**文件中,找到perpareForSegue(_:sender:)
方法,找到如下代码:
let candy = candies[indexPath.row]
之后替换这如下代码:
let candy: Candy
if searchController.active && searchController.searchBar.text != "" {
candy = filteredCandies[indexPath.row]
} else {
candy = candies[indexPath.row]
}
一切正常了:
创建一个Scope Bar来对结果进行再次过滤
如果我们希望能给用户另一种过滤选择,我们可以增加一个Scope Bar来过滤category
字段。我们用来过滤的categories包括Chocolate, Hard, Other。
首先我们需要在MasterViewController
中创建一个Scope bar,这个scope bar实际上是一个segmented control
,来指定结果只能在某个范围内,在这个demo中我们的scope是category,但实际情况下,scope可能是types, ranges或一些完全不同的东西。
要使用scope bar,我们需要再实现UISearchBarDelegate
代理中的一个方法,加入如下extension:
extension MasterViewController: UISearchBarDelegate {
func searchBar(searchBar: UISearchBar, selectedScopeButtonIndexDidChange selectedScope: Int) {
filterContentForSearchText(searchBar.text!, scope: searchBar.scopeButtonTitles![selectedScope])
}
}
每当用户切换scope bar时,这个代理方法就会被调用,所以我们应该在此用新的scope进行重新搜索。
现在我们修改```filterContentForSearchText(_:scope:)来实现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()
}
我们还需要修改之前实现的updateSearchResultsForSearchController(_:)
:
func updateSearchResultsForSearchController(searchController: UISearchController) {
let searchBar = searchController.searchBar
let scope = searchBar.scopeButtonTitles![searchBar.selectedScopeButtonIndex]
filterContentForSearchText(searchController.searchBar.text!, scope: scope)
}
最后我们在search bar上加上scope bar,在** MasterViewController.swift**中,在viewDidLoad()
中,设置search controller后加入:
searchController.searchBar.scopeButtonTitles = ["All", "Chocolate", "Hard", "Other"]
searchController.searchBar.delegate = self
编译运行程序:
搞定!