iOS Today Extension开发心得

前段时间因为项目需要,接触了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

运行程序,然后下拉通知栏,点击今天,点击编辑,就看到可以添加刚刚自己创建的扩展了,扩展显示的名字可以在刚刚的info.plist中修改。

添加扩展

调试扩展的方式有两种:

1、选择这个扩展的scheme直接运行

2、先运行容器App,然后下拉通知栏打开今日扩展,点击Xcode菜单的Debug->Attach To Process ,选择Likly Targets中需要调试的扩展

新创建的Today扩展,只包括一个Hello World的Label,根据UI设计,我需要在上面添加一个音量调节的Slider。啥都憋说,先拖个去storyboard,运行爽一下。嗯,看上去好像挺完美(此处无图),忍不住拖动一下,先往右拖~~~手感很顺滑,再往左拖~~~。。。。(╯‵□′)╯︵┻━┻!什么鬼!滑到通知界面去了!好吧,查了下资料,原来官方建议不要在这个界面上添加可以滑动的插件,不然会导致用户误操作。好吧,看来只能修改UI了

修改后的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 就行。

开启 App Groups

然后,使用以下代码,就可以读写共享的数据了

- (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!!百思不得骑姐其解,只能看文档了:

CFNotificationCenterPostNotification 文档

妈蛋,传不过去!

but!无所谓啦~ㄟ( ▔, ▔ )ㄏ,一开始不是已经解决了共享数据的问题了么,扩展发送通知前先存储数据,容器App接收到通知后,再读取共享数据那就好了~~~用这个思路,就能实现大部分的扩展与容器App之间的交互功能了~

BTW,如果想实现更复杂的功能,推荐使用MMWormhole这个开源库,它专门用于在Container app 与 Extension间传递消息,苹果婊 Watch OS 也适用~

对了,如果用Jenkins打包,一定要注意widget 的app id (不是容器app 的app id)和app groups是正确的,否用 application loader提交程序的时候,会报错。

App Groups 设置

最后分享一个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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,607评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,047评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,496评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,405评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,400评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,479评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,883评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,535评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,743评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,544评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,612评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,309评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,881评论 3 306
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,891评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,136评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,783评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,316评论 2 342

推荐阅读更多精彩内容