主要参考文章 : https://www.cnblogs.com/fengmin/p/6118592.html
demo地址,里面额外添加了一个分享的扩展 : https://github.com/guochaoshun/iosAppExtension
Today extensions (今日扩展):
Today extension就是我们上面所说的通知中心扩展,因为这个扩展会显示在我们通知中心的 “今天” 这个标签下面。这个扩展的作用很简单,能够让用户更快速方便的看到app最及时的信息,比如中国移动的流量显示,我不用再每次打开移动的app去查看流量,而是直接在通知中心,甚至锁屏界面就可以查看,当然也可以像上面那个小游戏一样,在通知中心直接玩起来,而不用每次都去打开一个游戏程序。
1. 如何创建Today Extension
2. 如何在扩展和宿主App之间共享数据
3. 如何在扩展和宿主App之间共享代码
4. 如何从扩展中打开宿主App
最后完成的是一个非常简单的记事demo,效果图如下:
1. 如何创建Today Extension
首先,我们先创建一个最基本的项目,项目创建完成之后,选中项目文件,选择 xcode ->Editor ->Add Target,如下图,选中Today Extension项,然后点击Next,命名(本文中为TodayWidget),在弹出框中选择Activate,**这个scheme。
**之后,项目中就会多出一个TodayWidget的扩展,还有如下图左侧的TodayWidget文件夹。
文件夹中的MainInterface.storyboard和TodayViewController这个类就是我们要在通知中心显示的界面的控制器。你可以点开storyborad看一下,里面已经有一个很小的界面,其中包含了一个label,如果你选择这个viewcontroller,将它的高度调高,然后将其中label的文字颜色调一下。
如上图,选中TodayWidget为Target后直接运行,你就会看到下面的界面(如果开始没有出现这个界面,不是项目有问题,可以来回滑动切换到首页和这个页面,多试几次,因为它需要刷新,真机上如果不显示,可能是因为扩展的Target中 iOS Deployment Target默认的都是最高,需要调到与真机一致或者更低)
一个最简单的通知中心扩展已经完成了。
2. 如何在扩展和宿主App之间共享数据
现在让我们来完成这个简易记事app最基本的功能,由于这个app非常简单,一个简单的tableview展示数据,一个简单的新增记事页面来添加一条记事,所以我就不详细写了,最后的效果如下。
唯一要提出的一点是,因为这个是简易记事,所以我也没有用到CoreData,sqlite之类的数据库,而是直接使用的NSUserDefaults来存储,这个也是我后面要介绍的如何共享数据。
记事功能完成之后,我们现在要做的就是如何在App中新增一条记事之后,在通知中心扩展中可以查看到这条记事,甚至当我们程序退出之后,可以在通知中心扩展中查看到我们最近的记事。
扩展与宿主App之间共享数据有两种方式:
1.通过NSUserDefaults,这也是本文介绍的方式
2.通过一个扩展与App都可以访问的共享容器,来存放文件,数据(Core Data, Sqlite等都可以存放在这个共享的容器中)。
首先,我们需要创建一个app group,如下图,选中项目的Target -> Capabilities -> App Groups,打开,如果你以前创建过group,会自动列出来。选择+号,填入group的名称(复制这个名称,因为后面要用到)
创建完成之后,选择扩展的Target,打开,这次直接选择我们刚才所创建的group,如下图。
在group创建完成之后,项目中会多出两个文件,如下图
在我们新增一条数据之后,我们将这条数据保存到共享的NSUserDefaults中去
if (_noteField.text.length > 0) {
//先查找是否已存在数据 suiteName就是我们创建的group名称 ,
//必须是groupID, 否则其他名字的UserDefault是创建在主app沙盒下的
NSArray *myNote = [[[NSUserDefaults alloc] initWithSuiteName:@"group.todayextension.widget"]
valueForKey:@"MyNote"];
NSMutableArray *note = [NSMutableArray arrayWithArray:myNote];
if (!note) {
note = [NSMutableArray arrayWithCapacity:0];
}
[note insertObject:_noteField.text atIndex:0];
//存入数据
[[[NSUserDefaults alloc] initWithSuiteName:@"group.todayextension.widget"] setValue:note forKey:@"MyNote"];
[self.navigationController popViewControllerAnimated:YES];
}
然后在TodayViewController中,我们需要从这里面取数据
[_noteList removeAllObjects];
NSArray *myNote = [[[NSUserDefaults alloc] initWithSuiteName:@"group.todayextension.widget"]
valueForKey:@"MyNote"];
if (myNote) {
[_noteList addObjectsFromArray:myNote];
}
这样就已经将数据存在一个可共享的地方,宿主App和扩展都是从一个地方取数据,现在我们在App中添加一条记事,然后就可以下拉打开通知中心查看了
如果需要存储更多的数据,可以通过文件或者数据库(Core Data, Sqlite等)。这个时候共享数据的方法就是要创建一个共享的文件夹,在这个共享文件中可以写入数据库啊,或者归档解档之类.
/// 写入数据
NSString *groupID = @"group.todayextension.widget";
NSError *err = nil;
NSURL *containerURL = [[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:groupID];
containerURL = [containerURL URLByAppendingPathComponent:@"Library/Caches/test"];
NSString *value = @"我是测试的数据";
BOOL result = [value writeToURL:containerURL atomically:YES encoding:NSUTF8StringEncoding error:&err];
if(result){
NSLog(@"写入成功");
}
/// 读取数据
NSString *groupID = @"group.todayextension.widget";
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就都可以访问这个共享的文件夹了,将数据库,文件等存储在这个文件夹中,也同样的达到数据共享的目的。
3. 如何在扩展和宿主App之间共享代码
目前我见到了四种共享代码的方法:
- 将代码打包成Framework,然后link到主App和Widget中(原文中是这样做的,但是我也这样试了,没有走通)
- 不怕安装包变大的话,可以考虑将需要的代码第三方库在主App和Widget中分别复制一份(不推荐,如果是一模一样的话 , 维护2份代码很累)
-
将需要共享的文件按图中进行勾选配置(推荐 , 简单有效 , demo中就是这样做的)
- 通过Pods导入,不太建议通过pod分别向两个Target中导入第三方库,因为很容易发生一些不好处理的问题, 类似于这样
# Uncomment this line to define a global platform for your project # platform :ios, '6.0' inhibit_all_warnings! target 'JMImgure' do pod 'SDWebImage' pod 'AFNetworking' end target 'JMImgure Share' do pod 'SDWebImage' pod 'AFNetworking' end
4. 如何从扩展中打开宿主App
一般在扩展中,我们会直接与宿主App进行通信,比如本文中的记事App,我需要在扩展中直接能添加新的记事,或者点击扩展中的某条记事直接打开我们的App。
通过别的App打开我们自己的App,我们需要先设置URL Types,如下图,添加一个新的URL Type,然后设置URL Schemes,命名为todaywidget,通过这个Schemes,就可以在别的App中调用下面方法:
[[UIApplication sharedApplication] openURL:[NSURL URLWithString:@"todaywidget://"]];
来打开这个App了。
但是很遗憾的是,扩展不是一个完整的程序,所以它并没没有[UIApplication sharedApplication] 这个对象。
所以Apple给每个UIViewController加了一个extensionContext
属性,在我们的宿主App中,这个属性是nil,而在扩展中,我们就可以通过下面的方法:
//打开首页
[self.extensionContext openURL:[NSURL URLWithString:@"todaywidget://home" ]completionHandler:nil];
//打开添加新记事页面
[self.extensionContext openURL:[NSURL URLWithString:@"todaywidget://add"] completionHandler:nil];
来打开宿主App。
然后我们可以在AppDelegate的openURL代理方法中,通过URL后面的参数不同,来打开不同的页面,下面是直接打开 “添加新记事的页面”
- (BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary<UIApplicationOpenURLOptionsKey,id> *)options {
if ([url.absoluteString hasPrefix:@"todaywidget"]) {
if ([url.absoluteString hasSuffix:@"add"]) {//判断是否是直接跳入到添加页面
UIViewController *addVC = [[UIStoryboard storyboardWithName:@"Main" bundle:nil] instantiateViewControllerWithIdentifier:@"AddVC"];
UINavigationController *rootNav = (UINavigationController*)self.window.rootViewController;
[rootNav pushViewController:addVC animated:YES];
}
}
return YES;
其他的补充知识 :
iOS 10以后,Widget可玩性更高了,有了两种显示模式
- NCWidgetDisplayModeCompact, // Fixed height,高度固定为110
- NCWidgetDisplayModeExpanded, // Variable height,高度可变,但是不能超过系统给定的最大高度
// 5s模拟器下:
// NCWidgetDisplayModeCompact模式下:{304, 110}
// NCWidgetDisplayModeExpanded模式下:{304, 528}
// 6s模拟器下:
// NCWidgetDisplayModeCompact模式下:{359, 110}
// NCWidgetDisplayModeExpanded模式下:{359, 616}
viewDidLoad的代码如下:
- (void)viewDidLoad {
[super viewDidLoad];
// 必须设置成NCWidgetDisplayModeExpanded才有右上角的 "展开/折叠"
// 系统会记住你的上一次设置,第二次进入的时候会自动调整到上一次的 "展开/折叠" 状态
self.extensionContext.widgetLargestAvailableDisplayMode = NCWidgetDisplayModeExpanded;
}
当显示模式设置为NCWidgetDisplayModeExpanded时,点击折叠和打开时,会触发下面这个方法,在这个方法中可以修改对应状态的高度
- (void)widgetActiveDisplayModeDidChange:(NCWidgetDisplayMode)activeDisplayMode
withMaximumSize:(CGSize)maxSize {
if (activeDisplayMode == NCWidgetDisplayModeCompact) {
// 在NCWidgetDisplayModeCompact下改是不管用的,系统不管你设置的大小,只会使用默认大小
// self.preferredContentSize = CGSizeMake(UIScreen.mainScreen.bounds.size.width, 110);
} else {
self.preferredContentSize = CGSizeMake(UIScreen.mainScreen.bounds.size.width, 350);
}
}
在下面的方法中更新视图
-(void)widgetPerformUpdateWithCompletionHandler:(void (^)(NCUpdateResult))completionHandler {
// NCUpdateResultNewData 新的内容需要重新绘制视图
// NCUpdateResultNoData 部件不需要更新
// NCUpdateResultFailed 更新过程中发生错误
completionHandler(NCUpdateResultNoData);
}
Share extensions (分享扩展):
分享扩展可以使用户在不同的app之间分享内容。这个功能在iOS5的时候就已经出来了,但是仅限于相册分享图片到tweeter,iOS6中可以分享到Facebook,但是现在,我们可以写分享扩展来分享到我们自己的服务器。
比如我有一张相册中的图片想要通过微信发送给我的朋友,如果没有分享扩展,我只能打开微信与朋友聊天的界面 -> 选择发送图片 -> 到相册中选择图片 -> 然后发送。但是有了分享扩展,我可以直接在相册中点击分享按钮,点击微信,选择好友后,直接分享给好友,而不用打开微信来发送了。
Action extensions(行为扩展):
行为扩展这个名字有点难理解,它可以让用户查看和改变一个app中的某些内容,而不用离开这个app。
比如我在知乎看帖子,碰到一个不会的单词,咋办?如果没有应用扩展,我只能切换到有道词典,输入这个单词来查看,然后再切回知乎,但是现在有了有道词典的行为扩展,我只要复制这个单词,点击共享,在下面 选择有道词典的扩展,就可以不用打开有道词典这个app了,而直接能够显示出翻译结果。
Photo Editing extensions (图片编辑扩展):
图片编辑扩展可以使用户直接在iphone的手机相册中利用第三方图片编辑软件提供的扩展来编辑图片。
比如我现在有一张自拍照,想要编辑一下。如果没有图片编辑扩展的话,我只能打开美图秀秀之类的图片编辑软件,导入图片,编辑保存。有了图片编辑扩展之后,我只需要在系统相册中找到这张图片,点击分享按钮调出菜单,选择第三方的图片编辑扩展,就可以直接进入编辑界面,编辑完直接保存,而不用再打开这个图片编辑软件导入图片来进行编辑了。但是这个扩展仅限于在自带的相册中进行编辑,而不是所有app中图片都可以。
Document Provider extensions (文件提供者扩展):
文件提供者扩展会显示一个文件选择视图给用户,这些选择项可以让用户导入,导出,或者用其他app来打开这个文件。
(这个扩展之前我的理解有问题,写的也是错误的,所以撤销了,网上关于这个扩展的资料非常少,后面补上吧)
Custom Keyboard extensions(自定义键盘扩展):
自定义键盘扩展可以让开发者创建系统键盘之外的自定义键盘,比如搜狗输入法。
在iOS8之前应该是没有第三方输入法的,自从iOS增加了自定义键盘扩展之后,各种第三方输入法都蜂拥而至。
-
iOS9中的扩展点:
网络相关的扩展点,很多的v*n,网络工具等软件都是基于这三个网络扩展点。
Packet Tunnel Provider extension :
可以利用这个扩展点来实现客户端的自定义v*n隧道协议。
App Proxy Provider extension:
利用这个扩展点可以实现客户端自定义透明网络代理协议。
Filter Data Provider and the Filter Control Provider extension:
利用这个扩展点可以实现动态的,基于设备的网络内容过滤。Safari相关的扩展点,很多的Safari广告屏蔽软件都是基于下面这两个扩展点
Shared Links extension:
利用这个扩展点可以使用户在Safari的分享链接里面看到app的内容
Content Blocking extension :
利用这个扩展点,可以给Safari提供一个拦截列表,在这个拦截列表里面你可以描述当用户再使用Safari的时候你想要拦截的内容。其他
Index Maintenance extension:
利用这个扩展点实现在不重启app的情况下对app内的数据重新建立索引。
Audio Unit extension:
这个扩展点允许你的应用提供乐器、声音效果、声音发生器等,它们可以在GarageBand、Logic这类AU宿主应用里使用。扩展点还可以将完整的音频插件模式搬到iOS上并允许你在App Store里销售Audio Units插件。 -
iOS10中的扩展点:
Call Directory extension:
Intents extension:
Intents UI extension:
Messages extension:
Notification Content extension:
Notification Service extension:
Sticker Pack extension:
iOS10中又新增了6个扩展点,这些扩展点的加入,使得iOS10功能更加强大。由于iOS10扩展的资料还比较少看到,我也没有做过相关的,所以我在网上找了一篇介绍的非常好的文章,如果对iOS10新增的扩展有兴趣的,大家可以去看看。iOS 10 应用扩展的剧变,对你的 iPhone 有什么影响?。
3.App Extension的工作原理,生命周期
-
工作原理:
应用扩展本身不是一个app,而只是对于某个app内容和功能的扩展,所以不能够单独的上架AppStore,而是跟随着你的app一起打包,这个包含应用扩展一起打包的app就叫做container app(容器app)。虽然应用扩展是包含在container app中打包的,但是运行时它并不是跟你的app在同一个进程上面,而且有可能同一个app extension会同时运行在不同的进程,因为有可能同时有几个程序都打开了这个app extension,这个用来打开某个app Extension的应用就叫做host app(宿主应用)。
当一个应用扩展在运行的时候,它能够直接和host app进行通信,但是无法和container app进行通信,甚至经常在应用扩展运行的时候,你的container app可能都没有打开。比如对于微信分享扩展来说,如果我要从一个新闻软件分享一篇新闻到微信,通过微信的分享扩展,我可以不用打开微信,甚至微信的进程都没有启动,我只要在新闻软件中直接通过扩展分享到微信就可以了,下次打开微信就可以看到。如果一个app extension 一定要和container app 进行通信的时候,可以利用opeURL()或者是 数据共享 (本文只是概念基础介绍,后面会有单独的文章来介绍如何实现)。
-
生命周期:
因为应用扩展不是一个完整,独立的app,所以它的生命周期跟我们正常的app并不一样。应用扩展是在用户从其它软件的界面或者系统界面打开它的时候启动,一般都是host app发出一个request,app extension对应的响应这个请求,在response结束之后,app extension的生命周期也就终止了。
4.Info.plist
应用扩展创建之后会有自己的info.plist文件,info.plist文件中包含一个NSExtension作为key的字典,NSExtension中的内容根据每个扩展点的类型而各不相同,但是其中都必须包含NSExtensionPointIdentifier 这个key,对应的是扩展点的类型。NSExtension中还可以通过NSExtensionActivationRule 这个key对应的值来包含什么时候显示这些扩展的规则,通过这些规则,来判定用户什么时候会唤起你的扩展。 还有一个必须声明的是NSExtensionMainStoryboard 和NSExtensionPrincipalClass 中的某一个key,或者同时声明两个,代表的是用storyboard还是class来作为你的应用扩展入口。
5.总结
这篇文章很简单的介绍了一下App Extension(应用扩展)是什么,其实之所以会写这个是因为我在看到很多iOS软件的时候,总会觉得这些功能基于我现在的知识根本不能实现,那别人是怎么实现的呢?(比如通知中心小游戏,v*n软件,Safari广告屏蔽软件),在知道有应用扩展这个功能之后,我就恍然大悟了!所以,这篇文章主要就是给跟我一样,平时没有接触过App Extension的同学来大概的了解一下App Extension是什么。