新增待办事项
你现在有了一个新增待办事项的界面并且允许用户通过虚拟键盘在文本框中输入文字。同时app也可以通过有效的方式来判断输入的内容为空时不允许点击Done按钮提交。
但是你如何将用户输入的文本新增到Checklistiem对象中去并且将它在展示在待办事项的屏幕上呢?
你需要通过某种方法使新增待办事项界面能够通知Checklist视图控制器知道这件事情。这是每一个iOS app都具备的基础功能:从一个视图控制器发送消息到另一个。
练习:你会如何解决这个问题?done()方法需要使用用户输入的文本创建一个新的ChecklistItem对象,然后添加它到items数组中,并且同时添加到table view的视图控制器ChecklistViewController中。
也许你会这样开始:
class AddItemViewController: UITableViewController, . . . {
// This variable refers to the other view controller
var checklistViewController: ChecklistViewController
@IBAction func done() {
// Create the new checklist item object
let item = ChecklistItem()
item.text = textField.text!
// Directly call a method from ChecklistViewController
checklistViewController.add(item)
}
}
在这个方案中,AddItemViewController有了一个变量用于引用ChecklistViewController,并且done()方法调用它的add()方法来把这个新的ChecklistItem对象添加到ChecklistViewController。
这样的确游有用,但这不是iOS的方法。这个方案的最大缺点就是两个不同的视图控制器被禁锢在一起了。
原则上讲,如果界面A和界面B有交互,而你不想让界面B对界面A有所了解时,那么就不要把两个视图控制器捆绑起来。
让AddItemViewContrller直接引用ChecklistViewController,会使你无法在app的其他任意地方打开新增待办事项界面,只能退回到ChecklistViewController再打开新增待办事项界面,这是非常不好的,不利于app的灵活应用。
虽然我们这个app中也只有一个地方能打开新增代办事项界面,但是其他的app通常都可以从不同的地方跳转到同一个页面。例如:购物类app,总是有很多地方可以提交购买,恨不得整个屏幕都是提交购买,所以我们养成一个良好的习惯是必须的。在我们的下一个课程中,就会遇到这个情况。
因此AddItemViewController不要去管谁引用了它是最好的,它只需要对引用者作出回应就可以了。
但是这种情况下,两个对象之间应该如何通信呢?
答案是是使用委托。
你已经在不同的几个场合见过几次委托了:table view有一个委托用于响应被点击的行。文本框有一个委托用于验证用户是否输入了文本,并且app中也有一个文件叫做AppDelegate(可以在工程导航器中看到)。
基本上你出门买包烟斗能撞见一个委托。
委托模式被普遍的用于处理这种情况:界面A打开界面B。某些时候界面B需要给界面A返回一些信息,通常是界面B要关闭的时候。
我们的解决方法就是在界面A中创建关于界面B的委托,这样界面B就可以在任何时候返回A需要的信息。
委托模式中最棒的一点就是界面B无须知道任何界面A的内容。它仅仅知道一些对象是它的委托。除此以外,界面B并不关心它是谁。
就像UITableView不关心你的视图控制器一样,仅仅是当table view需要的时候传递table view cell给它。
原理上,当界面B和界面A通信时,它们是相互独立的,这被称作松耦合,是一种非常好的软件设计方式。
你将要使用委托模式来使AddItemViewController发送通知返回给ChecklistViewController。
委托和协议是手拉手出现的,这是Swift语言的一个重要特色。
在AddItemViewController.swift的顶部,import语句和class关键字之间添加以下代码(不要添加到class的花括号里面去):
protocol AddItemViewControllerDelegate: class {
func addItemControllerDidCancel(_ controller: AddItemViewController)
func addItemController(_ controller: AddItemViewController,didFinishAdding item: ChecklistItem)
}
这样就定义了一个叫做AddItemViewControllerDelegate的协议,protocol{...}内部就是方法的声明,但是和一般申明方法不一样,你不需要给它们写任何代码。协议仅仅是方法名称的列表。
把委托协议想象为界面B和任何想要使用这个委托协议的界面之间的一份合约,在我们这个例子里,这个界面是Add Item View Controller。
协议(Protocol)
在Swift中,协议指的并不是和电脑网络间的协议类似的东西。它仅仅是一组方法的名称列表。
协议并不执行任何它自己声明的方法。它仅仅表示:任何遵循这一协议的对象必须执行其中的方法。
AddItemViewControllerDelegate协议中的两个方法分别是:
addItemViewControllerDidCancel()
addItemViewController(didFinishAdding)
委托经常具备非常长的方法名称!
第一个方法是用于用户点击Cancel时,第二个方法是用于用户点击Done按钮时。在这个情况下didFinishAdding参数会传递新的ChecklistItem对象。
为了使ChecklistViewController遵循这一协议,它必须执行以上两个方法。从那时起,你可以使用协议名称引用ChecklistViewController。(如果你有其他语言的编程经验,你会发现协议和接口的概念非常类似)
在AddItemViewController中,你可以使用以下语句转回到ChecklistViewController:
var delegate: AddItemViewControllerDelegate
这个delegate变量仅仅是引用执行这些协议中的方法的对象。你可以在完全对这些对象不知情的情况下,从delegate变量向这些对象发送消息。
当然,你现在知道委托中的对象是ChecklistViewController,但是AddItemViewController并不知道。它只知道某些对象执行了这些委托方法。
如果你想的话,你可以让任何对象执行这个协议并且AddItemViewController不会有任何异议。这就是委托的能力:让AddItemViewController和app的其他地方的依赖关系解除了。
这些内容对于我们这个简单的app而言有些过头了,但是委托是iOS开发中的基础部分之一,你越早掌握越好。
我们在AddItemViewController.swift中的工作还没有做完。这个视图控制器必须有一个属性可以指向这个委托。
在class AddItemViewController的内部,outlet的下面添加以下语句:
weak var delegate: AddItemViewControllerDelegate?
这看起来和一般声明实例变量的方法一样,有两点小区别:weak和问号。
委托通常都以weak形式声明,这里的weak并不是字面意思,而是描述委托和视图控制器之间的关系。同时委托也是可选型的(问号就是代表可选型)
我们会在后面深入的学习这两个知识点。
将cancel()和done()方法替换为下面的样子:
@IBAction func cancel() {
delegate?.addItemControllerDidCancel(self)
}
@IBAction func done() {
let item = ChecklistItem()
item.text = textField.text!
item.checked = false
delegate?.addItemController(self, didFinishAdding: item)
}
我们来看看这些变化。当用户点击Cancel按钮,你发送addItemControllerDidCancel()信息返回给委托。
Done按钮的处理方法类似,除了消息是addItemController(didFinishAdding)并且传递了一个包含用户输入文本的ChecklistItem对象。
⚠️:委托方法通常以第一个参数来指代它们的属主。
这样做不是必须的,但是是更好的。例如,对于table view来说也许会出现这种情况,一个委托的对象或者数据源对应多个table view。在这种情况下,你需要区别每个table view。为了实现这个目的,table view的委托方法必须有一个参数用于指代接收消息的UITableView对象。这样可以避免使你必须给每个table view声明一个outlet。
这就解释了为什么你传递self给你的委托方法。回忆一下,self指代的是对象自己,在这里例子里就是AddItemViewController。这也是为什么方法都以addItemViewController开头。
运行app并且试试效果,你会发现Done和Cancel按钮根本不工作了!
我希望你没有太晕。。现在新增待办界面依赖一个委托来关闭它,但是你还没有告诉新增待办事项界面它的委托是谁。
这意味着delegate根本没有值并且也没有给任何人发送消息,因为没有人听它说话。
可选型(Optionals)
我有几次提到过Swift中的变量或者常量必须有一个值。在其他编程语言里这个特殊的符号nil或者NULL经常被用于表示一个变量没有值。在Swift中这是不允许的。
nil和NULL经常会使app挂掉。当一个变量突然变为nil时,app就会挂掉。这就是令人闻风丧胆的“null pointer dereference(空指针运算)”
swfit通过禁止使用nil来避免这一情况。
无论如何,变量有时确实不会有值。在这种情况下你可以将变量声明为可选型。在swfit中你通过使用问号或者感叹号来标注某些东西为可选型。
只有可选型变量可以拥有nil这个值。
你已经见过IndexPath?这种可选型变量了,它是tableView(willSelectRowAt)的返回类型。这个方法返回nil是合法的;它意味着这个表格中不能选定这一行。
问号的作用是告诉swift这个方法返回nil是可行的。
引用委托的变量通常也是可选型的。你可以通过末尾的问号来声明可选型:
weak var delegate: AddItemViewControllerDelegate?
感谢这个问号,它使得委托可以接受nil。
你也许想知道为什么委托有时会是nil,有两个原因。
委托确实经常是可选的;一个UITableView可以在不执行委托方法的情况下也可以运行(但是必须有数据源方法)。
更重要的是,当AddItemViewController被故事模版读取并且实例化的时候,它并不知道它的委托是谁。在视图控制被读取与delegate变更被分配的这段时间内,delegate变量就是nil,虽然这段时间很短暂,但是delegate必须是可选型的。
当delegate为nil时,你不想cancel()和done()发送任何消息。这样做会使app挂掉,因为此时没有任何对象接受消息。
当delegate没有配置好的时候,swift有一种非常便利的方法处理这一情况:
delegate?.addItemControllerDidCancel(self)
这里的问号告诉swift如果delegate为nil的话就不要发送消息。你可以把它读作:“delegate在这里吗?”如果存在则发送消息,否则就不发,这一行为叫做“可选型链接”,我们会频繁的用到它。
在我们的app里delegate永远不会为nil,这样会使用户卡在新增待办事项界面,但是swift并不知道这一点,所以你需要假装delegate会为nil,并且使用可选型链接来向delegate发送消息。
在其他编程语言里可选型并不常见,所以使用其他语言的人学习swift时,会花一些时间来习惯可选型。我发现可选型使程序更加整洁,大多数变量永远不会为nil,所以最好避免它们为nil并且避免这些事情带来的BUG。
记住,如果你在swift中见到问号或者感叹号,就是你正在面对可选型。在我们的课程中,我们有时会回到这个课题几次并且展示更多的使用可选型的细节。
在你可以给AddItemViewController它的委托之前,你首先要让ChecklistViewController扮演委托的角色。
打开ChecklistViewController.swift,改变类声明的这一行,像下面这样:
class ChecklistViewController: UITableViewController,AddItemViewControllerDelegate
这样就告诉了编译器现在ChecklistViewController承诺执行AddItemViewControllerDelegate协议中的内容。
如果你试着运行app的话,Xcode会给出一个报错:“Type 'ChecklistViewController' does not conform to protocol 'AddItemViewControllerDelegate'”,意思是ChecklistViewController没有遵守协议AddItemViewControllerDelegate。这是正常的,你还需要把AddItemViewControllerDelegate中的方法都添加进去。
将承诺履行的方法都添加到ChecklistViewController中:
func addItemControllerDidCancel(_ controller: AddItemViewController) {
dismiss(animated: true, completion: nil)
}
func addItemController(_ controller: AddItemViewController, didFinishAdding item: ChecklistItem) {
dismiss(animated: true, completion: nil)
}
目前方法仅仅是简单的起到关闭新增待办事项界面。这是曾经AddItemViewController中的cancel()和 done()动作做的事情。你只是简单的把这个功能移到了委托中。
把新的ChecklistItem对象放进table view的代码我们先等一等。我们需要先来了解点别的事情。
五步定义委托
在两个对象之间创建委托可以用以下固定的步骤,比如对象A是对象B的委托,对象B发送消息返回给对象A,步骤如下:
1、为对象B定义一个委托协议
2、给对象B一个可选型的委托变量,这个变量必须是weak的。
3、当某些事件触发时,让对象B发送消息到它的委托,比如用户点击Cancel或者Done按钮时,或者它需要一点信息时。你可以用语句delegate?.methodName(self,...)来完成这个功能。
4、让对象A遵守委托协议。将协议的名称放入类声明,class的哪一行,并且执行协议中的方法列表。
5、告诉对象B,对象A现在是你的委托了。
你现在已经做了步骤1-4,所以你还需要把步骤5补上:告诉AddItemViewController现在ChecklistViewController是它的委托了。
做这件事最合适的时机就是把它放在prepare(for:sender:)方法中,就是用于转场的那个方法。
prepare(for:sender:)方法会在一个界面将要向另一个界面转场时被UIKit调用。回忆一下转场就是故事模版中两个视图控制器之间的那个大箭头。
使用prepare(for:sender:)可以使你在新的视图控制器展现前向它发送数据,通常会在这一时刻对新界面进行各种配置。
在ChecklistViewController.swift中添加以下方法:
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
//1
if segue.identifier == "AddItem" {
//2
let navigationController = segue.destination as! UINavigationController
//3
let controller = navigationController.topViewController as! AddItemViewController
//4
controller.delegate = self
}
}
这就是prepare(for:sender:)的工作,非常步骤化的:
1、因为前一个视图控制器可能具有多个转场,所以最好给每一个转场赋予一个独一无二的名称以区分它们,并且每次操作前先检查是不是正确的转场。swift中的“==”等于比较符,不仅用于数字,也可以用于字符串和大多数类型的对象。
2、新的视图控制器可以通过segue.destination被找到。故事模版中转场并不是直接指向AddItemViewController,而是指向包含它的导航控制器(navigation controller)。因此首先你要抓取到这个UINavigationController对象。
3、为了找到AddItemViewController,你可以查看导航控制器的顶层视图(topViewController)属性。这个属性正是引用目前被嵌入导航控制的界面。
4、一旦你有了一个指向AddItemViewController的引用,你设置它的delegate属性到self,至此这个链接就完毕了。从现在起AddItemViewController就知道了,这个self指代的对象就是它的委托。但是这里的“self”是什么呢?很简单,当你在ChecklistViewController.swift中写了self时,这个self就指代ChecklistViewController。
非常棒!ChecklistViewController现在就是AddItemViewController的委托了,这花了你不少功夫,但是这些工作都值了。
打开故事模版选定右边的Checklist View Controller与Navigation Controller之间的转场。
在属性检查器中,找到Identifier并且键入AddItem:
运行app看看是否一切正常。(确保运行前保存一下,否则app会挂掉)
点击➕号按钮会执行带有设置Checklist界面做为它的委托的到新增待办事项界面转场。
当你点击Cancel或者Done按钮,AddItemViewController会发送信息到它的委托—ChecklistViewController。目前这个委托仅仅是简单的关闭新增待办事项界面,现在是我们来完善这个功能时候了。
让我们把新的ChecklistItem添加到数据模型和table view中,这一刻终于来临了。
打开ChecklistViewController.swift,改变“didFinishAdding”方法为下面这个样子:
func addItemController(_ controller: AddItemViewController, didFinishAdding item: ChecklistItem) {
let newRowIndex = items.count
items.append(item)
let indexPath = IndexPath(row: newRowIndex, section: 0)
let indexPaths = [indexPath]
tableView.insertRows(at: indexPaths, with: .automatic)
dismiss(animated: true, completion: nil)
}
这很大程度上和你之前在addItem()中做的相似。事实上,我只是简单的把addItem()中的内容黏贴到这个委托方法中了。自己对比一下两个方法看看。
唯一的区别就是你不在这里创建ChecklistItem对象,而是在AddItemViewController中创建。你只不过是把这个新对象插入到items数组中了。
和以前一样,你告诉table view这里有新的一行需要插入,并且之后关闭新增待办事项界面。
在ChecklistViewController.swift中删除addItem()方法,你不在需要它了。
为了以防万一,打开故事模版,检查➕号按钮不再链接到addItem()了,如果这个链接还在的话,会发生不好的事情。
(你可以打开➕号按钮的链接检查器,看看Sent Actions下面是否还存在链接,如果有的话,点击小x号按钮把它删除掉)
运行app,你已经可以将自己输入的待办事项增加到主界面的列表中了。
weak
我还欠你一个关于weak的解释。两个对象之间的关系可以是weak或者strong。使用weak可以避免所谓的循环引用(ownership cycles)。
当对象A和对象B的关系是强引用,并且同时对象B对对象A也是强引用的时候,这两个对象就陷入了一种危险的关系:循环引用。
通常,当一个对象不再有其他对象强引用它时,这个对象就会被释放掉。但是如果A和B相互强引用的话,那么它们会互相保持存活,永远不会被释放。
当一个对象应该被释放的时候没有被释放会带来潜在的内存危机,它所占用的内存不会被重新分配。如果这样的对象足够多的话,iOS会耗尽内存导致系统挂掉。这是很危险的。
由于相互间的强引用,A拥有B,同时B拥有A。
为了避免循环引用,你需要把其中之一设置为弱引用,这就是weak关键字的作用。
在我们的例子里是一个视图控制器和它的委托,界面A对界面B是强引用,但是界面B对它的委托A是弱引用,这样就避免了循环引用。
(还有一种引用类型叫做“无主的unowned”,它和weak类似,也可以用于委托,区别在于weak变量允许为nil,你眼下不用记住这些)
@IBOutlets也通常使用weak关键字。这里并不是为了避免循环引用,而是为了清晰的表明视图控制器并不真正的拥有输出的视图。
在这节课中你学习了一些关于weak,strong,optionals(可选型)以及对象之间的关系的知识。这些都是swift中的重要概念,但是你可能需要消化一阵子,不要逃避这些概念。