iOS App Extensions

一、扩展概述

扩展(Extension)是iOS 8中引入的一个非常重要的新特性。扩展让app之间的数据交互成为可能。用户可以在app中使用其他应用提供的功能,而无需离开当前的应用。

在iOS 8系统之前,每一个app在物理上都是彼此独立的,app之间不能互访彼此的私有数据。

而在引入扩展之后,其他app可以与扩展进行数据交换。基于安全和性能的考虑,每一个扩展运行在一个单独的进程中,它拥有自己的bundle, bundle后缀名是.appex。扩展bundle必须包含在一个普通应用的bundle的内部。

iOS 8系统有6个支持扩展的系统区域,分别是Today、Share、Action、Photo Editing、Storage Provider、Custom keyboard。支持扩展的系统区域也被称为扩展点。

Today Widget

对于赛事比分,股票、天气、快递这类需要实时获取的信息,可以在通知中心的Today视图中创建一个Today扩展实现。Today扩展又称为Widget。

Share

在iOS 8之前,用户只有Facebook,Twitter等有限的几个分享选项可以选择。如果希望将内容分享到Pinterest,开发者则需要一些额外的努力。在iOS 8中,开发者可以创建自定义的分享选项。

Action

action在所有支持的扩展点中扩展性最强的一个。它可以实现转换另一个app上下文中的内容。苹果在WWDC大会上演示了一个Bing翻译动作扩展,它可以将在Safari中选中的文本翻译成不同的语言。

Photo Editing

在iOS 8之前,如果你想为你的照片添加一个特殊的滤镜,你需要进入第三方app中,这个过程是相当繁琐的。在iOS 8中,你可以直接在Photos中使用第三方app,如Instagram,VSCO cam、Aviary提供的Photo Editing扩展完成对图片的编辑,而无需离开当前的app。

Storage Provider

Storage Provider让跨多个文件存储服务之间的管理变得更简单。类似Dropbox、Google Drive等存储提供商通过在iOS 8中提供一个Storage Provider扩展,app直接可以使用这些扩展检索和存储文件而不再需要创建不必要的拷贝。

Custom Keyboard

苹果公司在2007年率先推出了触摸屏键盘,但一直没多大改进。在这一方面,Android则将键盘权限开放给了第三方开发者,所以出现了许多像Swype,SwiftKey等优秀的键盘输入法。在iOS 8中,苹果终于将键盘权限开发给了第三方开发者,自定义键盘输入法可以让用户在整个系统范围内使用。

二、创建扩展与发布扩展

在创建扩展之前,你需要创建一个用来包含扩展的常规的app项目。这个包含扩展的app被称为containing app。在创建好containg app之后,选择File->New->Target菜单,从弹出的对话框中选择一个适当的扩展目标模板。每一个扩展目标模板都包含了与扩展点相关的文件和设置。一个containing app可以包含多个不同类型的扩展。

每一个扩展目标模板包含一个头文件和实现文件,一个Info.plist文件,以及一个storyboard文件。Info.plist文件包含了对扩展的配置信息,其中最重要的键是NSExtension。下面列出了一个NSExtension可能包含的常用键值对。

<key>NSExtension</key>

<dict>

<key>NSExtensionAttributes</key>

<dict>

<key>NSExtensionActivationRule</key> <!--1-->

<dict>

<key>NSExtensionActivationSupportsImageWithMaxCount</key>

<integer>10</integer>

<key>NSExtensionActivationSupportsMovieWithMaxCount</key>

<integer>1</integer>

</dict>

<key>NSExtensionJavaScriptPreprocessingFile</key> <!--2-->

<string>MyJavaScriptFile</string>

<key>NSExtensionPointVersion</key>

<string>1.0</string>

</dict>

<key>NSExtensionMainStoryboard</key>  <!--3-->

<string>MainInterface</string>

<key>NSExtensionPointIdentifier</key>  <!--4-->

<string>com.apple.ui-services</string>

<key>NSExtensionPrincipalClass</key>  <!--5-->

<string>ActionViewController</string>

</dict>

1)   NSExtensionActivationRule定义了当前的扩展支持的数据类型及数据项个数,例如当前的设置只支持图片格式和视频格式的数据,并且最多不超过10张图片和1个视频。

2)   NSExtensionJavaScriptPreprocessingFile用于配置与脚本交互的JS脚本文件的名字。

3)   NSExtensionMainStoryboard配置扩展的Storyboard文件名。

4)   NSExtensionPointIdentifier用于表示扩展点,每一个扩展点拥有一个唯一的名字。

5)   NSExtensionPrincipalClass配置当扩展启动时,扩展点首先要实例化的类

为了将扩展提交苹果商店,你需要提交你的containg app。并且需要注意,除了扩展必须包含功能以外,同时containg app还需要提供一些功能,而针对OS X平台的扩展则无此限制。当用户安装了你的containg app,containg app中包含的扩展也会一同被安装。

三、理解扩展如何运作

在安装扩展之后,扩展并不会自动运行,用户必须执行特定的操作来启用扩展。如果是Today扩展,用户可以在通知中心的Today视图中编辑启用扩展。如果是自定义键盘扩展,用户需要在系统设置的通用选项下的键盘选项中启用自定义键盘扩展。而如果是Share扩展,用户只需点击系统提供的分享按钮,即可在分享列表中找到分享扩展。

一个扩展并不是一个app,它的生命周期和运行环境不同于普通app。在生命周期方面,扩展的生命周期从用户在另一个app中选择了扩展开始,一直到扩展完成了用户的请求生命周期结束。在运行环境方面,扩展的限制要比普通app更严格,扩展的可用内存上限以及可用的API都比普通app要少。严格限制扩展的内存是因为在同一时间可能会有多个扩展同时运行,如Widget扩展。如果API声明包含NS_EXTENSION_UNAVAILABLE宏,则此API在扩展中将不可用,常见的API如:

+ (UIApplication *)sharedApplication NS_EXTENSION_UNAVAILABLE_IOS("Use view controller based solutions where appropriate instead.");

调用扩展的应用称为host app,对于Widget扩展,host app就是Today。host app会在扩展的有效生命周期内定义一个扩展上下文。通过扩展上下文,host app可以和扩展互传数据。注意,扩展只和host app直接通信,扩展与containg app以及containing app与host app之间不存在通信关系,如果扩展需要打开containg app,则通过自定义URL scheme方式实现,而不是直接向containg app发送消息。三者的关系见下图:

扩展是一个单独的个体。扩展拥有独立的target,独立的bundle文件,独立的运行进程,独立的地址空间。这意味着即使你的containing app不在运行,系统也可以启动扩展。或者你的containing app处于挂起状态,同样不会影响扩展的运行。所以系统可以单独对扩展执行优化。扩展与containg app的关系:

四、设计扩展过程中常见的几个问题

1. containg app与扩展如何通过扩展上下文互传数据

在iOS 8中,UIViewController新增了一个扩展上下文属性extensionContext。来处理containing app与扩展之间的通信,上下文的类型是NSExtensionContext。假设你现在需要在host app中将一张图片传递给扩展做滤镜处理,host app中的代码如下:

UIActivityViewController *activityViewController = [[UIActivityViewController alloc] initWithActivityItems:@[[self.imageView image]] applicationActivities:nil];

[self presentViewController:activityViewController animated:YES completion:nil];

当用户在弹出的Action列表中选择了扩展,扩展将被启动,然后在扩展的viewDidLoad方法中,通过extensionContext检索host app传回的数据项。扩展中的代码如下:

- (void)viewDidLoad {

[super viewDidLoad];

NSExtensionItem *imageItem = [self.extensionContext.inputItems firstObject];

if(!imageItem){

return;

}

NSItemProvider *imageItemProvider = [[imageItem attachments] firstObject];

if(!imageItemProvider){

return;

}

// 检查是否包含文本

if([imageItemProvider hasItemConformingToTypeIdentifier:(NSString *)kUTTypeImage]){

[imageItemProvider loadItemForTypeIdentifier:(NSString *)kUTTypeImage options:nil completionHandler:^(UIImage *image, NSError *error) {

if(image){

dispatch_async(dispatch_get_main_queue(), ^{

self.imageView.image = image;

});

}

}];

}

}

上述代码中,extensionContext表示一个扩展到host app的连接。通过extionContent,你可以访问一个NSExtensionItem的数组,每一个NSExtensionItem项表示从host app传回的一个逻辑数据单元。你可以从NSExtensionItem项的attachments属性中获得附件数据,如音频,视频,图片等。每一个附件用NSItemProvider实例表示。上述代码中NSItemProvider的loadItemForTypeIdentifier实例方法的第一个参数是(NSString *)kUTTypeImage,如果你需要处理的是文本类型,参数则为(NSString *)kUTTypeText,相应的处理代码则变成:

if([itemProvider hasItemConformingToTypeIdentifier:(NSString *)kUTTypeText]){

[imageItemProvider loadItemForTypeIdentifier:(NSString *)kUTTypeText options:nil completionHandler:^(NSAttributedString *string, NSError *error) {

if (string) {

// 在这里处理文本

}

}];

}

当扩展处理完host app传回的图片数据后,它需要将处理好的数据再传给host app。在扩展中的代码如下:

-(IBAction)done:(id)sender{

NSExtensionItem* extensionItem = [[NSExtensionItem alloc] init];

[extensionItem setAttachments:@[[[NSItemProvider alloc] initWithItem:[self.imageView image] typeIdentifier:(NSString*)kUTTypeImage]]];

[self.extensionContext completeRequestReturningItems:@[extensionItem] completionHandler:nil];

}

最后一步是host app接收来自扩展传回的数据,在host app中的代码如下:

[activityViewController setCompletionWithItemsHandler:^(NSString *activityType, BOOL completed, NSArray *returnedItems, NSError * error){

if([returnedItems count] > 0){

NSExtensionItem* extensionItem = [returnedItems firstObject];

NSItemProvider* imageItemProvider = [[extensionItem attachments] firstObject];

if([imageItemProvider hasItemConformingToTypeIdentifier:(NSString *)kUTTypeImage]){

[imageItemProvider loadItemForTypeIdentifier:(NSString *)kUTTypeImage options:nil completionHandler:^(UIImage *item, NSError *error) {

if(item && !error){

dispatch_async(dispatch_get_main_queue(), ^{

[self.imageView setImage:item];

});

}

}];

}

}

}];

上述代码主要是通过设置一个completionBlock处理数据回调。

注意,所有的扩展都是一个UIViewController。所以UIViewController的所有生命周期方法,如viewWillAppear:、viewWillDisappear:等在扩展中都是可以使用的。

2. 如何在扩展中打开containing app

在一般情况下,扩展和containing app不存在通信关系。但是有时候需要在扩展中打开containing app,如iOS 7中预置的日历Widget。在常规的app中,可以使用如下代码在A app中打开B app:

if ([[UIApplication sharedApplication] canOpenURL:[NSURL URLWithString:customURL]]) {

[[UIApplication sharedApplication] openURL:[NSURL URLWithString:customURL]];

}

但是之前有讲到,sharedApplication API在扩展中被禁止使用,所以为了实现同样的功能,NSExtensionContext定义了一个新的方法用来打开containing app:

- (void)openURL:(NSURL *)URL completionHandler:(void (^)(BOOL success))completionHandler;

在调用此方法之前,需要在containg app中定义一个自定义URL Scheme。定义方法可参见链接,最终的结果如下图:

在扩展中打开containing app的代码如下:

- (IBAction)openContainingApp:(id)sender {

NSURL *url = [NSURL URLWithString:@"ExtensionDemo://"];

[self.extensionContext openURL:url completionHandler:^(BOOL success) {

}];

}

3. 如何实现containing app与扩展共享数据

扩展和containing app各自拥有自己的数据容器,虽然扩展内嵌在containing app的内部,但是它们并不可以互访彼此的数据。为了实现containing app与扩展的数据共享,苹果在iOS 8中引入了一个新的概念——App Group。为了开启App Group,找到你的containing app目标,在右侧找到Capabilities标签,定位到App Groups分组,如下图所示。

然后选择你需要共享数据的扩展目标,重复执行一次操作,注意两次的App Group名要相同,不要添加新的条目。当开启App Group后,你可以使用NSUserDefaults方法访问共享区域,如下述代码,注意不是[NSUserDefaults standardUserDefaults]:

_sharedUserDefault= [[NSUserDefaults alloc] initWithSuiteName:@"group.com.aegeaon.ExtensionDemo"];

你也可以使用NSFileManager的containerURLForSecurityApplicationGroupIdentifier方法访问共享数据区:

- (BOOL)saveTextByNSFileManager {

NSError *err = nil;

NSURL *containerURL = [[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:@"group.wangzz"];

containerURL = [containerURL URLByAppendingPathComponent:@"Library/Caches/good"];

NSString *value = _textField.text;

BOOL result = [value writeToURL:containerURL atomically:YES encoding:NSUTF8StringEncoding error:&err];

if (!result) {

NSLog(@"%@",err);

} else {

NSLog(@"save value:%@ success.",value);

}

return result;

}

App Group区域在containing app与扩展之间所处的关系图:

你可能注意到了,在Xcode 6中iPhone模拟器的位置已经发生了变化。与此同时,在iOS 8 release Note中有提到,app的沙盒结构已经发生了改变,现在它被划分成了三个容器,Bundle容器、Data容器、iCloud容器。iOS 8 app沙盒目录结构如下图:

为了具体了解沙盒目录的布局,使用如下代码分别在containing app和扩展中打印出App Group目录,app bundle目录,以及Document目录:

- (void)logAppPath

{

//app group路径

NSURL *containerURL = [[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:@"group.com.aegeaon.ExtensionDemo"];

NSLog(@"app group:\n%@",containerURL.path);

//打印可执行文件路径

NSLog(@"bundle:\n%@",[[NSBundle mainBundle] bundlePath]);

//打印documents

NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);

NSString *path = [paths objectAtIndex:0];

NSLog(@"documents:\n%@",path);

}

在containing app中执行logAppPath方法的结果如下:

app group:

/Users/aegaeon/Library/Developer/CoreSimulator/Devices/72CA5D31-9509-4076-BC94-BF4D29DC0151/data/Containers/Shared/AppGroup/5B4CFBD8-D95D-4F01-9268-D9F79792147D

bundle:

/Users/aegaeon/Library/Developer/CoreSimulator/Devices/72CA5D31-9509-4076-BC94-BF4D29DC0151/data/Containers/Bundle/Application/EED1F771-A8AD-4A97-97F3-2B0A57936C17/ExtensionDemo.app

documents:

/Users/aegaeon/Library/Developer/CoreSimulator/Devices/72CA5D31-9509-4076-BC94-BF4D29DC0151/data/Containers/Data/Application/95DBF43A-8B4B-426C-9A3A-C1745FCB3FA2/Documents

在扩展中执行logAppPath方法的结果如下:

app group:

/Users/aegaeon/Library/Developer/CoreSimulator/Devices/72CA5D31-9509-4076-BC94-BF4D29DC0151/data/Containers/Shared/AppGroup/5B4CFBD8-D95D-4F01-9268-D9F79792147D

bundle:

/Users/aegaeon/Library/Developer/CoreSimulator/Devices/72CA5D31-9509-4076-BC94-BF4D29DC0151/data/Containers/Bundle/Application/EED1F771-A8AD-4A97-97F3-2B0A57936C17/ExtensionDemo.app/PlugIns/ExpressExt.appex

documents:

~/Documents

其中标注为红色的意思是每次运行目录名都会发生变化。标注为绿色的表示文件名不会变化的,标准为橘色也验证了iOS 8中沙盒目录被划分的说法。其中也可以看出扩展扩展名为appex,它包含在containing app的PlugIns目录内。下图展示了扩展目录在Finder中的结构:

4. 如何让扩展访问到网页内容

在WWDC上,苹果演示了在Safari for iOS中使用Bing Action扩展将当前页面翻译为其他语言。考虑一下,为了完成这个功能,扩展和浏览器之间一定要建立一个连接,浏览器负责将选中的文本发给扩展,扩展将翻译的结果发回浏览器。为了实现这个机制,这里需要借助一个JavaScript脚本,使用JS脚本可以访问网页的DOM。脚本的内容很简单,只包含两个方法,脚本文件名为MyJavaScriptFile.js。代码如下:

var MyExtensionJavaScriptClass = function() {};

MyExtensionJavaScriptClass.prototype = {

run: function(arguments) {

arguments.completionFunction({"baseURI": document.baseURI});

},

finalize: function(arguments) {

var newContent = arguments["content"];

document.write(newContent);

}

};

var ExtensionPreprocessingJS = new MyExtensionJavaScriptClass;

其中包含一个run()和finalize()方法。当Safari一加载好你的JS文件,就会立即调用run方法,当你在扩展中调用了completeRequestReturningItems:expirationHandler:completion:方法,Safari会调用finalize()方法。在run()方法中,Safari提供了一个arguments参数,使用它可以利用键值对的形式将数据传给扩展。在上述代码中,传给扩展的键值对是:@{@"baseURI" : document.baseURI}。在finalize()方法中,当你调用了completeRequestReturningItems:expirationHandler:completion:方法,方法第一个参数的值会传给finalize()方法的arguments形参。在上述代码中,finalize()接收到参数后,将内容写入了当前的文档。

为了Safari能够调用正确调用到JS文件,需要在扩展的Info.plist文件中添加如下配置:

<key>NSExtensionAttributes</key>

<dict>

<key>NSExtensionJavaScriptPreprocessingFile</key>

<string>MyJavaScriptFile</string>

</dict>

在你的扩展中,为了取得从JS脚本传回的键值对,你需要为NSItemProvider的方法loadItemForTypeIdentifier:options:completionHandler:指定kUTTypePropertyList数据类型,在取得返回的键值字典后,使用NSExtensionJavaScriptPreprocessingResultsKey键取值,代码如下:

NSExtensionContext *context = self.extensionContext;

NSExtensionItem *item = context.inputItems.firstObject;

NSItemProvider *provider = item.attachments.firstObject;

[provider loadItemForTypeIdentifier:(NSString *)kUTTypePropertyList

options:nil

completionHandler:^(id<NSSecureCoding> item, NSError *error) {

NSDictionary *results = (NSDictionary *)item;

NSString *baseURI = [[results objectForKey:NSExtensionJavaScriptPreprocessingResultsKey] objectForKey:@"baseURI"];

NSLog(@"%@", baseURI);

}];

为了在扩展中将处理后的结果传给脚本,你需要使用NSItemProvider的initWithItem:typeIdentifier:包装键值对。代码如下:

NSExtensionItem *extensionItem = [[NSExtensionItem alloc] init];

extensionItem.attachments = @[[[NSItemProvider alloc] initWithItem: @{NSExtensionJavaScriptFinalizeArgumentKey: @{@"content":@"Hello World"}} typeIdentifier:(NSString *)kUTTypePropertyList]];

[[self extensionContext] completeRequestReturningItems:@[extensionItem] expirationHandler:nil completion:nil];

5. 如何在containing app与扩展之间共享代码

iOS 8中,你可以内嵌一个framework文件在扩展和containing app之间共享代码。假设你希望在你的containing app与扩展之间共享图片处理的代码,此时你可以将代码打包成framework文件,内嵌到两个目标中。对于内嵌框架中的代码,确保不包含扩展不允许使用的API。

如何将代码打包成framework文件这里就不敖述了,感兴趣的同学可以参见:http://blog.sina.com.cn/s/blog_407fb5bc01013v6s.html。当你创建好.framework文件后,你可以直接将.framework文件同时拖入containing app和扩展中,如下图所示:

这里使用公司ILSLib目录下的的MagicalRecord21.framework文件作为素材,讲解如何在containing app和自定义键盘扩展之间实现共享Core Data数据库。在你的扩展和containing app中中配置好引用头文件。分别在containing app的AppDelegate文件的application: didFinishLaunchingWithOptions: launchOptions与自定义键盘扩展的UIInputViewController子类文件中viewDidLoad方法中添加如下代码:

[MagicalRecord setupCoreDataStackWithStoreNamed:@"demo.sqlite"];

上述代码分别对containing app和扩展执行Core Data栈初始化,其中包括数据模型、sqlite存储文件等配置。运行containing app,此时AppDelegate中的数据库配置代码会被执行,接着打开系统设置中的通用选项下的键盘选项,在这里启用自定义键盘。然后回到containing app,切换到自定义键盘扩展,此时自定义键盘扩展中viewDidLoad方法中的数据库配置代码执行,但是控制台出现错误提示:

CoreData: error: -addPersistentStoreWithType:SQLite configuration:(null) URL:~/Library/Application%20Support/CustomKeyboardExt/demo.sqlite -- file:/// options:(null) ... returned error Error Domain=NSCocoaErrorDomain Code=512 "The operation couldn’t be completed. (Cocoa error 512.)" UserInfo=0x7b48a720 {reason=Failed to create file; code = 2} with userInfo dictionary {

reason = "Failed to create file; code = 2";

}

上述错误表示在扩展的~/Library/Application%20Support/CustomKeyboardExt/demo.sqlite目录创建.sqlite文件失败。翻阅MagicalRecord源代码(需要从github重新下载源代码,.framework看不到源代码),其中在创建.sqlite存储文件路径的代码中会发现:

+ (NSURL *) MR_urlForStoreName:(NSString *)storeFileName {

NSArray *paths = [NSArray arrayWithObjects:[self MR_applicationDocumentsDirectory], [self MR_applicationStorageDirectory], nil];

NSFileManager *fm = [[NSFileManager alloc] init];

for (NSString *path in paths) {

NSString *filepath = [path stringByAppendingPathComponent:storeFileName];

if ([fm fileExistsAtPath:filepath]) {

return [NSURL fileURLWithPath:filepath];

}

}

//set default url

return [NSURL fileURLWithPath:[[self MR_applicationStorageDirectory] stringByAppendingPathComponent:storeFileName]];

}

其中MR_applicationStorageDirectory方法返回的是Application Support目录,而这个目录是处在Library目录内的。上文中已经讲过,扩展没有Documents目录,同样也是没有Library目录。所以文件创建会发生失败。为了实现扩展与containing app之间共享.sqlite文件,这里需要将.sqlite文件创建在App Group区域。问题是MagicalRecord21.framework文件只暴露了头文件,无法对其源文件中的MR_urlForStoreName:方法做修改。这里使用Objective-C的动态运行时技术——Method Swizzling,在运行时将MR_urlForStoreName:方法的实现使用新的实现进行替换。 (注:这里可以直接给setupCoreDataStackWithStoreNamed方法传递一个包含文件路径的URL类型参数实现修改.sqlite文件的存放位置,methodSwizzling只是另一种通用处理方法)

首先需要为自定义键盘扩展创建一个Category文件NSPersistentStore+Tracking.h/m,.m文件中的完整的代码如下:

#import "NSPersistentStore+Tracking.h"

#import <objc/runtime.h>

#import <MagicalRecord21/CoreData+MagicalRecord.h>

static NSString * const kGroupName = @"group.com.aegeaon.ExtensionDemo";

static NSString * const kContainingDirectory = @"CoreDataStore/";

@implementation NSPersistentStore (Tracking)

+ (void)load {

static dispatch_once_t onceToken;

dispatch_once(&onceToken, ^{

Class class = [self class];

SEL originalSelector = @selector(MR_urlForStoreName:);

SEL swizzledSelector = @selector(ILS_urlForStoreName:);

SwizzleClassMethod(class, originalSelector, swizzledSelector);

});

}

void SwizzleClassMethod(Class c, SEL orig, SEL new) {

Method origMethod = class_getClassMethod(c, orig);

Method newMethod = class_getClassMethod(c, new);

c = object_getClass((id)c);

if(class_addMethod(c, orig, method_getImplementation(newMethod), method_getTypeEncoding(newMethod)))

class_replaceMethod(c, new, method_getImplementation(origMethod), method_getTypeEncoding(origMethod));

else

method_exchangeImplementations(origMethod, newMethod);

}

#pragma mark - Method Swizzling

+ (NSURL *) ILS_urlForStoreName:(NSString *)storeFileName {

NSURL *storeURL = [[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:kGroupName];

storeURL = [storeURL URLByAppendingPathComponent:[kContainingDirectory stringByAppendingString:storeFileName]];

return storeURL;

}

@end

在当前的代码一载入内存,load方法将被执行,它比AppDelegate的application: didFinishLaunchingWithOptions: launchOptions方法要先被执行,上述代码会将MR_urlForStoreName:的实现替换成ILS_urlForStoreName:,在ILS_urlForStoreName:方法中,使用NSFileManager的containerURLForSecurityApplicationGroupIdentifier方法设定App Group,最终的.sqlite文件将保存在App Group目录内的CoreDataStore目录下。同样需要为containing app中使用此方法,可以直接将NSPersistentStore+Tracking.h/m拖入containing app目标内。再次运行自定义键盘扩展,数据库文件已成功保存到App Group中。如下图:

同时被共享的代码框架MagicalRecord21.framework被containg app和扩展共享,双方共用一个框架文件,如下图:

6. 如何在扩展中处理长时间任务

用户希望在扩展完成他们的任务之后能够立即返回到host app中。但是如果扩展执行的任务是一个长时间任务,比如下载。在这种情况下,需要使用NSURLSession来创建一个下载session,并初始化一个后台下载任务。当扩展初始化了上传下载任务后,就算是完成了host app的请求,扩展就可以被终止。这不会影响到任务的结果。如果当后台任务完成后,你的扩展不在运行,系统将在后台启动你的contaiing app并调用appdelegate的aplication:handleEventsForBackgroundURLSession:completionHandler:方法。为了在扩展中初始化一个后台的NSURLSession任务,你必须设置一个containing app和扩展都可以访问的共享容器。

相关代码如下:

NSURLSession *mySession = [self configureMySession];

NSURL *url = [NSURL URLWithString:@"http://www.example.com/LargeFile.zip"];

NSURLSessionTask *myTask = [mySession downloadTaskWithURL:url];

[myTask resume];

- (NSURLSession *) configureMySession {

if (!mySession) {

NSURLSessionConfiguration* config = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:@“com.mycompany.myapp.backgroundsession”];

config.sharedContainerIdentifier = @"com.mycompany.myappgroupidentifier";

mySession = [NSURLSession sessionWithConfiguration:config delegate:selfdelegateQueue:nil];

}

return mySession;

}

原文链接:http://blog.csdn.net/songhongri/article/details/38709455

时间: 2024-10-26 02:53:14

iOS App Extensions的相关文章

WWDC2014之App Extensions学习笔记

一.关于App Extensions extension是iOS8新开放的一种对几个固定系统区域的扩展机制,它可以在一定程度上弥补iOS的沙盒机制对应用间通信的限制. extension的出现,为用户提供了在其它应用中使用我们应用提供的服务的便捷方式,比如用户可以在Today的widgets中查看应用展示的简略信息,而不用再进到我们的应用中,这将是一种全新的用户体验:但是,extension的出现可能会减少用户启动应用的次数,同时还会增大开发者的工作量. 几个关键词 extension poin

App Extensions篇之Share Extension

转载请注明出处:http://www.cnblogs.com/zhanggui/p/7119572.html 1.前言 这里主要是对App Extension的一些介绍以及详细给大家介绍一下Share Extension,后期会添加其他的Extension介绍. 2.开始 主要对App Extension和Share Extension进行介绍.请继续往下看: 2.1: App Extension的介绍 官方给的说法是:App Extension可以让你扩展你的APP的自定义功能和内容,使用户可

用Xamarin和Visual Studio编写iOS App

一说开发 iOS app,你立马就会想到苹果的开发语言 Objective C/Swift 和 Xcode.但是,这并不是唯一的选择,我们完全可以使用别的语言和框架. 一种主流的替换方案是 Xamarin,这是一个跨平台框架,允许你开发 iOS.Android 和 OSX.Windows app,它使用的是 C# 和 Visual Studio.最大的好处在于,Xamarin 允许你在 iOS 和 Android app 间共享代码. Xamarin 与其他跨平台框架相比有一个最大的好处:使用

[iOS]App上架流程[利用Archive进行上传]

今天给大家带来项目如何上架的教程 准备: 1. 一个开发者账号(需要交过钱的,??好黑) 2. 一个做完的项目 检查: 1. 你的Xcode必须是正式版,不可以是beta版本(不能上传项目) 2. 请确认你的Xcode是从App Store或者从开发者网站下的Xcode安装,而不是非法获取的安装包,因为可能有Ghost.如何检查? 开始吧. 一.生成发布证书(证书的作用:类似于驾照,证明你的身份可以进行开发者一些操作) 打开https://developer.apple.com 点击右上角开发者

iOS App上架发布到app store市场最新中文教程

iOS -App发布Appstore最新中文教程 这个教程教你怎么把开发好的ios应用上架到appstore市场,一步一步带截图,包括最新的appstore后台系统itunesconnect使用,证书创建和管理,应用id创建和管理,上传ipa等都将一步一步详细介绍. 下面从七个步骤进行详细介绍! 1.创建唯一标示符App IDs 2.申请发布证书 3.申请发布描述文件 4.到iTunes Connect创建App并填写信息 5.Windows下选择证书编译打包 6.Windows下上传IPA到A

hbuilder IOS APP 打包与发布

---恢复内容开始--- 准备:苹果开发者账号,一个Mac系统 没有账号可以再这里注册 https://developer.apple.com/ 因为账号是公司的,自己并没有注册过,这里就不进行阐述了. iOS有两种证书和描述文件: 1 开发(Development)证书和描述文件   (用于开发测试,在HBuilder中打包后可在真机环境通过Safari调试) 2 发布(Distribution)证书和描述文件  (用于提交Appstore,在HBuilder中打包后可使用Applicatio

iOS App的状态

Refer: App States and Multitasking iOS设备的系统资源有限,所以App在前台运行和在后台运行的时候是有区别的.为了节省电池寿命和用户体验,App在后台运行的时候,有一些功能会被操作系统限制. 当App在前台运行的时候,可以处理系统发送的touch事件.UIKit基础架构已经做了大部分的工作用来把事件分发到自定义的对象上,开发者只需要重写一些相应的方法. 在App的开发过程中建议遵守的方针: (Required)对状态的切换做出适当的相应,否则可能会造成数据丢失

使用Application Uploader工具在windows、linux下上传iOS App

我们知道发布一个app,一般是用到苹果的application loader助手上传应用,用过的都知道使用起来很繁琐,经常出错.而且只能运行在mac系统上,需要一定的硬件条件. 而上架辅助工具Application Uploader,可以实现在windows,linux或mac上,不需要应用程序加载器和mac计算机,就可以发布app到app store,试用了下,感觉不错,分享给各位开发者,方便大家. Application Uploader是一个专门为IOS app上架的开发助手,可以快速,轻

ios app开发步骤

虽然开发一个app的任务看上去可能很艰巨,但是整个过程可以抽象成几个相对简单的步骤,下面这些步骤会在你开发第一个app时帮你步入正途. 定义Concept 每个好app都是从一个concept开始. 获得这个concept的最好方法就是考虑你打算用你的app解决什么问题,好的app解决的问题都是单一,定义清晰的问题,比如,Settings app允许用户调整设备的所有设置,它给用户提供了一个独立界面让用户来完成一系列相关的任务. 下面是获得一个好concept的一些关键问题: 受众是谁?你app