UIKit框架(三十九) —— iOS 13中UISearchController 和 UISearchBar的新更改(一)

版本记录

版本号 时间
V1.0 2020.05.05 星期二

前言

iOS中有关视图控件用户能看到的都在UIKit框架里面,用户交互也是通过UIKit进行的。感兴趣的参考上面几篇文章。
1. UIKit框架(一) —— UIKit动力学和移动效果(一)
2. UIKit框架(二) —— UIKit动力学和移动效果(二)
3. UIKit框架(三) —— UICollectionViewCell的扩张效果的实现(一)
4. UIKit框架(四) —— UICollectionViewCell的扩张效果的实现(二)
5. UIKit框架(五) —— 自定义控件:可重复使用的滑块(一)
6. UIKit框架(六) —— 自定义控件:可重复使用的滑块(二)
7. UIKit框架(七) —— 动态尺寸UITableViewCell的实现(一)
8. UIKit框架(八) —— 动态尺寸UITableViewCell的实现(二)
9. UIKit框架(九) —— UICollectionView的数据异步预加载(一)
10. UIKit框架(十) —— UICollectionView的数据异步预加载(二)
11. UIKit框架(十一) —— UICollectionView的重用、选择和重排序(一)
12. UIKit框架(十二) —— UICollectionView的重用、选择和重排序(二)
13. UIKit框架(十三) —— 如何创建自己的侧滑式面板导航(一)
14. UIKit框架(十四) —— 如何创建自己的侧滑式面板导航(二)
15. UIKit框架(十五) —— 基于自定义UICollectionViewLayout布局的简单示例(一)
16. UIKit框架(十六) —— 基于自定义UICollectionViewLayout布局的简单示例(二)
17. UIKit框架(十七) —— 基于自定义UICollectionViewLayout布局的简单示例(三)
18. UIKit框架(十八) —— 基于CALayer属性的一种3D边栏动画的实现(一)
19. UIKit框架(十九) —— 基于CALayer属性的一种3D边栏动画的实现(二)
20. UIKit框架(二十) —— 基于UILabel跑马灯类似效果的实现(一)
21. UIKit框架(二十一) —— UIStackView的使用(一)
22. UIKit框架(二十二) —— 基于UIPresentationController的自定义viewController的转场和展示(一)
23. UIKit框架(二十三) —— 基于UIPresentationController的自定义viewController的转场和展示(二)
24. UIKit框架(二十四) —— 基于UICollectionViews和Drag-Drop在两个APP间的使用示例 (一)
25. UIKit框架(二十五) —— 基于UICollectionViews和Drag-Drop在两个APP间的使用示例 (二)
26. UIKit框架(二十六) —— UICollectionView的自定义布局 (一)
27. UIKit框架(二十七) —— UICollectionView的自定义布局 (二)
28. UIKit框架(二十八) —— 一个UISplitViewController的简单实用示例 (一)
29. UIKit框架(二十九) —— 一个UISplitViewController的简单实用示例 (二)
30. UIKit框架(三十) —— 基于UICollectionViewCompositionalLayout API的UICollectionViews布局的简单示例(一)
31. UIKit框架(三十一) —— 基于UICollectionViewCompositionalLayout API的UICollectionViews布局的简单示例(二)
32. UIKit框架(三十二) —— 替换Peek and Pop交互的基于iOS13的Context Menus(一)
33. UIKit框架(三十三) —— 替换Peek and Pop交互的基于iOS13的Context Menus(二)
34. UIKit框架(三十四) —— Accessibility的使用(一)
35. UIKit框架(三十五) —— Accessibility的使用(二)
36. UIKit框架(三十六) —— UICollectionView UICollectionViewDiffableDataSource的使用(一)
37. UIKit框架(三十七) —— UICollectionView UICollectionViewDiffableDataSource的使用(二)
38. UIKit框架(三十八) —— 基于CollectionView转盘效果的实现(一)

开始

首先看下主要内容:

在本UISearchController教程中,您将了解UISearchTokenUISearchTextFieldiOS 13中引入的其他新api。来自翻译。

下面看下写作环境

Swift 5, iOS 13, Xcode 11

接着就是正文了。

UISearchBarUISearchController是iOS应用开发的主要部分。不过,虽然UISearchBariOS 2引入以来一直在周期性地变化,但自从苹果在iOS 8中引入它以来,UISearchController就一直是相当稳定的。在iOS 13中,苹果对两者都进行了更新。

苹果还推出了UISearchToken,为UISearchController提供了急需的能力。只需很少的努力,您就可以让用户执行复杂的搜索查询,以及他们习惯的基于文本的搜索。

如果您曾经编写过冗长、脆弱的代码来遍历搜索栏的视图层次结构以获得对搜索text field的引用,那么这里有更多的好消息。UISearchTextField现在作为一个属性公开,这使得定制更加容易。

在本教程中,您将学习:

  • 如何控制搜索结果控制器的显示。
  • 关于搜索令牌(search tokens)的所有信息。
  • 如何创建和使用搜索令牌。
  • 如何自定义UISearchControllersearch bar 和它的text field

在本教程中,您将在L.I.S.T.E.D中进行学习,它是Large International Sorted Tally of Earth DwellersL.I.S.T.E.D.是一个跟踪世界人口总数的组织。他们目前有2018年和2019年所有国家的人口。

预计到2020年人口数据的发布,他们希望扩大搜索能力。作为一个地球居民,你将帮助重构应用程序,并将其搜索功能提升到一个全新的组织层面。

首先在starter文件夹中打开列表LISTED.xcodeproj中,然后打开Main.storyboard

你会发现这个应用程序非常简单。只有两个视图控制器,它们被封装在一个导航控制器中。

构建和运行。你会看到:

现在,搜索“new”。你会看到这些搜索结果:

结果显示所有年份的匹配。在范围栏中点击2018或2019将缩小搜索范围。现在就试试2019年,你会看到:

很好,基本的搜索工作得很好。现在,你需要进一步优化!


Using the Search Results Controller

UISearchController为显示结果提供了两个选项:在显示原始数据的相同视图中显示结果,或者使用搜索结果控制器。

L.I.S.T.E.D.使用搜索结果控制器(search results controller)以与主控制器稍微不同的格式显示结果。当用户点击搜索栏时,主视图控制器仍然可见。结果视图控制器在用户开始输入搜索后显示。

iOS 13之前,你几乎无法控制这种行为,但现在你可以在UISearchController中使用新添加的showsSearchResultsController来定制你的结果。在下一节中您将看到如何实现。

1. Displaying Results: You’re in Control

要控制何时显示搜索结果,您需要对搜索栏中的更改做出反应。为了让它工作,主视图控制器将遵循UISearchResultsUpdating

当搜索栏成为第一个响应器或文本发生更改时,委托接收updateSearchResults(for:)调用。您将使用它来触发结果控制器的显示。

打开MainViewController.swift,并在UISearchBarDelegate之后添加以下内容:

// MARK: -

extension MainViewController: UISearchResultsUpdating {
  func updateSearchResults(for searchController: UISearchController) {
    searchController.showsSearchResultsController = true
  }
}

搜索栏成为第一个响应者时,搜索结果现在显示。

在进行测试之前,添加以下代码作为viewDidLoad()中的最后一行:

searchController.searchResultsUpdater = self

search results updater负责更新搜索结果控制器(search results controller)。这里,您将该职责分配给主控制器。

构建和运行。点击搜索栏,你会看到一个空的搜索结果控制器。

你现在可以完全控制搜索结果的显示和隐藏。正如您现在所感受到的那样强大,显示一个空白的结果控制器并不是一个很好的用户体验。您将很快解决这个问题,但是首先,您需要了解关于搜索令牌(search tokens)的更多信息。


Everything You Need to Know About Search Tokens

搜索令牌(Search token)可以说是苹果在ios13中添加的最有趣的搜索功能。如果你在iOS 13上使用苹果的Mail or Photo应用程序,你可能已经看到了搜索令牌的作用。

Mail应用程序使用搜索令牌来创建复杂的搜索。点击搜索栏会显示unread messagesflagged messages等建议。

Tokens可以表示复杂的搜索,比如通过地理位置进行搜索,或者使用预先确定的文本进行简单的搜索。搜索令牌的键在UISearchToken中是representedObject

representedObject是一个Any?可以包含对您有用的任何类型的数据。

一定要记住,representedObject是强引用的。它所引用的任何对象都可能在周围停留相当长的一段时间。使用轻量级数据来避免问题。String、intNSManagedObjectID都是不错的选择。

1. Creating Tokens

现在是处理单击搜索栏时看到的空搜索结果视图的时候了。这是显示用户可用令牌列表的好地方。

打开ResultsTableViewController.swift。在类的顶部,countries之后,添加以下内容:

var searchTokens: [UISearchToken] = []

这里,您正在创建一个数组来保存搜索标记。在这一行之后,添加以下内容:

var isFilteringByCountry: Bool {
  return countries != nil
}

当用户进行搜索时,这个计算得到的布尔值将返回true;当用户不进行搜索时,将返回false。你很快就会用到它。

在文件末尾的类下面,添加以下扩展名:

// MARK: -

extension ResultsTableViewController {
  func makeTokens() {
    // 1
    let continents = Continent.allCases
    searchTokens = continents.map { (continent) -> UISearchToken in
      // 2
      let globeImage = UIImage(systemName: "globe")
      let token = UISearchToken(icon: globeImage, text: continent.description)
      // 3
      token.representedObject = Continent(rawValue: continent.description)
      // 4
      return token
    }
  }
}

这些代码的内容如下:

  • 1) 创建一个所有continents的数组。
  • 2) 创建表示token的图像。接下来,使用图像和当前continents的描述,创建一个search token
  • 3) 将continents的描述赋给tokenrepresentedObject。稍后您将使用它来将搜索范围缩小到特定的continents。使用轻量级值(如字符串)非常适合这种情况。
  • 4) 返回token,它附加到您先前创建的searchTokens

viewDidLoad()中,添加这一行作为最后一行:

makeTokens()

在这里,您将在视图加载时创建search token。就是这样!创建search token就是这么简单。

在重新构建和运行之前,您需要更新结果控制器来显示这些新token。你将在下一步中做。

2. Making a UI for Selecting Tokens

现在,您将创建一个UI,以使用Mail应用程序作为灵感来选择token。首先,将tableView(_:numberOfRowsInSection:)替换为:

override func tableView(
  _ tableView: UITableView,
  numberOfRowsInSection section: Int
) -> Int {
  return isFilteringByCountry ? (countries?.count ?? 0) : searchTokens.count
}

使用前面创建的isFilteringByCountry,您可以确定是使用search tokens计数还是使用countries计数来设置表视图中的行数。如果用户正在搜索,则发送国家计数(如果countriesnil,则发送零)。当它们不进行搜索时,您发送token count

接下来,用以下代码替换tableView(_:cellForRowAt:)

override func tableView(
  _ tableView: UITableView,
  cellForRowAt indexPath: IndexPath
) -> UITableViewCell {
  // 1
  if
    isFilteringByCountry,
    let cell = tableView.dequeueReusableCell(
      withIdentifier: "results",
      for: indexPath) as? CountryCell {
    cell.country = countries?[indexPath.row]
    return cell
  
  // 2
  } else if
    let cell = tableView.dequeueReusableCell(
      withIdentifier: "search",
      for: indexPath) as? SearchTokenCell {
    cell.token = searchTokens[indexPath.row]
    return cell
  }

  // 3
  return UITableViewCell()
}

你可以这样处理这些代码:

  • 1) 首先检查用户是否正在搜索某个国家。如果是,则使用CountryCell。然后将国家分配给单元格的国家并返回单元格。
  • 2) 否则,使用SearchTokenCell。将token分配给celltoken并返回cell
  • 3) 如果所有操作都失败,则返回一个UITableViewCell

构建和运行。点击搜索栏,但不要输入任何文本。您将看到为选择search token而创建的UI

这是一个非常棒的方法,但是有一个问题:如果您点击其中一个token,什么也不会发生。嘘!别担心,你下次会修好的。

3. Adding Tokens to the Search Bar

当您在结果视图中点击一个token entries时,什么也不会发生。应该发生的是将token添加到搜索栏中。这向用户表明他们正在搜索他们指定的大陆。

results controller不能添加token,因为它不是搜索栏的所有者。当用户点击一个token时,你必须通知主视图控制器。为此,您将使用委托协议。

在类之前,将以下代码添加到ResultsTableViewController.swift的顶部:

protocol ResultsTableViewDelegate: class {
  func didSelect(token: UISearchToken)
}

在类的顶部,在isFilteringByCountry之后,添加以下内容:

weak var delegate: ResultsTableViewDelegate?

您已经创建了一个简单的协议,当用户点击一个token时,您将使用它来通知委托。现在,在tableView(_:cellForRowAt:)之后添加以下代码:

override func tableView(
  _ tableView: UITableView,
  didSelectRowAt indexPath: IndexPath
) {
  guard !isFilteringByCountry else { return }
  delegate?.didSelect(token: searchTokens[indexPath.row])
}

首先,检查视图是否显示tokens or countries。如果是一个国家,则忽略行选择。否则,您将通知委托用户所点击的search token

MainViewController.swift,在文件底部添加如下内容:

// MARK: -

extension MainViewController: ResultsTableViewDelegate {
  func didSelect(token: UISearchToken) {
    // 1
    let searchTextField = searchController.searchBar.searchTextField
    // 2
    searchTextField.insertToken(token, at: searchTextField.tokens.count)
    // 3
    searchFor(searchController.searchBar.text)
  }
}

当通知主视图控制器时,用户已经选择了一个token,你:

  • 1) 获取搜索栏的text field
  • 2) 使用fieldinsertToken(_:at:)token添加到已经在field中的tokens的末尾。
  • 3) 运行搜索算法。

viewDidLoad()中,在resultsTableViewController实例化之后,添加:

resultsTableViewController.delegate = self

现在,主视图控制器将是结果控制器的代理。

构建并运行,然后点击搜索栏。当结果控制器出现时,点击“Search by Europe”。然后,输入“united”,你会看到:

令人兴奋!您已经向搜索栏添加了一个search token。然而,它似乎不起作用。

除非地理环境从你上高中起就发生了变化,否则美国和阿拉伯联合酋长国不会出现在欧洲。

问题出在搜索算法上:您没有更新它以使其考虑到tokens。解决这个问题是你的下一个挑战。


Modifying Your Search to Use Tokens

当前的搜索算法使用搜索栏的文本和范围来执行搜索。您还将重构它以使用tokens

在此之前,您需要创建两个helper属性。在MainViewController.swift中,在resultsTableViewController之后添加以下内容:

var searchContinents: [String] {
  // 1
  let tokens = searchController.searchBar.searchTextField.tokens
  // 2
  return tokens.compactMap {
    ($0.representedObject as? Continent)?.description
  }
}

该计算属性将:

  • 1) 创建在搜索栏的text field中包含的tokens数组。
  • 2) 使用每个tokenrepresentedObject返回大陆字符串数组。

searchContinents之后,添加代码:

var isSearchingByTokens: Bool {
  return
    searchController.isActive &&
    searchController.searchBar.searchTextField.tokens.count > 0
}

如果搜索控制器处于活动状态,并且搜索栏包含搜索token,则此属性返回true

使用这些新属性,将searchFor(_:)替换为:

func searchFor(_ searchText: String?) {
  // 1
  guard searchController.isActive else { return }
  // 2
  guard let searchText = searchText else {
    resultsTableViewController.countries = nil
    return
  }
  // 3
  let selectedYear = selectedScopeYear()
  let allCountries = countries.values.joined()
  let filteredCountries = allCountries.filter { (country: Country) -> Bool in
    // 4
    let isMatchingYear = selectedYear == Year.all.description ? 
      true : (country.year.description == selectedYear)
    // 5
    let isMatchingTokens = searchContinents.count == 0 ? 
      true : searchContinents.contains(country.continent.description)
    // 6
    if !searchText.isEmpty {
      return
        isMatchingYear &&
        isMatchingTokens &&
        country.name.lowercased().contains(searchText.lowercased())
    // 7
    } else if isSearchingByTokens {
      return isMatchingYear && isMatchingTokens
    }
    // 8
    return false
  }
  // 9
  resultsTableViewController.countries = 
    filteredCountries.count > 0 ? filteredCountries : nil
}

新的搜索算法做以下工作:

  • 1) 如果搜索控制器(search controller)当前不是活动的,它将终止。
  • 2) 如果搜索文本为nil,则将结果控制器的countries设置为nil并终止。
  • 3) 从范围栏中获取选定的年份。接下来,创建一个所有国家的数组并开始过滤该数组。
  • 4) 国家每年出现一次。根据2018年和2019年的数据,每个国家都被列出了两次。过滤时的第一步是创建一个布尔值,如果选择的年份是“all”,则为true。如果不是,则返回一个布尔值,该布尔值基于该国的年份是否等于所选年份。
  • 5) 使用之前创建的searchContinents,如果国家的大陆匹配任何选定的tokens,就创建一个布尔值。如果searchContinentsnil,则返回true,因为nil表示匹配所有大陆。
  • 6) 如果有任何搜索文本,如果年份匹配,令牌匹配,并且国家名称包含搜索文本中的任何字符,那么您将返回国家。
  • 7) 如果您有token但没有文本,则返回与年份和token所在大陆匹配的国家。
  • 8) 当这两种情况都不为true时,您将返回false
  • 9) 将任何经过筛选的国家分配给结果控制器的countries。如果没有,赋值为nil

构建和运行。点击搜索栏,像上次一样,点击Search by Europe,然后输入united。你只会看到英国2018年和2019年的entries

虽然这很神奇,但现在还不要庆祝。确保您所做的更改没有影响到在没有tokens的情况下进行搜索的能力。

在搜索text field中选择Europe token并删除它,但不删除单词“united”。你会看到:

哇!你做了一些伟大的工作。只有一些小的改变,你已经彻底改变了L.I.S.T.E.D.的搜索功能。


Hiding the Scope Bar

在您可以将其称为准备好投入生产之前,您还有一些事情要做。L.I.S.T.E.D.设计团队已经确定,当结果控制器显示token selection UI时,scope bar不应该是可见的。

要实现这一点,请转到viewDidLoad()并将其添加为最后一行:

searchController.automaticallyShowsScopeBar = false

ios13之前,scope bar总是会自动显示。现在,您可以通过使用新的1automaticallyShowsScopeBar1来控制这种行为。

注意:有一个类似的automaticallyShowsCancelButton。此属性允许您控制搜索栏的“取消”按钮的可见性。虽然在这个项目中不需要它,但是应该知道它的存在。

接下来,找到selectedScopeYear()并在其后面添加以下内容:

func showScopeBar(_ show: Bool) {
  guard searchController.searchBar.showsScopeBar != show else { return }
  searchController.searchBar.setShowsScope(show, animated: true)
  view.setNeedsLayout()
}

在这里,检查搜索栏的showsScopeBar是否匹配show。如果是这样,你就会停下来,因为你无事可做。

如果不匹配,则使用新的setShowsScope(_:animated:)来显示或隐藏scope bar

最后,必须在视图控制器的视图上调用setNeedsLayout()

现在,您将使用这个新函数来显示和隐藏scope bar。在UISearchBarDelegate中,找到searchBar(_:textDidChange:)并添加以下内容作为最后一行:

let showScope = !searchText.isEmpty
showScopeBar(showScope)

如果搜索文本不是空的,应该显示scope bar

searchBarCancelButtonClicked(_:)中,添加以下内容作为最后一行:

showScopeBar(false)

现在,当用户点击搜索栏的取消按钮时,您将隐藏scope bar

最后,在ResultsTableViewDelegate中,在didSelect(token:)结尾添加以下内容:

showScopeBar(true)

当用户选择一个token时,您现在将显示scope bar

构建并运行,然后点击搜索栏。您将不再看到scope bar

点击一个tokenscope bar就会出现。


Customizing the Search Bar and Text Field

最后的任务是在搜索栏中添加一个主题。在ios13中显示search text field之前,自定义field充满了问题。现在,随着text field的暴露,您可以像定制任何其他UITextField一样定制它。

1. Changing Text and Background Color

设计团队希望text field更突出一些,所以您的第一个任务是更改文本的颜色。

viewDidLoad()中,添加这一行作为最后一行:

searchController.searchBar.searchTextField.textColor = .rwGreen()

该项目有一个UIColor扩展。这个扩展返回一个非常特殊的绿色,您最喜欢的教程站点使用它。在这里。您正在将search text field的文本颜色设置为华丽的绿色阴影。

下一步是改变背景颜色。当search bar成为第一个响应时,它应该变成透明的绿色。当用户取消搜索时,它应该返回到默认颜色。

要实现这一点,在UISearchResultsUpdating扩展中找到updateSearchResults(for:),并将它替换为:

func updateSearchResults(for searchController: UISearchController) {
  // 1
  if searchController.searchBar.searchTextField.isFirstResponder {
    searchController.showsSearchResultsController = true
    // 2
    searchController.searchBar
      .searchTextField.backgroundColor = UIColor.rwGreen().withAlphaComponent(0.1)
  } else {
    // 3
    searchController.searchBar.searchTextField.backgroundColor = nil
  }
}

在这里,你是:

  • 1) 如果search text field是第一个响应者,则显示结果控制器。
  • 2) 将search text field的背景颜色更改为rwGreen。使用alpha组件使颜色透明。
  • 3) 如果search text field不是第一个响应者,则将背景颜色设置回默认值。

UISearchDelegate中,查找searchBarCancelButtonClicked(_:)并将其添加为最后一行:

searchController.searchBar.searchTextField.backgroundColor = nil

如果用户取消搜索,则将text field的背景设置为默认值。

构建和运行。点击搜索栏,点击Search by Africa,输入“faso”。您将看到以下内容:

您现在已经设置了搜索text field的文本和背景主题。事情看起来很尖锐!

但是,现在这个token看起来不太对。总会有办法的,不是吗?别担心,你下次会修好的。

2. Changing the Color of Tokens

Token有几个主题选项。例如,您可以像前面那样设置Token的图标。你也可以改变背景颜色,这是你接下来要做的。

viewDidLoad()中,添加这一行作为最后一行:

searchController.searchBar.searchTextField.tokenBackgroundColor = .rwGreen()

这里,您将tokens的默认背景颜色设置为rwGreen

构建和运行。点击search,点击Search by Oceania,你会看到这个:

看那些绿色!它真的可以帮助你出色的新搜索功能脱颖而出。

看看这些很好的资源,以了解更多关于UISearchController

  • WWDC 2019 Modernizing Your UI for iOS 13。

后记

本篇主要讲述了iOS 13UISearchControllerUISearchBar的新更改,感兴趣的给个赞或者关注~~~

你可能感兴趣的:(UIKit框架(三十九) —— iOS 13中UISearchController 和 UISearchBar的新更改(一))