转载

从0到1思考与实现iOS-Widget

原文

讲述之前首先看下demo效果图:

从0到1思考与实现iOS-Widget

基本的展开收起、本App本体交互

从0到1思考与实现iOS-Widget

然后再展示几个效果不错的 Widget app

从0到1思考与实现iOS-Widget

毒物 && Keep

从0到1思考与实现iOS-Widget

ESPN

从0到1思考与实现iOS-Widget

PCalc

从0到1思考与实现iOS-Widget

Musixmatch

从0到1思考与实现iOS-Widget

Fantastical 2

从0到1思考与实现iOS-Widget

Carrot Weather

demo 地址在此!欢迎star

一、Widget总览

  • Widget 是 iOS8 推出第一版,在iOS 10 进行大幅度的优化

  • Widget可以让用户更快地访问到其感兴趣的内容,官方的说法是用来呈现功能比较简单的,交互性不强的东西,在不打扰或者中断用户使用当前应用的前提下完成自己的功能点.对于这个说法,国内的开发者表示呵呵,因为几乎所有的 Widget都绑定了对应的点击事件

二、Widget代码实现

因为 Widget 属于单独的进程,因此需要再新建一个target:File -> New ->target

从0到1思考与实现iOS-Widget

初次构建 UI 时,运行 Widget 后会发现,Widget左侧距离屏幕左侧始终有一段距离,导致效果不佳,可以通过下面的代理方法消除间距

// 取消widget默认的inset,让应用靠左
- (UIEdgeInsets)widgetMarginInsetsForProposedMarginInsets:(UIEdgeInsets)defaultMarginInsets {
    return UIEdgeInsetsZero;
}

Widget 的收起、展开 则是通过这个代理方法:

/**
 activeDisplayMode有以下两种
     NCWidgetDisplayModeCompact, // 收起模式
     NCWidgetDisplayModeExpanded, // 展开模式
 */
- (void)widgetActiveDisplayModeDidChange:(NCWidgetDisplayMode)activeDisplayMode withMaximumSize:(CGSize)maxSize {
    if(activeDisplayMode == NCWidgetDisplayModeCompact) {
        // 尺寸只设置高度即可,因为宽度是固定的,设置了也不会有效果
        self.preferredContentSize = CGSizeMake(0, 110);
    } else {
        self.preferredContentSize = CGSizeMake(0, 310);
    }
}

在设置 UI 的过程中,若想使用本体 Target 中的类:

从0到1思考与实现iOS-Widget

在对应类的 Target Membership 勾选 Widget 即可

如果想使用Pod 管理的第三方库,那么只需要以下三步就可以愉快地玩耍了(比如我想使用 Masonry 布局)
1、 在podfile文件中

从0到1思考与实现iOS-Widget

2、 按照如图所示配置configurations

从0到1思考与实现iOS-Widget

3、 最后分别配置两个 Target 的 link Binanry

从0到1思考与实现iOS-Widget

当然有些第三方包含 source 文件的可能还需要别的操作,最简单粗暴的方式就是-->拖进去!

使用图片也是必不可少,然而 imageNamed: 和 imageWithContentsOfFile: 两种方式加载都不行,即使设置了文件的 target 为 Widget Extension,后来在其target 内部建立一个 .xcassets 文件即可加载图片

从0到1思考与实现iOS-Widget

然而在 Widget Extension 里面新建类又出现了如下报错

从0到1思考与实现iOS-Widget

造成这个的原因是新建的时候默认是 C header,而且没有指向对应的target,按照下图所示修改一下type,选一下target,再次编译就木有问题了

从0到1思考与实现iOS-Widget

如果需要网络请求,记住在 Extension 的plist文件中添加App Transport Security Settings 属性

在开发过程中,那么怎么一直有个“Hello World”显示,最后看了一下原来是 Storyboard 加载,去 Storyboard 文件删除对应 label 即可

如果你的项目中要求纯代码

删除 Storyboard 文件和plist 对应键值对

添加 NSExtensionPrincipalClass 字段并设置为 TodayViewController

从0到1思考与实现iOS-Widget

三、与 App 本体交互

与本体 app 进行交互之前,要明白的一个概念是:Widget 与 app 本身 是两个target,appId 也是独立的,因此 Widget 与本体 app 是通过 app group 进行交互

1、设置群组关系

在 本体 App 的 target > Capabilities添加 container 标识符

从0到1思考与实现iOS-Widget

这个写好之后,再去扩展的target做相同的操作,标识符一定要一样!!

从0到1思考与实现iOS-Widget

切换 target 的方法在这里

报错信息:[_NCWidgetExtensionContext openURL:completionHandler:]_block_invoke failed: Error Domain=NSOSStatusErrorDomain Code=-50 "(null) 如果报这个错说明 urlScheme有问题,没有标准对应,比如下划线识别等

2、设置 scheme 进行交互

设置 app 的 scheme 标识符

从0到1思考与实现iOS-Widget

在plist 文件内添加以下键值对

然后!就可以在 Widget 对应的点击事件里面

// 扫一扫按钮的点击事件
- (void)scanBtnTapped:(UIButton *)sender {
    [self.extensionContext openURL:[NSURL URLWithString:@"wpfWidgetTest://action=richScan"] completionHandler:^(BOOL success) {
        NSLog(@"scanBtnTapped   open url result:%d",success);
    }];
}

在 app 本体的 AppDelegate 方法里面

// 处理 Widget 相关事件
- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url sourceApplication:(NSString *)sourceApplication annotation:(id)annotation {

    NSString* prefix = @"wpfWidgetTest://action=";
    NSString *urlString = [url absoluteString];

    if ([urlString rangeOfString:prefix].location != NSNotFound) {
        NSString *action = [urlString substringFromIndex:prefix.length];
        if ([action isEqualToString:@"richScan"]) {
            // 进入到扫一扫页面
            [self.rootVC transferToRichScanVC];
        } else if ([action isEqualToString:@"web"]) {
            // 进入到 web 活动页
            [self.rootVC transferToWebVCWithUrlString:@"webTest"];
        } 
    }
    return  YES;
}

数据共享:widget项目必然经常要和主项目共享数据,可以通过NSUserDefault,注意和平时用有些不同,创建UserDefault的时候,要指定groupid。上代码:

// widget项目里取数据
+ (NSString*)widgetStringForKey:(NSString*)defaultName {
     NSUserDefaults*shared = [[NSUserDefaultsalloc] initWithSuiteName:@"group.com.widgetTest"];
     return[shared stringForKey:defaultName];
}

// 主项目里存数据
+ (void)widgetSetObject:(id)value forKey:(NSString*)defaultName {
    NSUserDefaults*shared = [[NSUserDefaultsalloc] initWithSuiteName:@"group.com.widgetTest"];
    [shared setObject:value forKey:defaultName];
    [shared synchronize];
}

#warning 涉及到大量数据交互也可以使用 NSFileManager 进行数据共享

在demo中,实现了从Widget入口 点击未读消息后,下次不再展示该未读消息项

从0到1思考与实现iOS-Widget

四、关于刷新时机

Widget 自身的更新机制,是进入到 Widget 页面后(iOS 10 左滑,之前是下拉),先执行 viewDidLoad 方法,然后是 viewWillAppear 方法,但是经测验,Widget 页面在屏幕消失超过两秒后(手机没有停留在 Widget 页面 或者 停留在别的app 的Widget页面,自己的没显示)

由于以上特性,更新代码最好写在 viewWillAppear 方法里面,对于更新时效性特别强的,比如天气类 app,这种最好就是 在该方法里面添加一个 NSTimer 定时进行刷新,在 viewWillDisAppear 方法中 进行 取消NSTimer invalidate定时更新即可

知乎、得到 app的 Widget,只要走 viewDidLoad 方法就会闪一下(如下图),因为每次Widget加载请求的数据后会进行替换造成的。这里可以做个缓存优化,判断如果请求来的数据和当前数据内容一致,那么就不进行刷新列表操作

不信你看

从0到1思考与实现iOS-Widget

五、关于 iOS 8 适配

iOS8、9是老式的下拉刷新,并没有折叠和展开功能,默认的Widget高度为self.preferredContentSize设置的高度

iOS8 默认的背景是黑色磨砂效果,iOS10默认的背景色是白色磨砂效果。因此在控件颜色上做下适配

从0到1思考与实现iOS-Widget

iOS8效果图

iOS8下所有组件默认右移30pt

六、其他注意点

1.当程序内存不足时,苹果优先会杀死扩展,因此需要注意内存的管理。

2.在配置team是账号需要一致(免费账号不行,需要付费的账号),上传包的时候一定注意选择 Product -> Archive -> 选择 distribution 模式!

3.3D touch 对应的也有Widget!?答案是 YES!,只要设置了3D touch,Widget的第一栏就会自动显示

从0到1思考与实现iOS-Widget

再次附上 demo Github 地址,欢迎star

正文到此结束
Loading...