翻译:UISearchController Tutorial: Getting Started

本文译自:UISearchController Tutorial: Getting Started

当一个app要显示大量的数据,滑动列表并不会让人愉悦。所以允许用户搜索指定的内容变得刻不容缓。
好消息是,UIKit已经将UISearchBar和UITableView无缝结合在一起了。

在本教程中,你将用标准的table view创建一个可以搜索糖果的app。
使用iOS8的新特性UISearchController,赋予table view搜索的功能,包含动态过滤,
还要添加一个可供选择的scope bar。
最后,学会如何让app更加友好,满足用户的需求。

UISearchController-Square.png

开始

点击这里下载初始项目并打开它,
这个项目已经有了一个带样式的navigation controller。运行它,你将看到一个空的列表:

UISearchController-Starter-281x500.png

回到Xcode,文件Candy.swift中有一个类用来保存每一个糖果的信息,
这个类有两个属性,分别对应糖果的名称和种类。
当用户使用你的app搜索糖果时,你将根据用户输入的文字定位到对应的那一项。
在教程的最后你要实现一个Scope Bar,到时你就明白种类字符串有多重要。

创建Table View

打开 MasterViewController.swiftcandies属性用来管理所有不同的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:

UISearchController-Data-281x500.png

选择一行后会展示相应的糖果详细:

darkchocolate.png

糖果太多了,需要一些时间才能找到想要到!你需要一个 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

下面是代码的说明:

  1. searchResultsUpdaterUISearchController中的一个属性,遵循了协议UISearchResultsUpdating
    这个协议允许类接收UISearchBar文本变化的通知。过一会就要使用这个协议。
  2. 默认情况下,UISearchController会将presented视图变暗。
    当你使用另一个viewController作为searchResultsController会非常有用,
    在现在的实例中,你已经设置了当前的view来展示结果,所以不需要让它变暗。
  3. 通过设置definesPresentationContexttrue,能够确保UISearchController被激活时,
    用户跳转到另一个viewController,而search bar依然保留在屏幕上。
  4. 最后,将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
}

这两个方法都使用了searchControlleractive属性来决定呈现哪个数组。

当用户点击Search Bar的文本框,active会自动设置为true
如果search controller处在激活状态,会看到用户已经输入了一些文字。
如果已经存在了,便会返回filteredCandies的数据,否则返回完成列表的数据。

回想一下,search controller会自动显示或隐藏table,所有代码要做的就是根据用户的输入提供正确的数据。

ragecomic.png

编译运行一下,你有一个Search Bar来过滤数据。

UISearchController-Filter-281x500.png

试试这个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:)相同的检查,
但现在提供了正确的糖果对象给详细视图。

再次运行一遍,看看是不是正确的。

CandySearch-DetailView-281x500.png

使用Scope Bar来筛选数据

还有另一种方法过滤数据,添加一个Scope Bar根据糖果的类别来过滤。
类别就是创建Candy对象时添加的,如ChocolateHardOther

先在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”的分类,它将忽略种类的过滤。

UISearchController-Filter-with-Scope-281x500.png

何去何从

恭喜你,已经有了一个可以搜索的table view。
点击这里下载完整的项目代码。

越来越多的app都使用了表格视图,搜索功能成了标配。
没理由不使用UISearechBarUISearchController

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,684评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,143评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,214评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,788评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,796评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,665评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,027评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,679评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,346评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,664评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,766评论 1 331
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,412评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,015评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,974评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,073评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,501评论 2 343

推荐阅读更多精彩内容

  • 本文翻译自:http://www.raywenderlich.com/113772/uisearchcontrol...
    Xiao_Li阅读 13,245评论 3 20
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,596评论 18 139
  • 1、searchBar 本例子实现布局:上面是一个navigationController,接下来一个search...
    lilinjianshu阅读 3,405评论 1 8
  • 17/02/08 你有没有看过一座没有顶的房子 里面被一面面墙隔成一个个小格子 每个格子里都住着一个人 他们都裸着...
    _冷漠懒人阅读 108评论 0 0
  • 偶尔有很多感慨,宁愿发一条qq动态或者朋友圈,也不愿意长篇大论,去记录。懒,更多的,是害怕,写完后的杂乱无...
    琴二的哆啦阅读 190评论 0 0