依旧是因为繁琐,所以这个文章也一直没有写。明天情人节,今天就赶紧写一写吧,因为明天肯定是各种虐狗分享。汪~
小组件widget就是例如懒猪清单的生日提醒这样,左滑直接在系统显示,比如每日天气等,方便快捷,可以当成一个展示的工具。
一、widget的创建
widget是一个依附于app的插件,所以如果你想创建对应项目的widget的话,在对应的项目中,新建一个target
target类型选择Today Extension,然后填写对应的Product Name创建这个target
1.1、证书和配置文件的配置
如果你的这个项目的证书签名是自动管理的话,那可以跳过这一段,如果是手动管理的话,需要配置一下widget和项目的配置文件
选择这个widget对应的target,如果你是手动管理证书的话,需要去开发后台创建widget对应的bundle id和对应的配置文件,和创建app的的bundle id和配置文件一样,去后台对应的创建即可。因为基本上widget都需要读取app的信息,所以需要将app的bundle id和widget的bundle id绑定同一个group里面去读取沙盒信息,所以先去创建一个app group
然后将app的bundle id和widget的bundle id都绑定一下这个app group,这样是为了两个数据交互,绑定之后去创建下载对应的Provisioning Profiles就行了,app和widget都需要创建对应的开发环境的配置文件和App Store送审的配置文件,这样才能去一起送审。
1.2、修改widget的样式为代码布局
widget虽然和项目是一个整体的工程,但是文件和图片都是不互通的,如果有一张图片在app路径下面,你在widget使用是读取不到的。所以如果有什么布局想复用,需要在widget的文件下也复制一份。
因为我喜欢使用代码布局,所以在widget的info.plist文件修改成代码布局的设置。如果你是用storyboard布局的话可以跳过
首先将原有NSExtensionMainStoryboard
字段删除,添加字段NSExtensionPrincipalClass
,value是你所写的controller的名称,一般默认的都是TodayViewController。
如果使用的swift
,会出现报错crash
Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '*** -[__NSDictionaryM setObject:forKey:]: object cannot be nil (key: A8ED0DD2-EC6A-4A70-8B99-20BE30875FDA)'
这个是因为如果使用的swift,NSExtensionPrincipalClass
字段需要设置项目名称,比如在OC中填写的是TodayViewController
,那么swift
可以写成$(PRODUCT_NAME).TodayViewController
。如果需要修改显示的名字,在info.plist
直接把默认的display name里面的$(PRODUCT_NAME)
修改为名字即可
1.3、开始布局widget
在TodayViewController中,开始布局widget的样式。通用组件都可以使用,但是tableview等滚动视图是无法滚动的,所以可以使用tableview布局,但是要注意高度,是不能滚动的。
iOS10之后,Widget支持展开及折叠两种展现方式,通过设置widgetLargestAvailableDisplayMode
属性可以让Widget程序实现展开布局,同时在左滑到widget显示的时候,会调用这个viewWillAppear,这时候可以去刷新数据获取最新的数据。
- (void)viewDidLoad {
[super viewDidLoad];
if (@available(iOS 10.0, *))
{
self.extensionContext.widgetLargestAvailableDisplayMode = NCWidgetDisplayModeExpanded;
}
self.preferredContentSize = CGSizeMake([UIScreen mainScreen].bounds.size.width, 110);
[self createUI];
[self loadData];
}
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
[self loadData];
}
宽高切换时的代理布局,因为我用的是tableview布局,所以在宽高变化时,将tableview的宽高也进行变换即可。
- (void)widgetActiveDisplayModeDidChange:(NCWidgetDisplayMode)activeDisplayMode withMaximumSize:(CGSize)maxSize
{
NSLog(@"maxWidth %f maxHeight %f",maxSize.width,maxSize.height);
if (@available(iOS 10.0, *)) {
if (activeDisplayMode == NCWidgetDisplayModeCompact)
{
[self.m_tableView setFrame:CGRectMake(10, 0, maxSize.width-20, 110)];
self.preferredContentSize = CGSizeMake(maxSize.width, 110);
}
else
{
[self.m_tableView setFrame:CGRectMake(10, 0, maxSize.width-20, 310)];
self.preferredContentSize = CGSizeMake(maxSize.width, 310);
}
} else {
[self.m_tableView setFrame:CGRectMake(10, 0, maxSize.width-20, 110)];
self.preferredContentSize = CGSizeMake(maxSize.width, 110);
}
}
// iOS10之前,视图原点默认存在一个间距,可以实现以下方法来调整视图间距,这句是为了兼容性
// 注:该方法在iOS10之后被遗弃,iOS10默认不存在间距。
- (UIEdgeInsets)widgetMarginInsetsForProposedMarginInsets:(UIEdgeInsets)defaultMarginInsets
{
return UIEdgeInsetsMake(0, 10, 0, 10);
}
二、widget和app之间的数据处理
2.1、widget给app传值
widget中可以有按钮事件,也可以使用tableview的点击代理等,点击时相当于其他app使用scheme打开这个app,比如tableview的点击。
#pragma mark - UITableViewDelegate
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath{
NSURL *url = [NSURL URLWithString:@"lanzhuList://"];
//打开app
[self.extensionContext openURL:url completionHandler:^(BOOL success) {
NSLog(@"isSuccessed %d",success);
}];
}
当然scheme里面也可以传值,然后在app的appdelegaete的URL回调中处理
- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url sourceApplication:(NSString *)sourceApplication annotation:(id)annotation
{
if ([[url absoluteString] hasPrefix:@"lanzhuList"])
{
//处理widget的传值
}
return YES;
}
2.2、app和widget的数据共享
在上面的配置文件中,已经给app和widget配置过app group,所以在app和widget工程中,都将App Groups打开
打开之后,有两种数据存储方式,NSUserDefault适合小量存储,而一般如果想多种数据的话,还是推荐第二种使用文件数据共享。两个都可以写入和读取,但是正常情况下,基本都是app负责写入,widget负责读取显示
2.2.1、使用NSUserDefault
由于沙盒机制,拓展应用是不允许访问宿主应用的沙盒路径的,因此上述用法是不对的,需要搭配app group完成实例化UserDefaults。这里不能使用
[NSUserDefaults standardUserDefaults];
方法来初始化NSUserDefault对象,而是需要设置的app groups去读取
写入数据
NSUserDefaults *userDefaults = [[NSUserDefaults alloc] initWithSuiteName:@"group.com.huaimayi.lanzhuList"];
[userDefaults setObject:self.textField.text forKey:@"widget"];
[userDefaults synchronize];
读取数据
NSUserDefaults *userDefaults = [[NSUserDefaults alloc] initWithSuiteName:@"group.com.huaimayi.lanzhuList"];
self.contentStr = [userDefaults objectForKey:@"widget"];
2.2.2、通过文件NSFileManager共享数据
写入数据
- (void)updateWidgetData {
//获取到共享数据的文件地址
NSURL *containerURL = [[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:@"group.com.huaimayi.lanzhuList"];
NSURL *birthdayContainerURL = [containerURL URLByAppendingPathComponent:@"Library/Caches/birthday.json"];
//将需要存储的数据写入到该文件中
NSString *jsonString = @"需要写入的数据或者json字符等";
//写入数据
NSError *err = nil;
BOOL result = [jsonString writeToURL:birthdayContainerURL atomically:YES encoding:NSUTF8StringEncoding error:&err];
if (!result)
{
NSLog(@"%@",err);
}
else
{
// NSLog(@"save value:%@ success.",jsonString);
}
}
读取数据
-(NSString *)readDataByNSFileManager
{
NSError *err = nil;
NSURL *containerURL = [[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:@"group.com.huaimayi.lanzhuList"];
containerURL = [containerURL URLByAppendingPathComponent:@"Library/Caches/birthday.json"];
NSString *value = [NSString stringWithContentsOfURL:containerURL encoding: NSUTF8StringEncoding error:&err];
return value;
}
三、测试,审核发布
3.1、测试
如果你想测试,需要用模拟器去测试,因为如果你这个之前没有发不过,就算能安装成功,手机上也没有显示的,但是模拟器上可以
3.2、打包送审
选择主项目,然后正常archive打包,提交即可,如果是导出使用application launcher提交的话,会自动让你选择app的配置文件和widget的配置文件,你选择对应的配置文件即可,两个类似于独立的app,是不同的bundle id和配置文件。
3.3、注意事项
最好将widget的版本号和构建版本号设置的和app一致,不然提交到app store的时候会出现警告,虽然不影响提交审核和正常使用,但是最好还是一致比较好。
版权属于:东哥笔记 - DongGe.org
本文链接:https://dongge.org/blog/880.html
自2017年12月26日起,『转载以及大段采集进行后续编辑』须注明本文标题和链接!否则禁止所有转载和采集行为!
4 条评论
大佬有改变过widget头部的背景色吗
widget头部那个标题、icon都是系统固定的,不能修改
博主,官仁博客已将域名 http://mrxiao.cc更换为 http://uycool.com 得空了还望更新一下,多谢!
已更新