iOS apprentice中文版 - Section 4: Store Search

译者语:目前正在学习iOS apprentice 第四部分的内容:Store Search,因之前跟随学习的博主曾翻译到第三部分的内容时便断更了,且目前版本使用的方法较之前已有多处改动,决定自行翻译原书iOS12 swift 4.2版本的第四个部分,并在此做下记录方便日后查阅复习。 _(:」∠) _

Chapter 32:Search Bar

移动应用程序最常见的任务之一是与互联网上的服务器对话——如果你在编写移动应用程序,你需要知道如何上传和下载数据。
通过这个名为StoreSearch的新应用程序,您将了解如何对web服务执行HTTP GET请求,如何解析JSON数据,以及如何下载图像等文件。

你将创建一个可以让你搜索iTunes商店的应用程序。当然,你的iPhone已经有了相应的应用程序——“App Store”和“Apple Music”等,但再写一个又有什么害处呢?
苹果公司推出了一项网络服务,可以搜索整个iTunes商店,你可以用它来学习网络。

完成后的应用程序将是这样的:

您将向老朋友table view添加搜索功能。当您点击表格中的一个项目时,会弹出一个带有额外信息的动画窗口。当你将iPhone翻转到横屏模式时,应用程序的布局会完全改变,以一种不同的方式显示搜索结果。
这款应用还将推出iPad版,并为iPad定制用户界面:


StoreSearch填补并补充了您从以前开发的应用程序中获得的知识。您还将学习如何将应用程序分发给beta测试人员,以及如何将其提交到应用程序商店。
在本章中,您将做以下工作:

1.创建项目:

为您的新应用程序创建一个新项目。使用Git设置版本控制。

2.创建UI:

为StoreSearch创建用户界面。

3.执行伪搜索:

通过获取搜索项并使用伪搜索结果填充表视图,了解搜索栏的工作原理。

4.创建数据模型:

创建一个数据模型来保存搜索结果的数据,并允许将来进行扩展。

5.没有找到数据:

在进行搜索时处理“没有数据”的情况。


1.创建项目

启动Xcode并创建一个新项目。选择Single View App模板,填写选项如下:

Product Name: StoreSearch
Team: Default value
Organization Name: your name
Organization Identifier: com.yourname
Language: Swift
Use Core Data, Include Unit Tests, Include UI Tests: leave these unchecked

当您保存项目Xcode时,您可以选择创建一个Git存储库。到目前为止,您忽略了这个选项,但现在应该启用它:

如果你没有看到这个选项,点击对话框左下角的Options按钮。

Git和版本控制

Git是一个版本控制系统——它允许您对您的工作进行快照,这样您就可以随时回头查看对项目所做更改的历史记录。更好的是,Git这样的工具允许您在同一代码库上与多人协作。
想象一下,如果两个程序员同时更改同一个源文件,将会是多么混乱。您的更改可能会被同事意外覆盖。我曾经在做一份工作时不得不在大厅里对另一个程序员大喊:“你在使用文件X吗?”这样来避免我们破坏彼此的工作。

有了Git这样的版本控制系统,每个程序员都可以独立地处理相同的文件,而不用担心撤销其他程序员的工作。Git非常聪明,可以自动合并所有更改,如果有冲突的编辑,可以手动解决。
Git不是唯一的版本控制系统,但它是iOS中最流行的。许多iOS开发人员在GitHub (github.com)上共享他们的源代码,GitHub是一个使用Git作为引擎的免费协作网站。另一个流行的系统是Subversion,通常缩写为SVN。Xcode内置了对Git的支持,虽然它在过去的版本中支持Subversion,但自从Xcode 10之后就不再是这样了。

对于StoreSearch,您将使用一些基本的Git功能。即使你是一个人工作,也不用担心其他程序员把你的代码弄乱,使用它仍然是有意义的。毕竟,可能是你把自己的代码搞砸了。而使用Git,您总是有办法回到原来的代码中——并且是能够正常工作的代码版本。

第一个屏幕

StoreSearch中的第一个屏幕是有一个带有搜索栏的表视图——让我们为该屏幕创建视图控制器。
在项目导航器中,选择ViewController.swift,将光标移到ViewController的类名上,右键单击以显示上下文菜单。从菜单中选择Refactor→Rename…并将类(以及相关文件和sb引用)重命名为SearchViewController。

注意:有时候,Refactor会做所有正确的事情,除了正确地重命名文件。如果发生这种情况,您将在项目导航器中看到红色的新文件名,因为Xcode期望新文件但该文件实际上仍然是旧文件名。如果发生这种情况,只需通过Finder进入项目文件夹,手动重命名文件。

运行应用程序以确保一切正常。您应该会看到一个白色屏幕,顶部有状态栏。

注意,项目导航器现在在列表中的一些文件名旁边显示了M和R图标:

如果您没有看到这些图标,那么从Xcode菜单栏中选择Source Control→Fetch and Refresh Status选项。如果这给出了一个错误消息或仍然不能工作,只需重新启动Xcode。一般来说,这是一个很好的提示:如果Xcode行为怪异,请重新启动它。
M表示自上次提交以来文件已被修改,R表示该文件已重命名。

那么什么是commit呢?

当您使用Git这样的版本控制系统时,应该经常创建快照。通常情况下,你会在你的应用程序中添加了一个新功能之后,或者当你修复了一个bug之后,或者当你觉得你已经做出了想要保留的更改时才这样做。
这就是所谓的commit。

Git 版本控制

创建项目时,Xcode进行了初始提交。您可以在项目历史记录窗口中看到。
➤从navigator窗格中选择Source Control navigator,然后点击项目根(顶部的蓝色文件夹图标)查看项目历史:


你可能会得到一个弹出窗口,请求访问你的联系人。这允许Xcode将联系人信息添加到提交历史记录中的名称中。如果您正在与其他开发人员协作,这将非常有用。你之后可以在System Preferences的 Security & Privacy 中修改它。

注意:您的Git历史记录可能与我的不一样,因为我的历史记录也显示了一个名为ch-32的分支。分支是一种Git机制,用于沿着不同的路径处理相同的代码基。在后面的章节中,您将了解更多关于Git分支的知识。现在,只要忽略你在截图中看到的ch-32分支就可以了,你要知道,如果你没有其他分支,也没关系——你不应该这样做:

让我们来提交你刚刚做出的改变。从“Source Control”菜单中,选择“Commit……


这将打开一个新窗口,详细显示您所做的更改。这是快速检查代码更改的好时机,只是为了确保您没有提交任何您不打算提交的内容:



在底部的文本框中写一个简短但明确的提交理由总是一个好主意。在这里有一个好的描述将帮助您以后在您的项目历史中找到特定的提交。

将 “ViewController重命名为SearchViewController” 作为提交消息。

按下提交3个文件的按钮。您将看到,在项目导航器中,M和R图标不见了——至少在您进行下一个更改之前是这样。
源代码控制导航器现在应该显示两次提交。如果没有,单击列表中的另一个分支,然后再次单击根文件夹。


如果双击某个特定的提交,Xcode将显示该提交的更改。你将会定期提交并且在本书的最后,你会成为这方面的专家:]


2.创建UI

StoreSearch还没有太大的进展。在本节中,您将构建这样的UI—在表视图上的搜索栏:



即使这个屏幕使用熟悉的表视图,它也不是一个表视图控制器,而是一个常规的UIViewController——如果你不确定的话,检查SearchViewController.swift中的类定义。

你不需要使用UITableViewController作为视图控制器的基类因为你的UI中有一个表格视图。对于这个应用程序,我将向您展示如何实现。

UITableViewController vs. UIViewController

那么,TableViewController和ViewController之间到底有什么区别呢?

首先,UITableViewController是UIViewController的子类,它能做ViewController能做的一切。不过,它经过了优化,适合与表视图table View一起使用,并且具有一些很酷的额外功能。

例如:当表格cell包含text field时,单击该文本字段将弹出屏幕键盘。UITableViewController会自动滚动单元格,让你能看到你在输入什么。

你不能用一个普通的UIViewController免费获得那个行为——如果你想要那个特性,你必须自己编程。

UITableViewController确实有一个很大的限制:它的主视图必须是一个UITableView,它占据了整个屏幕空间,除了顶部的导航栏和底部的工具栏或标签栏。
如果你的屏幕只包含一个UITableView,让它成为UITableViewController是有意义的。但如果你想要有其他视图,更基本的UIViewController是你要做的选项。

这就是你不在这个app中使用UITableViewController的原因。除了表格视图,app还有另一个视图,UISearchBar。你可以把搜索栏searchBar放在tableView中作为一个特殊的头视图,或者把searchBar作为导航栏navigation bar的一部分,但是对于这个应用程序,你会把它放在tableView的上方。

设置 storyboard

➤打开storyboard并使用View as: panel切换到iPhone SE。你在这里选择什么型号的iPhone并不重要,但iPhone SE让你更容易跟随这本书。”
➤将一个新的tableView——而不是tableViewController——拖拽到现有的视图控制器中。
➤让tableView和主视图一样大(320 * 568),然后使用底部的Add New Constraints菜单将tableView紧贴到屏幕边缘:



还记得这是怎么回事吗?这个应用程序使用Auto Layout,您已经在以前的应用程序中使用过。通过Auto Layout,你可以创建一些约束条件来决定视图的大小以及它们在屏幕上的位置。

➤首先,如果勾选了Constrain to margins(约束到页边距),取消它。每个屏幕的左右两侧都有16个点的空白,但是您可以更改它们的大小。当“约束到页边距”被启用时,您将对这些页边距进行固定。这里不行;你想把tableView固定在屏幕边缘。
➤在Spacing to nearest neighbor中,选择红色的“工”构成四个约束,分别在tableView的每一边。将间距值保持为0。

这将tableView固定到它的父视图的边缘。现在,无论设备屏幕的大小,表格总是会填满整个屏幕。

➤点击Add 4 Constraints按钮完成。

如果您成功了,那么现在表格视图周围应该有四个蓝色的条框,每个约束一个。在文件大纲中还应该有一个新的约束部分。


从对象库中拖拽一个Search Bar到视图中——注意选择搜索栏"Search Bar",而不是搜索栏和搜索显示控制器 "Search Bar and Search Display Controller"。将它放在Y = 20处,这样它就位于状态栏下。
确保searchBar没有放在表视图中。它应该与文档大纲中的tableView位于同一层:


如果你确实把搜索栏放到了表格视图中,你可以在文档大纲中找到它,并把它拖到tableView下面。

➤将searchBar固定在顶部、左侧和右侧边缘——总共有3个约束。



你不需要固定searchBar的底部或者给它一个高度限制。searchBar的固有高度为44points。

➤在searchBar的属性检查器Attributes inspector中,将占位符文本Placeholder更改为“App name, artist, song, album, e-book”。

视图控制器的设计应该是这样的:


连接到outlets

你知道接下来会发生什么:将searchBar和tableView连接到视图控制器上的outlet。
➤将以下outlets添加到SearchViewController.swift:

@IBOutlet weak var searchBar: UISearchBar!
@IBOutlet weak var tableView: UITableView!

回忆一下,一旦一个对象不再有任何强引用,它就会消失——它被释放——而且任何对它的弱引用都会变成nil。
根据苹果的建议,你已经使你的outlets为weak。你可能会想,如果对这些视图对象的引用是weak,那么对象不会很快被释放吗?

练习:是什么阻止这些视图被释放?

回答:视图总是视图层次结构的一部分,它们总是有一个具有Strong引用的所有者——它们的父视图。

SearchViewController的主视图对象包含对searchBar和tableView的引用。这是在UIKit中完成的,你不用担心。只要视图控制器存在,这两个outlet也会存在。

➤切换回storyboard,将searchBar和tableView连接到它们各自的outlet——从视图控制器control-拖动到你想连接的对象。

Table View 内容集

如果你现在运行这个应用程序,你会注意到一个小问题:Table视图的第一行隐藏在搜索栏下面。


这并不奇怪,因为您将searchBar放在表的顶部,遮住了tableView的一部分。
你可以用几种不同的方法来解决这个问题:
1.更改表视图的顶部布局约束,以匹配搜索栏的底部边缘。
2.使搜索栏部分透明,以便让表格单元格的内容发光。
3.使用表视图的content inset属性允许搜索栏覆盖的区域。

你会选择选项3。不幸的是,content inset属性不能通过Interface Builder使用。所以,这必须通过代码来实现。

➤在SearchViewController.swift的viewDidLoad()的末尾添加以下一行:

tableView.contentInset = UIEdgeInsets(top: 64, left: 0,  bottom: 0, right: 0)

这告诉table view在顶部添加64个point的空白—状态栏为20个point,searchBar为44个point。

现在第一行总是可见的,当您滚动表视图时,单元格仍然在searchBar下。


3.执行伪搜索

在实现iTunes商店搜索之前,最好了解UISearchBar组件是如何工作的。
在本节中,您将从搜索栏中获得搜索项,并使用它将一些虚假的搜索结果放入表视图中。一旦您掌握了这些工作,您就可以在web服务中构建了。婴儿般的第一步!

➤运行应用程序。如果你点击搜索栏,屏幕上的键盘会出现——如果你在模拟器,您可能需要按⌘K弹出键盘,和Shift +⌘K允许从你的Mac键盘打字。
然而,当你输入一个搜索词并点击搜索按钮时,它不会做任何事情。

监听搜索栏就完事了——还有别的办法吗?-委托。让我们把这个委托代码放到一个extension扩展中。

添加搜索栏委托

将以下内容添加到SearchViewController.swift的底部,在最后一个右括号之后:

extension SearchViewController: UISearchBarDelegate {
  func searchBarSearchButtonClicked(_ searchBar: UISearchBar) {
    print("The search text is: '\(searchBar.text!)'")
  }
}

请记住,您可以使用extension来组织源代码。通过将所有UISearchBarDelegate内容放到它自己的extension中,您可以将它们放在一起,而不影响其他代码。

UISearchBarDelegate协议有一个方法searchBarSearchButtonClicked(_:),当用户轻击键盘上的搜索按钮时调用该方法。您将实现此方法将一些假数据放入表中。稍后,您将让这个方法向iTunes商店发送一个网络请求,以查找与用户输入的搜索文本匹配的歌曲、电影和电子书,但是我们不要同时做太多的新事情!

目前,所有的新代码所做的就是将搜索项从搜索栏输出到Xcode控制台。

提示:当我使用print()时,我总是在单引号之间加上字符串。这样,您就可以很容易地看到字符串中是否有尾随或前导空格。还要注意搜索栏。文本是optional型,因此我们需要展开它。它永远不会返回nil,所以使用一个nil就可以。

➤ 在storyboard中,从搜索栏control-拖动到SearchViewController或者顶部的黄色圆圈。连接到委托。
➤ 运行应用程序,在搜索栏中输入一些内容并按下搜索按钮。Xcode Debug窗格现在应该打印您输入的文本。


显示伪结果

➤给SearchViewController.swift添加以下新的(空的)extension:

extension SearchViewController: UITableViewDelegate, UITableViewDataSource {
}

上面的extension将处理所有与tableView相关的委托方法。当然,如果您愿意,也可以将它们作为两个单独的extension添加进来,但是我更愿意将所有与tableView委托相关的代码保存在一个地方。

添加UITableViewDataSource和UITableViewDelegate协议对于之前的应用程序是不必要的,因为你在每种情况下都使用了UITableViewController。UITableViewController已经根据需要遵守这些协议。
SearchViewController是一个常规视图控制器,因此你必须自己连接数据源和委托协议。

➤现在Xcode应该抱怨你的代码不符合UITableViewDataSource协议。使用Xcode“Fix”选项添加协议存根,然后修改代码,如下所示,添加您目前需要的最小代码:

extension SearchViewController: UITableViewDelegate, UITableViewDataSource {
  func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    return 0
  }
  
  func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    return UITableViewCell()
  }
}

这只是告诉table view它还没有行。很快您就会给它提供一些要显示的假数据,但是现在您只想能够编译没有错误的代码。

通常,您可以声明符合协议,而不需要实现协议的任何方法——这种工作方式对于UISearchBarDelegate来说很好。

协议可以有可选的和必需的方法。如果你忘记了一个必需的方法,你通常会看到Xcode发出抱怨,就像你在上面所做的那样。

➤在storyboard中,control-拖动从Table View到Search View Controller。连接到数据源。重复此操作以连接到委托。

可能你想知道为什么搜索视图控制器中要连接到委托两次——第一次是searchBar,第二次是Table View。Interface Builder展示的方式有点误导:委托的出口outlet不是来自SearchViewController,而是你control-拖动关联的对象。因此,您将SearchViewController关联searchBar的delegate outlet,以及tableView的delegate(和数据源dataSource)outlet:


➤构建并运行应用程序,以确保一切正常运行。

注意:你注意到这些数据源方法和之前的应用程序有什么不同吗?仔细看…
答:它们没有override关键字。
在之前的应用程序中,重写是必要的,因为我们正在处理UITableViewController的子类,它已经提供了自己版本的tableView(:numberOfRowsInSection:)和tableView(:cellForRowAt:)方法。
在这些应用程序中,您要“覆盖”或用自己的版本替换这些方法,因此需要使用override关键字。
但是在这里,你的基类不是一个表格视图控制器而是一个常规的UIViewController。这样的视图控制器还没有任何表格视图方法,所以这里你没有重写任何东西

正如您现在所知道的,表视图需要某种数据模型。让我们从一个简单的数组开始。
➤为数组添加一个实例变量——它在class括号中,而不是在任何extension中:

var searchResults = [String]()

搜索栏委托方法会将一些假数据放入这个数组中,然后使用表格显示它。
➤将searchBarSearchButtonClicked(_:)方法替换为:

func searchBarSearchButtonClicked(_ searchBar: UISearchBar) {
  searchResults = []
  for i in 0...2 {
    searchResults.append(String(format: 
        "Fake Result %d for '%@'", i, searchBar.text!))
  }
  tableView.reloadData()
}

这里的符号[]表示实例化一个新的字符串数组并用它替换searchResults属性的内容。这是在用户每次执行搜索时完成的。如果前面已经有一个结果数组,那么该数组将被丢弃并重新分配。您也可以编写searchResults = [String]()来执行相同的操作。

向数组中添加一个带文本的字符串。只是为了好玩,这个过程重复了3次,所以您的数据模型将包含3行。

当你写for i in 0...2 的时候,它创建了一个循环,该循环重复三次,因为闭区间0…2包含数字0、1和2。注意,这不同于半开放区间0..<2,其中只包含0和1。你也可以写1…3但是,正如您现在所发现的,程序员喜欢从0开始计数:]

您以前见过格式化字符串。格式说明符%d是整数的占位符。同样,%f是浮点数。占位符%@适用于所有其他类型的对象,比如字符串。

方法中的最后一条语句重载table视图以使新行可见,这意味着您还必须调整数据源方法来从这个数组中读取数据。
➤将表视图委托扩展中的方法替换为:

func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
  return searchResults.count
}

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
  let cellIdentifier = "SearchResultCell"
  
  var cell:UITableViewCell! = tableView.dequeueReusableCell(withIdentifier: cellIdentifier) 
  if cell == nil {
    cell = UITableViewCell(style: .default, reuseIdentifier: cellIdentifier)
  }

  cell.textLabel!.text = searchResults[indexPath.row]
  return cell
}

到目前为止,您应该对上面的所有代码都非常熟悉。只需根据searchResults数组的内容返回要显示的行数,然后手工创建一个UITableViewCell来显示行。
运行应用程序。如果你搜索任何东西,一些虚假的结果会被添加到数据模型中,并显示在表格中。
搜索其他内容,表格视图就会用新的假结果更新。



UI的改进

在这一点上,你可以对应用的功能做一些改进。

搜索时隐藏键盘

按下搜索按钮后,键盘还留在屏幕上,这不是很好。它模糊了表格视图的一半,而且无法忽略键盘。
➤将下面的一行添加到searchBarSearchButtonClicked(_:)的顶部:

searchBar.resignFirstResponder()

这告诉UISearchBar它不应该再监听键盘输入。结果,键盘会隐藏起来,直到你再次点击搜索栏。

您还可以配置table视图以用手势关闭键盘。
➤在storyboard中,选择Table View。到Attributes inspector中,设置键盘互斥(Keyboard to Dismiss interactively)。

将搜索栏扩展到状态区域

搜索栏上方的状态栏仍然有一个难看的白色空白。如果状态栏区域与搜索栏统一起来,效果会好得多。UINavigationBar和UISearchBar项目有一个委托方法,允许项目显示它的顶部位置。
➤将以下方法添加到SearchBarDelegate的extension中:

func position(for bar: UIBarPositioning) -> UIBarPosition {
  return .topAttached
}

现在这款应用看起来好多了:


如果你在UISearchBarDelegate的API文档中查找,你不会找到这个position(for:)方法。相反,它是UIBarPositioningDelegate协议的一部分,UISearchBarDelegate协议扩展了这个协议——就像类一样,协议可以从其他协议继承。

API文档

Xcode附带了一个用于开发iOS应用程序的大型文档库。基本上所有你需要知道的都在这里。学习使用Xcode文档浏览器——它将成为你最好的朋友!
有几种方法可以在Xcode中找到项目的文档。有一个快速的帮助,它显示了关于文本光标下的项目的信息:


只要打开Quick Help inspector快速帮助检查器——检查窗格中的第二个选项卡——它就会显示上下文相关的帮助。将文本光标放在想要了解更多信息的项上,检查器将提供摘要。您可以单击摘要中的任何蓝色文本链接跳转到完整的文档。
您还可以获得弹出式帮助。按住Option (Alt)键,将鼠标悬停在想要了解更多信息的项目上。然后点击鼠标:



当然,还有一个完善的文档窗口。您可以从“Help”菜单的“开发人员文档(Developer Documentation)”下访问它。使用顶部的工具栏搜索你想知道更多的信息:

4.创建数据模型

到目前为止,您已经向searchResults数组添加了String对象,但这有点限制。您将从iTunes商店返回的搜索结果包括产品名称、艺术家的名称、到图像的链接、购买价格等等。
你不能把所有这些都放在一个字符串中,所以让我们创建一个新的类来保存这些数据。

SearchResult类

➤使用Swift文件模板为项目添加一个新文件。新类命名为SearchResult。
➤将以下内容添加到SearchResult.swift中。

class SearchResult {
 var name = ""
 var artistName = ""
}

这将向新的SearchResult类添加两个属性。您还将添加一些其他内容。

在SearchViewController中,你需要修改searchResults数组来保存SearchResult的实例。

var searchResults = [SearchResult]()

下一步,将搜索栏委托方法中的for in循环更改为:

for i in 0...2 {
  let searchResult = SearchResult()
  searchResult.name = String(format: "Fake Result %d for", i)
  searchResult.artistName = searchBar.text!
  searchResults.append(searchResult)
}

这将创建SearchResult对象的一个实例,并在它的name和artistName属性中放入一些假文本。同样,你在循环中执行此操作,因为仅拥有一个搜索结果对它来说有点难受。

此时,tableView(_:cellForRowAt:)仍然期望数组包含字符串。更新那个方法

func tableView(_ tableView: UITableView, 
    cellForRowAt indexPath: IndexPath) -> UITableViewCell {
  . . .  
  if cell == nil {
    cell = UITableViewCell(style: .subtitle,       // change
           reuseIdentifier: cellIdentifier)
  }
  // Replace all the code below this point
  let searchResult = searchResults[indexPath.row]
  cell.textLabel!.text = searchResult.name  
  cell.detailTextLabel!.text = searchResult.artistName
  return cell
}

代码现在使用的是“subtitle”单元格样式,而不是常规的表视图单元格。将artistName属性的内容放入subtitle文本标签中。
➤运行应用程序;应该是这样的:



5. 没有找到数据

当你在应用程序中添加搜索功能时,你必须处理以下情况:

  1. 用户还没有执行搜索。
  2. 用户执行搜索并收到一个或多个结果。这就是当前版本的应用程序所发生的:对于每个搜索,您都会得到返回的一些SearchResult对象。
  3. 用户执行搜索,没有结果。明确地告诉用户没有结果通常是一个好主意。如果根本不显示任何内容,用户将不知道是否实际执行了搜索。

尽管这款应用还没有进行任何实际的搜索,但你没有理由不去伪造最后一个场景。

处理“没有得到任何结果”

为了保护用户的品味,当用户搜索“justin bieber”时,该应用程序将返回0个结果,这样你就知道该应用程序可以处理这种情况。
➤在searchBarSearchButtonClicked(_:)中,在for In循环中放置以下if语句:

. . .
if searchBar.text! != "justin bieber" {
  for i in 0...2 {
    . . .
  }
}
. . .

这里的更改非常简单——您添加了一个if语句,如果文本等于“justin bieber”,则可以阻止创建任何SearchResult对象。

运行应用程序,搜索“justin bieber”——注意全是小写字母。table应该是空的。
此时,您不知道搜索是否失败,或者是否没有结果。您可以通过显示文本“Nothing found”来改进用户体验,这样用户就可以毫无疑问地知道没有搜索结果。
➤将tableView(_:cellForRowAt:)的最后一部分更改为:

if cell == nil {
  . . .
}
// New code
if searchResults.count == 0 {
  cell.textLabel!.text = "(Nothing found)"  
  cell.detailTextLabel!.text = ""
} else {
  let searchResult = searchResults[indexPath.row]
  cell.textLabel!.text = searchResult.name
  cell.detailTextLabel!.text = searchResult.artistName
}
// End of new code
return cell

仅仅这样是不够的。当数组中没有内容时,searchResults.count是0,对吧?但这也意味着numberOfRowsInSection将返回0,而table视图将保持空—“Nothing found”这一行将永远不会出现。

➤将tableView(_: numberOfRowsInSection:)改为:

func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
  if searchResults.count == 0 {
    return 1
  } else {
    return searchResults.count
  }
}

现在,如果没有结果,为了文本为“(Nothing Found)”的行,该方法将返回1。这是因为numberOfRowsInSection和cellForRowAt都会检查这种特殊情况。

试一试:


解决app启动时没有结果

不幸的是,当用户还没有搜索任何内容时,文本“Nothing found”也会首先出现。那是愚蠢的。
问题是您无法区分“尚未搜索”和“未找到”。现在,您只能判断searchResults数组是否为空,但不能确定是什么原因导致了这种情况。

练习:你怎么解决这个小问题?

我想到了两个显而易见的解决方案:

  1. 将searchResults更改为optional类型。如果它是nil,也就是说它没有值,那么用户还没有搜索。这与用户搜索但没有找到匹配项的情况不同。
  2. 使用一个单独的布尔变量来跟踪搜索是否已经完成。

选择optional可能很诱人,但如果可以最好避免选择这种方式。它们会使逻辑复杂化,如果不正确地展开,可能会导致应用程序崩溃,而且它们需要if let语句无处不在。
所以,我们将选择布尔值。但是,您可以自由地回来尝试您自己的optional选项,并比较它们之间的差异。这将是一个很好的练习!

➤仍然在SearchViewController.swift中,添加一个新的实例变量:

var hasSearched = false

➤在搜索栏的委托方法中,将这个变量设置为true。你在哪里做这些并不重要,只要它发生在表格视图重载之前。

func searchBarSearchButtonClicked(_ searchBar: UISearchBar) {
  . . .
  hasSearched = true      // Add this line
  tableView.reloadData()
}

➤最后,改变tableView(_: numberOfRowsInSection:)来查看这个新变量的值

func tableView(_ tableView: UITableView,
     numberOfRowsInSection section: Int) -> Int {
  if !hasSearched {
    return 0
  } else if searchResults.count == 0 {
    return 1
  } else {
    return searchResults.count
  }
}

现在,table视图仍然是空的,直到您第一次搜索某个东西。试一下!
稍后,您将看到使用枚举处理此问题的更好方法,它会让您大吃一惊!

选择处理

还有一件事,如果你现在点击一行,它就会被选中,并一直被选中。
要解决这个问题,可以在表视图委托扩展中添加以下方法:

func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
  tableView.deselectRow(at: indexPath, animated: true)
}
  
func tableView(_ tableView: UITableView, 
     willSelectRowAt indexPath: IndexPath) -> IndexPath? {
  if searchResults.count == 0 {
    return nil
  } else {
    return indexPath
  }
}

tableView(_:didSelectRowAt:)方法会简单地用动画取消选择行,而willSelectRowAt会确保只有在有实际搜索结果时才能选择行。
如果您现在点击(Nothing Found)行,您将注意到它根本不会变成灰色。实际上,如果短时间内按下一行,它可能仍然会变成灰色。这是因为您没有更改单元格的selectionStyle属性。你很快就会修好的。

➤现在是提交所有更改的好时机。点击Source Control → Commit,或者按键盘快捷键⌘+Option+ C。
确保所有修改的文件都在左边的列表中被选中/选中,检查您的修改,并输入提交备注——类似于“添加搜索栏和表视图,现在搜索会暂时将伪结果放入table中”。
按下Commit按钮完成提交。

版本编辑器

如果你想回顾你的提交历史,你可以通过源代码控制导航器——正如你在本章开头学到的那样——或者版本编辑器(Version editor),如下图所示:


您可以使用Xcode窗口右上角的相关工具栏按钮切换到版本编辑器。

在上面的截图中,左边显示的是以前的版本,右边显示的是当前版本。您可以使用每个窗格底部的跳转栏在版本之间切换。版本编辑器是查看源文件更改历史的非常方便的工具。

这个应用程序还不是很令人印象深刻,但是您已经为接下来的工作打下了基础。
您有一个搜索栏,并且知道当用户按下search按钮时如何操作。该应用程序还有一个简单的数据模型,由一个带有SearchResult对象的数组组成,它可以在一个表视图中显示这些搜索结果。

你可能感兴趣的:(iOS apprentice中文版 - Section 4: Store Search)