Chapter 33:自定义 Table Cells
在你的应用程序搜索iTunes商店之前,首先让我们让表格视图看起来更好一些。对于应用程序来说,外观确实很重要!
你的应用程序仍然会使用相同的伪数据,但你会让它看起来更好。这是你在本章结尾会看到的:
在这个过程中,你会学到以下几点:
1. 自定义表单元格和nib:
如何通过nib文件创建、配置和使用自定义表单元格。
2. 改变App的外观:
改变App的外观,使它更令人兴奋和充满活力。
3. commit标签:
使用Xcode的内置Git支持标记特定的commit,以便稍后识别代码库中的重要里程碑。
4. 调试器:
使用调试器识别常见的崩溃并找出崩溃的根本原因。
1. 自定义表单元格和nib:
对于之前的应用程序,您使用原型单元格创建自己的表视图单元格布局。这很好,但还有另一种方法。在本章中,您将创建一个带有单元格设计的“nib”文件,并从中加载表视图单元格。原理与原型单元非常相似。
nib,也称为xib,非常像一个storyboard,只是它只包含一个item的设计。该item可以是视图控制器,但也可以是单个view 或 table view cell。nib实际上是一个“冻干”对象的容器,你可以在Interface Builder中编辑它。
实际上,许多应用程序由nib和storyboard文件组成,所以最好知道如何同时使用这两种文件。
添加 assets
➤ 首先,将应用程序资源中的Images文件夹的内容添加到项目的资产目录Assets.xcassets中。(原版的电子书有附源码和相关资源)
每张图片都有两种版本:2x和3x。没有低分辨率的1x设备可以运行最新版本的iOS。所以没有必要包含1x张图片。
添加一个 nib 文件
➤给工程添加一个新文件。在User Interface中选择Empty模板。这会产生一个新的空nib。
➤点击Next并将新文件保存为SearchResultCell。
打开SearchResultCell.xib你会看到一块空画布。
Xib或nib
我一直称它为nib,但是文件扩展名是.xib。有什么不同呢?实际上,这些术语可以互换使用。从技术上讲,xib文件被编译成nib文件,并放入应用程序包中。nib这个术语主要是由于历史原因而保留下来的——它代表的是NeXT Interface Builder,来自上世纪90年代的旧NeXT平台。
您可以认为术语“xib文件”和“nib文件”是等价的。首选项似乎是nib,所以从现在开始我将使用nib。这不会是计算机术语最后一次混淆、模糊或不一致。编程的世界充满了术语。
➤将View as:面板切换到iPhone SE设备。和往常一样,我们将为这个设备进行设计,但是使用自动布局使用户界面适应更大的设备/屏幕。
从对象库中,拖拽一个新的表格视图单元格到画布上:
➤选择新的表格视图单元格,并进入尺寸检查器。在Height字段中键入80(而不是行高度Row Height)。确保宽度Width为320,即iPhone SE屏幕的宽度。
cell现在看起来是这样的:
注意: 有时,单元格可能有一个蓝色边框,它与实际单元格的位置略有偏移。这是一个接口构建器Interface Builder的bug。如果发生这种情况,只需切换到其他文件,然后切换回SearchResultCell.xib——一切应该都会恢复正常。
➤将一个 Image View和两个Label拖拽到cell中,就像这样:
注意:如果你像上面那样在每一项周围都有蓝色矩形——或者想用矩形看到每一项的完整边界——那么使用 Editor → Canvas → Show Bounds Rectangles 来打开/关闭边界矩形。
➤将图像视图定位在X:16, Y:10,Width:60,Height:60。
➤将第一个Label的text设置为Name,字体设置为System 18, X:84, Y:16,Width设置为220,Height设置为22。
➤将第二个Label的text设置为Artist Name,字体设置为System 15,颜色设置为黑色,透明度opacity设置为50%,X:84, Y:44,Width:220,Height:18。
正如您所看到的,编辑nib就像编辑storyboard一样。区别在于画布要小得多,因为你只编辑一个表格视图单元格,而不是整个视图控制器。
Table View Cell本身需要有一个重用标识符identifier。您可以在属性检查器中将其设置为SearchResultCell.
imageView将放置搜索到的艺术作品,比如相册封面、书籍封面或应用程序图标。加载这些图像可能需要几秒钟,因此在此之前,最好显示占位符图像。这个占位符是您刚刚添加到项目中的图像文件的一部分。
➤选择 Image View。在属性检查器中,将Image设置为Placeholder。
单元格cell的设计现在应该是这样的:
你还没做完呢。这款手机的设计只有320点宽,但也有一些iOS设备的屏幕比这还要宽。单元格本身会调整大小以适应那些更大的屏幕,但Label不会,这可能会导致它们的text被切断。你必须添加一些自动布局约束,让Label和单元格一起调整大小。
设置自动布局约束
当设置自动布局约束时,最好从一条边开始——就像从左到右屏幕的左上角一样,但要记住也有可以从右到左的屏幕——然后向左向下移动。当您设置自动布局约束时,视图将移动以匹配这些约束,通过这种方式,您可以确保您设置的每个视图相对于前一个视图都是稳定的。
如果你随机为视图设置布局约束,你会看到你的视图到处移动,过一段时间你可能就不记得你最初放置视图的位置了。
➤选择ImageView并打开Add New Constraints菜单。取消对边距的约束,并将ImageView固定在单元格的顶部和左侧。也要限制它的宽度和高度,使它的大小总是固定在60×60的点上:
➤点击Add 4 Constraints来添加约束。
➤选择Name Label,再次使用Add New Constraints菜单。取消对页边距的约束,选择顶部、左侧和右侧(但不包括底部):
➤点击Add 3 Constraints。
➤最后,通过添加4个新的约束,将Artist Name Label分别钉在left、top、right和bottom——同样不受边距的限制。
这就是这个单元格的设计。现在你必须告诉应用程序使用这个nib。
注册nib文件用于代码
➤打开SearchViewController.swift,将这些行添加到viewDidLoad()的末尾:
let cellNib = UINib(nibName: "SearchResultCell", bundle: nil)
tableView.register(cellNib, forCellReuseIdentifier: "SearchResultCell")
UINib类用于加载nib。在这里,您告诉它加载您刚刚创建的nib—注意,您没有指定.xib文件扩展名。然后,您要求tableView为重用标识符“SearchResultCell”注册这个nib。
从现在开始,当你为标识符“SearchResultCell”调用dequeueReusableCell(withIdentifier:)时,UITableView会自动从nib中创建一个新的单元格——或者重用一个现有的单元格(如果有的话)。这就是你需要做的。
➤在tableView(_:cellForRowAt:)中修改这段代码:
let cellIdentifier = "SearchResultCell"
var cell: UITableViewCell! = tableView.dequeueReusableCell(withIdentifier: cellIdentifier)
if cell == nil {
cell = UITableViewCell(style: .subtitle, reuseIdentifier: cellIdentifier)
}
最后的方法是这样的:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "SearchResultCell", for: indexPath)
if searchResults.count == 0 {
. . .
} else {
. . .
}
return cell
}
你可以用一条语句替换一段代码。现在,它几乎与使用原型单元格完全一样,只是您必须创建自己的nib对象,并且需要预先将其注册到表视图中。
注意:dequeueReusableCell(withIdentifier:)的调用现在接受第二个参数for:,它接受一个IndexPath值。dequeue方法的这种变体使表视图更智能一些,但它只在向表视图注册了nib或使用原型单元格时才有效。
运行应用程序并进行(伪)搜索。哎呀,应用程序崩溃了。
练习:想想是为什么?
答:因为你做了自己的自定义单元格设计,你不能使用UITableViewCell的textLabel和detailTextLabel属性。
每个表格视图单元格——甚至是你从nib加载的自定义单元格——都有一些标签和自己的图像视图,但是你应该只在使用标准单元格样式之一时使用这些:.default、.subtitle等等。如果您在自定义单元格上使用它们,那么这些内置标签就会妨碍您自己的标签。
在这种情况下,您不应该使用textLabel和detailTextLabel将文本放入单元格—您需要为标签创建自己的属性。
你把这些属性放在哪里?当然是在一个新的class里。你将创建一个新的类,名为SearchResultCell,它扩展了UITableViewCell,并具有属性和逻辑,用于在这个应用程序中显示搜索结果。
添加一个自定义UITableVIewCell子类
➤使用Cocoa Touch Class模板向项目添加一个新文件。将它命名为SearchResultCell并使它成为UITableViewCell的子类——请注意类名的更改,如果在设置名称之后选择子类,“Also create XIB file”应取消选中,因为您已经有一个。
这将创建Swift文件,与您之前创建的nib文件一起使用。
➤打开SearchResultCell.xib,并选择表视图单元格Table View Cell——确保选择的是实际的表视图单元格对象,而不是它的内容视图Content View。
➤在Identity inspector中,将它的类从“UITableViewCell”更改为SearchResultCell。
你这样做是为了告诉nib它包含的顶层视图对象不再是UITableViewCell而是你自己的SearchResultCell子类。从现在开始,无论何时调用dequeueReusableCell(),表视图都会返回一个SearchResultCell类型的对象。
➤将以下outlet属性添加到SearchResultCell.swift:
@IBOutlet weak var nameLabel: UILabel!
@IBOutlet weak var artistNameLabel: UILabel!
@IBOutlet weak var artworkImageView: UIImageView!
➤将这些outlet连接到nib中各自的标签和图像视图上。从SearchResultCell的连接检查器中最容易做到这一点:
您还可以打开助理编辑器并从标签和图像视图Control-drag到它们各自的outlet定义。如果您以前使用过nib文件,那么您可能会想要将outlet连接到文件的所有者,但在本例中这是行不通的;它们必须连接到tableView单元格。
现在一切都设置好了,您可以告诉SearchViewController使用这些新的SearchResultCell对象。
在app中使用自定义表格视图单元格
➤打开SearchViewController.swift,把cellForRowAt改成:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "SearchResultCell",
for: indexPath) as! SearchResultCell
if searchResults.count == 0 {
cell.nameLabel.text = "(Nothing found)"
cell.artistNameLabel.text = ""
} else {
let searchResult = searchResults[indexPath.row]
cell.nameLabel.text = searchResult.name
cell.artistNameLabel.text = searchResult.artistName
}
return cell
}
注意第一行中的变化。之前它返回了一个UITableViewCell对象,但是现在你已经在nib中更改了类名,你保证总是会收到一个SearchResultCell——你只需要用as!转换它。
给定这个单元格,您可以将搜索结果中的名称和艺术家名称放入适当的标签中。您现在使用的是单元格的nameLabel和artistNameLabel输出口,而不是textLabel和detailTextLabel。你也不需要再写!来展开,因为outlet是隐式展开的optional。
➤运行这个应用程序,应该是这样的:
还有一些需要改进的地方。注意到您在几个不同的地方使用了字符串“SearchResultCell”吗?一般来说,为这种场合创建一个常量会更好。
为表单元格标识符使用常量
假设由于某种原因,您或您的某个同事在某个位置重命名reuse identifier。然后,您还必须记住其他所有使用标识符“SearchResultCell”的地方并更改它。最好使用符号名将这些更改限制在一个位置。
➤将以下内容添加到SearchViewController.swift中,在class定义的某个地方:
struct TableView {
struct CellIdentifiers {
static let searchResultCell = "SearchResultCell"
}
}
这定义了一个新的struct,TableView,包含一个二级struct名为cellidentifier,它包含一个常量名为searchResultCell,值为“SearchResultCell”。
如果你想改变这个值,你只需要在这里更改,任何使用TableView.CellIdentifiers.searchResultCell的代码将自动更新。
使用符号名而不是实际值还有另一个原因:它赋予了额外的含义。仅仅看到文本“SearchResultCell”显然不及TableView.CellIdentifiers.searchResultCell更能说明它的预期用途。
注意: 在Swift中,将符号常量作为static let成员放在一个struct(或一系列struct)中是一个常见的技巧。静态static值可以在没有实例的情况下使用,所以在使用它之前不需要实例化TableView.cellidentifier——就像您需要使用一个类一样。
Swift允许在类中放置结构体,这允许不同类都有自己的TableView.CellIdentifier结构。如果你把结构体放在类的外面,这就行不通了——那么你就会在全局命名空间中有多个同名结构体,这是不允许的。
➤SearchViewController.swift,用TableView.CellIdentifiers.searchResultCell替换字符串“SearchResultCell”。
例如,viewDidLoad()现在看起来像这样:
override func viewDidLoad() {
. . .
let cellNib = UINib(nibName: TableView.CellIdentifiers.searchResultCell, bundle: nil)
tableView.register(cellNib, forCellReuseIdentifier: TableView.CellIdentifiers.searchResultCell)
}
另一个变化是tableView(_:cellForRowAt:)。
➤运行应用程序,确保一切正常运行。
一个新的“No results”单元格
还记得我们的朋友Justin Bieber吗?寻找他的过程是这样的:
那不太好看——更不用说有点不好看了。如果你能给它一个自己的样子会更好。这并不难:你可以为它再做一个nib。
➤给项目添加另一个nib文件。这又是一个空的nib。命名为NothingFoundCell.xib。
➤将一个新的Table View Cell拖放到画布上。将它的宽度设置为320,高度设置为80,并给它一个重用标识符“NothingFoundCell”。
➤拖拽一个 Label到单元格中,然后给它一个“Nothing Found”的文本。将文本颜色设置为50%不透明黑色(opaque black),字体系统设置为15。
➤使用Editor → Size to Fit Content 来匹配内容,使标签Lablel与文本Text完全匹配——你可能需要取消选择并再次选择Label来启用菜单选项。
➤将Label居中,使用蓝色的导航条将Label准确地对齐到中心。
它应该是这样的:
为了让文本在所有设备上居中,你可以使用自动布局对齐菜单(Align menu):
➤ 选择 Horizontally in Container 和 Vertically in Container 然后点击 Add 2 Constraints.
约束条件应该是这样的:
还有一件事要解决。还记得在willSelectRowAt中,如果没有搜索结果来阻止行被选中,那么返回nil吗?如果您持续的点击,您仍然可以使行显示为灰色,就像它被选中一样。
出于某种原因,UIKit会绘制选定的背景如果你按下单元格足够长时间,即使这不算真正的选择。为了防止这种情况,您必须告诉单元格不要使用选定的颜色。
➤选择单元格本身。在Attributes inspector中,将Selection设置为None。现在,轻击或按住“Nothing Found”行将不再显示任何类型的选择。
你不需要为这个单元格创建UITableViewCell子类因为没有文本要修改,也没有属性要设置,你只需要将这个nib注册到表格视图中。
➤在SearchViewController.swift中为结构体添加了一个新的重用标识符。
struct TableView {
struct CellIdentifiers {
static let searchResultCell = "SearchResultCell"
static let nothingFoundCell = "NothingFoundCell" // New
}
}
➤将这些行添加到viewDidLoad()中,在其他注册nib的代码下面:
cellNib = UINib(nibName: TableView.CellIdentifiers.nothingFoundCell, bundle: nil)
tableView.register(cellNib, forCellReuseIdentifier:TableView.CellIdentifiers.nothingFoundCell)
这还要求您更改let cellNib为var cellNib,因为您正在重用cellNib局部变量。
➤最后,将tableView(_:cellForRowAt:)改为
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if searchResults.count == 0 {
return tableView.dequeueReusableCell(withIdentifier:
TableView.CellIdentifiers.nothingFoundCell,
for: indexPath)
} else {
let cell = tableView.dequeueReusableCell(withIdentifier:
TableView.CellIdentifiers.searchResultCell,
for: indexPath) as! SearchResultCell
let searchResult = searchResults[indexPath.row]
cell.nameLabel.text = searchResult.name
cell.artistNameLabel.text = searchResult.artistName
return cell
}
}
这里的逻辑已经进行了一些调整。只有在实际有任何结果时才生成SearchResultCell。如果数组是空的,您只需取出标识符为nothingFoundCell的单元格,并返回它,因为没有为该单元格配置任何东西。
➤运行这个应用程序。现在Justin Bieber的搜索结果是这样的:
你也可以在更大的屏幕设备上尝试一下。Label应该始终以单元格为中心。
很好,你上次提交工作已经有一段时间了,所以现在似乎是保障工作的好时机。
Source Control的变化
但在提交更改之前,请在编辑器视图中查看SearchViewController.swift。你可能会注意到沿着排水沟的一些蓝线,像这样:
那些蓝线是什么意思?
这实际上是Xcode 10中的一个新特性——那些蓝色的行出现在启用了源代码控制Source Control的项目中,它们表示开发人员自上次提交以来所做的更改。
但除此之外,如果你与其他开发人员一起工作,而其他人正好更改了你正在工作的文件并提交了他们的修改到Git, Xcode甚至会显示这些pending changes,这样你会意识到有人进行了可能会影响你工作的修改。很方便!
➤将更改提交到存储库。我使用备注“为搜索结果使用自定义单元格”。
2. 改变应用程序的外观
我写这篇文章的时候,外面灰蒙蒙的,下着雨。这个App看起来也相当灰色和沉闷。让我们用更鲜艳的颜色让它看起来更活泼一点。
➤将以下方法添加到AppDelegate.swift:
// MARK:- Helper Methods
func customizeAppearance() {
let barTintColor = UIColor(red: 20/255, green: 160/255, blue: 160/255, alpha: 1)
UISearchBar.appearance().barTintColor = barTintColor
window!.tintColor = UIColor(red: 10/255, green: 80/255, blue: 80/255, alpha: 1)
}
这改变了UISearchBar的外观——事实上,它改变了app中的所有搜索栏。
UIColor(red:green:blue:alpha:)方法根据您指定的RGB和alpha color组件创建一个新的UIColor对象。
许多绘图程序允许您选择RGB值,范围从0到255,所以这是许多程序员习惯于考虑的颜色值范围。然而,UIColor初始化器接受0.0到1.0之间的值,因此必须将这些数字除以255才能将它们缩小到这个范围。
➤在application(_:didFinishLaunchingWithOptions:)调用这个新方法:
func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
customizeAppearance() // Add this line
return true
}
运行应用程序并注意区别:
搜索栏是蓝绿色的,但仍然是半透明的。现在的整体色调是深绿色,而不是默认的蓝色——你目前只能在文本框的光标上看到色调,但稍后会变得更加明显。
App Delegate的角色
可怜的AppDelegate经常被滥用。人们赋予它太多的责任。实际上,App Delegate没有太多要做的事情。
它获取关于应用程序状态的许多回调——例如,应用程序是否即将关闭——处理这些事件应该是它的主要职责。App Delegate还拥有主窗口(main window)和顶层视图控制器(top-level view controller )。除此之外,它不应该做太多。
一些开发人员使用App Delegate作为他们的数据模型,那是个糟糕的设计。你真的应该有一个或好几个单独的类来作为数据模型。
另一些人让App Delegate作为他们的主控制中心。又错了!应该把这些放到顶层视图控制器中。
如果你曾经在某人的源代码中看到以下类型的东西,这是一个很好的迹象,表明应用程序委托正在被错误地使用:
let appDelegate = UIApplication.shared.delegate as! AppDelegate
appDelegate.someProperty = . . .
当一个对象想从app委托中获取一些东西时,就会发生这种情况。它能工作,但不是好的架构。
在我看来,用另一种方式来设计代码更好:app委托可能会进行一定数量的初始化,但随后它会将任何数据模型对象交给根视图控制器(root view controller),并移交控制权。根视图控制器将这些数据模型对象传递给任何需要它们的控制器,以此类推。
这也被称为依赖注入(dependency injection)。我在MyLocations应用程序(Section 3 的App)的“传递上下文(Pass the context)”小节的第27章中描述了这一原则。
更改行选择颜色
目前,点击一行用灰色突出显示。这和茶色的主题不太协调。所以,你会给行选择同样的蓝绿色。
这很简单,因为所有表格视图单元格都有一个selectedBackgroundView属性。当单元格被选中时,来自该属性的视图被放置在单元格背景的顶部,但低于其他内容。
➤将以下代码添加到SearchResultCell.swift中的awakeFromNib()中:
override func awakeFromNib() {
super.awakeFromNib()
// New code below
let selectedView = UIView(frame: CGRect.zero)
selectedView.backgroundColor = UIColor(red: 20/255, green: 160/255, blue: 160/255, alpha: 0.5)
selectedBackgroundView = selectedView
}
awakeFromNib()方法在单元格对象从nib加载之后调用,但是在单元格添加到表视图之前调用。您可以使用此方法做额外的工作来准备要使用的对象。这对于创建带有选择颜色的视图来说是完美的。
为什么不在init方法中这样做,比如init?(编码器)?公平地说,在这种情况下你可以。但值得注意的是,awakeFromNib()是在init?(编码器)之后的某个时候调用的,而且也是在nib中的对象连接到它们的输出口outlet之后调用的。
例如,在init?(coder)中,nameLabel和artistNameLabel输出口仍然是nil,但在awakeFromNib()中,它们将正确地连接到它们的UILabel对象。所以,如果你想在代码中对这些输出口做些什么,你需要在awakeFromNib()中做,而不是在init?(coder)中。
这就是为什么awakeFromNib()是这类东西的理想位置——它类似于在视图控制器中使用viewDidLoad()。
不要忘记首先调用super.awakeFromNib()——这是必需的。如果您忘记了,那么超类UITableViewCell——或任何其他超类——可能没有机会初始化它们自己。
提示:在重写的方法中调用super.methodName(…)总是一个好主意,比如viewDidLoad()、viewWillAppear()、awakeFromNib()等等,除非文档中另有说明。
当你运行这个应用程序,搜索并点击一行,它应该是这样的:
添加App图标
当你进行到这里,你可能想给这个App一个图标。
➤打开资产目录(Assets.xcassets)并选择AppIcon组。
➤将图片从资源文件夹的Icon文件夹拖拽到匹配的位置。
记住,对于2x插槽,您需要使用两倍大小(以像素为单位)的图像。例如,将Icon-152.png文件拖放到iPad应用程序76pt, 2x中。对于3x,你需要将图像大小乘以3。”
运行应用程序,注意它现在有了一个漂亮的新图标:
app启动显示键盘
我想做的最后一个用户界面调整是,当你启动应用程序时,键盘应该立即可见,这样用户就可以立即开始打字。
➤在SearchViewController.swift中给viewDidLoad()添加以下代码:
searchBar.becomeFirstResponder ()
正如您从Checklists应用程序(Section 2中的App)中了解到的,becomeFirstResponder()将为searchBar提供“第一响应”并显示键盘。你输入的任何东西都会出现在搜索栏里。
➤试一试,commit你的改变。你重新设计了搜索栏,还添加了应用图标。
3. commit标签:
如果您查看到目前为止所做的各种提交,您将注意到一串奇怪的数字,比如“bb55701”:
这些都是Git用来唯一标识提交的内部数字(称为散列)。对于我们人类来说,这样的数字不是很好记,也不是很有用,所以Git还允许您用更友好的标签“标记”某个提交。
用Xcode给提交打上标签就像在Source Control navigator视图中选择commit一样简单,右键点击获得上下文菜单并选择Tag选项。
输入“v0.1”作为标签Tag,并提供一条描述该标签所包含内容的可选信息。然后单击Create创建标签。
您可以在源代码控制导航器视图中看到新标记:
Xcode在Git上运行得很好,但是您可能需要更强大的功能来执行复杂的Git操作。如果你这样做了,你可能需要学习如何使用终端,或者使用SourceTree这样的工具,这在Mac应用商店是免费的。
4. 调试器
Xcode有一个内置的调试器。不幸的是,调试器并不能把错误从程序中清除出去;它只是让它们以慢镜头碰撞,这样你就能更好地了解出了什么问题。
就像侦探一样,调试器允许您在造成损害之后挖掘证据,以便找到造成损害的歹徒。多亏了调试器,您不会在黑暗中跌倒却不知道发生了什么。相反,你可以用它来快速查明哪里出了问题。一旦你知道了这两件事,弄清楚为什么会出错就容易多了。
索引超出范围bug
让我们在应用程序中引入一个bug,这样它就会崩溃——知道当应用程序崩溃时该做什么是非常重要的。
➤更改SearchViewController.swift的numberOfRowsInSection方法:
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if !hasSearched {
. . .
} else if searchResults.count == 0 {
. . .
} else {
return searchResults.count + 1 // This line changes
}
}
在运行应用程序并搜索一些内容。应用程序崩溃,Xcode窗口变成这样:
崩溃是:Thread 1: Fatal error: Index out of range. (线程1:致命错误:索引超出范围)。听起来非常急!
根据错误消息,用于访问某个数组的索引大于数组中项的数量。换句话说,该指数“超出了范围”。这是数组中常见的错误,在您的编程生涯中,您可能不止一次地犯这种错误。
现在你知道哪里出了问题,最大的问题是:哪里出了问题?您的应用程序中可能有许多对array[index]的调用,您不希望必须遍历整个代码才能找到罪魁祸首。
幸运的是,您有调试器来帮助您。在源代码编辑器中,它已经指出了有问题的一行:
重要的是:这条线不一定是崩溃的原因——毕竟,您没有在这个方法中更改任何东西——但它是崩溃发生的地方。从这里你可以追溯原因。
数组是searchResults,索引由indexPath.row给出。如果能深入了解行号就好了,有几种方法可以做到这一点。
我们将在这里看到的是使用调试器的命令行界面,就像电影中的黑客神童一样:]
➤在Xcode控制台,在(lldb)提示符之后,输入p indexPath.row并按回车键:
输出应该是这样的:
(Int) $R1 = 3
这意味着indexPath.row的值是3,类型是Int (你可以忽略$R1)。
我们也来看看数组中有多少项。
键入p searchResults并按enter键。如果您使用自动完成功能,请注意searchResult和searchResults都是选项,末尾没有“s”。一定要选对。
输出显示了一个包含三个项的数组。
现在您可以对这个问题进行推理了:table视图正在请求第4行(即索引3处的单元格)的单元格,但是数据模型中只有3行(第0行到第2行)。
table视图知道从numberOfRowsInSection返回的值中有多少行,所以可能这个方法返回的行数是错误的?
当然,这确实是原因所在,因为您故意在那个方法中引入了错误。
我希望这说明了你应该如何处理崩溃:首先找出崩溃发生的地方,找出真正的错误是什么,然后向后推理,直到找到原因。
Storyboard outlet bug
➤将numberOfRowsInSection恢复到之前的状态,然后向SearchViewController.swift添加一个新的outlet属性:
@IBOutlet weak var searchBar2: UISearchBar!
➤打开storyboard并从SearchViewController control -拖动到搜索栏Search Bar。从弹出框中选择searchBar2。
现在,搜索栏也连接到这个新的searchBar2 outlet——对于一个对象一次连接到多个outlet是完全没问题的。
➤在源代码中删除SearchViewController.swift中的searchBar2 outlet属性,而不是在storyboard中。
这对我来说是一个制造另一次崩溃的卑鄙伎俩。storyboard包含到不再存在的属性的连接。如果您认为这是一个复杂的示例,那么等到您在自己的某个应用程序中犯了这个错误时再做决定。这比你想象的要频繁得多!
运行应用程序,它会立即崩溃。崩溃是“Thread 1: signal SIGABRT”(线程1:信号SIGABRT)。
在Debug窗格中向上滚动Xcode控制台输出,应该会看到:
*** Terminating app due to uncaught exception 'NSUnknownKeyException', reason: '[<StoreSearch.SearchViewController 0x7fb83ec09bf0> setValue:forUndefinedKey:]: this class is not key value coding-compliant for the key searchBar2.'
*** First throw call stack:
(
0 CoreFoundation 0x0000000111da1c7b __exceptionPreprocess + 171
. . .
这条信息的第一部分非常重要:它告诉你这个应用程序因为一个“NSUnknownKeyException”而终止。在一些平台上,异常是一种常用的错误处理机制,但在iOS上,这总是一个致命的错误,应用程序被迫停止。
应该引起你兴趣的是:
this class is not key value coding-compliant for the key searchBar2
嗯,这有点神秘。它确实提到了searchBar2,但是“key value-coding compliant”(键值编码兼容)是什么意思呢?这种错误我已经见过很多次了,所以知道哪里出了问题,但是如果您是新手,那么这样的信息就不会很有启发性。
让我们看看Xcode认为崩溃发生在哪里:
这也不是很有用。Xcode说应用程序在AppDelegate中崩溃了,但这不是真的。
Xcode遍历调用堆栈,直到找到一个它拥有源代码的方法,也就是它所显示的方法。调用堆栈是最近调用的方法列表。你可以在调试器窗口的左边看到它。
➤点击Debug导航器底部最左边的图标来查看更多信息。
顶部的方法是最后一个被调用的方法——它实际上是一个函数,而不是方法。它从pthread_kill调用,它从abort调用,它从abort_message调用,等等,一直到main函数,这是应用程序的入口点也是应用程序启动时调用的第一个函数。
这个调用堆栈中列出的所有方法和函数都来自系统库,这就是为什么它们是灰色的。如果你点击其中一个,你会得到一堆难以理解的汇编代码:
很明显,这种方法不会给你带来任何好处。不过,您还可以尝试另一件事——设置异常断点(Exception Breakpoint)。
断点是代码中的一个特殊标记,它将暂停应用程序的执行并启动调试器。
当您的应用程序遇到断点时,应用程序将在该断点处暂停。然后,您可以使用调试器逐行逐行地遍历代码,以便以慢动作运行它。如果你真的弄不明白为什么有些东西会崩溃,这是一个很方便的工具。
您不会在本书中逐步了解代码,但是您可以在苹果开发人员支持站点的调试部分阅读更多相关内容。或者,您可以在Xcode的Help→Xcode Help菜单选项下检查您的应用程序调试主题。
您将设置一个特殊的断点,它将在发生致命异常时触发。这将使程序在即将崩溃时停止运行,这会让你对正在发生的事情有更多的了解。
➤切换到断点导航器(Breakpoint navigator),点击底部的+按钮添加一个异常断点(Exception Breakpoint):
这将添加一个新的断点:
➤现在再次运行应用程序。它仍然会崩溃,但Xcode显示了更多的信息:
现在调用堆栈中有更多的方法。让我们看看能不能找到一些线索来解释到底发生了什么。
引起我注意的是对[UIViewController _loadViewFromNibNamed:bundle:]的调用。这是在加载nib文件或本例中的故事板时发生此错误的一个很好的提示。
使用这些提示和线索,以及您在没有异常断点的情况下得到的有点神秘的错误消息,您通常可以找出是什么原因导致应用程序崩溃。
在本例中,我们已经确定应用程序在加载storyboard时崩溃,错误消息提到“searchBar2”。把两者结合起来,你就得到了答案。
在源代码中快速浏览一下就可以确认searchBar2 outlet不再存在于视图控制器中,但是storyboard仍然引用它。
➤打开storyboard,在连接检查器中断开搜索视图控制器与searchBar2的连接,以修复崩溃。那是另一个被解决的bug!
注意:启用异常断点意味着如果应用程序崩溃,您将不再在控制台中获得有用的错误消息—断点将在异常发生之前停止应用程序。
如果在稍后的开发过程中,您的应用程序在另一个bug上崩溃,您可能希望禁用此断点以实际查看错误消息。您可以通过简单地选择断点并单击深蓝色箭头,在断点导航器中实现这一点。如果箭从深蓝色变成淡蓝色,它就会失效。
总结:
如果你的应用程序在运行Xcode的时候崩溃了,Xcode调试器经常会显示一条错误消息,以及崩溃发生在代码的什么地方。
如果Xcode认为崩溃发生在AppDelegate上——这不是很有用!——添加一个异常断点(Exception Breakpoint)以获取更多信息。
如果应用程序是SIGABRT崩溃,但控制台中没有错误消息,则禁用可能存在的任何异常断点,使应用程序再次崩溃。或者,从“调试器”工具栏中多次单击“继续执行程序(Continue program execution)”按钮。最终这也将显示错误信息。
EXC_BAD_ACCESS错误通常意味着内存管理出了问题。一个对象可能被“释放”过多次,或者“保留”得不够。对于Swift,这些问题基本上都是过去的事了,因为编译器通常会确保做正确的事情。但是,如果您在与Objective-C代码或低级api对话,仍然有可能出错。
EXC_BREAKPOINT不是错误。应用程序在断点处停止,蓝色箭头指向应用程序暂停的行。您可以设置断点来在代码中的特定位置暂停应用程序,以便在调试器中检查应用程序的状态。“继续程序执行”按钮恢复应用程序。
这应该能帮助你弄清大多数App崩溃的真相!
build日志
如果您想知道Xcode在构建应用程序时实际做了什么,那么请查看报表导航器(Report navigator)。它是navigator窗格中的最后一个选项卡。
报表导航器跟踪您的构建和调试会话,以便您可以回顾发生了什么。它甚至能记住应用程序前几次运行的调试输出。
确保选中 All Messages。要获取关于特定日志项的更多信息,请选择该项并单击右侧出现的小细节图标。这一行将展开,您将确切地看到执行了哪些命令Xcode以及结果是什么。
如果您遇到一些奇怪的编译问题,那么这里就是进行故障排除的地方。此外,不时看看Xcode的动向也很有趣。