iOS开发 Widget(小组件)的开发
在iOS开发中,Widget(小组件)的功能属于鸡肋的功能,因为大多数的人都很少去使用,或者很多人都不知道还有这功能。不过在项目完成时可以添加上去,增加项目的bigger。本文会详细介绍小组件的开发以及开发中会遇到的坑,如果大家又遇到新的坑,可以在下面留言,大家一起交流。
声明:本文是使用Xcode11.0beta6版本+iOS13.1beta2版本作为展示用例。但是项目创建是使用Xcode10.3创建的,这是因为害怕部分用户下载Demo后无法在Xcode10中运行。现在使用Xcode的11创建APP会多一个SceneDelegate,在Xcode10中无法识别,也会简单的说明一下当前Demo找Xcode10与Xcode11的简单区别。所展示的支付宝版本为10.1.72,今日头条版本为7.4.0。(避免今后两款APP更新以后与本文展示的不一致)
1.什么是小组件
小组件即如图的下面两种呈现方式:第一种是在最左页(俗称第0页)或者是往下滑的时候会出现的页面,展示如下:
最左页
这个需要用户去点击编辑才会显示出来。
第二种展示方式是长按当前APP后会出来(也就是3DTouch),展示如下:
长按显示1 长按显示2
这种方式只要长按就会触发,不需要用户去添加。
常见的小组件都是这两种方式显示,一种是支付宝的那种固定样式的,点击某一个功能后就跳转到APP的某个页面去。另外一种是今日头条的动态显示的,每次都回去请求数据然后刷新显示。接下来两种方式的开发都会进行讲解。
2.静态小组件的开发(类似支付宝的样式)
对于想做成类似支付宝的点击小组件的某个按钮就跳转的指定页面的是最简单的。有一些其他文章上来就说需要去添加APP Groups的对于这种类型的来说是不必要的,这个不涉及到数据共享的东西,就简单的跳转,完全没必要去进行创建APP Groups。下面说明详细步骤:
完成主项目后,点击如下两个步骤完成widget的创建:
添加方式1.png 添加方式2.png
选择Today Extension:
Today Extension
填写项目名称即可完成创建。接下来在TodayViewController的viewDidLoad里面进行按钮的创建:
CGFloat width = [UIScreen mainScreen].bounds.size.width;
//NCWidgetDisplayModeCompact 不需要折叠样式,整个完整显示
//NCWidgetDisplayModeExpanded 有折叠按钮,点击可以显示更多
self.extensionContext.widgetLargestAvailableDisplayMode = NCWidgetDisplayModeCompact;
//设置整个小组件的宽高
self.preferredContentSize = CGSizeMake(width, 130);
NSArray *titleArray = @[
@"扫一扫",
@"扫一扫",
@"扫一扫",
@"扫一扫",
];
NSArray *imageArray = @[
@"widget_scan",
@"widget_scan",
@"widget_scan",
@"widget_scan",
];
CGFloat btnWidth = 60;
CGFloat btnHeight = 60;
CGFloat margin = (width - titleArray.count * btnWidth) / (titleArray.count + 1);
CGFloat y = 20;
for (NSInteger i = 0; i < titleArray.count; i ++) {
CGFloat x = margin + i * (btnWidth + margin);
UIButton *btn = [[UIButton alloc] init];
btn.frame = CGRectMake(x, y, btnWidth, btnHeight);
[btn setImage:[UIImage imageNamed:imageArray[i]] forState:UIControlStateNormal];
[btn setTitle:titleArray[i] forState:UIControlStateNormal];
btn.tag = i;
[btn addTarget:self action:@selector(btnDidClick:) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:btn];
}
运行后效果如下图:
运行效果图
这是为什么呢?没有写hello world确显示出来了。这是因为系统默认是用小组件的MainInterface.storyboard进行显示的,如果你喜欢使用storyboard直接在里面布局就行了,但是我一般都用代码开发,所以需要去当前小组件的文件下的info.plist删除 NSExtensionMainStoryboard 选项,增加 NSExtensionPrincipalClass,value为类的名字TodayViewController即可用纯代码进行布局。 配置纯代码.png
配置完成后再次运行。 运行结果
现在能看到文字显示出来了,但是图片没有,这是因为在放进去图片的时候,没有勾选在widget项目里面也能共,所以当前项目找不到图片,只需要去勾选就可以在当前文件使用即可。 图片添加 顺便说一下,主项目里面的文件如何让widget也能使用,如果是单纯的.h文件,比如之前存放的一些宏的定义,颜色的定义之类的,只需要在这个地方设置导入即可: 引入头文件
如果想引入主项目里面的其他文件,也和图片一样的勾选,只是在上面那里导入是不行的,必须进行勾选才能使用,比如我需要导入一个分类文件:
分类文件导入
勾选图片以后就能看到图片了。
demo有不完美的地方,按钮的图片和title不能对齐,不过不影响demo的演示:)
布局完成能显示以后,接下来进行点击按钮跳转,由于需要跳转到主项目的某个页面,所以需要在主项目中用到URL Types。强烈建议设置成包名的形式,否则和其他APP一样的schemes可能会跳转他们的APP去 URL Types
ok,配置完成后,只需要在widget的按钮里面实现点击事件然后在主项目的AppDelegate的openURL里面实现点击以后的判读即可。
widget的点击事件如下:
- (void)btnDidClick:(UIButton *)btn{
NSInteger index = btn.tag;
//因为我是需要跳转到某个页面,所以我直接传递跳转页面的类名
NSString *vcString = @"";
if (index == 0) {
vcString = @"OneViewController";
}else if (index == 1){
vcString = @"OneViewController";
}else if (index == 2){
vcString = @"ViewController";
}else if (index == 3){
vcString = @"TwoViewController";
}
[self.extensionContext openURL:[NSURL URLWithString:[NSString stringWithFormat:@"com.widgetDemo://%@",vcString]] completionHandler:nil];
}
AppDelegate的openURL如下:
- (BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary<UIApplicationOpenURLOptionsKey,id> *)options{
[self appCallbackWithOpenUrl:url];
return YES;
}
- (void)appCallbackWithOpenUrl:(NSURL *)url{
// 针对url进行不同的操作
NSString *URLString = [NSString stringWithFormat:@"%@",url];
NSArray *array = [URLString componentsSeparatedByString:@"://"];
NSString *vcString = @"";
//这里最好进行判断URL的开头是不是设置的schemes,如果用到其他的第三方SDK,有可能是他们的URL
if (array.count >= 2) {
vcString = array[1];
}
//这里可以先判断是否登录或者其他逻辑在进行下面的操作
if ([vcString isEqualToString:@"OneViewController"]) {
UINavigationController *nav = ((UITabBarController*)self.window.rootViewController).selectedViewController;
UIViewController *VC = [[NSClassFromString(vcString) alloc] init];
[nav pushViewController:VC animated:YES];
}else if ([vcString isEqualToString:@"ViewController"]){
UINavigationController *nav = ((UITabBarController*)self.window.rootViewController).selectedViewController;
UIViewController *VC = [[NSClassFromString(vcString) alloc] init];
[nav pushViewController:VC animated:YES];
}else if ([vcString isEqualToString:@"TwoViewController"]){
UITabBarController *tabBarVC = (UITabBarController *)self.window.rootViewController;
tabBarVC.selectedIndex = 1;
}
}
一个固定点击跳转的小组件就这样开发完成,很简单的。如果不需要数据共享,根本不需要其他文章说的先去配置APP Groups,就如此简单即可。
3.动态小组件的开发(类似今日头条的样式)
做了静态的小组件以后,动态的也不难,类似于今日头条的小组件,会有数据的共享,以及数据的请求。但是有以下注意点:
3.1主项目框架的使用
如果项目中用CocoaPods导入了第三方框架,比如Masonry和AFNetworking。在小组件的项目中有可能是能直接导入头文件的,但是实际使用却是不行的。这时候需要在podfile文件中加入小组件的target然后在pod install,这样在小组件的项目中才能使用。 pod文件这样以后才能在小组件的项目里面使用第三方框架。
3.2数据共享
如果需要将主APP的某些数据共享给小组件使用,这时候就需要进行数据共享,使用到APP Groups,本次以Xcoede11截图。在Xcode10中,直接把开关打开添加即可。因为我直接在Xcode里面登录了账号,所以不需要去网页上进行证书那些的配置即可,如果是用其他方式的,需要去网页上进行相关的设置。 APP Groups然后就可以在需要存储的地方进行数据的存储了,不过现在只支持两种存储,一个是NSUserDefaults,一个是NSFileManager。
NSUserDefaults存储数据
NSUserDefaults *sharedData = [[NSUserDefaults alloc] initWithSuiteName:GroupID];
[sharedData setValue:@"测试存储" forKey:Name];
[sharedData synchronize];
NSFileManager 存储数据
NSError *error = nil;
NSURL *containUrl = [[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:GroupID];
containUrl = [containUrl URLByAppendingPathComponent:Data];
NSString *text = @"NSFileManager 存储数据";
BOOL result = [text writeToURL:containUrl atomically:YES encoding:NSUTF8StringEncoding error:&error];
if (result){
NSLog(@"save success");
}else {
NSLog(@"error:%@", error);
}
在这个方法你可以获取到时候折叠
// 展开/折叠监听
- (void)widgetActiveDisplayModeDidChange:(NCWidgetDisplayMode)activeDisplayMode withMaximumSize:(CGSize)maxSize{
}
至此,两种方式创建的小组件都完成。
如果大家还遇到什么其他问题可以在下面留言,大家一起交流交流。
最后附上Demo地址