博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
[译]UISearchController 教程:开始使用
阅读量:6085 次
发布时间:2019-06-20

本文共 9567 字,大约阅读时间需要 31 分钟。

(原文出处:https://www.raywenderlich.com/157864/uisearchcontroller-tutorial-getting-started)

注:本指南已经由 Tom Elliott 适配 Xcode 9,Swift 4,以及 iOS 11。原版教程编写者是 Andy Pereira

划过复杂混乱的列表既慢又使人心烦。当数据源巨大的时候,提供搜索功能搜索指定条目是对用户十分重要的功能。UIKit 提供了 UISearchBar,允许你无缝集成到 UINavigationItem ,并可快速响应信息过滤。

在本教程中,你将基于标准 TableView 构建一个可搜索的 Candy app。你将赋予 tableView 搜索和动态过滤能力,以及添加范围栏,这些全部依赖 UISearchController 的特性。在最后,我们将讨论如何让你的 App 更加友好,以及更满足用户的需要。

准备好了么?开始吧!

开始

初始项目源码并打开。此时已经设置了一个导航控制器。在 Xcode 项目导航,选择项目 CandySearch,然后选择 target CandySearch,然后找signing 栏目中,配置你的开发者信息。编译运行 App,你将看到一个空列表:

回到 Xcode,文件 Canday.swift 中包含一个结构体 Candy,这是你要显示在列表中的元素。这个结构体有两个属性:category 和 name。

当用户在 App 中搜索糖果时,使用的是 name 字段作为搜索条件。在本教程快结束的时候,你将看到 category 字符串作为 Scope Bar 来实现分类。

构建 Table View

打开 MasterViewController.swiftcandies 属性将用来存储所有糖果对象,供用户搜索。说到这儿,是时候创建糖果对象啦!在本教程中,你只需要创建有限数量的对象,用来演示 search bar 的工作;在正式 App 中,你可能有数千个对象用于搜索。但是不管是有数千对象用于搜索,还是数个对象用于搜索,使用方法是不变的。伸缩性很好。

添加以下代码在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"),  Candy(category:"Other", name:"Candy Floss"),  Candy(category:"Chocolate", name:"Chocolate Coin"),  Candy(category:"Chocolate", name:"Chocolate Egg"),  Candy(category:"Other", name:"Jelly Beans"),  Candy(category:"Other", name:"Liquorice"),  Candy(category:"Hard", name:"Toffee Apple")]复制代码

编译运行你的项目。因为 delegate 和 dataSource 已经被实现,所以此时 tableView 已经存在数据:

在 table 中随意选择一行将展示该糖果的详情:

糖果有很多,查找起来需要一些时间,所以你需要一个 UISearchBar。

介绍 UISearchController

如果你看过 UISearchController 的文档,你会发现这是个懒惰的对象。关于搜索的工作其实它啥也没做。这个类提供了一组用户所期待的那种标准的交互操作方式。

UISearchController 通过代理协议连接让 App 知道用户的输入。具体的字符串匹配和结果过滤过滤必须由你来完成。

虽然这有点吓人,但编写自定义搜索功能让你严格控制在 App 中的返回结果,你的用户将开心的用上智能、快速的搜索。

如果你之前编写过搜索功能,你也许会熟悉UISearchDisplayController。从 iOS8 开始,这个类已经被标记为废弃。UISearchController 被推荐使用且简化了整个的搜索流程。

不幸的是,截止到本文编写,Interface Builder 还并不支持 UISearchController,所以你必须使用代码构建你的 UI。

MasterViewController.swift 文件中,增加一个新的属性:

let searchController = UISearchController(searchResultsController: nil)复制代码

如果使用 nil 初始化 UISearchController,那么搜索结果也使用相同的视图来进行现实。如果这里指定了一个非空的 View Controller,那它将被用于显示搜索结果。

响应搜索框用户输入的信息,需要给 MasterViewController 实现 UISearchResultUpdating 协议定义的方法。给 MasterViewController.swift 增加以下扩展:

extension MasterViewController: UISearchResultsUpdating {  // MARK: - UISearchResultsUpdating Delegate  func updateSearchResults(for searchController: UISearchController) {    // TODO  }}复制代码

updateSearchResults(for:)是唯一一个需要你的类实现的 UISearchResultUpdating 协议方法。我们等下就会把细节填满。

接下来,需要设置一些参数给 searchController。仍然是在MasterViewController.swift,在viewDidLoad()super.viewDidLoad() 调用之后:

// Setup the Search Controller  searchController.searchResultsUpdater = self  searchController.obscuresBackgroundDuringPresentation = false  searchController.searchBar.placeholder = "Search Candies"  navigationItem.searchController = searchController  definesPresentationContext = true复制代码

总结一下这些代码都做了什么:

  1. UISearchControllersearchResultsUpdater 属性指向一个 UISearchResultsUpdating 协议。响应用户在 UISearchBar 中的输入由这个协议完成。

  2. 默认情况下,UISearchController 弹出来的时候,视图的背景是模糊的。这是因为我们传递了别的 ViewController 作为 searchResultsController。在我们刚刚的代码中,我们使用了相同的 ViewController 用于搜索结果返回,所以视图的背景没有模糊。

  3. 设置占位符文本。

  4. 在 iOS 11 中,添加searchBarNavigationItem。目前在Interface Builder还不能直接操作。

  5. 设置了 ViewControllerdefinesPresentationContexttrue,确保当 UISearchController 为活跃状态时,用户导航到了新的 ViewController(如从搜索结果), 搜索栏还在屏幕最上方。

过滤搜索结果

设置完之后 SearchController,需要添加一些代码使它工作。首先增加下面这个属性给MasterViewController:

var filteredCandies = [Candy]()复制代码

这个属性将保存用户搜索用的 candies 数据集合。 接下来,添加下面这些辅助方法给 MasterViewController 类:

// MARK: - Private instance methods  func searchBarIsEmpty() -> Bool {  // Returns true if the text is empty or nil  return searchController.searchBar.text?.isEmpty ?? true}  func filterContentForSearchText(_ searchText: String, scope: String = "All") {  filteredCandies = candies.filter({( candy : Candy) -> Bool in    return candy.name.lowercased().contains(searchText.lowercased())  })  tableView.reloadData()}复制代码

searchBarIsEmpty() 是一个便利方法。filterContentForSearchText(_:scope:) 方法根据searchText文本过滤candies数组,然后将过滤后的结果生成filterdCandies数组。不要担心scope参数,下一节我们来出来它。

filter() 方法接受一个闭包(candy: Candy) -> Bool。在这个闭包中,我们判断数组是不是我们想要的,如果是,返回true,否则返回false,我们根据返回结果生成数组。

我们使用lowercased()方法把文本先转换成小写。使用contains(_:)方法对文本内容进行判断。

注释:大多数时候,用户不想去刻意区分大小写版本的搜索结果,所以我们对用户输入的内容和蛋糕名称都进行小写处理然后比较。你输入`Chocolate`或者`chocolate`都应该能找到蛋糕。这很有用,对吧?复制代码

还记得 UISearchResultsUpdating协议么?你留了一个TODOupdateSearchResults(for:)方法里面。现在补充一个方法调用,更新搜索结果。

替换 updateSearchResults(for:) 方法中的TODOfilterContentForSearchText(_:scope:)方法调用:

filterContentForSearchText(searchController.searchBar.text!)复制代码

现在,不管用户何时增加或者删除搜索框中的文本,UISearchController将通知MasterViewController,通过updateSearchResults(for:)方法。在其中调用filterContentForSearchText(_:scope:)对搜索结果进行过滤。

编译然后运行 App 现在,滚动到下面,你将看到在搜索框下面的列表。

当输入搜索文本时,你什么返回结果也看不到。这是因为你没有写处理返回数据的代码给 TableView。

更新 TableView

回到 MasterViewController.swift,增加一个方法在过滤时候调用:

func isFiltering() -> Bool {  return searchController.isActive && !searchBarIsEmpty()}复制代码

替换tableView(_:numberOfRowsInSection:)方法:

func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {  if isFiltering() {    return filteredCandies.count  }      return candies.count}复制代码

这里没有做很多变动;简单的检查用户是否在搜索状态下,然后决定使用正常数据源或者搜索的数据源并更新 tableView。

接下来,替换tableView(_:cellForRowAt:)

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {  let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)  let candy: Candy  if isFiltering() {    candy = filteredCandies[indexPath.row]  } else {    candy = candies[indexPath.row]  }  cell.textLabel!.text = candy.name  cell.detailTextLabel!.text = candy.category  return cell}复制代码

这两个方法都使用了 isFiltering(),来决定加载哪个数据源。

当用户点击搜索框时候,active属性自动被设置为true。然后从 filteredCandies 数组加载数据。正常的时候是加载完整数据的。

回顾search controller的展示结果的处理过程,我们所做的只是根据状态提供正确的数据源。

此时编译并运行 App。现在已经可以过滤 SearchBar 的搜索内容啦!

虽然现在列表的内容显示正确,但详情页展示的数据有误。我们这就来修复它。

传递数据给详情视图

当想要传递数据给详情视图控制器,你需要确保视图控制器知道用户是从哪个视图控制器进行的操作:所有数据列表,还是搜索返回结果列表。仍然在MasterViewController.swift,在prepare(for:sender:),找到以下代码:

let candy = candies[indexPath.row]Then replace it with the following:let candy: Candyif isFiltering() {  candy = filteredCandies[indexPath.row]} else {  candy = candies[indexPath.row]}复制代码

这里执行相同的isFiltering() 方法进行过滤。当用户执行操作的时候,你需要提供正确的 Candy 传递给详情视图控制器。

此时编译并执行代码,不管用户是从数据列表视图还是从搜索结果视图操作,App 都可以正确的导航至详情视图了。

创建一个范围栏过滤返回结果

如果你想给用户提供另一种过滤返回结果的方式,你可以添加一个范围栏结合搜索栏对搜索结果进行分类。这里分类的依据是甜品的种类:Chocolate,Hard,以及 Other。首先你必须在MasterViewController 中创建一个范围栏。范围栏是一个分段控件,通过它限制搜索范围。范围是你所定义的。这里是甜品的种类。范围是可以自定义的,你可以使用类型、范围或者其他完全不同的东西。使用范围栏就像实现一个代理方法一样容易。

MasterViewController.swift 中,你需要增加实现了UISearchBarDelegate协议的扩展:

extension MasterViewController: UISearchBarDelegate {  // MARK: - UISearchBar Delegate  func searchBar(_ searchBar: UISearchBar, selectedScopeButtonIndexDidChange selectedScope: Int) {    filterContentForSearchText(searchBar.text!, scope: searchBar.scopeButtonTitles![selectedScope])  }}复制代码

当用户切换范围栏上的不同分类时,该方法会被调用。这时你需要调用filterContentForSearchText(_:scope:)

现在修改filterContentForSearchText(_:scope:)方法,将范围考虑在里面:

func filterContentForSearchText(_ searchText: String, scope: String = "All") {  filteredCandies = candies.filter({( candy : Candy) -> Bool in    let doesCategoryMatch = (scope == "All") || (candy.category == scope)          if searchBarIsEmpty() {      return doesCategoryMatch    } else {      return doesCategoryMatch && candy.name.lowercased().contains(searchText.lowercased())    }  })  tableView.reloadData()}复制代码

现在的过滤逻辑是先匹配的分类,然后过滤掉所有名字不包含用户输入到搜索框文本的对象。现在更新 isFilteing() 方法,以适配范围栏被选择时返回正确的结果

func isFiltering() -> Bool {  let searchBarScopeIsFiltering = searchController.searchBar.selectedScopeButtonIndex != 0  return searchController.isActive && (!searchBarIsEmpty() || searchBarScopeIsFiltering)}复制代码

已经接近成功了,但范围过滤机制还不能很好的工作。还需要修改扩展中的updateSearchResults(for:),传递选择的分类:

func updateSearchResults(for searchController: UISearchController) {  let searchBar = searchController.searchBar  let scope = searchBar.scopeButtonTitles![searchBar.selectedScopeButtonIndex]  filterContentForSearchText(searchController.searchBar.text!, scope: scope)}复制代码

最后一个问题是用户还不能看到范围栏。我们把目光移动到search controller初始化的地方。在MasterViewController.swiftviewDidLoad()方法中,添加以下代码在生成 candies 数组之前:

// Setup the Scope BarsearchController.searchBar.scopeButtonTitles = ["All", "Chocolate", "Hard", "Other"]searchController.searchBar.delegate = self复制代码

这会给搜索条添加范围栏,范围栏中的标题来自甜品的分类。同样的,包括一个「所有」分类,选择这个分类,在搜索的时候,显示全部的分类内容。现在当你在搜索框输入搜索文本,返回结果会包括所选择的分类。

编译并运行 App,输入一些搜索文本,然后切换范围试试看。

增加一个指示器

为了解决这一问题,我们将添加一个底部视图到我们的页面中。当过滤状态下它将显示,并告诉用户关于搜索结果的信息。打开 SearchFooter.swift。这就是我们要用的底部视图,它包含一个 Label 和接口。

回到MasterViewController.swift。你已经设置了底部视图的IBOutlet,它在 Main.storyboard文件中。接下来在 viewDidLoad() 方法中设置它:

// Setup the search footertableView.tableFooterView = searchFooter复制代码

这将自定义 tableView 的底部视图。接下来,你需要更新它的信息在用户执行搜索的时候。替换 tableView(_:numberOfRowsInSection:) 方法的代码:

func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {  if isFiltering() {    searchFooter.setIsFilteringToShow(filteredItemCount: filteredCandies.count, of: candies.count)    return filteredCandies.count  }      searchFooter.setNotFiltering()  return candies.count}复制代码

到此,底部视图添加完毕。

编译并运行App,执行搜索,观察底部信息的更新。点击键盘上的「搜索」,隐藏键盘然后可以看到底部视图。

转载地址:http://nokwa.baihongyu.com/

你可能感兴趣的文章
Javascript中闭包(Closure)的探索(一)-基本概念
查看>>
spark高级排序彻底解秘
查看>>
ylbtech-LanguageSamples-PartialTypes(部分类型)
查看>>
福建省促进大数据发展:变分散式管理为统筹集中式管理
查看>>
开发环境、生产环境、测试环境的基本理解和区别
查看>>
tomcat多应用之间如何共享jar
查看>>
Flex前后台交互,service层调用后台服务的简单封装
查看>>
MySQL入门12-数据类型
查看>>
Windows Azure 保留已存在的虚拟网络外网IP(云服务)
查看>>
修改字符集
查看>>
HackTheGame 攻略 - 第四关
查看>>
js删除数组元素
查看>>
带空格文件名的处理(find xargs grep ..etc)
查看>>
华为Access、Hybrid和Trunk的区别和设置
查看>>
centos使用docker下安装mysql并配置、nginx
查看>>
关于HTML5的理解
查看>>
需要学的东西
查看>>
Internet Message Access Protocol --- IMAP协议
查看>>
Linux 获取文件夹下的所有文件
查看>>
对 Sea.js 进行配置(一) seajs.config
查看>>