iOS Apprentice中文版-从0开始学iOS开发-第十二课

模型-视图-控制器(MVC:Model-View-Controller)

没有任何一个关于iOS开发的课程可以逃避关于模型视图控制器的话题,简写为MVC,如果有,那么一定是假的iOS开发教程。

MVC是iOS中三大基本设计原理之一。其他两个你已经见识过了:delegation(委托):用一个对象代表另一个对象做一些事情;target-action(动作目标):连接事件—例如点击按钮,到一个action method(动作方法)。

模型-视图-控制器意味着你app的对象可以被分为三组:

1、模型对象(Model objects),这些对象包含你的数据以及数据之间的运算。例如,如果你正在做一个菜谱app,那么模型就应该由食谱组成。而在一个游戏中模型应该是玩家的等级,分数和怪物们的位置。

数据模型之间执行的运算通常被称为“业务逻辑”或者“域逻辑”。在我们这个app里,任务类别和里面具体的待办内容(比如会议类别里有很多条会议记录)就是数据模型。

2、视图对象(View objects),这些对象由屏幕上可见的部分组成:图片,按钮,标签,文本框,表格单元(table view cell)等等。在一个游戏中,可见的视图就是整个游戏世界,比如怪物,动画,子弹...。

一个视图可以通过对自己做图(简单说就是改变自己的外观),来响应玩家的输入,但是这不是典型应用程序处理事件的逻辑。许多视图,比如UITableView,可以在许多app中被应用,因为它不挑数据模型。

3、控制器对象(Controller objects),控制器是一种连接数据模型对象和视图的对象的对象。它监听视图上的信号,并且使数据模型做出某些运算来响应这个信号,然后将数据模型的新状态更新到视图上。

概念上讲,下图就是这三个模块的关系:

iOS Apprentice中文版-从0开始学iOS开发-第十二课_第1张图片
MVC的工作原理

view controller有一个主视图,它的可以拥有许多子视图作为它的成员。它并非在一个界面上同时拥有许多视图。最顶层的视图通常占据整个屏幕。你可以在storyboard上设计view controller界面的布局。

在我们的这个Checklists应用中,主视图(main view)就是UITableView,子视图就是表格单元(table view cell)。每个cell同样拥有属于它自己的一些子视图,比如标签和配件(那个对勾符号)。

一个view controller只控制app中的一个界面。如果你的app有多个界面,那么每一个界面都有属于自己的一个view controller和自己的视图。你的app跟随一个view controller到另一个。

你经常会需要创建你自己的view controller,但是iOS同时也具备一些已经为你准备好了的view controller,比如用语拍照的image picker controller(图像采集控制器),你写邮件使用的mail compose controller(邮件构成控制器),以及用于发送推特消息的推特页面。

⚠️:视图和视图控制器(Views vs view controller)
记住,一个视图和一个视图控制器是两种不同的东西。
一个视图是一种在屏幕上显示某种的东西的对象,例如按钮或者标签。视图就是你能看到的东西。
view controller是那种在后台工作的东西。它是视图和数据模型之间的桥梁。
许多初学者将它们的视图控制命名为FirstView或者MainView。这非常让人头疼!如果它是视图控制器,那么它的命名就应该以ViewController结尾,而不是以View结尾。
我真心希望苹果公司可以将view controller词组中的view这个单词给删掉,这样会减少很多误导。

创建数据模型

目前为止,你手工做了许多数据放到table view中。这些数据包括字符串文本以及一个可以被开关的对勾符号。

如你所见,你无法使cell在它被重用时还能记住其中的数据,这些数据在cell被重用时会被覆盖掉。

table view cell是视图的一部分。它们的目的是显示app中的数据,但是这些数据应该来自其他地方,那就是数据模型。

同时也记住这一点:table view中的行是数据,而cell是视图。table view controller是通过执行table view的数据源和委托方法来将两者联系在一起的一种东西。

iOS Apprentice中文版-从0开始学iOS开发-第十二课_第2张图片
table view的数据源从数据模型中获取数据并且将它放入cell

我们这个app中的数据源由许多待办事项组成。这里的每一项都有属于自己的一行。

对于每一个待办事项你需要存储两部分信息:文本(“Walk the dog”,“Brush my teeth”,“Eat ice cream”)以及对勾符号是否显示。

每一行都有两部分信息,所以你要为每一行创建两个变量。

首先我们会用一个很笨的方法来实现这个目的。它可以正常工作,但是不怎么聪明。虽然这不是最好的办法,但是我希望你可以跟随我做完这一步。

只有你明白为什么这个方法不好的时候,你才能明白那个好的方法好在哪里。

打开ChecklistViewController.swift,在class ChecklistViewController这一行下面添加以下实例变量:

class ChecklistViewController: UITableViewController {
    var row0text = "Walk the dog"
    var row1text = "Brush my teeth"
    var row2text = "Brush my teeth"
    var row3text = "Soccer practice"
    var row4text = "Eat ice cream"
...

这些变量定义在所有方法的外面,所以他们不是局部变量,因此它们可以被ChecklistViewController中的所有方法使用。

将数据源方法改变为下面这个样子:

override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return 5
    }
    
    
    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "ChecklistItem", for: indexPath)
        
        let label = cell.viewWithTag(1000) as! UILabel
        
        if indexPath.row % 5 == 0 {
            label.text = row0text
        } else if indexPath.row % 5 == 1 {
            label.text = row1text
        } else if indexPath.row % 5 == 2 {
            label.text = row2text
        } else if indexPath.row % 5 == 3 {
            label.text = row3text
        } else if indexPath.row % 5 == 4 {
            label.text = row4text
        }
        
        return cell
    }

运行app,你会看到和最开始一样,只显示5行。

你做这些的意义在哪里?你给每一行分配了一个实例变量,来代表每一行的文本。这5个新的实例变量组成了你新的数据模型。

在tableView(cellForRowAt)中,你通过indexPath来指出你要显示的是那一行,并且将相应的实例变量放入对应的cell中。

现在来修改对勾符号的相关逻辑。你不再将对勾符号和cell绑定,而是和行绑定。为了达到这一目的,你需要添加5个新的实例变量来跟踪每一行对勾符号的状态。这些新的变量也数据数据模型。

添加以下实例变量:

    var row0checked = false
    var row1checked = false
    var row2checked = false
    var row3checked = false
    var row4checked = false

这些变量的数据类型是bool(布尔)型的。你之前已经遇到过Int(整数),Float(浮点数)以及String(字符串)等数据类型。bool类型的数据取值范围仅有两个值:true和false。

bool是“boolean”的缩写,在、英国人乔治·布尔在很久以前发明了这一类型的逻辑,它成了现代计算机的基础。事实上计算机使用0和1来工作很大程度上是因为乔治·布尔。

你可以使用bool变量来记住某些东西是否是1或者0。bool型的变量经常以“是(is)”和“有(has)”这两个动词开头,比如“是饿了(isHungry)”,或者“有冰淇淋(hasIceCream)”。

如果第一行有一个对勾,那么实例变量row0checked为true,如果没有,则将row0checked设置为false。同样的,row1checked反应第二行是否有一个对勾符号。以此类推,其他的实例变量也是为这一目的服务的。

⚠️:编译器是怎么知道这些变量是bool型的?你从没在任何地方说明。
原因是Swift有一种非常聪明的机制,叫做类型推断(type inference),如果你没有具体生命变量类型的话,类型推断会根据变量的值来确定变量的类型。
因为你说了:“var row0checked = false”,由于false这个值仅可以被bool型变量使用,所以Swift推定row0checked为bool型。

处理每一行被点击后做出响应的委托方法现在应该使用这些新的实例变量来决定每一行是否应该存在一个对勾符号。

tableView(didSelectRowAt)应该被修改为下面这个样子,现在你不要跟着去改!仅仅是理解一下这段代码的内容:

override func tableView(_ tableView: UITableView,
                        didSelectRowAt indexPath: IndexPath) {
  if let cell = tableView.cellForRow(at: indexPath) {
    if indexPath.row == 0 {
      row0checked = !row0checked
      if row0checked {
        cell.accessoryType = .checkmark
      } else {
        cell.accessoryType = .none
      }
    } else if indexPath.row == 1 {
      row1checked = !row1checked
      if row1checked {
        cell.accessoryType = .checkmark
      } else {
        cell.accessoryType = .none
      }
    } else if indexPath.row == 2 {
      row2checked = !row2checked
      if row2checked {
        cell.accessoryType = .checkmark
      } else {
        cell.accessoryType = .none
      }
    } else if indexPath.row == 3 {
      row3checked = !row3checked
      if row2checked {
         cell.accessoryType = .checkmark
      } else {
        cell.accessoryType = .none
      }
    } else if indexPath.row == 4 {
      row4checked = !row4checked
      if row4checked {
        cell.accessoryType = .checkmark
      } else {
        cell.accessoryType = .none
      }
} 
}
  tableView.deselectRow(at: indexPath, animated: true)
}

非常明显,这段代码通过indexPath.row来寻找被点击的那一行,并且通过“row checked”这个实例变量执行一些逻辑。但是这里有一些你之前没见过的东西。

我们取其中一个if indexPath.row来仔细看看:

if indexPath.row == 0 {
      row0checked = !row0checked
      if row0checked {
        cell.accessoryType = .checkmark
      } else {
        cell.accessoryType = .none
      }
        ...

如果indexPath.row是0,那么就是说用户点击了第一行,其相应的实例变量是row0checked。

然后你对它做了一次逻辑运算:

row0checked = !row0checked

这个感叹号是逻辑非操作符。还有一些其他的逻辑操作符可以对bool型变量进行逻辑运算,比如and和or,很快你就会遇到它们。

逻辑非的运算相当简单,它就是对值取反。如果row0checked是true,那么!row0checked就是false,如果row0checked是false,那么!row0checked就是true。

把它理解为“not”:not yes就是no,not no就是yes,懂了吗?

当你的row0checked有了新的值以后,你就可以用它来判断是不是要显示对勾符号了:

if row0checked {
        cell.accessoryType = .checkmark
      } else {
        cell.accessoryType = .none

其他4行的逻辑和这个一样。只是用到的变量不是同一个。

我们可以把这段一个接着一个的if语句优化一下,写的更可读一些。

将tableView(didSelectRowAt)改变为下面这个样子:

override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        var isChecked = false
        
        if indexPath.row == 0 {
            row0checked = !row0checked
            isChecked = row0checked
        } else if indexPath.row == 1 {
            row1checked = !row1checked
            isChecked = row1checked
        } else if indexPath.row == 2 {
            row2checked = !row2checked
            isChecked = row2checked
        } else if indexPath.row == 3 {
            row3checked = !row3checked
            isChecked = row3checked
        } else if indexPath.row == 4 {
            row4checked = !row4checked
            isChecked = row4checked
        }
        
        if isChecked {
            cell.accessoryType = .checkmark
        } else {
            cell.accessoryType = .none
        }
        
        tableView.deselectRow(at: indexPath, animated: true)
    }

这下短多了!

注意一下这段逻辑将在cell上设置对勾符号移到了方法的底部。仅有这一处代码负责对勾符号的开关。

可以这样做的理由是,你将实例变量“rowXchecked”存储到了局部变量isChecked中。仅用这个临时的变量来记忆被选择的行是否应该显示一个对勾符号。

通过使用这个局部变量,你可以删除掉大量重复的代码,这是一本万利的事情。你还将所有行从原来单独的if语句中做了大量简化,把原来所有的逻辑移动了一个独立的if语句里。

⚠️:重复的代码会使得可读性极差。更坏的是,它们会带来很多隐患,引发一些极难定位的BUG。你应该盯紧每一个可以减少重复代码的机会。
练习:事实上,在之前那个很长的版本里确实存在一个BUG,你能指出来吗?它就发生在你黏贴复制代码的过程中。

运行app,并且很明显你可以看到,效果很不理想。你要连点两次才能使对勾消失掉。

为什么会这样?很简单:当你申明 rowXchecked 这些变量时,你将它们初始化为false。

所以row0checked和其他变量认为目前每一行上都不存在对勾符号,但是表格上确实显示的有一个。这是因为你在cell的配件属性里设置了显示对勾符号。所以你第一次点击的时候,会为你显示一个对勾符号,看起来就是你点击第一次时对勾符号没有消失,而第二次才消失。

换而言之,就是说数据模型(rowXchecked变量)和视图(cell中的配件对勾符号)不同步。

有几种办法可以解决这个问题:你可以将这些bool变量的初始值设置为true,或者你可以在storyboard中将cell中的对勾符号先去掉。

还有一种更本质的方法,这个不同步的结果其实和“rowXchecked”的初始值以及storyboard中的cell配件设计这两者其实没有太大的关系,真正的原因是你没有在tableView(cellForRowAt)中正确的设置cell的配件(对勾符号)属性的值。

当你请求一个新的cell时,你需要对它的全部属性进行配置。这样调用tableView.dequeueReusableCell(withIdentifier)就可以返回一个预先为每一行配置好对勾符号的一个cell。如果新的一行不需要显示对勾符号,那么你就在cell中预先关闭显示(反之亦然)。

开始打补丁。

在ChecklistViewController.swift中添加如下方法:

func configureCheckmark(for cell: UITableViewCell,at indexPath: IndexPath) {
        var isChecked = false
        
        if indexPath.row == 0 {
            isChecked = row0checked
        } else if indexPath.row == 1 {
            isChecked = row1checked
        } else if indexPath.row == 2 {
            isChecked = row2checked
        } else if indexPath.row == 3 {
            isChecked = row3checked
        } else if indexPath.row == 4 {
            isChecked = row4checked
        }
        
        if isChecked {
            cell.accessoryType = .checkmark
        } else {
            cell.accessoryType = .none
        }
    }

这个新的方法会通过指定indexPath来照看某一行的cell,并且依据相应的“rowXchecked”变量来决定是否显示对勾符号,如果“rowXchecked”为true则显示,如果为false则不显示。

这里的逻辑看起来非常眼熟!唯一的区别在于,你无需在控制“rowXchecked”的状态,而仅仅是通过读取它的值来配置cell的配件。

你需要在tableView(cellForRowAt)中调用这个方法,就在你返回cell之前调用。

将这个方法改变为下面这个样子(回忆一下,...三个点的省略符号代表不要动前面的代码):

override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        ...
        configureCheckmark(for: cell, at: indexPath)
        return cell
    }

再一次运行app。

现在一切都正常了。所有行初始都不显示对勾符号。点击其中的一行,就会显示对勾符号,在点一次则关闭对勾符号。现在行和cell终于同步了。这些代码保证了每个cell总是和相应的行保持一致。

参数的外部名称和内部名称

这个新的方法configureCheckmark()有两个参数,for 和 at,因此它的全名是configureCheckmark(for:at:)。

for和at是所谓的参数外部名称。

在Swift中使用短小的介词是非常平常的一件事情,比如“at”,“with”或者“for”。它们使得方法的名称类似于英文中的短句。比如configureCheckmark(for:cell, at:indexPath)就可以读作:“configure checkmark for this cell at that index-path.(为行号为index-path的这一行的cell配置对勾符号)”,这样这个方法的功能是不是就脱口而出了?

当你调用这个方法的时候,你必须使用这些外部名称:

configureCheckmark(for:someCell   ,at:someIndexPath)

这里的someCell(某cell)是引用UITableViewCell对象的一个变量,同样的someIndexPath是类型为IndexPath的一个变量。

你千万不能写成下面这个样子:

configureCheckmark(someCell  ,someIndexPath)

这是不会被编译的。因为不存在一个名为configureCheckmark()的方法。for和at是方法名称中的一部分。

而在方法的内部,你使用cell和indexPath来引用方法的参数。

func configureCheckmark(for cell: UITableViewCell,at indexPath: IndexPath) {
        ...
        if indexPath.row == 0 {
           ...
        }
        ....
            cell.accessoryType = . checkmark
        }
    ...

你不能写成 if at.row == 0或者for.accessoryType = .checkmark。它们看起来也很古怪,不是吗?

这种将内部名称和外部名称分离开的是Swift和Object-C独有的,如果你以前使用别的语言编程的话,那么你需要花一点时间去适应它。

你可以使用configureCheckmark(for:at:)做一些事情来来简化tableView(didSelectRowAt)。

把tableView(didSelectRowAt)改为下面这个样子:

override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        var isChecked = false
        
        if let cell = tableView.cellForRow(at: indexPath) {
        if indexPath.row == 0 {
            row0checked = !row0checked
        } else if indexPath.row == 1 {
            row1checked = !row1checked
        } else if indexPath.row == 2 {
            row2checked = !row2checked
        } else if indexPath.row == 3 {
            row3checked = !row3checked
        } else if indexPath.row == 4 {
            row4checked = !row4checked
        }
        
        configureCheckmark(for: cell, at: indexPath)
        }
        
        tableView.deselectRow(at: indexPath, animated: true)
    }

这个方法不在对cell中的对勾符号做任何设置了,而仅仅是改变数据模型中“rowXchecked”的开关状态,然后通过调用configureCheckmark(for: at: )来更新视图中的对勾符号是否存在。

运行app,app依然工作正常。

把5个已声明好的实例变量改成下面这个样子,然后再次运行app:

    var row0checked = false
    var row1checked = true
    var row2checked = true
    var row3checked = false
    var row4checked = true

现在第2,3,5行被初始化为有对勾符号,其他行没有。

iOS Apprentice中文版-从0开始学iOS开发-第十二课_第3张图片
数据模型和table view cell完全同步

通过这个方法使哪一行有对勾符号而哪一行没有的情况工作良好,但是你得承认通过手动设置每一行效率太低了。

仅有5行的时候是完全没问题的,但是假如你有100行呢?你要去添加另外95个“row text”和“row checked”吗?我希望你没有这样做。

我们有一个更好的办法:数组(arrays)

数组(arrays)

数组是一种有序列表的对象。如果你把变量想象为容纳一个值的容器(或者会说对象),那么数组就是能够容纳许多值的容器。

iOS Apprentice中文版-从0开始学iOS开发-第十二课_第4张图片
数组可以容纳许多值

当然,数组本身也是一个对象(Array对象),你可以把数组放到一个变量里面。因为数组是一种对象,所以数组也可以容纳其他数组。

iOS Apprentice中文版-从0开始学iOS开发-第十二课_第5张图片
在数组中放入其他数组

数组内的对象会被给予数字索引,通常是从0开始。请求数组中的第一个对象,使用array[0]语句,第二个是array[1],以此类推。

数组是有序的,这就是说它会对其中包含的内容排序。index 1总是出现在index 0之后。

⚠️:数组是一种所谓的集合对象。还有一些其他类型的集合对象,它们都以不同的方式组织其中的包含的对象。比如说字典(Dictionary),是以“键-值(key-value)”方式组织其内部数据的,就和真正的字典由字和字的释义组成一样。在我们后面的课程中你会用到这些集合类型。

数组组织数据的方式和列表以行组成这种方式非常相似,它们都是以某种序列将数据列出来,所以道理上讲你可以将列表的数据模型放到数组里。

数组的每个索引存储一个对象,但是目前你的每一行中包含两个独立的数据:文本和对勾符号的状态。如果你的每一行仅包含一个数据的话事情就简单多了,因为此时每一行的行号正好和数组的index一一对应。

让我们来讲文本和对勾符号放到一个属于你自己的新的对象中!

选择工程导航器中的Checklists组(黄色文件夹图标的那个,不是蓝色的那个),然后点击鼠标右键,选择New File...

iOS Apprentice中文版-从0开始学iOS开发-第十二课_第6张图片
向工程中添加一个新的文件

在Source分节中选择Swift File:

iOS Apprentice中文版-从0开始学iOS开发-第十二课_第7张图片
选择Swift File

点击Next继续。将文件命名为ChecklistItem(不需要加.swift):

iOS Apprentice中文版-从0开始学iOS开发-第十二课_第8张图片
保存新的swift文件

最后点击Create,就完成了文件的创建:

iOS Apprentice中文版-从0开始学iOS开发-第十二课_第9张图片
新的文件已经出现在工程导航器中了

在新的ChecklistItem.swift文件中,紧接着import Foundation这一行下面添加以下语句:

class ChecklistItem {
    var text = ""
    var checked = false
}

你所看到的这些就是制作一个新的对象所需的最少材料。class关键字定义对象的名称,然后其中的两个实例变量作为数据项。

text属性负责存储待办项目的名称(就是table view cell上显示的文本内容),checked属性决定这个cell是否显示对勾符号。

⚠️:你也许想知道属性和实例变量是如何区分的,它们俩都引用一个对象的数据项。你可以高兴的听到这个事实,它俩没啥区别,就是一种东西。
在swift术语中,一条属性就是一个变量或者一个常量的上下文中使用的一个对象。其实这就是所谓的实例变量。所以你可以交替使用属性和实例变量。
(在Object-C中属性和变量仅仅是相似,而在swfit中,属性和变量是相等的)
(译者语:这段概念比较抽象,如果你不理解也没关系,就把它们都当变量处理)

目前对ChecklistItem做这些事就够了。这个ChecklistItem对象目前仅用来将text和checked组合为一个对象。过会,我们会继续加工它。

在你正式开始使用数组前,我们先来将之前view controller的String和Bool实例变量替换为新的ChecklistItem对象类型。

打开ChecklistViewController.swift,删掉之前的所有String和Bool型变量,并且替换为以下变量:

class ChecklistViewController: UITableViewController {
    var row0item: ChecklistItem
    var row1item: ChecklistItem
    var row2item: ChecklistItem
    var row3item: ChecklistItem
    var row4item: ChecklistItem
    ...

操作完毕后,你肯定会看到许多报错,那是因为view controller中还有许多地方在引用这些变量,但是你已经把它们删了。我们这就开始着手解决这些问题。

⚠️:我非常期望你自己用手敲代码,而不是黏贴复制。因为这样会使你的学习效率更高,并且投入程度更高。
虽然说复制黏贴更快,但是不幸的是,黏贴过程中很可能包含进去一些莫名其妙的特殊字符,这样Xcode会不停的向你抱怨。如果你非要黏贴复制的话,最好的办法就是先黏贴到纯文本编辑器中,然后在从纯文本编辑器中向Xcode中黏贴。
当然,如果你购买的是纸质版,那么恭喜你,你可以以最佳的方式进行学习了,你必须自己手动敲入代码。

还是在ChecklistViewController.swift中,找到tableView(cellForRowAt),把if语句替换为下面这个版本:

if indexPath.row == 0 {
            label.text = row0item.text
        } else if indexPath.row == 1 {
            label.text = row1item.text
        } else if indexPath.row == 2 {
            label.text = row2item.text
        } else if indexPath.row == 3 {
            label.text = row3item.text
        } else if indexPath.row == 4 {
            label.text = row4item.text
        }

找到tableView(didSelectRowAt),把if语句替换为下面这个版本:

if indexPath.row == 0 {
            row0item.checked = !row0item.checked
        } else if indexPath.row == 1 {
            row1item.checked = !row1item.checked
        } else if indexPath.row == 2 {
            row2item.checked = !row2item.checked
        } else if indexPath.row == 3 {
            row3item.checked = !row3item.checked
        } else if indexPath.row == 4 {
            row4item.checked = !row4item.checked
        }

最后,在configureCheckmark(for: at:)中,将if语句替换为:

if indexPath.row == 0 {
            isChecked = row0item.checked
        } else if indexPath.row == 1 {
            isChecked = row1item.checked
        } else if indexPath.row == 2 {
            isChecked = row2item.checked
        } else if indexPath.row == 3 {
            isChecked = row3item.checked
        } else if indexPath.row == 4 {
            isChecked = row4item.checked
        }

现在你用rowXitem.text和rowXitem.checked替代了原来独立的rowXtext和rowXchecked两个变量。

这下我们就处理了大部分的错误,但不是全部的还。Xcode仍然对一件事情不满,它说:“Class ChecklistViewController has no initializers(ChecklistViewController没有初始化).”之前版本是不会有这个报错的,为什么现在有了呢?

之前你在声明row text和row checked变量的时候都给了它们一个初始值,像下面这样:

var row0text = "Walk the dog"
var row0checked = false

但是对于新的ChecklistItme你无法这样做,因为ChecklistItem由两个值组成。

你通过所谓的类型注释(type annotation)告诉swift这个row0item是一个ChecklistItem类型的对象:

var row0item: ChecklistItem

但是此时row0item并没有实际的值,它只是一个ChecklistItem对象的空的容器。

这就是问题所在了,在Swift中,所有的变量总是具备一个值,容器永远不能是空的。

如果你在声明一个变量的时候不能给它一个值,那么你就要通过一个叫初始化方法(initializer method)给它一个值。

将下面的代码添加到ChecklistViewController.swift中,这是一种特殊类型的方法(你可以看到它的名称前没有func关键字)。习惯上这个方法会写在比较靠顶部的地方,紧接着实例变量的下面。

required init?(coder aDecoder: NSCoder) {
        row0item = ChecklistItem()
        row0item.text = "Walk the dog"
        row0item.checked = false
        
        row1item = ChecklistItem()
        row1item.text = "Brush my teeth"
        row1item.checked = true
        
        row2item = ChecklistItem()
        row2item.text = "Learn iOS development"
        row2item.checked = true
        
        row3item = ChecklistItem()
        row3item.text = "Soccer practice"
        row3item.checked = false
        
        
        row4item = ChecklistItem()
        row4item.text = "Eat ice cream"
        row4item.checked = true
        
        super.init(coder: aDecoder)
    }

Swift中的每一个对象都有一个init方法或者初始化器,有些对象甚至有多个。

当对象形成时Swift会调用init方法。

对view controller而言,在app启动阶段,它被从storyboard取出时,会调用它的init?(coder)方法。

这使得init?(coder)成为了将值放入任何变量的理想场所。(很快你会学习更多的关于coder参数的作用)。

在init?(coder)中,首先你创建了新的ChecklistItem的对象:

var row0item: ChecklistItem

接下来设置它的属性:

row0item.text = "Walk the dog"
row0item.checked = false

然后你将上述工作重复了4次。使表格中的每一行得到自己的ChecklistItem对象,之后存储它们的实例变量。

从本质上讲,我们现在做的事情和以前一模一样,除了这次一我们将原先独立的text和checked变量整合到了一个对象中,它们不再是view controller的实例变量,而是ChecklistItem对象的属性。

运行app,确定一下它的功能和以前一样。

将text和checked属性放倒它们自己的ChecklistItem对象中是一个很大的进步,但是这远远不够。

目前的这种办法,你需要为每一行单独创建一个ChecklistItem的实例变量。这不是一个好办法,特别是你需要处理很多行,而不是只有四五行的时候。

是到了引入数组的时候了!

打开ChecklistViewController.swift,删掉所有的实例变量,用一个名为items的数组代替它们:

class ChecklistViewController: UITableViewController {
    var items: [ChecklistItem]

现在你用一行取代了五行,每一行都是数组中的一个变量。

声明一个数组和我们之前声明变量的方式差不多,只是多了一对方括号将ChecklistItem围了起来。这一对方括号表明了这是一个数组。

将init?(coder)修改为下面这个样子:

required init?(coder aDecoder: NSCoder) {
        items = [ChecklistItem]()      //新增这一行
        
        let row0item = ChecklistItem()     //添加let
        row0item.text = "Walk the dog"
        row0item.checked = false
        items.append(row0item)    //新增这一行
        
        let row1item = ChecklistItem()          //添加let
        row1item.text = "Brush my teeth"
        row1item.checked = true
        items.append(row1item)             //新增这一行
        
        let row2item = ChecklistItem()          //添加let
        row2item.text = "Learn iOS development"
        row2item.checked = true
        items.append(row2item)          //新增这一行
        
        let row3item = ChecklistItem()         //添加let
        row3item.text = "Soccer practice"
        row3item.checked = false
        items.append(row3item)               //新增这一行
        
        let row4item = ChecklistItem()    //添加let
        row4item.text = "Eat ice cream"
        row4item.checked = true
        items.append(row4item)             //新增这一行
        
        super.init(coder: aDecoder)
    }

这和之前区别不大,除了在一开始新创建了一个数组对象:

items = [ChecklistItem]()

我们之前说了 [ChecklistItem]这个符号是代表一个ChecklistItem型的数组。但这只是items变量的数据类型;它还不是一个真正的数组对象。

为了得到一个数组对象我们需要去构建它。这就是()这对括号的作用:它们告诉Swift,这是一个数组型对象。

可以这样理解这件事情,数据类型就像汽车的品牌,你只说一句“保时捷”,是不会凭空出现一辆车的,你必须花钱去经销商那里买。

而我们跟随在数据类型后面的那对括号()就是代表去经销商对象那里买一个特定类型的对象。这对圆括号负责告诉Swift的对象工厂:“为我建造一个ChecklistItem型的数组对象”。

记住这一点非常重要,仅仅是声明你有一个变量是不能自动创建一个相应的对象的。这个变量仅仅是对象的容器。你还需要实例化这个对象并且将它放入容器中。变量只是一个盒子,而对象是盒子中的内容。

所以直到你从工厂中订购一个实际的ChecklistItem型的数组对象并且将它放入items前,这个变量都是空的。而空的变量是Swift中的大忌。

通过代码详细说明一下:

var items: [ChecklistItem]
//这一行声明了items会用来存储一个ChecklistItem对象的数组
//但是它并没有实际创建一个数组
//在这一时刻,items还没有值

items = [ChecklistItem]()
//这一行实例化了这个数组。现在items包含了一个有效的数组对象
//但是这个数组内部还不存在ChecklistItem对象

每一次你生成一个ChecklistItem对象,你需要把它添加到数组中去:

let row0item = ChecklistItem()
这一行实例化了一个新的ChecklistItem对象。注意这里有一对圆括号

row0item.text = "Walk the dog"
row0item.checked = false
给这个新的ChecklistItem对象内部的数据项赋值。

items.append(row0item)
这一行负责将ChecklistItem对象添加到数组中去。

注意一下,你在创建每一个独立ChecklistItem对象时,你都使用了一对圆括号。

还有一点也很重要,现在row0item和其他四个都是init方法内部的局部常量了。它们不再是有效的实例变量名称(因为我们之前把它们都删了)。这就是为什么你要用let关键字重新声明它们,不这样做的话,编译器就会报错。

在init?(coder)中,你给予了items数组五个ChecklistItem对象。这就是你新的数据模型。

现在你的所有行都存到数组里了,你可以用它来简化table view的数据源和委托方法。

将下面的几个方法改变一下:

override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "ChecklistItem", for: indexPath)
        
        let item = items[indexPath.row]
        let label = cell.viewWithTag(1000) as! UILabel
        label.text = item.text
        configureCheckmark(for: cell, at: indexPath)
        return cell
    }
    
    override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        
        if let cell = tableView.cellForRow(at: indexPath) {
        let item = items[indexPath.row]
        item.checked = !item.checked
        configureCheckmark(for: cell, at: indexPath)
        }
        
        tableView.deselectRow(at: indexPath, animated: true)
    }
    
    func configureCheckmark(for cell: UITableViewCell,at indexPath: IndexPath) {
        let item = items[indexPath.row]
        
        if item.checked {
            cell.accessoryType = .checkmark
        } else {
            cell.accessoryType = .none
        }
    }

这比之前就简单太多了!每个方法现在仅仅只有几行。

在每个方法中你使用:

let item = items[indexPath.row]

来向数组请求与行号对应的索引中的ChecklistItem对象。当你有了这个对象,你可以简单的访问其中的text和checked属性,然后去做你想做的事。

如果你想要增加100个待办事项,也不需要对那些方法做出变更。它们处理五行和处理一百行是一样的。甚至上千行也是一样的。

对于待办事项的数量,你现在可以改变为返回itmes数组中的对象数量,而不是用指定的数字。

将tableView(numberOfRowsInSection) 改变为下面这个样子:

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

这样做不仅使代码可读性增加,而且它可以返回任意多的行,数组中有多少行就返回多少行。

运行app,看看效果。它的运行效果和之前没什么区别,但是内部的结构优化的很好了。

练习:添加一些新的行到列表中去。你仅仅需要在init?(coder)中操作就可以了。

清理代码

我们还可以对代码进一步优化。

将 configureCheckmark(for:at:)方法改变为下面这个样子:

func configureCheckmark(for cell: UITableViewCell,with item: ChecklistItem) {
        if item.checked {
            cell.accessoryType = .checkmark
        } else {
            cell.accessoryType = .none
        }
    }

我们改变了这个方法的参数,用直接传递一个ChecklistItem对象取代了index-path。

这又是一个关于参数名称的例子,它的外部名称为with。现在这个方法的全名叫做configureCheckmark(for:with:),在app中的其他地方,你通过这个名字来调用它。在方法的内部,这个新的参数叫做item,这是它的内部名称。

为什么我们要改变这个方法呢?之前它接受一个index-path并且根据这个值找到相应行的ChecklistItem:

let item = items[indexPath.row]    //之前版本

但是我们在“cellForRowAt”和“didSelectRowAt”中已经那样做了。所以我们可以简单的把上面两个方法中已经找到的ChecklistItem对象传递到configureCheckmark()中就可以了,并不需要再找一遍。所以我们可以简化configureCheckmark()。

同时添加一个新的方法:

func configureText(for cell: UITableViewCell,with item: ChecklistItem) {
        let lable = cell.viewWithTag(1000) as! UILabel
        lable.text = item.text
    }

这个方法的作用是将checklistitem的文本放到cell的文本中。之前我们单独写语句完成这个功能,现在我们用一个方法替代它。

在tableView(cellForRowAt)中调用这个新的方法:

override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "ChecklistItem", for: indexPath)
        
        let item = items[indexPath.row]
        configureText(for: cell, with: item)
        configureCheckmark(for: cell, with: item)
        return cell
    }

同时也改变下tableView(didSelectRowAt):

override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        
        if let cell = tableView.cellForRow(at: indexPath) {
        let item = items[indexPath.row]
        item.toggleChecked()
        configureCheckmark(for: cell, with: item)
        }
        
        tableView.deselectRow(at: indexPath, animated: true)
    }

这个方法不再自己修改ChecklistItem中的checked属性,而是调用一个新的方法toggleChecked()来修改它。

你现在要把这个新的方法写到代码中去,否则app是不会工作的。

打开ChecklistItem.swift并且添加这个方法:

func toggleChecked() {
        checked = !checked
    }

如你所见,这个方法和之前“didSelectRowAt”中之前做的事一样,只是你将它添加到ChecklistItem中,作为它功能的一部分。

一个好的面向对象设计原则是你应该尽可能的去让每个对象自己去改变自己的状态。在之前的版本是view controller执行对勾符号的开关状态,但是现在是由ChecklistItem自己去做这件事。

运行app,很明显运行结果没有变化,但是代码确实优化了很多,它现在处理上千行数据也不费事。

⚠️:清理烂摊子
如果app的运行结果一模一样,那么为什么我们花那么大的力气来做这些变更呢?其实只为一件事,代码更加整洁并且可以避免出现bug。通过使用数组你也使得程序灵活多了。现在可以轻易的添加上百行到table view中。
你会体验到,在你编程的过程中你会不断的重构你的代码使它们更加优化。因为不可能一次性把事情做到完美。
所以你在写代码的过程中会不断的重复优化它们的工作。写一会就会发现有乱的地方,然后你就需要去优化一下,然后继续写,一直重复这个过程。这个过程就称之为代码的重构,它只有开始,没有结束。
有很多开发者从来不清理它们的代码。结果就是出现所谓的“意面代码(想想你怎么能把一坨面条一根根清理出来)”,并且这种代码基本上使无法维护的。
如果你已经几个月没有看你的代码,并且在几个月后你来添加一些新功能或者修复一个bug的时候,你也许会重新重头看一遍才能理解它们是怎么工作的。
所以说就算了为了你自己,你也需要尽可能的将代码整理干净,否则真的当你需要将面条一根根清理出来的时候,就是一场灾难了。

你可能感兴趣的:(iOS Apprentice中文版-从0开始学iOS开发-第十二课)