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

上节课我们已经解锁了新技能,选择图标。

但是我们仍然有些细节有待改进,目前,在done方法中,你做了这件事:

  let checklist = Checklist(name: textField.text!)
 checklist.iconName = iconName

设置图标的名称可以认为是初始化Checklist的一部分,所以我们写成下面这样会更好一些:

let checklist = Checklist(name: textField.text!, iconName: iconName)

打开ListDetailViewController.swift,done方法中把新的这一行代码替换进去。

仅仅是这样,app肯定不会工作,你还需要给Checklist.swift新增一个具有两个参数name和iconName的新的init方法

打开Checklist.swift,添加新的init方法:

init(name: String,iconName: String) {  
        self.name = name
        self.iconName = iconName  
        super.init()
    }

注意,是加一个新的,不要在老的上面改。Checklist现在有三个init方法了:

init(name):仅需要名称时,使用这个方法。

init(name, iconName):同时需要名称及图标名称时,用这个方法。

init?(coder):从plist文件中读取对象时,用这个方法。

init(name)和init(name, iconName)几乎是一样的。除了参数有所不同。

所以我们可以改进一下这个地方,使用init(name)调用一个将图标名称默认为“No Icon”的init(name, iconName)方法。

将init(name)替换为下面的代码:

convenience init(name: String) {
        self.init(name: name, iconName: "No Icon")
    }

这里用self.init(name, iconName)代替了super.init()。

因为它将一部分工作移交给了另一个init方法,所以此时init(name)方法被称为便利初始化。

它和init(name, iconName)的功能一模一样,只是节省了你需要在多处指定使用“No Icon”的工作。

init(name, iconName)成为了Checklist的指定初始化。它是创建一个新的Checklist对象的首要方法,而init(name)仅用于给比较懒的人,比如你和我。

运行app,确认一下一切工作正常。

练习:给ChecklistItem一个init(text)方法,或者一个init(text,checked)方法。

给app整个容

你会使用一些简单的办法来提高颜值,比如化个妆什么的,而不是手术级别的。导航控制器和table view默认的样子已经比较好看了,虽然色调有点单一。本节课你将了解如何自定义这些UI元素的外观。

虽然现在app外观有点单调,但是你可以使用一些比较简单的办法让它立马个性起来,我们说的就是tint color。

tint color是UIKit用来表示某种东西可以交互的一个颜色系统,比如按钮上的文字是浅蓝色的,用户无论用什么app,看到这种浅蓝色,基本就会明白,这个按钮可以点击。

所有的按钮都是一个颜色

改变tint color是非常简单的。

打开故事模版,找到文件指示器(File inspector),就是第一个子页。

点击Global Tint就可以打开颜色选择器了,我们将颜色设置为Red:4,Green:169,Blue:235,这样就得到了比较亮一些的蓝色。

改变故事模版的Global Tint Color

小贴士:如果颜色选择器中仅显示黑白灰三种颜色,那么你就点击一个名字叫做Gray Scale Slider的下拉框,在下拉框中选择为RGB Slider。

如果对勾符号不用黑色,而改用tint color那就更加完美了。

为了实现这个目的,在ChecklistViewController.swift中的configureCheckmark(for:with)方法中,添加一行代码:

label.textColor = view.tintColor

运行app,是不是看起来感觉不一样了?如果感觉一样,请把这节课当作皇帝的新装就好。

颜色变的活波了一些

任何一个完整的app,都有会有自己的图标。在本节课附加的资源中的Icon文件夹中你可以找到各种尺寸的图标(附件不提供下载,请大家支持正版,或者自己去网上找些图标素材练习),注意一下,图标的颜色和我们刚才选择的tint color是一致的。

把这些图标添加到asset catalog(Assets.xcassets)。回忆一下,我们仅仅是把这些图标拖入AppIcon中相应的位置就好了。

app,还有一个加载用的图片和文件。在app还在读取时,显示一个静态图片可以造成app启动很快的幻觉,这些都是骗人的把戏。(其实这里就是app打广告的好地方,但是本书的作者字里行间都透露着对广告的鄙视,所以。。。没有然后)

Xcode的模版中包含一个叫做LaunchScreen.storyboard的文件,在运行的时候会先加载它。你可以把它做成和app差不多的样子,但是我们还有一个更简单的方法。

打开工程设置界面,在General子页中,向下滚动,找到一个叫做App Icons and Launch Images的分节。

在Launch Screen File下拉框中,选择Main.storyboard。

这样app就会使用故事模版中的设计作为启动时加载的文件。

启动时,app会找到初始界面并且将它转换为一个静态图片。对我们的app而言,就是All Lists View Controller。

从工程导航栏中删除LaunchScreen.storyboard。

然后选择菜单Product->Clean。或者在模拟器中删除掉app,再重新运行一次,这样就不会有任何残留下的缓存了。(删除的方法和真实手机一样,用鼠标一直按住app的图标,然后app图标会开始晃动)

然后运行app,你就可以看到app一启动,就显示出了主界面,好像app立即启动了一样。

使用合适的启动界面,可以使app显得更加专业。

对于许多app而言,你都可以无脑的使用main storyboard来当启动界面,此外,你要需要使启动界面适合所有的设备,比如6s,7,plus等。

支持所有设备类型

我们的app应该在现有的所有iPhone型号上运行正常,从屏幕最小的iPhone SE到最大的iPhone 7 plus。table view controller在这方面非常灵活,它可以自动识别设备类型并且转换尺寸,无论是变大还是变小都灵活自如。你可以自己在各种类型的模拟器上试试。

那么我们面临的问题是什么呢?这里还是有很多东西需要微调的。

目前为止,我给你看的截图都是基于iPhone SE的,并且我在自己的界面建造器中也是使用的iPhone SE尺寸进行设计。但是我们在大屏幕设备上运行的话会发生什么事呢?比如我们用iPhone 7 plus模拟器运行一下试试:

table view倒是没事,但是图标错位了

图标不再完美的对齐cell的右侧边缘了。再试试输入点文本上去:你会发现文本被截短了,因为text field变小了。为什么会发生这些情况呢?

当你在界面建造器中为你的app设计用户界面的时候,它并不会自动匹配所有的设备类型,只是匹配你正在使用的模拟器型号。你需要帮助界面建造器,告诉它面对不同尺寸的屏幕时应该如何调整UI的大小及位置。这就我要介绍给你们的自动布局(Auto Layout)。

你想要的是,图像永远贴在右侧边缘,总是和详细信息按钮保持固定的距离。当屏幕大小发生变化时,图像应该自动的调整自己的位置。

解决办法就是给image view添加自动布局约束,告诉app屏幕边缘和image view的关系。

选定Icon Image View。打开画布底部的Pin Menu,然后按照一下步骤操作:

1、取消选择Constrain to margins。

2、激活顶部和右侧的连线(菜单上方有十字形的四条红色虚线,激活后会变成实线)

3、勾选Width和Height复选框

4、For update Frames下拉框选择为Items of New Constraints。

5、点击Add 4 Constraints。

给image view添加自动布局

(有可能你会看不到For update Frames下拉框,这样的话就忽略第四条)

现在image view看起来应该是这个样子:

确认一下,代表约束的线条是蓝色实心线条。如果它们是橙色或者红色,那么你肯定是漏掉了上面的某个步骤。(重新添加一次约束,或者使用菜单Editor → Resolve Auto Layout Issues → Update Frames,看看能不能自动更新过来)

最重要的约束是右侧那个,这一条告诉UIKit,image view的右手边总是以固定距离贴着table view cell的右侧边缘。

换而言之,无论当前界面是宽还是窄,image view总是和详细信息按钮的相对位置不变。

剩下的三条约束,顶部、宽和高也都是必要的,因为所有的视图都必须有足够的约束指明它们的位置和大小。

如果你不指明约束,那么界面建造器会按照默认约束处理。但是哪怕你只是添加了一条约束,那么默认约束就失效了,你必须把所有的约束都补全。

确认约束是否生效,并不是非得在模拟器中运行app,那样太消耗时间,你可以使用View as功能,这个功能在画布的底部,我们上一个课程中也使用过它,它可以在界面建造器的内部切换iPhone的类型。如果你的约束添加的没有问题,那么任何尺寸的iPhone上,它的位置都应该是固定的。

接下来,我们来让text field在比较宽的屏幕中,自动延长。

选定Text Field,打开Pin menu激活四条红线:

上下左右全部激活

这个操作会把text field固定到table view cell的四个边上。(红线上下左右4个框里的数字是几都没关系,这些数字代表text field和cell四个边的间距,重要的是4条红线都需要被激活)

对Add/Edit Item界面的text field也做同样的操作。

现在你输入多长的文本都没关系了,文字会自动向左滚动:

输入多长都可以

让我们来输入一条非常长的文本,这个文本传递到其他table view时会发生什么呢?

对All Lists界面完全没问题:

内建的cell风格,自动调整了大小

使用“Subtitle”cell风格的table view,会自动随着屏幕调整宽度。当文本过长时,它会自动缩短它们。

但是对于to-do items(待办事项界面),看起来就不是那么漂亮了。在这个界面了,文本被过早的截短了。

文本被过早的截短了

因为这是一个自定义的cell设计,你要添加一些约束来避免这种事情发生。

打开故事模版,找到Checklist界面并且选定cell内的label。

首先使用Xcode菜单 Editor → Size to Fit Content,给label一个可以自由伸缩的尺寸。这样做后也许会把label变得非常小,但是没有关系。如果不这样做的话,下面的步骤就会无法进行。(即使这一操作移动了label也没关系)

你想要把label固定在视图的右侧边缘,紧挨着详细信息按钮。我们来添加这个约束。

打开Pin菜单,取消选择Constrain to margins。

激活右侧的红线,并且将其中的值修改为0,这样它就紧贴着详细信息按钮了。

将Update Frames设置为Items of new Constraints(如果看不到这个选项就忽略它)。点击Add 1 Constraint,结束。

将label固定在右侧边缘

好像有啥东西看起来有点不对。

标签没有足够的约束

记住,你总是要添加足够的约束指定一个视图的尺寸和位置。这里你仅仅添加了关于右侧边缘的约束,这是不够的。

不要慌!当你添加约束时,漏点东西是很正常的。解决的办法非常简单,就是把漏掉的东西添加上就好了。

还是选中label,打开Align menu,就是Pin菜单左边的那个。选中Vertically in Container。设置Update Frames为Items of New Constraints(看不到这个就忽略掉)

使label垂直居中

现在所有代表约束的线都应该是蓝色了。label在X轴和Y轴上都有了有效的位置。

⚠️:即使你没有对label的尺寸指定任何的约束,约束也完美的生效了,这是为什么呢?
没有指定尺寸的话,label会根据它的内容,文本和字体来计算它自己应该有多大。这叫做内容自适应尺寸(应该是这么个名词吧!)
使用内容自适应调整的UI组件,比如label,不需要添加宽和高的相关约束,但是自适应调整生效的前提是之前你必须使用了菜单中的Size to Fit Content选项。

不幸的是,label现在虽然右对齐了,但是这并不是你想要的,它的左边必须紧挨着cell的左边。

最简单的办法就是给左边同样添加一条约束,使label紧挨着左侧边缘。

但是你不能使用Pin菜单来做这个事,因为这样会使label和对勾符号连接起来,对勾符号的尺寸依赖于它是否被触发显示在屏幕上。你需要使用新的技术来完成这件事。

再次选中label,按住ctrl拖拽label到cell的内部任意一个位置上。放开鼠标,会弹出一个菜单。菜单上的选项依赖于你拖拽的方向,所以你看到的菜单也许和截图中的有所不同。

在弹出菜单上选择Leading Space to Container Margin,就可以完成这个约束的添加了。

像这样

这样就新增了一条长长的蓝色约束线,但是标签的位置看起来还是有点问题。

选择这条蓝色的线,打开尺寸检查器,将Constant设置为30。

现在看起来好多了:

现在label的两侧边缘都固定好了,所以它会自动伸缩,保持和table view cell一致。

运行app,现在文本不会过早的被截短了。

特色功能:本地通知

我希望你还可以跟上我的思路。我们详细的讨论了关于视图控制器(view controller)、导航控制器(navigation controller)、故事模版(storyboard)、转场(segues)、表视图(table view)、以及数据模型(data model)的相关内容。

如果你想要成为iOS app开发的大师的话,你必须精通这些课题,因为每一个app都用到了它们中间的一个或者几个。

在本节课,你要了解一个扩展的课题:local notifications(本地通知),使用iOS 10中的User Notifications框架。

本地通知允许app在app没有运行的时候,按照事先的安排,对用户进行消息通知。

你要新增一个“due date(处理时间)”到ChecklistItem对象中,然后使用本地通知来告诉用户某条待办事项的戒指时间。

如果你对这个课题感兴趣的话,那么,太好了。。。

这节课的内容是这个样子的:

1、尝试使用本地通知,观察它是如何工作的。

2、允许用户为待办事项选择一个处理时间。

3、创建一个时间选择器。

4、对待办事项使用本地通知,并且当用户修改处理时间时,更新本地通知的时间。

在你了解如何把这些和app融为一体之前,我们先来安排本地通知计划,看看会发生什么。

顺便说一下,本地通知和推送消息是不一样的(推送消息,也叫远程通知)。推送消息允许你的app接收外部事件触发的通知,比如你最喜欢的球队夺冠了。

本地通知更像是一个闹钟:你设定一个时间,然后它就会发出蜂鸣声。

app仅仅在用户允许的情况下,可以进行本地通知,如果用户不允许,那么通知永远不会出现。你需要询问用户是否同意消息通知,我们就从这里开始入手。

打开AppDelegate.swift,导入一个新的框架:

import UserNotifications

这样就告诉了Xcode,我们将要使用User Notifications框架。

在application(didFinishLaunchingWithOptions)方法中添加以下代码,就添加在return true语句前面:

let center = UNUserNotificationCenter.current()
        center.requestAuthorization(options: [.alert,.sound], completionHandler: {
            granted,error in
            if granted {
                print("We have permission")
            } else {
                print("Permission deied")
            }
        })

回忆一下,application(didFinishLaunchingWithOptions)是在app启动时被调用。它是app的准入点。是你在app启动后要做某些操作时的最佳代码位置。

因为你仅仅是使用一下本地通知,所以在这里用来请求用户许可是最合适的。

你告诉了iOS这个app想要发送类型为“alert”的通知,并且附带一个声音效果。稍候,你会将这段代码移入更加恰当的地方。

用点号开头的东西

在我们的app中,见过了类似于.none, .checkmark, .subtitle以及现在的.alert和.sound。它们是枚举符号。

一个枚举enumeration,或者简写为enum,是一种数据类型,它由一系列符号和它们对应的值的列表组成。

例如UNAuthorizationOptions枚举包含以下符号:

.badge
.sound
.alert
.carPlay

你可以把它们整合到一个数组中,来定义app发送给用户的消息种类。这里你使用语句[.alert,.sound]组合了alert和sound。

在那里使用了枚举是非常容易被识别的,因为它们名称前面都有一个点。这是一种速写的方法,它的完整写法其实是:

UNAuthorizationOptions.alert,UNAuthorizationOptions.sound

幸运的是Swift是非常聪明的,它可以识别出.alert和.sound是来自UNAuthorizationOptions,这为你省了不少事。

运行app,这时你应该会得到一个许可请求的弹窗。

请求用户允许通知

点击Allow,那么下次app就不会再进行询问了。iOS会记住第一次的结果。

如果你不小心点击了Don't,也没关系,你可以重置模拟器恢复这个弹窗,或者在app的setting中进行设置,就和其他app一样。

中断app运行,在didFinishLaunchingWithOptions方法中添加以下代码:

let content = UNMutableNotificationContent()
        content.title = "Hello"
        content.body = "I am a local notifcation"
        content.sound = UNNotificationSound.default()
        let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 10, repeats: false)
        let request = UNNotificationRequest(identifier: "MyNotification", content: content, trigger: trigger)
        center.add(request)

这样就创建了一个新的本地通知。因为你写了timeInterval: 10,所以这个通知会在app运行10秒后被触发。

UNMutableNotificationContent描述了本地通知的内容。就是你在alert消息中设置的两个文本信息,以及声音。

最后,你将通知添加到UNUserNotificationCenter。这个对象是用于跟踪所有本地通知并且在时间到了的时候触发它们。

运行app,在app启动后,按下Home间,回到手机的主界面(使用模拟器菜单Hardware->Home)

等待10秒,这也许会是你人生中比较漫长的10秒之一,然后你会看到一条弹出消息,当然还伴随着一个提示音。

这就是本地通知,酷吗?

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

推荐阅读更多精彩内容