3D Touch
在现在的 Multi-Touch 技术下,我们见的最多的就是点,滑和缩放操作,这些操作都是在二维的维度下。而 3D Touch 则是可以给操作提供了一个新的维度——可以感受用户按压的力度。
适用场景
目前官方提供了三个可以适用 3D Touch 的场景:
Pressure Sensitivity
可以用于绘画的 App,通过不同的力度来画出不同的线条。
Peek and Pop
一个快速预览的功能。
Quick Action
主屏幕上的 App 快捷菜单,提供一个快接入口,简化一些操作。
目前来看,后两者更适合于我们的App。
关于更多 3D Touch 的介绍,详见:https://developer.apple.com/ios/3d-touch/ 或者 http://www.apple.com/cn/iphone-6s/3d-touch/
静态的 Quick Action
考虑到适配工作和当前业务的使用情况,我们的 PM 首先选择了在主屏幕上添加一个 Quick Action 这样一个快捷菜单,嗯……如下图所示
这是三个静态的菜单选项,何为静态的?就是从 App 安装后一直是长这个样子的,它是声明在 Info.plist 里面的 UIApplicationShortcutItems 数组中的。
我这里只用到了三个key:
UIApplicationShortcutItemType
,有点像是这个 item 的标识符,它会通过launchOption
传到appDelegate
里面然后供开发者判断 app 是通过哪个 item 启动的,以便处理自己的逻辑UIApplicationShortcutItemTitle
,顾名思义了,这个就是用于设置快捷菜单标题的UIApplicationShortcutItemIconFile
,这个是用于设置快捷菜单的 icon 的,不过是使用工程中的图像文件来设置的,这是一个 35 * 35 点,单色的图片。
其中前两个 key 是必须要声明的。这里还有其他的一堆 key 用于 item 的定制:
UIApplicationShortcutItemSubtitle
,这是用来设置副标题的,不是必要的,如果设置了这个将会在标题下方第二行出现一行副标题。另外,如果没有副标题的话,标题过长会自动折行到第二行。UIApplicationShortcutItemIconType
,这个是用来设置图标的,不过这个是使用系统提供的一堆图标,这个key其实是声明在 UIApplicationShortcutItemIcon 中的一个枚举,想用的话可以从这里面找,不是很多。另外,如果你又通过UIApplicationShortcutItemIconFile
来定制了自己的 icon,那么系统将会自动忽略这个 key 的设置而使用来自 file 的 icon 设置。UIApplicationShortcutItemUserInfo
,苹果说这个可以利用这个 key 让开发者提供一些类似 app 版本信息的东西,用于 app 升级后但是还没有被用户启动,但是用户又通过 Quick Action 来启动了 app,可能会存在的问题。这个时候,这个 key 所提供的类似版本信息的东西就可能会派上用场。但是我还没有发现它的使用场景。
当你在 Info.plist
里面设置好这些东西后,你就能在支持 3D Touch 的设备上通过用力按压 app 的 icon 呼出这个快捷菜单了,并且你还可以通过这个菜单来启动 app,只是没有做任何的处理,现在从这个菜单启动 app 和从 icon 启动没啥两样。接下来我们还要对从菜单启动 app 这个逻辑进行单独的处理。同样我们还是先说静态的菜单选项:
剩下的工作我们就要在 appDelegate
这里面来做了。首先我们要先认识下新增的一个回调:-application:performActionForShortcutItem:completionHandler:
这个回调会在 app 通过快捷菜单启动或者从后台唤醒的时候被调用,但是,如果你在 -application:didFinishLaunchingWithOptions:
或者 -application:willFinishLaunchingWithOptions:
返回的是 false
,这个回调就不会被调用。苹果官方给的 Demo 里面是这样写的:
1 | func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool { |
这个代码的意思是,我在程序启动的时候看下是不是通过快捷菜单启动的,如果是的话,就不让 performActionForShortcutItem:completionHandler
这个回调被调用。具体的原因没有说,但是通过这个回调来看,会有影响程序启动的可能性,所以为了保证程序能够顺畅的启动,先把这里面传过来的 UIApplicationShortcutItem
存下来,在下面程序启动完后另外找个机会再处理接下来的逻辑,什么时候呢?
1 | func applicationDidBecomeActive(application: UIApplication) { |
在 -applicationDidBecomeActive
这个回调中,通过判断是否有 launchedShortcutItem
,手动调起用于处理逻辑的方法。如果是从后台唤醒的话,就不需要这么麻烦了,直接通过系统正常调用 performActionForShortcutItem:completionHandler
这个回调就好啦:
1 | func application(application: UIApplication, performActionForShortcutItem shortcutItem: UIApplicationShortcutItem, completionHandler: Bool -> Void) { |
所以以防重复调用,这里在首次启动完后,需要在 -applicationDidBecomeActive
将这个手动存储的 property 置为 nil
。
大体上一个快捷菜单的启动流程就是这样了。可是真正在我们的工程中可能还有一些问题:
- 我们的启动很复杂,可能有各种启动画面
- 需要跳转好多界面啊,还需要各种参数,前置界面,bulabula…
- 需要登录怎么办?我们从哪个界面发起新页面的跳转?用户取消后返回到哪里?
- ……
首先,通过快捷菜单启动 app 的话,我们自定义的启动画面会让这整个过程变得很复杂,因为系统的整个启动流程很快,但是可能真正到跳转方法被执行的时候,我们冗长的启动画面还没有结束,首页还没有创建,结果…… Push Failed!
为了保证整个跳转过程能够在启动画面完成,并且首页创建(或者说我们自己写的 tabbarController
创建好)后执行,我们就需要观察这个至关重要的 tabbarController
:
1 | - (void)applicationDidBecomeActive:(UIApplication *)application |
至于 -application:performActionForShortcutItem:completionHandler:
这个回调,就比较简单了,教科书般的写法就好了:
1 | // 这个回调会在程序通过ShortcutItem启动的被调用 |
当然了,为了不影响我们正常的启动(尽管已经很慢了),我们还需要在 -application:didFinishLaunchingWithOptions:
里面做一下处理:
1 | - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions |
最重要的还是处理来自快捷菜单的启动(或唤醒)逻辑,说白了这个快捷菜单就是个快捷入口,处理起来也很简单,就是个跳转。例如用户选择了『美团券』,app 启动完后就需要进入到美团券列表页。有了上面的 tabbarController
的保证,我们就能够利用这个东西做一些事情:
1 | ...... |
发起跳转前,先做个 tab 的切换,保证是通过首页跳转的。
然后我们就要判断传入的快捷菜单选项是什么了,这里就用到了上面提到的 UIApplicationShortcutItemType
这个字段来判断了:
1 | ...... |
跳转这一块我们有现成的基于 URL 跳转的机制,能够实现通过 URL 跳转到指定的 viewController
,我们就利用自己的库来处理。在这里就不过多叙述。
如果处理完后,还要告诉外面通过快捷菜单启动的逻辑完成了,就需要个 boolean
,所以整个方法大致这个样子:
1 | // 以下 string 声明在 Info.plist 中 UIApplicationShortcutItems 内 |
一个静态的快捷菜单启动的流程差不多就是这个样子了。
动态的 Quick Action
静态的快捷菜单在 app 被安装后就会创建,哪怕你一次都没有启动过。
看起来是没问题的,但是我们的 app 在用户首次启动的时候会有漫长的引导界面还有一个城市选择界面,如果用户通过快捷菜单首次启动 app 的话,就会和引导界面还有城市选择界面冲突,并且实际上用户还没有启动 app 就展现这三个功能性的快捷菜单逻辑上也有点欠妥。
参考 EverNote 等一些 app 的做法之后,决定不用静态的快捷菜单,而是等到用户真正启动过一次 app 之后再动态生成,那就需要用到动态生成的快捷菜单。
具体判断逻辑跳转方法都不需要调整,唯一需要做的就是把声明在 Info.plist
的菜单项删掉,改成在 -application:didFinishLaunchingWithOptions:
里面动态生成:
1 | - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { |
这里面利用到了 UIApplicationShortcutItem 这个类,生成 UIMutableApplicationShortcutItem
并添加到 UIApplication
里面的 shortcutItems
这个属性中。可以看一下这个指定初始化器:
1 | - (instancetype)initWithType:(NSString *)type |
从上面的参数来看,和 Info.plist
里面设置的一样, type
和 title
是必须的,剩下的都是可选择的。
这些设置好后,直接赋值到 shortcutItems
这个属性中就好了。
小坑:为了防止有些用户没有完成首次启动 app 的城市选择设置,就把 app 杀掉,下次进来还会有可能冲突,索性我们就等用户完成所有的首次启动设置后,再创建这个菜单。
由于在我们的 app 中,如果没有选择城市,userDefaults
里面就不会存储城市信息,那就监听用于存储城市的 userDefaults
里面的 Dictionary
好了:
1 | ...... |
这样,一个动态生成的 Quick Action 菜单就生成了。
我们上面提到了,每一个 item
的 type
就像是一个唯一标示符,我们还可以利用这个 type
动态改变这个 item
的内容。然后再设置给这个 shortcutItems
这个属性就好了。
动态的 item
差不多就是这个样子了。
小结
其实如果我们的 app 中并没有这么多的启动逻辑,用静态的 Quick Action 应该是最佳选择,而如果说非要用静态实现,也到不难,在处理接受到 Quick Action 事件的回调中,对 userDefaults
进行监听,如果首次启动完成了我们再调跳转方法。
动态的 Quick Action 则给我们提供了很多可能,例如动态配置一个活动入口啥的。
随着支持 3D Touch 的设备越来越多,苹果也应该会给 3D Touch 加入更多的可操作性,到那时候,我们可能还会有更多针对 3D Touch 的探索和适配,这只是个开始。