[译]UISearchController 教程:开始使用

(原文出处: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: Candy
if 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 Bar
searchController.searchBar.scopeButtonTitles = ["All", "Chocolate", "Hard", "Other"]
searchController.searchBar.delegate = self
复制代码

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

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

增加一个指示器

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

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

// Setup the search footer
tableView.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,执行搜索,观察底部信息的更新。点击键盘上的「搜索」,隐藏键盘然后可以看到底部视图。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值