iOS 开发之 Widget 的实现

Posted by Calvin on 2017-06-29

添加 Today Extension 工程

在原有的项目基础上,想要使用 Today Extension,即 Widget。我们需要创建一个新的 target,点击File-->New-->Target-->Today Extention,如下图所示:

20170629149873901496022.png

2017062914987390541464.png

创建成功后如图所示:

20170629149873913639399.png

此时直接运行项目,如下图所示:

20170629149873922219531.png

Widget UI 简单实现

本人习惯使用纯代码布局,所以我删除了默认创建的 MainInterface.storyboard,并在info.plist 中删除 NSExtensionMainStoryboard 字段,添加NSExtensionPrincipalClassTodayViewController,如下图所示:

2017062914987393879662.png

当然,如果你习惯使用 xib 或者 storyboard 布局的话,可以直接在 MainInterface.storyboard 文件中进行 UI 实现。

实现下面的协议,配置 widget 的边距,否则你会发现 UI 的位置会与左侧边界有一定距离。

- (UIEdgeInsets)widgetMarginInsetsForProposedMarginInsets:(UIEdgeInsets)defaultMarginInsets {
// 配置边距为0
return UIEdgeInsetsMake(0.0, 0.0, 0.0, 0.0);
}

然后初始化一个 UILabel 显示未登录的提示,并给 UILabel 添加一个点击事件,使点击后能够打开 App 的登录页面。代码如下:

self.loginInLabel = [[UILabel alloc] init];
self.loginInLabel.textColor = [UIColor colorWithRed:(214.0/255.0) green:(33.0/255.0) blue:(25.0/255.0) alpha:1];
self.loginInLabel.backgroundColor = [UIColor clearColor];
self.loginInLabel.frame = CGRectMake(0, 0, [UIScreen mainScreen].bounds.size.width-16, 90);
self.loginInLabel.textAlignment = NSTextAlignmentCenter;
self.loginInLabel.text = @"未登录,点击登录账户";
self.loginInLabel.font = [UIFont systemFontOfSize:20];
self.loginInLabel.userInteractionEnabled = YES;
UITapGestureRecognizer *openURLContainingAPP = [[UITapGestureRecognizer alloc]initWithTarget:self action:@selector(openURLContainingAPP)];
[self.loginInLabel addGestureRecognizer:openURLContainingAPP];
[self.view addSubview:self.loginInLabel];

当然,widget 的 UI 实现是根据具体的业务来进行实现的,此处只是举例。

Widget 的展开和折叠

NSExtensionContext中,有widgetLargestAvailableDisplayMode属性,来确认当前widget是展开还是折叠状态。所以,我们可以先在viewWillAppear中设置widgetmode

- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
self.extensionContext.widgetLargestAvailableDisplayMode = NCWidgetDisplayModeExpanded;
}

然后,就是展开和折叠的处理了。在NCWidgetProviding协议中,有widgetActiveDisplayModeDidChange方法

- (void)widgetActiveDisplayModeDidChange:(NCWidgetDisplayMode)activeDisplayMode withMaximumSize:(CGSize)maxSize {
if (activeDisplayMode == NCWidgetDisplayModeCompact) {
self.preferredContentSize = CGSizeMake([UIScreen mainScreen].bounds.size.width, 110);
} else {
self.preferredContentSize = CGSizeMake([UIScreen mainScreen].bounds.size.width, 300);
}
}

在这里需要注意的是,Widget 的折叠和展开时的高度不是可以随便设置的。在 widgetActiveDisplayModeDidChange 协议方法里可以打印出
withMaximumSize

其中在 5s 模拟器下:

NCWidgetDisplayModeCompact模式下:{304, 110}
NCWidgetDisplayModeExpanded模式下:{304, 528}

6s模拟器下:

NCWidgetDisplayModeCompact模式下:{359, 110}
NCWidgetDisplayModeExpanded模式下:{359, 616}

从上面的限制可知,widget 在折叠状态下最低为110,最高也根据机型有最大限制。注意处理好折叠和展开时 widget UI 和数据的变化,在此不做赘述。

点击 Widget 进入 App

刚才我们在 widget 中添加了一个充满折叠视图的 UILabel,并想在用户点击时直接打开 App 的登录页面。

这里我们要设置一下 URL Schemes ,URL Schemes 主要作用是 App 之间相互调用打开,包括我们在实现第三方分享、第三方登录时都需要用到 URL Schemes

如图所示,在 info -> URL Types 里设置好 Schemes

20170629149874119223815.png

再在 Today Extention 对应的 info.plist 里设置 Schemes,如图所示:

20170629149874139991858.png

设置好 Schemes 后,实现 UILabel 的点击事件:

// 通过 openURL 的方式启 APP
- (void)openURLContainingAPP
{
[self.extensionContext openURL:[NSURL URLWithString:@"xiaozhumi://"]
completionHandler:^(BOOL success) {
NSLog(@"open url result:%d",success);
}];
}

此时我们可以处理点击事件,让用户点击后直接打开 App 的登录页面并登录。

当用户登录成功后再次查看 widget 时,这时候如果继续显示登录的提示显然是不妥的,此时根据业务需求,我需要在用户登录账号成功后再次查看 widget 时,widget 展示用户的个人积分。

App 和 Widget 的数据共享

由于沙盒机制,拓展应用是不允许访问宿主应用的沙盒路径的,因此上述用法是不对的,需要搭配 app group完成实例化 UserDefaults。

首先需要去苹果开发者中心 Identifiers -> APP Groups 中创建一个 APP Group,命名方式 group.com.companyName.xxx,如下图

20170629149874199157137.png

当创建好 App Group 后,分别在 主项目和 TodayCapabilities 设置选项中打开 App Group 选项,并选中在苹果开发者中心设置的 App Group

如下图所示:

20170629149874227325941.png

20170629149874228068268.png

此时,Todey 就可以和主项目进行数据共享了。

通过 NSUserDefaults 共享数据

当用户登录成功后,保存用户的积分到本地供 Widget 读取并展示。

// 保存积分供 Widget 使用
NSUserDefaults *shared = [[NSUserDefaults alloc] initWithSuiteName:@"group.com.xiaozhumi.today"];
[shared setObject:jf forKey:@"UserJF"];
[shared synchronize];

widget 读取积分

NSUserDefaults *shared = [[NSUserDefaults alloc] initWithSuiteName:@"group.com.xiaozhumi.today"];
NSString *value = [shared valueForKey:@"UserJF"];

展示结果如图所示:

20170629149874261861798.png

通过 NSFileManager 共享数据

- (BOOL)saveDataByNSFileManager
{
NSError *error = nil;
NSURL *containerURL = [[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:@"group.com.xxx.xxx"];
containerURL = [containerURL URLByAppendingPathComponent:@"Library/Caches/test"];
NSString *value = @"test";
BOOL result = [value writeToURL:containerURL atomically:YES encoding:NSUTF8StringEncoding error:&error];
if (!result) {
NSLog(@"%@",error);
} else {
NSLog(@"save value:%@ success.",value);
}
return result;
}
- (NSString *)readDataByNSFileManager
{
NSError *error = nil;
NSURL *containerURL = [[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:@"group.com.xxx.xxx"];
containerURL = [containerURL URLByAppendingPathComponent:@"Library/Caches/test"];
NSString *value = [NSString stringWithContentsOfURL:containerURL encoding:NSUTF8StringEncoding error:&error];
return value;
}

至此,基本已经实现了 widget 的基本功能。由于本文是在实际项目中截图记录的,所以暂不提供Demo查看。