Today Widget介绍

版权声明:书写博客,不为传道受业,只为更好的梳理更好的记忆,欢迎转载与分享,更多博客请访问:http://blog.csdn.net/myinclude 和 http://www.jianshu.com/u/b20be2dcb0c3 https://blog.csdn.net/myinclude/article/details/48138667

简介:Today Widget是App Extension的一种,作用是用户在使用iOS或者Mac OS下拉功能时,能够刷新显示一些用户关心的消息,比如看天气状况,查看股票行情,而且Today Widget能实现一些很小的功能,其实可以把他看成是一个阉割版的APP,一个运行在独立进程中的ViewController。

回到Today Widget的讨论中,我们先来看看Today Widget的生命周期:
1、开始 :在用户通过host app点击extension时,系统就会实例化extension应用,这是生命周期的开始。
2、执行任务 :在extension启动以后,开始执行它的使命。
3、终止 :在用户取消任务,或者任务执行结束,或者开启了一个长时后台任务时,系统会将其杀掉。
由此可见,extension就是为了任务而生!
附上一幅图更加详细的描述这一过程:
这里写图片描述

那到底是谁在控制着Today Widget呢?
我们接着来看一幅图:
这里写图片描述

上面那副图提到的三者到底是哪三者呢?那就是App Extension、Containing App和Host App,这里解释一下这三者:
1、App Extension:我们现在讨论的Today Widget就是App Extension的一种,还有其他几种也顺便说一下,请看下图,包含名称作用适用情形:
这里写图片描述
翻译出来就是:
Today(“今天”又称为Widget):可以快速获取更新或者在通知中心的今日视图中执行一项快速任务。
Share(共享):发布到一个共享网站或者与其它应用程序共享内容。
Action(动作):在另一个应用程序的上下文中操作或查看内容。
Photo Editing【照片编辑(仅限于iOS)】:在照片应用程序中编辑照片或视频。
Finder【查找器(仅限于iOS)】:在查找器中直接显示文件同步的状态信息。
Document Provider【文档提供程序(仅限于iOS)】:提供对文件库的访问和管理。
Custom KeyBoard【自定义键盘(仅限于iOS)】:用自定义键盘替代iOS系统键盘,并用于所有的应用程序中。
2、Containing App:字面意思是包含的APP,我们很容易想到我们的App Extension 就是由它产生出来的,但是这样说并不确切。当我们启动Containing App时,Extension也会启动,但是extensions cannot be stand alone apps.也就是说像Today Widget这种Extension并不是一个独立的APP,而需要依附在Containing App中。再看看这几句:
When an extension is running, it doesn’t run in the same process as the container app. Every instance of your extension runs as its own process.当一个Extension启动之后,它是运行在自己的进程中,它并不是Containing App的子进程,但是当Containing App被用户卸载之后,App Extension 自然也就不存在了。
*3、Host App:我们可以理解成调用Extension的APP,比如当我们编写了一个Today Widget运行起来的时候,Today App就是一个Host App,它和Extension的交互比Extension与Containing App的交互更直接,extension和host app之间可以通过extensionContext属性直接通信:

@interface UIViewController(NSExtensionAdditions) <NSExtensionRequestHandling>

@property (nonatomic,readonly,retain) NSExtensionContext *extensionContext NS_AVAILABLE_IOS(8_0);

@end

而Extension与Containing App却并不能直接通信,尽管extension的bundle是放在containing app的bundle中,但是他们是两个完全独立的进程,之间不能直接通信。不过extension可以通过openURL的方式启动containing app(当然也能启动其它app),不过必须通过extensionContext借助host app来实现:

//通过openURL的方式启动Containing APP
- (void)openURLContainingAPP
{
    [self.extensionContext openURL:[NSURL URLWithString:@"appextension://123"]
                 completionHandler:^(BOOL success) {
                     NSLog(@"open url result:%d",success);
                 }];
}

extension中是无法直接使用openURL的。

总结起来就是:扩展的生命周期和包含该扩展的你的容器 app (container app) 本身的生命周期是独立的,准确地说。它们是两个独立的进程,默认情况下互相不应该知道对方的存在。扩展需要对宿主 app (host app,即调用该扩展的 app) 的请求做出响应,当然,通过进行配置和一些手段,我们可以在扩展中访问和共享一些容器 app 的资源。
因为扩展其实是依赖于调用其的宿主 app 的,因此其生命周期也是由用户在宿主 app 中的行为所决定的。一般来说,用户在宿主 app 中触发了该扩展后,扩展的生命周期就开始了:比如在分享选项中选择了你的扩展,或者向通知中心中添加了你的 widget 等等。而所有的扩展都是由 ViewController 进行定义的,在用户决定使用某个扩展时,其对应的 ViewController 就会被加载,因此你可以像在编写传统 app 的 ViewController 那样获取到诸如 viewDidLoad 这样的方法,并进行界面构建及做相应的逻辑。扩展应该保持功能的单一专注,并且迅速处理任务,在执行完成必要的任务,或者是在后台预约完成任务后,一般需要尽快通过回调将控制权交回给宿主 app,至此生命周期结束。
按照 Apple 的说法,扩展可以使用的内存是远远低于 app 可以使用的内存的。在内存吃紧的时候,系统更倾向于优先搞掉扩展,而不会是把宿主 app 杀死。因此在开发扩展的时候,也一定需要注意内存占用的限制。另一点是比如像通知中心扩展,你的扩展可能会和其他开发人员的扩展共存,这样如果扩展阻塞了主线程的话,就会引起整个通知中心失去响应。这种情况下你的扩展和应用也就基本和用户说再见了..

抛开这些,重点讲讲Today Widget:

Today Widget的使用限制:
不能使用UIApplication这个类;
不能使用某些标记了NS_EXTENSION_UNAVAILABLE的API,以及一些诸如Health Kit、Event Kit的framework;
不能获取相机、麦克风;
不能长时间运行background tasks(If you want to create an app extension that enables a multistep task or helps users perform a lengthy task, such as uploading or downloading content, the Today extension point is not the right choice.引自苹果官方文档)
不能通过AirDrop得到数据,但可以向AirDrop发送数据。

Widget 是放在Today Tab之中,而它工作机制是只有用户下拉通知中心时才会去刷新获取最新数据,这种做法和Android不同在于,Android更偏向于把 整个Widget一直放在后台实时持续的更新.设想一下,如果我们看同样天气信息,Android会持续消耗资源去做一件用户不会实时预览信息,这也就能 解释为何经常看到Android用户抱怨耗电问题.而对于即时消息,iOS做法是直接把这些消息实时归类到通知Tab中.其实这种做法很好解决采用消 耗最少资源前提下保证其操作的灵活性.
因为现有Widget一般来说是展现在系统级别的 UI上,所以在App Extension Programming Guide中Apple对Widget交互提出如下明确的要求:扩展应该保持轻巧迅速,并且专注功能单一,在不打扰或者中断用户使用当前应用的前提下完成自己的功能点.

更新widget的状态需要我们去实现NCWidgetProviding 协议,当我们的Widget得到widgetPerformUpdateWithCompletionHandler:的调用的时候,更新我们的Widget,用一下这些宏来描述我们更新的状态:
NCUpdateResultNewData—重新绘制Widget的视图
NCUpdateResultNoData—widget不需要重新绘制视图
NCUpdateResultFailed—错误发生

定时更新机制
Widget 自身更新机制当用户下拉通知中心(Notification Center)时立即更新数据,但我们仔细研究Widget用户使用场景时发现,如果用户锁屏时间过长,打开Widget后不做任何操作,这个时候针对一 些即时类应用,类似我们天气中可能涉及到灾害预警它要求场景数据一旦产生就要实时展现给用户,这就需要我们基于Widget自身机制外还要处理这个场景下 天气数据自动更新的问题.
这个时候我们需要构建一个定时更新的NSTimer:
http://img.mukewang.com/551a209300018ca707760136.jpg
非常简单,在NSTimer固定更新间隔执行的方法调用就是更新数据方法,当然重点不在这里,而是触发和关闭这个NSTimer时机.按照 Widget生命周期来说,如果用户是第一次下拉查看Widget其实就是执行整个ViewController生命周期调用过程,这个并没有什么问题, 但是还是存在一个特殊情况.系统为了保证Widget上数据是及时更新的,默认会截取上次显示成功Widget的快照.这个快照会一直保存到新的数据或 UI被更新才回被替换,那这就会带来一个问题,当你拖拽通知中心(Notification Center)下拉过于频繁时,Debug跟踪代码执行路径你会发现整个Widget生命周期执行过程和第一次下拉执行的路径发生了变化.
第一次下拉执行路径是viewDidLoad->viewWillAppear,而如果下拉过于频繁你就会发现代码执行路径直接只会执行viewWillAppear方法,这个就是系统默认保存上次快照而导致的执行路径上变化.这对我们选择NSTimer更新时机以及后面会提到的Widget横竖屏处理都会有影响.
那么很明显,为了保证这个定时更新机制能够无论用户什么情况下操作都能起作用,我们需要把NSTimer fire触发代码调用放到viewWillAppear方法中来.同理当Widget关闭后在viewDidDisappear方法取消NSTimer invalidate定时更新即可.

实现:
跳转到主应用
我们在插件的storyboard上加几个按钮,分别跳转到主应用的不同页面,怎么办呢?
通过OpenUrl方法,self.extensionContext其实就是Today这个app,然后有Today和主应用进行进程间通讯,里面很复杂,但方法封装的很简单,就是OpenUrl:

- (IBAction)menuPressed:(id)sender
{
    UIButton* button = (UIButton*)sender;

    if (button.tag == 1) {
        [self.extensionContext openURL:[NSURL URLWithString:@"iOSWidgetApp://action=GotoHomePage"] completionHandler:^(BOOL success) {
            NSLog(@"open url result:%d",success);
        }];
    }
    else if(button.tag == 2) {
        [self.extensionContext openURL:[NSURL URLWithString:@"iOSWidgetApp://action=GotoOrderPage"] completionHandler:^(BOOL success) {
            NSLog(@"open url result:%d",success);
        }];
    }
}

协议名是iOSWidgetApp,这个要在主应用的plist里面注册一下
这里写图片描述
然后在主应用的AppDelegate解析协议,进行不同的操作。

- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url sourceApplication:(NSString *)sourceApplication annotation:(id)annotation
{
    NSString* prefix = @"iOSWidgetApp://action=";
    if ([[url absoluteString] rangeOfString:prefix].location != NSNotFound) {
        NSString* action = [[url absoluteString] substringFromIndex:prefix.length];
        if ([action isEqualToString:@"GotoHomePage"]) {

        }
        else if([action isEqualToString:@"GotoOrderPage"]) {
            BasicHomeViewController *vc = (BasicHomeViewController*)self.window.rootViewController;
            [vc.tabbar selectAtIndex:2];
        }
    }

    return  YES;
}

数据共享
Today Widget怎么能获取主应用的数据呢?要知道插件和主应用是独立的两个进程,以前是无法共享数据的,现在可以通过AppGroup来共享数据,同属于一个group的App共同访问并修改某个数据。

创建Group
选中主应用的Target,选择Capabilities,创建一个group,名字叫group.xxx,然后到插件的target勾选刚才创建的group,这样就ok了。
这里写图片描述
读写数据
通过NSUserDefaults来读写数据,注意NSUserDefaults是根据刚才创建的group来创建的。我们在主应用里加入如下代码,这样今日插件就有数据可读了。

NSUserDefaults* userDefault = [[NSUserDefaults alloc] initWithSuiteName:@"group.huijia"];
[userDefault setObject:@"nmj" forKey:@"group.huijia.nickname"];

今日插件里面的代码,这样就能根据主应用的状态更新插件的状态。

NSUserDefaults* userDefault = [[NSUserDefaults alloc] initWithSuiteName:@"group.huijia"];
    NSString* nickName = [userDefault objectForKey:@"group.huijia.nickname"];
    if (nickName) {
        NSString* message = @"您关注的XX小说又更新了,快去看看吧!";
        self.messageLabel.text = [NSString stringWithFormat:@"%@,%@",nickName,message];
    }

最终效果:根据用户是否已经在主应用里面登录,显示不同的message,有两个按钮,跳转到主应用不同的页面。

苹果官网文档有这么一句话:A Today widget can appear on the lock screen of an iOS device if the user has enabled this.也就是说Today widget能用在锁屏情况下,但是网上关于这个的资料非常有限,而且文档里也只是提了这么一句,并没有过多的介绍。

具体使用Today Widget可以参考这个链接:iOS8中Today Extension的使用

Today Widget使用进阶可以参考这个链接:ios8新特性widget开发

参考资料:
官方文档
WWDC2014之App Extensions学习笔记
iOS8Extension之Today插件
Introduction to iOS 8 App Extension: Creating a Today Widget
App Extension编程指南(iOS8/OS X v10.10):扩展类型–Today
iOS Today Widget written in Swift
iOS开发之构建Widget
WWDC 2014 Session笔记 - iOS 通知中心扩展制作入门

猜你喜欢

转载自blog.csdn.net/myinclude/article/details/48138667