前段时间因为项目需要,接触了iOS Today Extension 的开发,网络上关于完整的 Today Extension开发的资料也不多,所以期间遇到了不少的坑,现在把新心得写出来,希望会对读者有点用处。
先来科普一下:
扩展(Extension)是iOS 8加入的一个强大功能,可以通过系统给我们的扩展接入点,来为系统的服务提供某些附加的功能,扩展的接入点有以下几个:
今日(Today)- 在下拉通知的“今天”的界面中添加一个小插件
分享(Share)- 点击分享按钮后,将网站或者图片通过应用分享
操作(Action)- 点击Action按钮后发送内容到应用
图片编辑(Photo Editing)- 在系统的照片应用中提供编辑的功能
文档管理(Document Provider)- 提供和管理文件内容
自定义键盘(Custom keyboard)- 自定义键盘和输入法
iOS 9 新增了4个:
音频单元(Audio Unit)- 为音乐App提供扩展功能,例如GarageBand
Spotlight索引(Spotlight Index)- Spotlight搜索扩展
共享的链接(Shared Links)- Safair共享的连接扩展
广告拦截(Content Blocker)- Safair广告拦截扩展
需要注意的几件事:
1、扩展在 iOS 中是不能以单独的形式存在,是随着容器 App(Container App)一起打包提供的。
2、扩展的生命周期和容器 App(Container App)本身的生命周期是独立的,它们是两个独立的进程,默认情况下互相不应该知道对方的存在。也就是说,扩展本身就是一个小型的App,App id也与容器App不一样。
3、提供扩展的方式是在 App 的项目中加入相应的扩展的 Target
4、扩展应该保持轻巧迅速,并且专注功能单一,在不打扰或者中断用户使用当前应用的前提下完成自己的功能点。
扩展和应用的交互:
上面提到了,扩展和容器应用是两个独立的App,但是扩展可以共享容器应用本身的逻辑、界面和数据
1、使用iOS 8 新引入的自制 framework 的方式来组织需要重用的代码,这样在链接 framework 后容器App 和扩展就都能使用相同的代码了。
2、通过开启 App Groups 和进行相应的配置来开启在两个进程间的数据共享。这包括了使用NSUserDefaults进行小数据的共享,或者使用NSFileCoordinator和NSFilePresenter甚至是 CoreData 和 SQLite 来进行更大的文件或者是更复杂的数据交互。
3、可通过自定义的 url scheme ,从扩展向应用反馈数据和交互。
废话不多说了,下面进入项目实例的环节。
首先打开项目,点击Xcode菜单的File->New->Target,然后选择 iOS 中的 Application Extension 的 Today Extension
在弹出的菜单中将新的 Target 命名为xxxWidget,并且让 Xcode 自动生成新的 Scheme,以方便测试使用。我们的工程中现在会多出一个和新建的 Target 同名的文件夹,里面主要包含了TodayViewController.h和TodayViewController.m 的 ViewController 程序文件,一个叫做MainInterface的 storyboard 文件和 Info.plist。其中在 plist 里 的NSExtension中定义了这个 扩展的类型和入口,而配套的 ViewController 和 StoryBoard 就是我们的扩展的具体内容和实现了。
首先把info.plist的Bundle display name 改为自己产品的名字
运行程序,然后下拉通知栏,点击今天,点击编辑,就看到可以添加刚刚自己创建的扩展了,扩展显示的名字可以在刚刚的info.plist中修改。
调试扩展的方式有两种:
1、选择这个扩展的scheme直接运行
2、先运行容器App,然后下拉通知栏打开今日扩展,点击Xcode菜单的Debug->Attach To Process ,选择Likly Targets中需要调试的扩展
新创建的Today扩展,只包括一个Hello World的Label,根据UI设计,我需要在上面添加一个音量调节的Slider。啥都憋说,先拖个去storyboard,运行爽一下。嗯,看上去好像挺完美(此处无图),忍不住拖动一下,先往右拖~~~手感很顺滑,再往左拖~~~。。。。(╯‵□′)╯︵┻━┻!什么鬼!滑到通知界面去了!好吧,查了下资料,原来官方建议不要在这个界面上添加可以滑动的插件,不然会导致用户误操作。好吧,看来只能修改UI了
看修改后的UI,需要实现的功能就是,点击“音量减”、“音量加”按钮的时候,容器App的音量要作出相应的改变。
没关西(ง •̀_•́)ง,一步步来,首先来实现音量调整的数据同步问题:
在应用和扩展间共享数据 - App Groups
对 iOS 开发者来说,沙盒限制了我们在设备上随意读取和写入。但是对于应用和其对应的扩展来说,Apple 在 iOS 8 +中为我们提供了一种可能性,那就是 App Groups。App Groups 为同一个 开发商 的应用或者扩展定义了一组域,在这个域中同一个 group 可以共享一些资源。
首先我们需要开启 App Groups。得益于 Xcode 5 开始引入的 Capabilities,这变得非常简单(至少不再需要去 developer portal 了)。选择主 Target,打开它的 Capabilities 选项卡,找到 App Groups 并打开开关,然后添加一个你能记得的 group 名字,比如group.myWidget。接下来你还需要为xxxWidget这个 Target 进行同样的配置,只不过不再需要新建 group,而是勾选刚才创建的 group 就行。
然后,使用以下代码,就可以读写共享的数据了
- (NSUserDefaults*)loadGroupData
{
NSUserDefaults* userDefault = [[NSUserDefaults alloc] initWithSuiteName:@"group.myWidget"];// SuiteName必须和上面Capabilities配置填写的一致
return userDefault;
}
- (void)setVolume:(uint32_t)volume
{
[[self loadGroupData] setObject:@(volume) forKey:@"volume"];
[[self loadGroupData] synchronize];
}
- (uint32_t)getVolume:(uint32_t)volume
{
NSNumber* volumeNumber = [[self loadGroupData] objectForKey:@"volume"];
if(volumeNumber && [volumeNumber isKindOfClass:[NSNumber class]]){
return volumeNumber.unsignedIntValue;
}else{
return kDefaultVolume;
}
}
扩展或容器App在修改和获取音量的时候,调用这些方法就OK了。
(P.S.可以通过自制 framework 的方式来组织需要重用的代码,具体方法这里不多说了)
在 TodayViewController.m 的 viewWillAppear: 方法中编写读取共享数据并刷新界面的代码,每次下拉,音量值Label就可以正确显示了(没错,此处没有代码)。
接着。。。棘手的问题来了!怎么去实现扩展和容器App交互呢?使用 NSNotificationCenter,KVO,Delegate都无果,因为它们俩根本就不是同一个App!再试了下url scheme的方式,一点击按钮就跳到容器App去了,蛋疼!看来只能从共享数据上去做文章了,然后共享数据只能是主动获取数据,所以初步想了如下两个方案
1、容器App设置定时器去轮询共享数据,一旦发生变化,就相应地改变音量。
2、扩展发送请求到服务器,服务器再通知容器App。
But!我觉得这两个方案都好low好傻逼好费资源啊!!!为什么会酱紫!难道真的没有方法可以实现了么!!?在Baidu,Google,Stack OverFlow上各种搜也找不到好的解决方法!
就在我快要着手第一个方案的时候,突然无意中百度到了一个大救星(真的是Baidu,不是Google),没错,它就是 CFNotificationCenterGetDarwinNotifyCenter!这是CoreFoundation库中一个系统级的通知中心,苹果的系统自己也在用它,看清了“Darwin””了没有?哈哈!看了下CFNotificationCenter相关的API,跟NSNotificationCenter有点像。需要用到Toll-Bridge的知识与CoreFoundation相关的类进行桥接,这虽不常用但也不难。还需要注意下个别参数的使用。
// 添加监听
- (void)addObserver
{
CFNotificationCenterRef notification = CFNotificationCenterGetDarwinNotifyCenter ();
CFNotificationCenterAddObserver(notification, (__bridge const void *)(self), observerMethod, CFSTR(“通知名”), NULL, CFNotificationSuspensionBehaviorDeliverImmediately);
}
void observerMethod (CFNotificationCenterRef center, void *observer, CFStringRef name, const void *object, CFDictionaryRef userInfo)
{
//监听到notification后要做的处理
}
// 移除监听
- (void)removeObserver
{
CFNotificationCenterRef notification = CFNotificationCenterGetDarwinNotifyCenter ();
CFNotificationCenterRemoveObserver(notification, (__bridge const void *)(self), CFSTR(“通知名“), NULL);
}
// 发送通知
- (void)postNotificaiton
{
CFNotificationCenterRef notification = CFNotificationCenterGetDarwinNotifyCenter ();
CFNotificationCenterPostNotification(notification, CFSTR(“通知名”), NULL, NULL, YES);
}
使用这个方法后,容器App调用addObserver,扩展改变音量的时候调用 postNotificaiton ,容器App就能接收到通知了。
下一步是要传输数据,看看发送和接收的方法的参数,
void CFNotificationCenterPostNotification ( CFNotificationCenterRef center, CFStringRef name, const void *object, CFDictionaryRef userInfo, Boolean deliverImmediately );
void observerMethod (CFNotificationCenterRef center, void *observer, CFStringRef name, const void *object, CFDictionaryRef userInfo)
还真有 CFDictionaryRef userInfo ,太好了,赶紧爽一下!
CFNotificationCenterPostNotification(notification, CFSTR(“通知名”), NULL, 数据, YES);
燃鹅!
燃鹅!!
容器App接收到的 userInfo 居然是nil!!百思不得骑姐其解,只能看文档了:
妈蛋,传不过去!
but!无所谓啦~ㄟ( ▔, ▔ )ㄏ,一开始不是已经解决了共享数据的问题了么,扩展发送通知前先存储数据,容器App接收到通知后,再读取共享数据那就好了~~~用这个思路,就能实现大部分的扩展与容器App之间的交互功能了~
BTW,如果想实现更复杂的功能,推荐使用MMWormhole这个开源库,它专门用于在Container app 与 Extension间传递消息,苹果婊 Watch OS 也适用~
对了,如果用Jenkins打包,一定要注意widget 的app id (不是容器app 的app id)和app groups是正确的,否用 application loader提交程序的时候,会报错。
最后分享一个Podfile多个target引用部分相同pod库的编写方法:
def host_pods
pod 'SSKeychain', '~> 0.1.4'
pod 'INAppStoreWindow', :head
pod 'AFNetworking', '1.1.0'
end
def shared_pods
pod 'MMWormhole','~> 2.0.0'
end
target 'HostApp' do
shared_pods
host_pods
end
target 'Extension' do
shared_pods
end
引用:
https://onevcat.com/2014/08/notification-today-widget/
https://medium.com/@saberjack/ios-sending-notifications-between-your-apps-3fe7422d6a41#.5236ab5mt
http://www.cocoachina.com/ios/20150417/11597.html