iOS widget(小部件)开发初探
1、前言
现在很多应用都有小部件功能,用起来非常方便,在用户安装包含Today小部件的应用后,他们可以将小部件添加到Today视图。当用户在“今日”视图中选择“编辑”时,通知中心会显示一个视图,允许用户添加,重新排序和删除小部件。
常见的有支付宝、日历和天气,那么我们也想为自己的应用增加widget功能该怎么办呢,那就继续往下看喽。
老版本(iOS9之前)的是直接下拉出现【今天】和【通知】两个选项,iOS10进行了更改,下拉是【通知】,右滑最左侧是小部件,所以下文提到的【今天】就是我们的小部件
2、准备工作
了解这个功能,当然官方文档App Extension Programming Guide是最值得读的了。
官方对小部件的一段介绍是:
App extensions in the Today view are called widgets. Widgets give users quick access to information that’s important right now. For example, users open the Today view to check current stock prices or weather conditions, see today’s schedule, or perform a quick task such as marking an item as done. Users tend to open the Today view frequently, and they expect the information they’re interested in to be instantly available.
“今日”视图中的附加应用信息称为小部件,小部件使用户能够快速访问现在非常重要的信息。例如,用户打开今日视图以检查当前股价或天气状况,查看今天的时间表,或者执行快速任务,例如将项目标记为已完成。用户倾向于经常打开“今日”视图,他们希望他们感兴趣的信息立即可用。
注意: 小部件是不支持键盘输入的
交互要求
确保今天的扩展点适合您想要提供的功能。最好的小部件为用户提供快速更新或启用非常简单的任务。如果您想要创建支持多步骤任务的应用扩展程序,或者帮助用户执行冗长的任务(如上传或下载内容),则“今日”扩展点不是正确的选择。
3、创建项目
创建
Xcode->File->New->Target->Today Extension 创建我们的Widget-
项目结构
- 项目配置
项目默认是有storyboard的,这里我想使用纯代码,所以把他删除了,删除后我们要配置一下启动界面,在TodayWidget->Info.plist->Extension
删除 NSExtensionMainStoryboard 选项
增加 NSExtensionPrincipalClass,value 为 类的名字 TodayViewController
这个时候你就可以用纯代码构建布局了
- 项目配置
<key>NSExtension</key>
<dict>
<key>NSExtensionPointIdentifier</key>
<string>com.apple.widget-extension</string>
<key>NSExtensionPrincipalClass</key>
<string>TodayViewController</string>
</dict>
- 代码
创建布局什么的和平时开发一样,一些方法代码里也都有注释,下面主要说一下数据共享和打开app的方法
//TodayViewController.m
#import "TodayViewController.h"
#import <NotificationCenter/NotificationCenter.h>
@interface TodayViewController () <NCWidgetProviding>
@property (nonatomic, strong) NSTimer *timer;
@property (nonatomic, strong) UILabel *timeLabel;
@property (nonatomic, assign) int count;
@end
@implementation TodayViewController
- (void)viewDidLoad {
[super viewDidLoad];
[self initView];
}
- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
}
- (void)viewWillAppear:(BOOL)animated {
// 设置折叠还是展开
// 设置展开才会展示,设置折叠无效,左上角不会出现按钮, ❓
self.extensionContext.widgetLargestAvailableDisplayMode = NCWidgetDisplayModeExpanded;
}
// 展开/折叠监听
- (void)widgetActiveDisplayModeDidChange:(NCWidgetDisplayMode)activeDisplayMode withMaximumSize:(CGSize)maxSize{
if (activeDisplayMode == NCWidgetDisplayModeCompact) { //折叠
// 折叠后的大小是固定的,目前测试的更改无效,默认高度应该是110
self.preferredContentSize = CGSizeMake([UIScreen mainScreen].bounds.size.width, 100);
}else { // 展开
self.preferredContentSize = CGSizeMake([UIScreen mainScreen].bounds.size.width, 300);
}
}
- (void)widgetPerformUpdateWithCompletionHandler:(void (^)(NCUpdateResult))completionHandler {
// Perform any setup necessary in order to update the view.
// If an error is encountered, use NCUpdateResultFailed
// If there's no update required, use NCUpdateResultNoData
// If there's an update, use NCUpdateResultNewData
completionHandler(NCUpdateResultNewData);
}
- (void)initView {
// 和主应用的数据共享,获取主应用里的数据
NSUserDefaults *sharedData = [[NSUserDefaults alloc] initWithSuiteName:@"group.rs.testGroup"];
NSString *name = [sharedData objectForKey:@"name"];
// 官方建议使用自动布局创建控件,这里是写的固定的
UILabel *label = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, self.view.frame.size.width/2.0, 50)];
label.text = [NSString stringWithFormat:@"姓名:%@",name];
label.textAlignment = NSTextAlignmentCenter;
label.textColor = [UIColor blueColor];
[self.view addSubview:label];
UIButton *btn = [[UIButton alloc] initWithFrame:CGRectMake(self.view.frame.size.width/2.0, 0, self.view.frame.size.width/2.0, 50)];
[btn addTarget:self action:@selector(btnAction) forControlEvents:UIControlEventTouchUpInside];
[btn setTitle:[self readByFileManager] forState:UIControlStateNormal];
[btn setTitleColor:[UIColor blueColor] forState:UIControlStateNormal];
[self.view addSubview:btn];
// 添加计时器
_timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(timerAction) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:_timer forMode:NSRunLoopCommonModes];
_timeLabel = [[UILabel alloc] initWithFrame:CGRectMake(0, 50, self.view.frame.size.width, 50)];
_timeLabel.textAlignment = NSTextAlignmentCenter;
_timeLabel.textColor = [UIColor redColor];
[self.view addSubview:_timeLabel];
_count = 100;
}
// NSFileManager 读取数据
- (NSString *)readByFileManager {
NSError *error = nil;
NSURL *containUrl = [[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:@"group.rs.testGroup"];
containUrl = [containUrl URLByAppendingPathComponent:@"group.data"];
NSString *text = [NSString stringWithContentsOfURL:containUrl encoding:NSUTF8StringEncoding error:&error];
return text;
}
- (void)timerAction {
if (_count > 0){
_count -= 1;
}else {
_count = 100;
}
_timeLabel.text = [NSString stringWithFormat:@"倒计时:%ds",_count];
}
// 点击按钮打开主app
- (void)btnAction {
[self.extensionContext openURL:[NSURL URLWithString:@"TodayWidget://"] completionHandler:^(BOOL success) {
}];
}
@end
运行后的效果
![运行效果图](https://upload-images.jianshu.io/upload_images/1828346-bdbb97b174c2eeee.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
4、 调起app
因为 extension 和 主app 是两个完全独.立的进程,所以它们之间不不能直接通信(不能像应用内部点击按钮,跳转到指定页面)。为了了实现 Widget 调起 app,这里通过 openURL 的方式来启动 主app。 * 添加URL Schemes 在 主app 里配置 Targets->MCWidgetDemo-> Info->Url Types->+ 如下图 设置 URL Schemes 为 TodayWidget ![配置](https://upload-images.jianshu.io/upload_images/1828346-fb27aab32a2c1317.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)- 在 Widget中打开
// 点击按钮打开主app
- (void)btnAction {
[self.extensionContext openURL:[NSURL URLWithString:@"TodayWidget://"] completionHandler:^(BOOL success) {
}];
}
- 主应用中的监听
// AppDelegate.m
- (BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary<UIApplicationOpenURLOptionsKey,id> *)options{
if ([url.scheme isEqualToString:@"TodayWidget"]){
//这里监听到是通过widget打开,可以进行发送通知等操作
return YES;
}
return NO;
}
5、数据共享
扩展程序一般都不是脱离宿主程序单独运行的,难免需要和宿主程序进行数据交互。由于拓展与宿主应用是两个完全独立的App,并且iOS应用基于沙盒的形式限制,所以一般的共享数据方法都是实现不了数据共享,这里就需要使用App Groups(App Groups这是iOS8新开放的功能,在OS X上早就可用了。它主要用于同一Group下的App共享同一份读写空间,以实现数据共享)。
通过 App Groups 提供的同一 group 内 app 共同读写区域,可以用 NSUserDefaults 和NSFileManager 两种方式实现 extension 和 主app 之间的数据共享。
创建 App Groups
在开发者网站注册一个App Groups,点击加号,填入名字和id一路确认即可得到下图App Groups。在主程序和扩展程序中分别设置打开App Group,设置一个group的名称,这里要保证宿主APP和扩展APP的groupName要是相同的。
- 利用NSUserDefaults数据共享
在主应用中存储数据
NSUserDefaults *sharedData = [[NSUserDefaults alloc] initWithSuiteName:@"group.rs.testGroup"];
[sharedData setValue:@"Mr Right" forKey:@"name"];
[sharedData synchronize];
在widget中读取数据
NSUserDefaults *sharedData = [[NSUserDefaults alloc] initWithSuiteName:@"group.rs.testGroup"];
NSString *name = [sharedData objectForKey:@"name"];
注意:保存读取数据的时候必须指明group id;
- 利用NSFileManager共享数据
在主应用中存储数据
// NSFileManager 存储数据
- (void)saveFile {
NSError *error = nil;
NSURL *containUrl = [[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:@"group.rs.testGroup"];
containUrl = [containUrl URLByAppendingPathComponent:@"group.data"];
NSString *text = @"打开app";
BOOL result = [text writeToURL:containUrl atomically:YES encoding:NSUTF8StringEncoding error:&error];
if (result){
NSLog(@"save success");
}else {
NSLog(@"error:%@", error);
}
}
在widget中读取数据
// NSFileManager 读取数据
- (NSString *)readByFileManager {
NSError *error = nil;
NSURL *containUrl = [[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:@"group.rs.testGroup"];
containUrl = [containUrl URLByAppendingPathComponent:@"group.data"];
NSString *text = [NSString stringWithContentsOfURL:containUrl encoding:NSUTF8StringEncoding error:&error];
return text;
}
6、总结
至此,小部件的简单开发算是完成了,后续可能还有发布的证书配置,网络请求等情况,我还没有尝试,等实际应用了再进行补充,希望能对你有所帮助,笔者也是第一次尝试,如果有哪里不对的,请指正。
最后附上Demo地址
7、参考链接
上一篇: 【UGUI】侧面滑出的菜单栏,宽度自适应
下一篇: UGUI背包拖拽