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

我们先来讲几种循环数组的方法

首先是我们已经用过很多次的for in,像下面这个样子:

for category in categories { . . .

这一行的意思是把categories中的每一个对象依次放入名为category的临时常量中。

然而,为了得到每个对象的index-path,而不是每个对象的名称,你需要使用另一种方法:

for i in 0..<categories.count {
  let category = categories[i]
...
}

我们使用半开操作符0..<使临时i依次从0到categories.count-1递增。如果你需要数组中的索引而不是名称,这是一种常见的方式。

还有一种方法是使用enumerated()方法,你会在下一个课程中见到它,现在我们先大概了解一下:

for (i,category) in categories.enumerated() {
...
}

回到我们的app,打开storyboard,拖拽一个新的Table View Controller到画布中。在身份检查器中设置Class为CategoryPickerViewController。

选择table view cell,在属性检查器中设置Style为Basic,Identifier为Cell。

选定Category Cell,然后按住ctrl拖拽到这个新的table view controller上,然后在转场类型中选择Selection Segue下的Show。

将这个转场的Identifier设置为PickCategory。

注意:如果你的界面中,右边的table view controller顶部有一个返回Tag Location选项,是没问题的,不知道只作者截图有问题,还是Xcode版本升级导致的。

storyboard部分就到此结束了,我们下面开始代码部分。

打开LocationDetailsViewController.swift并且添加一个新的实例变量categoryName。你会用它来临时存储被选择的分类名称。

var categoryName = "No Category"

这个变量的初始值是 "No Category"。它同时也是分类列表中最上面的第一个选项。

修改一下viewDidLoad(),将categoryName的值放入标签中:

override func viewDidLoad() {
        super.viewDidLoad()
        descriptionTextView.text = ""
        categoryLable.text = categoryName   //修改这里
...

最后,添加转场方法prepare(for:sender:) :

override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
        if segue.identifier == "PickCategory" {
            let controller = segue.destination as! CategoryPickerViewController
            controller.selectedCategoryName = categoryName
        }
    }

这里只是简单的设置了category picker的属性selectedCategoryName。通过这一操作,现在app就有了一个分类。

运行app,实际看看效果:

嗯,看起来效果不错。你现在可以选择一个分类了,但是你选择某一行后不会自动关闭这个界面。当你点击返回按钮后,你选择的分类也不会显示在界面上。

练习:整个拼图中缺少了哪一部分?

答案:CategoryPickerViewController目前没有任何通讯方式向LocationDetailsViewController返回数据,比如用户选择了一个新的分类。

此时你一定会恍然大悟,原来如此!你忘记给它一个委托协议了。这就是为什么它无法给其他视图控制器传递消息。

确实,一个委托协议是个不错的方法,但是我想给你展示一个新的方法,这是storyboard的一个特色功能,可以达到和委托协议相同的效果,但是工作量要比创建一个委托协议小一些,它叫做:unwind segues(不知道怎么翻译这个术语合适T T)。

如果你想知道storyboard中的的红色“Exit”图标是什么,你现在有了你的答案,没错,它就是:unwind segues。

regular segue用于打开一个新的界面,而unwind segue用于关闭一个当前激活的界面。听起来很简单。然而,创建unwind segue的方法不是非常直观。

这个Exit图标似乎没有任何作用,试试按住ctrl拖拽cell上去,它不会形成一个链接。

首先,你要添加一个特殊类型的动作方法。

打开LocationDetailsViewController.swift,添加下面的方法进去:

@IBAction func categoryPickerDidPickCategory(_ segue: UIStoryboardSegue) {
        let controller = segue.source as! CategoryPickerViewController
        categoryName = controller.selectedCategoryName
        categoryLable.text = categoryName
    }

因为这个方法是以@IBAction前缀开头的,所以它是个动作方法。但是它和一般的动作方法有什么区别呢?区别在于它的参数,是一个UIStoryboardSegue对象。

通常,如果动作方法有一个参数的话,它应该是触发这个动作的控件,比如按钮和滑条。但是为了创建一个unwind segue,你需要将动作方法的参数写为UIStoryboardSegue。

这个方法内部代码的意思非常明显。你找到是那个视图转场到这个界面来的(就是源界面),在这里它就是CategoryPickerViewController,然后读取它的selectedCategoryName属性。它正好包含用户选择的分类名称。

打开storyboard。按住ctrl拖拽cell到Exit按钮上,这次应该能够创建链接了:

然后在弹出菜单的Selection Segue分节下选择categoryPickerDidPickCategory,就是你刚才创建的用于unwind segue的动作方法的名字。

如果无法创建链接,请确定你选中的是cell,而不是Content View或者其中的Label。

运行app,是不是非常简单?好像也不是那么简单,被选择的分类被忽视掉了...

这是因为虽然categoryPickerDidPickCategory()方法看到了selectedCategoryName属性,但是这个属性此时没有写入值。

你需要一个机制,当unwind segue转场被触发时,你可以把用户点击的那一行的分类名称写入到selectedCategoryName属性中。

我想这个机制应该就是prepare(for:sender:),没错,这个方法对各种转场都适用。

打开CategoryPickerViewController.swift,添加prepare(for:sender:)方法:

override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
        if segue.identifier == "PickedCategory" {
            let cell = sender as! UITableViewCell
            if let indexPath = tableView.indexPath(for: cell) {
                selectedCategoryName = categories[indexPath.row]
            }
        }
    }

这段代码看起来就是把被选择行的相应地category name(分类名称)放入selectedCategoryName属性中。

这段代码假设unwind segue转场的名称叫做“PickedCategory”,所以你还需要设置这个转场的名称。

不幸的事,unwind segue在storyboard中并不可见。没有一个普通转场那样的大大的箭头。你只能在略缩面板中选择它:

看这里,看这里

选择unwind segue,然后打开属性检查器,设置identifier为PickedCategory。

再次运行app,现在category picker应该可以正常工作了。只要你选择一条分类,界面就会自动关闭,并且新的分类的名称也可以显示在返回的界面中。

unwind segue非常棒,并且比使用委托协议要简单的多,特别是在我们设计的这个app中。

改进用户体验

虽然Tag Location界面已经具备了很多功能,但是它还是可以再改进一下。改进一些小细节,可以使你的app更加人性化,并且在竞争对手中脱颖而出。

我们先来看看Description text视图的设计:

在text view和cell的边界之间有10点的距离,但是因为它俩的背景都是白色的,这样会使用户无法分辨text view的起点位置。

有可能会导致用户刚好点在边边上,而无法编辑文本,这是非常让人讨厌的。你以为你已经点到了,但是其实没有点到,并且没有任何反馈提示,用户有可能会以为这个app就是垃圾,怒删之。

所以这里我们要改进一下,不管用户点击到了这个cell的任何位置,text view都应该被激活,即使用户正好点到了边边上。

在LocationDetailsViewController.swift的// MARK: - UITableViewDelegate注释后面添加下面的方法:

override func tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath? {
        if indexPath.section == 0 || indexPath.section == 1 {
            return indexPath
        } else {
            return nil
        }
    }
    
    override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        if indexPath.section == 0 && indexPath.row == 0 {
            descriptionTextView.becomeFirstResponder()
        }
    }

tableView(willSelectRowAt)方法限定了,仅前两个分节(section)的cell可以被点击。回忆一下,||操作符是或的意思,所以仅当section为0或者1时,其中的cell可以被点击,而其余的cell都是只读的。

tableView(didSelectRowAt)方法用来处理实际被选择的行。你不需要对Category或者Add Photo进行响应,这些cell是链接到转场的。

但是假如用户点击了第一个分节中的第一行,那么你立刻激活text view。&&操作符是与的意思,就是and。

运行app,试试效果,看看点击cell边缘,而不是text view内部,能否激活text view(如果模拟器中的小键盘没有自动弹出的话,可以使用快捷键command + K)

任何你可以挽救用户体验的工作都是非常值得的。

就text view而言,一旦你激活了小键盘,就无法在关闭它,要知道小键盘可是占据了一半的屏幕,这会让用户抓狂。

当你点击屏幕中其他位置时,让小键盘自动关闭,是非常棒的一个功能,实现起来也不是特别麻烦。

在viewDidLoad()方法的最后面添加下面的语句:

let gestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(hideKeyboard))
        gestureRecognizer.cancelsTouchesInView = false
        tableView.addGestureRecognizer(gestureRecognizer)

gesture recognizer(手势识别器)是非常便利的一个对象,它可以识别点击和手指的移动。你只是简单的创建一个gesture recognizer对象,当特定的手势被观察到时,调用一个你指定的方法,并且把这个识别器添加到视图中。

你使用了一个UITapGestureRecognizer,它可以识别简单的点击,还有一些其它的对象,可以识别扫动,按压,合拢等。

注意一下#selector()关键字:

...target: self, action: #selector(hideKeyboard))...

每当手势发生时,通过#selector告诉UITapGestureRecognizer要被调用的方法。

这个模式叫做target-action(目标-动作),并且你已经使用过多次了,比如链接UIButton,UIButtonItems,以及其它控件的动作方法时,其实用的都是这个模式。

traget就是接受被发送消息的对象,通常就是self,action就是发送的消息。

这里你做的就是,当在table view的其它地方出现点击这个行为时,hideKeyboard消息就会被发送,所以你要执行一个方法来响应这个消息。

打开LocationDetailsViewController.swift添加hideKeyboard()方法。把它放在viewDidLoad()方法的下面,其实放在其它地方也可以:

@objc func hideKeyboard(_ gestureRecognizer: UIGestureRecognizer) {
        let point = gestureRecognizer.location(in: tableView)
        let indexPath = tableView.indexPathForRow(at: point)
        if indexPath != nil && indexPath!.section == 0 && indexPath!.row == 0 {
            return
        }
        descriptionTextView.resignFirstResponder()
    }

⚠️:在Objective-C中,选择器(selector)是一种引用Objective-C方法名称的类型。 在Swift中,Objective-C选择器由Selector结构表示,可以使用#selector表达式来构造。 同时需要向Objective-C传递方法名称。
这就是在hideKeyboard()方法名称前加上@objc前缀的作用。
本书写作的时候Swift版本还是3,不需要添加这个前缀,但是Swift4中必须要这样做,我在后面翻译的时候,也会不断的将Swift4的新特性加进去。
话说Swift的本意是要摆脱Objective-C,但是毕竟iOS框架与Objective-C已经相爱相杀20余年了,所以你懂的。。

无论何时,用户点击table view内任何地方时,手势识别器就会调用这个方法。方便的是,它也传递了一个引用作为参数给自己,它可以让你向手势识别器询问点击的发生位置。

gestureRecognizer.location(in: tableView)方法返回一个CGPoint数。CGPoint是你在UIKit中随处可见的一种结构。它包含两个字段,x和y,用于描述界面上的位置。

使用这个CGPoint数,你就可以向table view询问目前是位置上是哪个index-path。这非常重要,因为当用户点击text view内部时,你不能把键盘隐藏掉。而如果点击的是其它地方,你则需要隐藏这个键盘。

练习:这里的if语句你熟悉吗?能不能试着解释一下呢?

答案:用户可能会在table view内部轻击,而不在单元格内,例如在两个部分之间的某个位置或部分text view上。 在这种情况下,indexPath将是nil,所以此时IndexPath是个可选型,需要使用if let或者感叹号来对它进行解包。

只有当index-path不是section为0和row为0时,你才隐藏小键盘。

⚠️:如果一个可选型有可能为nil的话,你不能对其强制解包,除非你愿意承担app崩溃掉的风险。所以上面方法中的indexPath!.section和indexPath!.row看起来很危险,但是这里是没问题的,因为前面有一个短路语句,就是indexPath != nil,如果indexPath为nil则整个if条件为假,其中的语句不会执行。

另外一个可选的写法是:

if indexPath == nil ||
          !(indexPath!.section == 0 && indexPath!.row == 0) {
  descriptionTextView.resignFirstResponder()
}

能看明白吗?这个if语句和之前的意思完全相反,但目的是相同的,这个if语句使用了||或操作符,意思是indexPath为nil或者index-path不是section为0和row为0时,隐藏小键盘(感叹号出现在表达式前面时,是非的意思)。

熟练的使用各种逻辑操作符,是你编程生涯中很重要的一个环节,幸好它不是很难。

当然你也可以用if let安全的解包,像下面这个样子:

if let indexPath = indexPath, indexPath.section != 0 &&
                              indexPath.row != 0 {
return
}
descriptionTextView.resignFirstResponder()

我只是给你简单的展示了一下if语句的多样性。你可以选择一个你喜欢的一直用下去就好了。

运行app,点击text view会自动弹出一个小键盘,如果没有就用快捷键command + K。

我们还可以实现,当用户进行滚动操作的时候,也自动隐藏小键盘。

打开storyboard,选择Tag Location界面中的table view。在属性检查器中找到Keyboard,选择其中的Dismiss on drag选项,这样就可以实现了,简单吧。

如果设置没有生效的话,就在真实设备上试试,模拟器中的虚拟键盘有时候不是很聪明。

同时试试Dismiss interactively选项,看看那个你更加喜欢。

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

推荐阅读更多精彩内容