美文网首页
iOS apprentice中文版 - Chapter 33:自

iOS apprentice中文版 - Chapter 33:自

作者: Jamwong | 来源:发表于2019-03-17 16:23 被阅读0次

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

    相关文章

      网友评论

          本文标题:iOS apprentice中文版 - Chapter 33:自

          本文链接:https://www.haomeiwen.com/subject/nuyemqtx.html