iOS上实现 3D Touch 功能

前言

WWDC 2016 - Session 228 - iOS 中提到了 3D Touch,对它简单定义是

3D TouchiOS 用户界面添加了一个全新的维度,并引入了一种全新的与 iPhone 交互的方式。

  • 设备限制: iPhone 6s+
  • 系统限制: iOS 9+

环境

macOS Sierra 10.12.3
Xcode 8.2.1
iPhone 6s 10.1.1

应用场景

3d Touch APIs 文档中,提到了

The Home screen quick action API is for adding shortcuts to your app icon that anticipate and accelerate a user’s interaction with your app.
The UIKit peek and pop API lets you provide easy access, within your app, to additional content while maintaining the user’s context. Use the peek quick actions API to provide a press-enabled replacement to your app’s touch-and-hold actions.
The Web view peek and pop API lets you enable system-mediated previews of HTML link destinations.
The UITouch force properties let you add customized force-based user interaction to your app.

3d Touch 大概有 3 个应用场景,主屏快速启动,而 UIKit peek and popWeb view peek and pop 可以合并为一个预览的功能, 可以根据 force properties 按压属性值做其它交互上的操作。

主屏快速启动(Home Screen Quick Actions)

关于这个功能,很多应用都是有的,长按并保持几秒就可以触发,算是一个应用功能的快速入口,当你觉得应用的某些功能很重要时,让用户能快速定位到这个功能,就可以添加到这儿。

关于数量上的限制,我查了一下资料,在 Class - UIApplication​Shortcut​Item 中有如下内容

The system limits the number quick actions displayed when a user presses a Home screen app icon. Within the limited set of displayed quick action titles, your* static quick actions are shown first, starting at the topmost position in the list. If your static items do not consume the permissible number for display and you have also defined dynamic quick actions using this class, then one or more of your dynamic quick actions is displayed.

结论:

  • 系统会限制 Quick Actions 的显示数量,
  • iOS 9 上最多显示 4 个,有些应用确实可以显示5个。

Quick Actions 截图

预览与弹出窗口(Peek and Pop)

Peek and PopGoogle 直接翻译意思是偷看和流行,很明显不是苹果想表达的意思。
Peek 英文解释为 look quickly or furtively 快速偷偷的瞥一眼,大约就是预览的意思,doc 上有的这一句

Optional navigation to the view shown in the preview—known as a pop

大概明白是什么意思了。就像微信中这个这个样子

IMG_0978

上面这个效果在未联网状态下不受影响,而

IMG_0980

加载的是网络资源,当未联网时,就无法实现预览功能, 属于 Web view peek and pop

按压属性(force properties)

iOS9.0 为我们提供了一个新的交互参数:力度。我们可以检测某一交互的力度值,来做相应的交互处理

比如系统的联系人

IMG_0981

实现

关于主屏快速启动

上面引用的一段话提到了 static quick actionsdynamic quick actions ,也就是说快速启动的设置方法有静态和动态。

静态

配置 info.plist 文件

摘一段酷狗的 info.plist 的配置方法,应该就一目了然了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
<key>UIApplicationShortcutItems</key>
<array>
<dict>
<key>UIApplicationShortcutItemTitle</key>
<string>听歌识曲</string>
<key>UIApplicationShortcutItemType</key>
<string>com.kugou.shortcut.recognize</string>
<key>UIApplicationShortcutItemIconFile</key>
<string>3d_touch_recognize.png</string>
</dict>
<dict>
<key>UIApplicationShortcutItemTitle</key>
<string>继续播放</string>
<key>UIApplicationShortcutItemType</key>
<string>com.kugou.shortcut.playmusic</string>
<key>UIApplicationShortcutItemIconFile</key>
<string>3d_touch_playmusic.png</string>
</dict>
<dict>
<key>UIApplicationShortcutItemTitle</key>
<string>搜索</string>
<key>UIApplicationShortcutItemType</key>
<string>com.kugou.shortcut.search</string>
<key>UIApplicationShortcutItemIconFile</key>
<string>3d_touch_search.png</string>
</dict>
<dict>
<key>UIApplicationShortcutItemTitle</key>
<string>播放本地音乐</string>
<key>UIApplicationShortcutItemType</key>
<string>com.kugou.shortcut.local</string>
<key>UIApplicationShortcutItemIconFile</key>
<string>3d_touch_localmusic.png</string>
</dict>
</array>
  • UIApplicationShortcutItems 数组类型 快捷启动的 key
  • UIApplicationShortcutItemTitle 标题(必填) [可监听该项判断用户是从哪一个标签进入App]
  • UIApplicationShortcutItemType 类型(必填 [可监听该项判断用户是从哪一个标签进入App]
  • UIApplicationShortcutItemIconFile 图标(默认是一个黑色的圆点)

动态

每一项快速启动对应一个 UIApplicationShortcutItem 对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
NS_CLASS_AVAILABLE_IOS(9_0) __TVOS_PROHIBITED
@interface UIApplicationShortcutItem : NSObject <NSCopying, NSMutableCopying>

- (instancetype)init NS_UNAVAILABLE;
- (instancetype)initWithType:(NSString *)type localizedTitle:(NSString *)localizedTitle localizedSubtitle:(nullable NSString *)localizedSubtitle icon:(nullable UIApplicationShortcutIcon *)icon userInfo:(nullable NSDictionary *)userInfo NS_DESIGNATED_INITIALIZER;
- (instancetype)initWithType:(NSString *)type localizedTitle:(NSString *)localizedTitle;

// An application-specific string that identifies the type of action to perform.
@property (nonatomic, copy, readonly) NSString *type;

// Properties controlling how the item should be displayed on the home screen.
@property (nonatomic, copy, readonly) NSString *localizedTitle;
@property (nullable, nonatomic, copy, readonly) NSString *localizedSubtitle;
@property (nullable, nonatomic, copy, readonly) UIApplicationShortcutIcon *icon;

// Application-specific information needed to perform the action.
// Will throw an exception if the NSDictionary is not plist-encodable.
@property (nullable, nonatomic, copy, readonly) NSDictionary<NSString *, id <NSSecureCoding>> *userInfo;

@end

快速启动项的图标对应的是 UIApplicationShortcutIcon

1
2
3
4
5
6
7
8
9
10
11
12
NS_CLASS_AVAILABLE_IOS(9_0) __TVOS_PROHIBITED
@interface UIApplicationShortcutIcon : NSObject <NSCopying>

// Create an icon using a system-defined image.
+ (instancetype)iconWithType:(UIApplicationShortcutIconType)type;

// Create an icon from a custom image.
// The provided image named will be loaded from the app's bundle
// and will be masked to conform to the system-defined icon style.
+ (instancetype)iconWithTemplateImageName:(NSString *)templateImageName;

@end

创建 UIApplicationShortcutItem 对象后,可以通过 UIApplicationshortcutItems 属性进行关联。

1
2
3
4
@interface UIApplication (UIShortcutItems)
// Register shortcuts to display on the home screen, or retrieve currently registered shortcuts.
@property (nullable, nonatomic, copy) NSArray<UIApplicationShortcutItem *> *shortcutItems NS_AVAILABLE_IOS(9_0) __TVOS_PROHIBITED;
@end

比如用如下代码动态创建酷狗的第 5 个 分享”酷狗”音乐

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
...

/**
* 通过代码实现动态菜单
* 一般情况下设置主标题、图标、type等,副标题是不设置的 - 简约原则
* iconWithTemplateImageName 自定义的icon
* iconWithType 系统的icon
*/
UIApplicationShortcutIcon *share = [UIApplicationShortcutIcon iconWithType:UIApplicationShortcutIconTypeFavorite];

UIApplicationShortcutItem *itemShare = [[UIApplicationShortcutItem alloc] initWithType:@"share" localizedTitle:@"分享 “酷狗”音乐" localizedSubtitle:nil icon:share userInfo:nil];

[UIApplication sharedApplication].shortcutItems = @[itemShare];
...
}

但是最后发现还是只有 4 个,看来快速启动的数量确实是有限制。

问题: 如何区分点击具体要跳转到哪个功能界面?

必定存在信息的传递

1
2
3
// Called when the user activates your application by selecting a shortcut on the home screen,
// except when -application:willFinishLaunchingWithOptions: or -application:didFinishLaunchingWithOptions returns NO.
- (void)application:(UIApplication *)application performActionForShortcutItem:(UIApplicationShortcutItem *)shortcutItem completionHandler:(void(^)(BOOL succeeded))completionHandler NS_AVAILABLE_IOS(9_0) __TVOS_PROHIBITED;

当用户通过快速启动的方式激活应用时,会触发 UIApplicationDelegate 的这个方法

实现该方法,并设置断点,通过快速启动激活应用,输出 shortcutItem 参数

1
<UIApplicationShortcutItem: 0x17026f380; type: com.kugou.shortcut.local, title: 播放本地音乐>

我们可以知道至少可以通过 typetitle 去区分不同的快速启动按钮。

关于预览与弹出窗口

经过授权的应用视图控制器可响应用户不同的按压力量,随着按压力量的增加,
会有三个交互阶段:

暗示预览功能可用,会有一个虚化的效果
Peek:重按一下后出现的预览,展示预览的视图以及快捷菜单
Pop:跳转到预览的视图控制器,是在Peek后进一步按压后进入预览的视图控制器

流程

  • 主视图控制器 用于显示 Peek 弹窗的视图控制器
  • Peek 视图控制器,用于提供预览的弹窗视图

主视图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
#import "ViewController.h"
#import "PreviewViewController.h"

@interface ViewController () <UIViewControllerPreviewingDelegate,UITableViewDataSource,UITableViewDelegate>

@property (nonatomic,strong) UITableView *tableView;
@property (nonatomic,strong) NSArray *datas;

@end

@implementation ViewController

- (void)viewDidLoad {
[super viewDidLoad];
_tableView = [[UITableView alloc] initWithFrame:self.view.bounds style:UITableViewStylePlain];
_tableView.dataSource = self;
_tableView.delegate = self;
[self.view addSubview:_tableView];
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return 10;
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"peekpop"];
if (cell == nil) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"peekpop"];
}

cell.textLabel.text = self.datas[indexPath.row];

/**
* UIForceTouchCapability 检测是否支持3D Touch
* 支持3D Touch
*/
if (self.traitCollection.forceTouchCapability == UIForceTouchCapabilityAvailable) {
// 系统所有cell可实现预览(peek)
[self registerForPreviewingWithDelegate:self sourceView:cell]; // 注册cell
}

return cell;
}

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath{
[tableView deselectRowAtIndexPath:indexPath animated:YES];

PreviewViewController *webVC = [[PreviewViewController alloc] init];
webVC.webUrl = self.datas[indexPath.row];

webVC.hidesBottomBarWhenPushed = true;

[self.navigationController pushViewController:webVC animated:true];
}

#pragma mark - UIViewControllerPreviewingDelegate
- (nullable UIViewController *)previewingContext:(id <UIViewControllerPreviewing>)previewingContext viewControllerForLocation:(CGPoint)location{

// 转化坐标
location = [self.tableView convertPoint:location fromView:[previewingContext sourceView]];

// 根据locaton获取位置
NSIndexPath *indexPath = [self.tableView indexPathForRowAtPoint:location];

PreviewViewController *webVC = [[PreviewViewController alloc] init];
webVC.webUrl = self.datas[indexPath.row];

return webVC;
}

- (void)previewingContext:(id <UIViewControllerPreviewing>)previewingContext commitViewController:(UIViewController *)viewControllerToCommit{

viewControllerToCommit.hidesBottomBarWhenPushed = YES;

[self.navigationController pushViewController:viewControllerToCommit animated:YES];
}

# pragma mark - 懒加载
- (NSArray *)datas {
if (_datas == nil) {
NSMutableArray *dataM = [NSMutableArray arrayWithCapacity:30];
for (int i = 0; i < 10; ++i) {
[dataM addObject:@"https://www.baidu.com"];
}
_datas = [dataM copy];
}
return _datas;
}

@end

Peek 视图控制器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#import "PreviewViewController.h"

@interface PreviewViewController ()
@property (nonatomic,strong) UIWebView *webView;
@end

@implementation PreviewViewController

- (void)viewDidLoad {
[super viewDidLoad];
_webView = [[UIWebView alloc] initWithFrame:self.view.bounds];
[_webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:_webUrl]]];
[self.view addSubview:_webView];
}

- (NSArray<id<UIPreviewActionItem>> *)previewActionItems{

// 赞
UIPreviewAction *admire = [UIPreviewAction actionWithTitle:@"赞" style:UIPreviewActionStyleDefault handler:^(UIPreviewAction * _Nonnull action, UIViewController * _Nonnull previewViewController) {
NSLog(@"点赞成功");
}];

// 举报
UIPreviewAction *report = [UIPreviewAction actionWithTitle:@"举报" style:UIPreviewActionStyleDestructive handler:^(UIPreviewAction * _Nonnull action, UIViewController * _Nonnull previewViewController) {
NSLog(@"举报成功");
}];

// 收藏
UIPreviewAction *collect = [UIPreviewAction actionWithTitle:@"收藏" style:UIPreviewActionStyleSelected handler:^(UIPreviewAction * _Nonnull action, UIViewController * _Nonnull previewViewController) {
NSLog(@"收藏成功");
}];

return @[admire,report,collect];
}

@end

对以上的代码进行简答说明:

  1. 主视图遵守 UIViewControllerPreviewingDelegate
  2. 注册触发 Peek 的视图对象
  3. 设置 Peek 视图控制器的操作 (可选)

该协议有两个方法

1
2
3
4
5
6
7
NS_CLASS_AVAILABLE_IOS(9_0) @protocol UIViewControllerPreviewingDelegate <NSObject>

// If you return nil, a preview presentation will not be performed
- (nullable UIViewController *)previewingContext:(id <UIViewControllerPreviewing>)previewingContext viewControllerForLocation:(CGPoint)location NS_AVAILABLE_IOS(9_0);
- (void)previewingContext:(id <UIViewControllerPreviewing>)previewingContext commitViewController:(UIViewController *)viewControllerToCommit NS_AVAILABLE_IOS(9_0);

@end

根据API-UIView​Controller​Previewing​Delegate,中可知

previewing​Context:​view​Controller​For​Location:​
Called when the user has pressed a source view in a previewing view controller, thereby obtaining a surrounding blur to indicate that a preview (peek) is available.

previewing​Context:​commit​View​Controller:​
Called to let you prepare the presentation of a commit (pop) view from your commit view controller.

这两个方法将会分别在用户按压操作注册了 Peek 的视图对象与 Peek 视图即将从主视图 Pop 时触发。

参数说明

previewingContext
The context object for the previewing view controller.

预览视图控制器的上下文对象,负责实现一系列预览的效果,其中的 sourceView 属性指向主视图中触发预览操作的视图对象

location
The location of the touch in the source view’s coordinate system.

locationsourceView 视图的坐标系中触摸的位置,比如在下面那张截图中代理方法传递进来的 locaiton 参数的值为 (x = 95, y = 33.5) 这个是相对于点击的 cell 为了确定点击的具体是哪个 cell 需要对进行转换,转换后的结果为 (x = 95, y = 77.5)

将来自于 fromView: 视图的坐标 location 转为相对于 self.tableViewlocation

Return
The view controller whose view you want to provide as the preview (peek), or nil to disable preview..

Peek 预览的视图控制器

1
2
3
4
5
6
7
8
/**
* UIForceTouchCapability 检测是否支持3D Touch
* 支持3D Touch
*/
if (self.traitCollection.forceTouchCapability == UIForceTouchCapabilityAvailable) {
// 系统所有cell可实现预览(peek)
[self registerForPreviewingWithDelegate:self sourceView:cell]; // 注册cell
}

注册与取消注册 API

1
2
3
4
5
6
7
@interface UIViewController (UIViewControllerPreviewingRegistration)

// Registers a view controller to participate with 3D Touch preview (peek) and commit (pop).
- (id <UIViewControllerPreviewing>)registerForPreviewingWithDelegate:(id<UIViewControllerPreviewingDelegate>)delegate sourceView:(UIView *)sourceView NS_AVAILABLE_IOS(9_0);
- (void)unregisterForPreviewingWithContext:(id <UIViewControllerPreviewing>)previewing NS_AVAILABLE_IOS(9_0);

@end

Peek 视图控制器中添加代码

1
2
3
4
5
6
7
8
9
10
- (NSArray<id<UIPreviewActionItem>> *)previewActionItems{

// 赞
UIPreviewAction *admire = [UIPreviewAction actionWithTitle:@"赞" style:UIPreviewActionStyleDefault handler:^(UIPreviewAction * _Nonnull action, UIViewController * _Nonnull previewViewController) {
NSLog(@"点赞成功");
}];
...

return @[admire,report,collect];
}

因为对预览的视图的操作写在 Peek 视图控制器是很正常

Swift 的实现风格的可以参考文章后的链接 ViewControllerPreviews: Using the UIViewController previewing APIs

关于按压属性

1
2
3
4
// Force of the touch, where 1.0 represents the force of an average touch
@property(nonatomic,readonly) CGFloat force NS_AVAILABLE_IOS(9_0);
// Maximum possible force with this input mechanism
@property(nonatomic,readonly) CGFloat maximumPossibleForce NS_AVAILABLE_IOS(9_0);

按压实现预览本质上也是一种根据按压实现的交互。在此就不在赘述了。

参考

理解HTTPS协议 IntelliJ IDEA Main函数传参
Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×