新增待办事项
你已经学会了在表格里添加新的一行,但是所有这些新添加的行的文本都是一样的。你现在要对addItem()做出修改,当这个方法被触发后打开一个新的界面,可以使用户自己输入文本。
我们的计划是:
1、使用故事模版创建新增待办事项的界面。
2、添加一个文本框,使用户可以输入文字。
3、识别用户点击的是Cancel还是Done按钮。
4、以用户输入的文本创建一个ChecklistItem对象。
5、将新创建的ChecklistItem对象添加到界面上。
一个新的界面意味着我们需要一个新的视图控制器,我们从添加一个新的界面到故事模版上开始。
从对象库中拖拽一个新的Table View Controller(不是View Controller)到故事模版的画布上。
也许你需要适当的缩小显示比例,可以在画布上右键点击鼠标选择zoom选项,或者使用画布底端的 -100% + 按钮。(也可以直接在画布的空白部分直接双击,控制显示比例)
选择Checklist View Controller上的➕按钮,按住ctrl拖拽到新的这个视图控制器上。
放开鼠标后会弹出一个菜单:
在这个菜单上有许多不同类型的链接供你选择。
我们选择Show方式连接。
这种类型的链接称作转场(segue)。
转场由两个视图间的一个大箭头代表:
运行app,看看会发生什么。
当你点击➕按钮时,会从右边滑出一个新的空白的表格。你可以点击返回按钮(就是名字是Checklist的那个),回到之前的界面。
你不需要写任何代码,你有一个会自己工作的导航控制器。
注意,现在➕按钮不再会添加新的一行到表格中。转场取代了按钮链接的动作方法。仅仅是为了以防万一,你还是需要移除这个按钮和addItem()之间的链接。
选择➕号按钮,打开链接指示器,点击addItem后面跟随的小的x符号,就可以删除这个链接了。
注意一下,在链接指示器中同时也显示了你刚刚添加的代表专场的链接(在Triggered Segues分节下)
你现在有了一个当你一点击➕就会滑动出来的新的table view controller。这正是你想要的:一个可以让你添加新的待办事项的界面,但是我们最好使用modal类型的转场(之前我们选择的是Show)。
单击选定那个代表转场的蓝色箭头。
转场和其他类型的对象一样(记住,所有的东西都是对象!),它也有属于自己的属性,并且你可以改变它们。
打开属性检查器,选择Kind选项中的:Present Modally。
你可以看到界面有明显的变化,在新的视图控制器上,导航栏消失了。这个新的界面不再属于导航层级的一部分,而是作为一个独立的界面,当它出现时,它会替代掉原来存在的视图。
运行app,看看效果。
你会发现,你没有办法回到上一个界面了。好像还不如我们刚才那个版本的效果。
我们想要的效果是一个带有两个按钮(一个Cancel,一个Done)的导航栏。(在有些app里是一个Send或者一个Save和一个Cancel),无论我们点击Cancel还是Done,都会关闭这个界面回到上一个界面,但是只有点击Done,才能将你做出的修改保存下来。
添加这样一个带有两个按钮的导航栏最简单的办法就是将这个界面嵌入到属于他自己的导航控制器中。这个步骤就和我们之前做过的一样:
选择新的这个table view controller,然后在菜单栏中选择Editor->Embed In->Navigation Controller.
现在故事模版看起来应该是这个样子了:
新的导航控制器插在两个表格视图之间。现在点击➕按钮则会通过modal类型转场到新的导航控制器上。
双击最右面的视图上的导航栏中间部分,将其重命名为Add Item(你也可以在导航控制器的属性检查器中设置这个名称)。
拖拽两个Bar Button Items到导航栏上,一个在左边位置,一个在右边位置。
选择左边一个按钮,在属性检查器中找到System选项并选择为:Cancel。
右边那个按钮在属性检查器中将System Item和Style选项都选择为Done。
不要直接去改变按钮的文本。这里的Cancel和Done模式是系统内建的按钮类型,会直接显示为Done和Cancel字样,如果你的iPhone设置不是英语,那么这两个按钮上的字会自动翻译为你所在国家的语言。
运行app,你就可以看到新的这两个按钮了。
新的按钮看起来不错!但是你还需要告诉它们当它们被点击后应该做些什么。
⚠️:Xcode也许会给出一个警告。“Prototype table cells must have reuse identifiers”,不用管它,我们很快会解决这个问题。
自己做一个视图控制器的对象
这个Cancel和Done按钮应该起到关闭新增代办事项界面并且回到主界面的作用,但是目前它们是办不到的,你点击这两个按钮后什么都不会发生。
在下一个课程中,你会学习直接在故事模版里实现“后退”转场,但是眼下你需要通过代码来实现这个功能,换而言之,你需要将这些按钮链接到它们的动作方法上。
那么你在哪里写这些方法呢?并不是在ChecklistViewController.swift中,因为这不是处理这些按钮的视图控制器。
取而代之的是,你需要专门做一个新的视图控制器源代码文件用于新增待办事项页面并且将它和你刚设计的这些场景链接起来。
右击工程导航器中的Checklists组(黄色文件夹图标的那个),并且选择New File...然后选择Swift File模版。
将文件名命名为AddItemViewController.swift。这样就将新的文件添加到工程中来,这个文件中目前除了几条注释和一行代码以外什么都没有。
添加以下代码到新文件中:
import UIKit
class AddItemViewController: UITableViewController {
}
这行代码告诉Swift你有了一个新的table view controller的对象,它的名字是AddItemViewController。我们稍后会完善其余的代码。首先,你要告诉故事模版,这里有了一个新的视图控制器。
打开故事模版,选择Add Item视图(点击下图中那个黄色图标)然后打开身份检查器,在Custom Class中,输入AddItemViewController。
这样就告诉故事模版新增的AddItemViewController对象是用于控制这个这个新的场景的:
千万不能漏掉这一步!没有这一步的话,新增的Add Item界面根本不会工作。
在你更改身份检查器前,确保你选中的是视图控制器(上图中半部分被选中的那个黄色图标)。一个经常出现的错误就是没有正确的选中视图控制器。
你现在可以在AddItemViewController.swift中实现动作方法了.
添加Cancel()和done()动作方法:
@IBAction func cancel() {
dismiss(animated: true, completion: nil)
}
@IBAction func done() {
dismiss(animated: true, completion: nil)
}
这样就可以通过一个小动画平滑的关闭掉Add Item界面了。
你还需要将Cancel按钮和cancel()动作链接起来,以及把Done按钮和done()动作链接起来。
打开故事模版并且找到Add Item View Controller。按住ctrl拖拽按钮到黄色的图标上,并且在弹出的菜单中选择对应的动作。
运行app试试看。这下点击Cancel和Done按钮后,都会关闭当前页面回到主页面了。
当你释放AddItemViewController对象时,会发生什么呢?当视图控制器从屏幕上消失时,它的对象就被破坏掉了,这块内存也就被系统回收利用了。
每一次用户打开新增代办事项界面时,app都会生成一个新的实例。这意味着视图控制器的生命周期仅在和用户交互期间存在,当交互结束后,就没有必要保留下来了。
视图控制器的容器
我说过一个视图控制器代表一个界面,但是这里确实出现了两个视图控制器指向一个界面:一个导航控制器(Navigation Controller)包含了一个表格视图控制器(Table View Controller)。
导航控制器是一种特殊的视图控制器,它扮演着其他视图控制器的容器的角色。它有一个导航栏并且具备简单的从一个界面跳转到另一个到作用,通过滑动的方式将界面滑入滑出。本质上讲,这个容器包含着这些界面。
导航控制器仅仅是一个包含视图控制器的容器,它本身并没有什么功能,它更像是一个控制器的“目录”。在这里ChecklistViewController是目录中的第一个界面,AddItemViewCOntroller是目录中的第二个界面。
还有一个经常被用到的容器就是Tab Bar Controller,我们会在下一个课程中见到它。
在iPad上,使用视图控制器的容器是一件司空见惯的事情。视图控制器在iPhone上经常会占满整个屏幕,而在iPad上它们通常都至占据屏幕的一部分。
静态表格单元(Static table cells)
我们来改变一下新增代办事项界面。目前这只是一个空的表格和一个导航栏,但是我想让它看起来像下面这个样子:
打开故事模版并且选择Add Item View中的Table View。
然后打开属性检查器,改变Content的设置为Static Cells。
如果表格中的节数和行数是确定的且不会发生变化的情况下,你就可以使用静态单元(static cells)。这种情况多数用于给用户提供输入数据的界面,就像你正在做的一样。
你可以在故事模版中直接设计这些行的样式。对于使用静态单元的表格而言,你无须向它提供数据源,并且你可以直接将标签或者其他类型的控件直接通过cell链接到视图控制的输入上。
你可以看一下左边的略缩图面板,这个table view下面现在挂了个Table View Section的对象,并且在该分节中包含三个Table View Cells。(如果看不到的话,就点击小三角将层级展开)
选定后两个cell然后点击delete删除它们,我们只需要一个cell就够了。
再一次选择Table View(略缩面板中的),并且打开属性检查器,将Style设置为Grouped。这下看上去就是我们想要的样子了。
下一步,你要在table view cell中添加一个文本框组件,使用户可以输入文字。
拖拽一个Text Field对象到cell中,并且调整下大小。
在text field的属性检查器中,将Border Style设置为no border(就是虚线方框的那个图标)
运行app,然后点击➕按钮,就可以打开新增代办事项界面了。点击界面上的行,你就可以看到自动弹出了一个键盘。
你在任何时候点击这一行都会激活文本框,然后键盘会自动出现。你可以在文本框中输入文字(在模拟器中,也可以使用Mac的键盘输入)
⚠️:如果模拟器中的键盘没有自动出现,可以使用command+K组合键或者在菜单Hardware-> Keyboard-> Toggle Software Keyboard中打开虚拟键盘。你也可以直接使用Mac的键盘输入,即使屏幕上的虚拟键盘不可见。如果不行的话,就选择菜单Hardware-> Keyboard中的Connect Hardware Keyboard选项。
当你正好点击到文本框的外部,而恰恰在cell的内部时,会发生什么呢?
这一行变成浅灰色了,因为你选择了这一行。这并不是我们想要的,所以我们要把这一行禁用掉。
打开AddItemViewController.swift,添加以下方法:
override func tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath? {
return nil
}
这又是一个table view的委托方法,当用户点击某一行时,table view发送一个“willSelectRowAt”的委托,它的意思是“嗨!委托,我马上就要选择这一行了”
委托通过返回nil这个特殊的值来禁用这一行,它的意思是“对不起,你不能这样做。”
给发送者的返回
你已经见过几次return这个关键字了。你在一个方法中使用return来发送一个值给调用它的方法。
让我们来了解一下具体的细节:
一个方法调用另一个方法,并且收到一个返回值。
你不能随心所欲的返回值。这个返回值的数据类型必须和方法名称中->符号后面跟随的数据类型一致。
例如:tableVIew(numberOfRowsInSection)必须返回一个整数型的值。
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 1
}
这样就是正确的,但是假如你像下面这样做:
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return "1"
}
编译器就会给出一个报错说“1”是一个字符串,因为你用双引号将它包围起来了。对人而言,看起来好像是没啥区别的,但是Swift并没有这么大度。数据类型必须严格对应,否则就会报错。
目前我们的这个方法是这个样子的:
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return items.count
}
这也是一种有效的返回,因为items是一个数组,而数组中的对象个数必然是整数,所以items.count的返回值就是一个整数。
而tableView(cellForRowAt)方法则需要返回一个UITableViewCell的对象:
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
}
这里的局部常量cell包含一个UITableViewCell的对象,所以这个返回也是ok的。
tableView(willSelectRowAt)方法需要返回一个IndexPath对象。然而,我们给它返回了一个nil,意思是没有对象。我们先具体看一个这个方法:
override func tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath? {
return nil
}
-> IndexPath? 这里的问号在Swift中的意思就是也可以返回空值。仅仅在这里有一个问号或者感叹号的情况下允许返回空值。
nil这个特殊的值代表的就是“没有值”,但是贯穿整个iOS的SDK这个nil具备多种意思。有时它代表“什么都没找到”或者“不要做任何事情”。在我们这个方法中它就代表当用户点击后着一行不能被选中。
我们如何确定nil在每一个方法中的意思呢?你可以在每一个方法的说明文档中找到具体的解释。
对于“willSelectRowAt”而言,iOS的文档中写着:
返回值:一个index-path对象用于确认或者修改被选定的行。如果你想要其他cell被选定的话,可以返回一个除给定IndexPath对象之外的任意indexpath。如果你不需要某一行被选定,可以返回nil。
上面的规则可以这样理解:
1、返回与你给定的index-path相同的index-path。这样就可以确定这一行被选定了。
2、返回另一个index-path,这样就可以通过点击A行选定B行。
3、返回nil来避免某一行被选中。
所以,请记住,你需要使用return语句返回一个期望的值来退出一个方法。如果你忘记了,那么Xcode会给出一个报错:“Missing return in a function expect to return(没有返回期望的值)”
你也已经见识过了,有些方法不需要返回任何东西:
@IBAction func addItem()
以及
func configureCheckmark(for cell: UITableViewCell,
with item: ChecklistItem)
这些方法没有->符号出现,这种方法不需要传递值返回给调用者,因此它们也不需要return语句。(你仍然可以通过使用return语句退出这些方法,只是return后不需要跟随任何值,哪怕是nil)
简单的说,即使方法没有要求返回一个具体类型的值,也可以使用return语句。将这个特殊的返回想象为真空状态,就是不存在任何东西(不要和nil弄混了,nil是一个具体的值)
有时你会看到类似下面的语句:
func methodThatDoesNotReturnValue() -> ()
func anotherMethodThatDoesNotReturnValue() -> Void
第一个方法指定返回一对空的括号,这就代表不需要返回任何值。术语void的意思和()空括号是一样的。
不过说真的,如果一个方法不需要返回任何值的时候,简单的把->省略掉就可以了。还有@IBAction永远不返回值,这是一个规则。
避免这一行被选定后变成浅灰色,还有一件事要做。虽然目前已经不可能选定这一行了,因为我们刚刚告诉了table view这一行不允许被选择。
但是,cell还是会被选中,并且呈现浅灰色。即使我们已经使这一行不能被选择了,但是有时你点击这一行时,UIKit仍然会将cell重新绘制为浅灰色。因此你最好将它也禁用掉。
打开storyboard,选择table view cell,然后打开属性检查器,将Selection属性设置为None。
现在运行app,这一行再也不能被选中了。
从文本框中读取值
你有了一个在cell内的文本框并且可以输入文字,但是你如何才能将你输入的文字读取出来呢?
当用户输入结束,你需要取到用户的输入内容并且以某种方法放到一个新的CheclistItem对象中并且将其添加到待办事项的列表里。这就意味着done()动作必须具备引用这个文本框的功能。
在我们之前的课程中你已经知道了如何从你的视图控制器中引用控件了:使用Outlet。当你在之前的课程中添加outlet时,我教你的方法是在源代码文件中通过输入的方式申明@IBOutlet并且在故事模版中制作链接。
这次我要给你展示一个魔法,以节省你写代码的时间。你可以使用界面建造器自动完成这些工作,通过按住ctrl拖拽的方式,直接将控件拖入源代码中。
首先,打开故事模版并且选定Add Item View Controller。然后打开辅助编辑器(Assistant editor),这个按钮在工具栏中,见下图。这个按钮看上去就是两个相交的圆形:
这时你的屏幕也许会很拥挤,现在一共打开的有五个水平面板。如果你想获得更多的空间你也许应该关闭工程导航器和实用工具面板。
辅助编辑器在屏幕的右边打开了一个新的面板。新版面的跳转栏,就是工具栏下面的那个,应该是叫做Automatic并且辅助编辑器中显示的应该是AddItemViewController.swift文件中的内容:
“ Automatic”的意思是辅助编辑器自动指向你正在编辑的界面对应的文件(Add Item View Controller对应的文件就是AddItemViewController.swift)。
(Xcode有时会犯糊涂,如果辅助编辑器中展现的不是AddItemViewController.swift,那么可以在上图中橙色大箭头所示的跳转栏中点击选择这个文件)
这时你的屏幕上一半是故事模版,一半是源代码文件。在辅助编辑器中选定文本框按住ctrl将文本框拖拽到源代码文件中:
当你放开鼠标时,会弹出这样一个窗口:
按照下面的指示选择:
Connection:Outlet
Name:textField
Type:UITextField
Storage:Weak
⚠️:如果Type选项不是UITextField而是UITableViewCell或者UIView,那就是你选择了错误的控件拖拽了过去。
确保你选定的是text field,而不是cell。界面上这两个控件分的不是很清楚,它俩基本叠在一起。你如你无法正确的点击选择到text field,也可以在故事模版中的纲要视图中直接选择。
最后点击Connect,Xcode就会自动为你创建好一个@IBOutlet并且已经链接好了text field对象。
在代码文件中是这个样子的:
@IBOutlet weak var textField: UITextField!
仅仅是通过拖拽你就成功的将文本框对象和新的成员textField链接起来了,多么容易啊!
现在,你需要修改done()动作,将文本框的内容打印到Xcode的调试区域,我们之前使用过这个调试区域,就是Xcode底部靠右一点的那个面板。这是一种确认是否成功的读取用户输入的简便方法。
打开AddItemViewController.swift,将done()方法修改为:
@IBAction func done() {
print("Contents of the text field: \(textField.text!)")
dismiss(animated: true, completion: nil)
}
你可以直接在辅助编辑器里完成这一工作。让故事模版和代码编辑区域位于一个屏幕上是很方便的,不过就是太占用屏幕空间了。
运行app,然后点击➕按钮,然后在文本框中输入点内容。当你点击done按钮时,新增待办事项的界面应该会被关闭掉并且Xcode会在调试区域打印出我们刚才写的内容:
Contents of the text field: hello world
非常棒,这样做行得通。print()这个方法应该是你老朋友了。它是我们忠诚的排除故障的伙伴。
回忆一下,你可以通过使用 ( ... ) 在字符串中插入一个值。这里你使用了(textField.text!)来打印文本框的内容(我会在之后介绍那个感叹号的作用)
⚠️:因为iOS 模拟器本身会在调试区域打印出很多内容,这样你也许比较难以寻找自己打印的内容,但是幸运的是调试区域下面有一个过滤器,你可以输入关键字搜索自己的文本信息。