Widget开发指南
目前负责的App新增了Widget功能,之后在组内分享中分享了下Widget的开发经验。基于之前的PPT提炼出了这篇文章。本篇文章只讲基于Widget关于iOS10+ 之后的知识点。
Widget是iOS8以后Apple推出的一项功能,并且在iOS10后进行了大幅的优化。
在主屏幕和锁定屏幕上向右滑动来访问Widget,也可以在对应的App图标上面使用3D Touch按压访问相应的Widget。
![](https://img.haomeiwen.com/i525360/5aa52b4509503da2.png)
Widget设计规范和要求
Widget是一个单独的进程,和主App独立,但是支持数据共享。在设计和开发Widget时候要注意以下几点设计规范:
- 设计一个友好的交互体验
Widget用来执行非常简单的任务,尽可能提供点击一次就能完成的任务,Widget不支持窗口滚动,不支持键盘输入(其实是可以做到键盘输入的 具体办法见后面)
详见《App Extension Programming Guide》
![](https://img.haomeiwen.com/i525360/8a7629d32d636e67.png)
![](https://img.haomeiwen.com/i525360/d600c4ec537f4d8f.png)
-
要快速显示内容
内容要尽量从本地加载,依赖网络的内容要在本地做缓存,以免长时间等待。确保每次出现都有内容
-
提供充足的边距和填充
避免将内容扩展到Widegt边缘。每行最多显示4个按钮或图标
-
适应屏幕
iOS10以后,Widget支持折叠和展开。折叠状态下默认高度为110且不可更改。展开高度不超过一个屏幕的高度。(官方文档说最低高度为2.5个默认行高 44*3.5=110)官方推荐使用AutoLayout布局。
横屏时候宽度还是默认屏幕宽度。不会拉伸
-
不要自定义背景色
系统自带模糊的背景色,尽量不要改(当然只是建议咯)。不要用照片做背景,会和壁纸冲突。
部分App设计了皮肤功能
-
注意字体颜色、取一个好名字、一个App可以有多个Widget
字体颜色尽量是深色或者深灰色(然而用白色的最多)。如果一个App存在多个Widget,要命名清晰。Widget的名字里面,英文字母系统会自动转换成大写。
Logo会自动使用主App的icon
-
适当的时候让用户跳转到主App来做更多的事
Widget尽量只给用户提供简单的功能(规范而已。。),不要在Widget中出现“打开App”等按钮。点击Widget icon后会自动吊起主App。和主App交互使用URLScheme方法。后面会讲到。
-
很短的生命周期、要注意内存问题
离开屏幕2s+就会被销毁,后面会讲到
建立Widget Target
选择主工程,在Project设置界面下方点击加号,新建Today Extension
![](https://img.haomeiwen.com/i525360/df1adf5fb5a2d83b.png)
![](https://img.haomeiwen.com/i525360/2b63a7d3402d8762.png)
系统会自动生成TodayViewController和storyBoard。不要忘记在Target设置里面设置基本信息,版本号和主App保持一致,否则上传iTunes Connect会有警告邮件
也要注意选择Deployment Target。Xcode10默认是iOS12
![](https://img.haomeiwen.com/i525360/3793f8a5b159e998.png)
和Widget共享代码
-
支持pod导入三方库,在podfile中新增Widget的target
Xcode10 后,如果在Build Phases中运行Script。执行pod可能报错。解决办法见
《#iOS知识小集# Xcode10 pod install 报错》
-
主工程代码共享
在需要共享的类的.m文件中的Target Membership中勾选Widget所在的Target
Widget代码实现
NCWidgetProviding协议
Widget工程建立后会自动生成TodayViewController。
会遵循NCWidgetProviding协议
iOS10以后这个协议只有两个方法
- (void)widgetPerformUpdateWithCompletionHandler:(void (^)(NCUpdateResult result))completionHandler;
- (void)widgetActiveDisplayModeDidChange:(NCWidgetDisplayMode)activeDisplayMode withMaximumSize:(CGSize)maxSize NS_AVAILABLE_IOS(10_0);
其中widgetPerformUpdateWithCompletionHandler 默认返回NCUpdateResultNewData
- (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);
}
这个可以忽略掉,直接返回NCUpdateResultNewData就好了
iOS10以后支持折叠和展开功能,折叠状态下默认高度为110且不可更改。展开高度不超过一个屏幕的高度。(官方文档说最低高度为2.5个默认行高 44*3.5=110)
在ViewDidLoaded方法中设置是否开启折叠功能
//NCWidgetDisplayModeCompact 收起模式
//NCWidgetDisplayModeExpanded 展开模式
self.extensionContext.widgetLargestAvailableDisplayMode = NCWidgetDisplayModeExpanded;
- (void)widgetActiveDisplayModeDidChange:(NCWidgetDisplayMode)activeDisplayMode withMaximumSize:(CGSize)maxSize {
if (activeDisplayMode == NCWidgetDisplayModeCompact) {
self.preferredContentSize = CGSizeMake(maxSize.width, 110);
} else {
self.preferredContentSize = CGSizeMake(maxSize.width, 200);
}
}
使用纯代码
示例工程会默认使用StoryBoard,如果想使用纯代码。进行以下步骤
-
删除MainInterface.storyboard文件和NSExtensionMainStoryboard键值对
2.添加NSExtensionPrincipalClass为key ,value为TodayViewController
![](https://img.haomeiwen.com/i525360/3191b5090c1294ea.png)
图片管理
Widget可以使用Asset Catalog管理图片,命名为Assets,和主工程使用方式一致
[图片上传失败...(image-63b41f-1548926798999)]
代码调试
在Widget工程更新代码后,可以运行主工程,然后添加Widget。就可以看到最新的效果展示。
如果想断点调试,要选择Widget Target
![](https://img.haomeiwen.com/i525360/477ca9ff7379e2a2.png)
和主工程共享数据
Widget和主工程是完全独立的两个工程,两个独立的进程。所以数据共享是通过App Groups进行的。
App Groups需要去开发者中心去创建。ID必须以group开头。后面一般跟公司名称。
![](https://img.haomeiwen.com/i525360/4b13ec223f932575.png)
建立完成后回到主工程,打开App Groups开关,就能刷新出刚刚创建的Groups,打钩远中
![](https://img.haomeiwen.com/i525360/b81a55c8ce0bdd30.png)
然后把Widget Target 也打开App Groups,选中同一个Groups
![](https://img.haomeiwen.com/i525360/151d35d8a57d6341.png)
App Groups可以通过NSUserDefaults和NSFileManager共享数据
- NSUserDefaults
//主工程中存
NSUserDefaults *shared = [[NSUserDefaults alloc] initWithSuiteName:@"group.YouDao.xxxx"];
[shared setObject:_targetLanguage.abb forKey:@"UD_TargetLanguage_Widget_key"];
[shared synchronize];
//Widget 中取
[[NSUserDefaults alloc] initWithSuiteName:@"group.YouDao.xxxx"] objectForKey:@"UD_TargetLanguage_Widget_key"];
- NSFileManager
//存
NSString *groupID = @"group.YouDao.xxxx";
NSError *err = nil;
NSURL *fileUrl = [[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:groupID];
fileUrl = [fileUrl URLByAppendingPathComponent:@"Library/Caches/test"];
NSString *value = @"test";
BOOL result = [value writeToURL:fileUrl atomically:YES encoding:NSUTF8StringEncoding error:&err];
if(result){
NSLog(@"写入成功");
}
//取
NSString *groupID = @"group.YouDao.xxxx";
NSError *err = nil;
NSURL *containerURL = [[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:groupID];
containerURL = [containerURL URLByAppendingPathComponent:@"Library/Caches/test"];
NSString *value = [NSString stringWithContentsOfURL:containerURL encoding:NSUTF8StringEncoding error:&err];
App Gropu是跨App的,只要在同一个开发中账号。不同的App使用同一个Gropu ID都是可以共享数据的。在Shared目录下还有AppGroup目录。里面有各个Group ID的文件夹。其中通过NSUserDefault共享的数据在Library/Prefrences下。是一个plist文件。
![](https://img.haomeiwen.com/i525360/68ed49a0d79abe61.png)
Widget吊起主工程
Widget吊起主App通过URLSchemes
-
为主App设置URLSchemes
2.Widget添加交互
[self.extensionContext openURL:[NSURL URLWithString:@"YDUDictionary://action=CameraTranslate"] completionHandler:^(BOOL success) {
NSLog(@"open url result:%d",success);
}];
3.主App中处理Scheme.在AppDelegate中实现application:openURL:options:
- (BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary<UIApplicationOpenURLOptionsKey,id> *)options {
NSString *urlStr = [url.absoluteString stringByRemovingPercentEncoding];
if ([urlStr hasPrefix:@"YDUDictionary://action="]) {
NSString *parameter = [urlStr stringByReplacingOccurrencesOfString:@"YDUDictionary://action=" withString:@""];
if ([parameter isEqualToString:@"CameraTranslate"]) {
//Do Somthinhg
}
}
}
主App中控制Widget是否显示
在Widget编辑页面可以进行Widget排序很删减。
![](https://img.haomeiwen.com/i525360/aeda1fc3bd93b5e9.png)
当添加Widget以后,主工程还可以控制Widget是否显示。
//为什么要引入NotificationCenter呢?可以思考下
#import <NotificationCenter/NotificationCenter.h>
//youdao.com.WidgetTest.Widget是Widget的Bundle ID
[[NCWidgetController widgetController] setHasContent:YES forWidgetWithBundleIdentifier:@"youdao.com.WidgetTest.Widget"];
刷新机制
Widget有自己进程,有特殊的生命周期和内存限制。通过测试得出
Widget离开屏幕2s以上,就会被销毁回收掉。每次离开前系统会做快照处理。下次进来先加载快照。
离开超过2s以上,下次进入就会调用ViewDidLoad,然后是viewWillAppear
离开不超过2s 下次进入会调用viewWillAppear
所以为了交互体验,最好是记录用户上次的使用状态,下次加载时候进行还原操作。
当内存不足时候,系统会优先kill掉Widget。所以要注意内存问题,不要进行需要大量内存的操作。
网络请求如果需要频繁刷新。可以在viewWillAppear方法中启用一个Timer,在Timer中请求接口数据。在viewWillDisAppear中取消定时器。
如何在Widget中使用键盘
Apple官方文档说Widget是不支持键盘输入的。如果在TodayViewController中新建一个输入框。点击是没有反应的。但是我们可以用另外一种办法绕过去。效果如下图。
做法就是做一个假的输入框,让用户点击。点击后present一个ViewController,在这个Controller新建UITextView或者UITextField就可以获取焦点,出现键盘啦
![](https://img.haomeiwen.com/i525360/661905094d21be6f.png)