licc

心有猛虎 细嗅蔷薇

0%

Widget开发指南

目前负责的App新增了Widget功能,之后在组内分享中分享了下Widget的开发经验。基于之前的PPT提炼出了这篇文章。本篇文章只讲基于Widget关于iOS10+ 之后的知识点。

Widget是iOS8以后Apple推出的一项功能,并且在iOS10后进行了大幅的优化。

在主屏幕和锁定屏幕上向右滑动来访问Widget,也可以在对应的App图标上面使用3D Touch按压访问相应的Widget。

Widget设计规范和要求

Widget是一个单独的进程,和主App独立,但是支持数据共享。在设计和开发Widget时候要注意以下几点设计规范:

  • 设计一个友好的交互体验

    Widget用来执行非常简单的任务,尽可能提供点击一次就能完成的任务,Widget不支持窗口滚动,不支持键盘输入(其实是可以做到键盘输入的 具体办法见后面)
    详见《App Extension Programming Guide》


  • 要快速显示内容

    内容要尽量从本地加载,依赖网络的内容要在本地做缓存,以免长时间等待。确保每次出现都有内容

  • 提供充足的边距和填充

    避免将内容扩展到Widegt边缘。每行最多显示4个按钮或图标

  • 适应屏幕

    iOS10以后,Widget支持折叠和展开。折叠状态下默认高度为110且不可更改。展开高度不超过一个屏幕的高度。(官方文档说最低高度为2.5个默认行高 443.5=110)官方推荐使用AutoLayout布局。
    *
    横屏时候宽度还是默认屏幕宽度。不会拉伸**

  • 不要自定义背景色

    系统自带模糊的背景色,尽量不要改(当然只是建议咯)。不要用照片做背景,会和壁纸冲突。

  • 注意字体颜色、取一个好名字、一个App可以有多个Widget

    字体颜色尽量是深色或者深灰色(然而用白色的最多)。如果一个App存在多个Widget,要命名清晰。Widget的名字里面,英文字母系统会自动转换成大写

    Logo会自动使用主App的icon

  • 适当的时候让用户跳转到主App来做更多的事

    Widget尽量只给用户提供简单的功能(规范而已。。),不要在Widget中出现“打开App”等按钮。点击Widget icon后会自动吊起主App。和主App交互使用URLScheme方法。后面会讲到。

  • 很短的生命周期、要注意内存问题

    离开屏幕2s+就会被销毁,后面会讲到

建立Widget Target

选择主工程,在Project设置界面下方点击加号,新建Today Extension



系统会自动生成TodayViewController和storyBoard。不要忘记在Target设置里面设置基本信息,版本号和主App保持一致,否则上传iTunes Connect会有警告邮件

也要注意选择Deployment Target。Xcode10默认是iOS12

和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,如果想使用纯代码。进行以下步骤

  1. 删除MainInterface.storyboard文件和NSExtensionMainStoryboard键值对
  2. 添加NSExtensionPrincipalClass为key ,value为TodayViewController

图片管理
Widget可以使用Asset Catalog管理图片,命名为Assets,和主工程使用方式一致

代码调试
在Widget工程更新代码后,可以运行主工程,然后添加Widget。就可以看到最新的效果展示。
如果想断点调试,要选择Widget Target

和主工程共享数据

Widget和主工程是完全独立的两个工程,两个独立的进程。所以数据共享是通过App Groups进行的。

App Groups需要去开发者中心去创建。ID必须以group开头。后面一般跟公司名称。

建立完成后回到主工程,打开App Groups开关,就能刷新出刚刚创建的Groups,打钩远中


然后把Widget Target 也打开App Groups,选中同一个Groups


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文件。

Widget吊起主工程

Widget吊起主App通过URLSchemes

  1. 为主App设置URLSchemes

    2.Widget添加交互
    [self.extensionContext openURL:[NSURL URLWithString:@"YDUDictionary://action=CameraTranslate"] completionHandler:^(BOOL success) {
    NSLog(@"open url result:%d",success);
    }];
  2. 主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排序很删减。
当添加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就可以获取焦点,出现键盘啦