iOS apprentice中文版 - Chapter 33:自定义 Table Cells

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的动向也很有趣。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,064评论 5 466
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,606评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,011评论 0 328
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,550评论 1 269
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,465评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 47,919评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,428评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,075评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,208评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,185评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,191评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,914评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,482评论 3 302
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,585评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,825评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,194评论 2 344
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,703评论 2 339