App Extension Programming Guide-App Extension Essentials AppExtension编程指南:扩展基础4html
Handling Common Scenarios 常见问题的处理方案ios
iOS8/OS X v10.10web
当编写自定义代码以执行app扩展任务时,你可能须要处理一些其余多种类型扩展也会出现的状况。在这一章节中,咱们将帮助你如何应对和处理这些常见的问题。编程
你能够建立一个内嵌框架,用于在应用扩展和它的主应用程序(containing app)之间共享代码。好比,你在照片编辑扩展中开发了图片滤镜功能,那么同时该扩展的主应用程序containing app也有这个功能,那么你能够将实现该功能的代码封装成一个框架,并在扩展target和主应用程序target中嵌入这个框架。swift
你要确保你建立的内嵌框架不包含应用扩展不能使用的API。这类API通常使用unavailability
宏来标记,好比像 NS_EXTENSION_UNAVAILABLE
。数组
若是你建立的内嵌框架中包含应用扩展不能使用的API,你可将其安全地Link到containing app,它能够正常使用框架中的API,可是不能与应用扩展共享代码(译者注:也就是应用扩展不能使用该框架提供的全部API,继而没法作到代码共享)。若是你上传App Store的应用扩展中有这种框架,或者其余部分使用了不可用的API,那么审核时会被拒绝。浏览器
若是咱们要想应用扩展使用内嵌框架,那么首先要配置一下。将target的Require Only App-Extension-Safe API
选项设置为Yes
。若是你不这样设置,那么Xcode会向你提示警告:linking against dylib not safe for use in application extensions
。安全
重要提示:若是containing app要连接至内嵌框架,那么必需要支持arm64架构,不然在上传App Store时会被拒绝。(如“建立应用扩展”章节中介绍的,全部应用扩展都要支持arm64架构。)bash
在配置配置Xcode项目时,必须在Copy Files
编译阶段选择“Frameworks”做为内嵌框架的目标。网络
重要提示:咱们一般要选择 Frameworks 做为 Copy Files 编译阶段目标。若是你将其设置为 SharedFramework,那么上传App Store时会被拒绝的。
你可让containing app支持iOS7或更早的版本,但当在iOS8或更新的版本中运行时,要特别注意内嵌框架的安全性。详细内容能够参阅 Deploying a Containing App to Older Versions of iOS。
有关建立和使用内嵌框架的更多内容,请观看WWDC 2014的视频“Building Modern Frameworks”。
应用扩展和它的containing app的安全域是有区别的。即使扩展包是嵌套在containing app包中的。默认状况下,应用扩展和containing app是不能直接访问对方的容器的。
BACKGROUND 要了解容器,阅读 About the iOS File System 中的 File System Programming Guid.
不过你能够经过数据共享来实现这个愿望。好比,你但愿应用扩展和它的containing app共享一个单一的大数据集。好比prerendered assets。
要实现数据共享,咱们要使用Xcode或者开发者门户网站容许应用扩展和它的containing app成为一个应用组,而后在开发者门户网站中注册应用组,并指明在containing app中使用该应用组。关于应用组的知识请查阅 Entitlement Key Reference 文档的 Adding an App to an App Group 章节。
当你设置好应用组后,应用扩展和它的containing app就能够经过 NSUserDefaults API共享访问用户的信息。咱们可使用 initWithSuiteName: 方法实例化一个 NSUserDefaults 对象,而后传入共享组的标示符。好比一个共享扩展,它或许会更新用户最近常用的共享帐号,那么咱们能够这样来写:
// Create and share access to an NSUserDefaults object.
NSUserDefaults *mySharedDefaults = [[NSUserDefaults alloc] initWithSuiteName:@"com.example.domain.MyShareExtension"];
// Use the shared user defaults object to update the user's account. [mySharedDefaults setObject:theAccountName forKey:@"lastAccountName"];
复制代码
下图向咱们展现了应用扩展和它的containing app是如何经过共享容器实现数据共享的.
Figure 4-1应用扩展的容器与其containing app的容器是不一样的。
重要提示:若是你的应用扩展使用NSURLSession类执行后台的上传下载任务时,你必需要设置一个共享容器,这样扩展和containing app就能够访问到转换传输的数据。后台上传下载的更多知识请参阅 Performing Uploads and Downloads。
若是你设置了共享容器,那么containing app和它包含的容许参与数据分享的扩展就能够对共享容器里的内容进行读写操做了。同时你还必需要对数据的操做进行同步,以免数据损坏或出错。使用UIDocument类、Core Data或者SQLite能够帮你可让用户经过要求Safari运行JS文件来访问网络内容,并将结果返回到扩展。
版本说明 在iOS 8.2及更高版本中,您也可使用UIDocument该类来协调共享数据访问。 在iOS 9及更高版本中,您能够NSFileCoordinator直接使用该类进行共享数据访问,可是若是您这样作,则必须NSFilePresenter在应用扩辗转换为后台时删除对象。
在分享扩展(iOS与OS X平台)和Action扩展(iOS平台)中,通常都容许用户使用Safari浏览器访问网页并经过执行JavaScript脚本,并将结果返回到扩展中。你也能够在你的扩展运行以前(适用于两个平台)或执行完任务以后(仅适用于iOS平台)经过JavaScript文件修改网页内容。好比分享扩展,它能够帮助用户分享网页上的内容,或者iOS上的Action扩展可能会显示当前网页的指定翻译内容。
若是想添加网页访问和操做应用扩展,那么须要遵循下面几个步骤: 1.建立一个JavaScript文件,并申明一个全局对象,命名为 ExtensionPreprocessingJS
,并为该对象分配一个新的自定义JavaScript类的实例。 2.在应用扩展的属性列表文件中添加关键字 NSExtensionJavaScriptPreprocessingFile
,给 Safari 浏览器指明使用哪一个 JavaScript 文件。 3.在NSExtensionActivationRule
字典中,将NSExtensionActivationSupportsWebURLWithMaxCount
赋值一个非零的值。(更多关于 NSExtensionActivationRule 字典的知识请参阅 Declaring Supported Data Types for a Share or Action Extension。) 4.当你的应用扩展开始运行时,使用NSItemProvider类得到运行JavaScript文件所返回的结果。 5.在iOS系统的应用扩展中,若是你但愿Safari在扩展执行完任务后更新网页,那么你要向JavaScript文件中传入值。(在这一步中也使用NSItemProvider
类。)
为了告知Safari你的应用扩展中包含一个JavaScript文件,你须要在应用扩展的Info.plist
文件中,向NSExtensionAttributes
字典添加NSExtensionJavaScriptPreprocessingFile
关键字来指明你的JavaScript文件。这个键的值就是你但愿当你的应用扩展运行前,Safari要加载的JavaScript文件的名称。好比:
<key>NSExtensionAttributes</key>
<dict>
<key>NSExtensionJavaScriptPreprocessingFile</key>
<string>MyJavaScriptFile</string> <!-- Do not include the ".js" filename extension -->
</dict>
复制代码
在iOS和OS X平台中,在你自定义的JavaScript类中能够定义一个run()函数,该函数就是Safari加载JavaScript文件的入口。在run()函数中,Safari提供了一个名为completionFunction的参数,你可使用键值对象的形式将结果传给应用扩展。
在iOS平台中,你还能够定义一个finalize()
函数,当应用扩展在任务结束阶段调用completeRequestReturningItems:expirationHandler:completion:
方法时Safari会调用finalize()
函数。在该函数中,能够经过向completeRequestReturningItems:expirationHandler:completion:
方法传值,来改变网页内容。
好比,你的iOS应用扩展须要基于一个网页URI启动,而且当它结束运行时改变网页的背景色,那么你须要这样写JavaScript代码:
清单4-1示例run()和finalize()函数
var MyExtensionJavaScriptClass = function() {};
MyExtensionJavaScriptClass.prototype = {
run: function(arguments) {
// Pass the baseURI of the webpage to the extension.
arguments.completionFunction({"baseURI": document.baseURI});
},
// Note that the finalize function is only available in iOS.
finalize: function(arguments) {
// arguments contains the value the extension provides in [NSExtensionContext completeRequestReturningItems:completion:].
// In this example, the extension provides a color as a returning item.
document.body.style.backgroundColor = arguments["bgColor"];
}
};
// The JavaScript file must contain a global object named "ExtensionPreprocessingJS".
var ExtensionPreprocessingJS = new MyExtensionJavaScriptClass;
复制代码
在iOS和OS X平台中,你须要编写代码来处理run()
函数返回的值,为获取到字典中的值,咱们须要指定kUTTypePropertyList
类型做为标示符传入NSItemProvider
类的 loadItemForTypeIdentifier:options:completionHandler:方法。在该字典中使用 NSExtensionJavaScriptPreprocessingResultsKey
做为key来取值。好比下面例子中咱们想要获取将 URI 传入 run()
的返回值:
[imageProvider loadItemForTypeIdentifier:kUTTypePropertyList options:nil completionHandler:^(NSDictionary *item, NSError *error) {
NSDictionary *results = (NSDictionary *)item;
NSString *baseURI = [[results objectForKey:NSExtensionJavaScriptPreprocessingResultsKey] objectForKey:@"baseURI"];
}];
复制代码
finalize()
函数是在当应用扩展执行完任务后传参并调用的,建立一个含有咱们须要处理的值的字典,而后用NSItemProvider
的 initWithItem:typeIdentifier:
方法来封装该字典。好比当扩展执行完任务后咱们想让网页变为红色,咱们能够这样写:
NSExtensionItem *extensionItem = [[NSExtensionItem alloc] init];
extensionItem.attachments = @[[[NSItemProvider alloc] initWithItem: @{NSExtensionJavaScriptFinalizeArgumentKey: @{@"bgColor":@"red"}} typeIdentifier:(NSString *)kUTTypePropertyList]];
[[self extensionContext] completeRequestReturningItems:@[extensionItem] completion:nil];
复制代码
用户通常的操做习惯都倾向于当使用你的应用扩展完成某个任务后,能够将结果当即反馈在使用扩展的应用中。若是一个扩展要处理的任务包含较长时间的上传下载操做时,你要确保当你的应用扩展关闭后能继续完成该任务。为实现这个功能,咱们须要使用NSURLSession类建立一个URL会话并建立后台的上传下载任务。
提示:你能够回想一下其余类型的后台任务,好比后台支持VoIP、后台播放音乐,这些是不能用应用扩展去实现的。更多信息请参阅Respond to the Host App’s Request。
当你的应用扩展准备好上传下载任务后,扩展会完成调用它的应用发出的请求,并在不影响上传下载任务的前提下终止扩展。更多关于扩展处理载体应用请求的知识请参阅Respond to the Host App’s Request。在iOS系统中,若是你的应用扩展在执行完后台任务时并无在运行,那么系统会自动在后台运行扩展的载体应用,并调用application:handleEventsForBackgroundURLSession:completionHandler: 代理方法。
重要提示:若是你的应用扩展在后台建立了 NSURLSession 任务,那么你必需要设置一个共享容器,以确保扩展和载体应用实现数据共享。咱们能够在 NSURLSessionConfiguration 类中使用sharedContainerIdentifier属性来指定一个共享容器的标示符,而后咱们就能够经过该标示符获取到共享容器。请参阅 Sharing Data with Your Containing App 文档来设置共享容器。
下面的例子展现了如何配置一个URL会话,并建立一个下载任务:
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”];
// To access the shared container you set up, use the sharedContainerIdentifier property on your configuration object.
config.sharedContainerIdentifier = @“com.mycompany.myappgroupidentifier”;
mySession = [NSURLSession sessionWithConfiguration:config delegate:self delegateQueue:nil];
}
return mySession;
}
复制代码
由于在单位时间内只能由一个进程使用后台会话,因此你须要为载体应用中的全部扩展建立不一样的后台会话(每一个后台会话都要有一个惟一的标示符)。在这里咱们建议当载体应用在后台处理扩展的任务时,只使用一个该扩展建立的后台会话。若是你要执行其余的网络相关的任务,那么就要建立相应的URL会话。
若是你须要在后台建立URL会话以前完成载体应用的请求,那么要确保建立和使用会话的代码是有效可执行的。当你的扩展调用 completeRequestReturningItems:completionHandler: 方法告知主叫应用已经完成相关请求后,系统就能够随时终止你的应用扩展。
在你的分享或Action扩展中,在它们的工做中可能会使用到一些数据,而且这些数据的类型各不相同。为了确保只有当用户在载体应用中选择了你的扩展支持的数据类型时,才会展现你的扩展功能。你须要在扩展的Info.plist
属性列表文件中添加 NSExtensionActivationRule
关键字。你也可使用该关键字指定扩展处理每种类型的最大数目。当你的应用扩展运行时,系统会将NSExtensionActivationRule
键的值与扩展项的attachments
属性中的信息进行比较。关于 NSExtensionActivationRule
关键字的详细信息能够参阅 Action Extension Keys文档中的 Information Property List Key Reference 章节。
好比,你能够申明你的分享扩展支持最多处理10张图片,一部影片和一个网站URL。您可使用如下字典做为该NSExtensionAttributes
键的值:
<key>NSExtensionAttributes</key>
<dict>
<key>NSExtensionActivationRule</key>
<dict>
<key>NSExtensionActivationSupportsImageWithMaxCount</key>
<integer>10</integer>
<key>NSExtensionActivationSupportsMovieWithMaxCount</key>
<integer>1</integer>
<key>NSExtensionActivationSupportsWebURLWithMaxCount</key>
<integer>1</integer>
</dict>
</dict>
复制代码
若是你想指定不支持的数据类型,那么你能够将该类型的值设置为0,或者在 NSExtensionActivationRule 中不添加该类型便可。
注意:若是你的分享扩展或iOS中的Action扩展须要访问网页,那你必需要确保 NSExtensionActivationSupportsWebURLWithMaxCount 关键字的值不为0(更多关于在应用扩展中经过JavaScript访问网页的内容请参阅Accessing a Webpage
你也可使用 NSExtensionItem 定义的 UTI子 类型以便数据检测器检测文本信息,好比电话号码或通信地址。
NSExtensionActivationRule
字典中的键足以知足大多数应用的过滤需求。若是你须要作更复杂的过滤,好比像 public.url
和 public.image
之间的区别,那么你就得在文本中建立断言语句。若是你要建立一个断言,那么就将NSExtensionActivationRule
关键字的值设置为你指定的断言字符串。(在运行时,系统会自动将该字符串编译为 NSPredicate 对象
好比,一个应用扩展的附件属性能够指定为PDF文件,能够这样写:
{extensionItems = ({
attachments = ({
registeredTypeIdentifiers = (
"com.adobe.pdf",
"public.file-url"
);
});
})}
复制代码
为了指定你的应用扩展能够处理PDF文件,你能够像这样建立断言字符串:
SUBQUERY (
extensionItems,
$extensionItem,
SUBQUERY (
$extensionItem.attachments,
$attachment,
ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "com.adobe.pdf"
).@count == $extensionItem.attachments.@count
).@count == 1
复制代码
如下是更复杂的断言语句的示例:
SUBQUERY (
extensionItems,
$extensionItem,
SUBQUERY (
$extensionItem.attachments,
$attachment,
ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "org.appextension.action-one" ||
ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "org.appextension.action-two"
).@count == $extensionItem.attachments.@count
).@count == 1
复制代码
此语句遍历一个NSExtensionItem
对象数组,其次是遍历attachments
每一个扩展项中的数组。对于每一个附件,谓词评估附件中每一个表示的统一类型标识符(UTI)。当附件表示UTI符合两个不一样的指定UTI中的任何一个(您在每一个UTI-CONFORMS-TO
操做员的右侧看到)时,收集该UTI以进行最终比较测试。TRUE
若是应用程序扩展名仅提供了一个支持UTI的扩展项附件,则返回最后一行。
开发过程当中,在你建立断言语句以前你可使用TRUEPREDICATE
常量(结果为true)测试你的代码路径。更多断言语句的语法知识请参阅Predicate Format String Syntax。
重要提示:在将你的载体应用上传App Store以前,要确保全部的 TRUEPREDICATE 常量已经替换为指定的断言语句或 NSExtensionActivationRule 关键字,否则载体应用会被App Store拒绝。
若是你在主体应用中使用了内嵌框架,那么它就能够在iOS8.0以后的版本中使用,即使内嵌框架不支持老版本的系统也不要紧。
使主体应用能作到上述这一点的是 dlopen
命令,它可使你使用条件连接和加载框架包的机制。你可使用这个命令来代替编译时连接,你能够在 Xcode 的 General 选项或 Build Phases 选项中对该命令进行编辑。其原理就是只有当主体应用在 iOS8.0 或更高的版本中运行时,才会连接使用内嵌框架。
您必须在有条件地 framework bundle的代码语句中使用Objective-C而不是Swift。您的应用程序的其他部分能够用任何一种语言编写,内嵌框架自己也能够用任何一种语言编写。
调用以后dlopen
,使用如下类型的语句访问内嵌框架类:
MyLoadedClass *loadedClass = [[NSClassFromString (@"MyClass") alloc] init];
复制代码
重要提示:若是你的主体应用使用了内嵌框架,那么就必需要支持arm64架构,不然会被App Store拒绝。
设置Xcode项目中应用扩展的条件连接
1.将每个应用扩展的运行系统版本设置为iOS8.0或更高,一般选中Xcode中的target,在General选项中设置Deployment info。 2.将你主体应用的运行系统版本设置为你想支持的最低iOS版本。 3.在你的主体应用中,经过 systemVersion 方法,在运行时检查判断iOS的版本,并判断是否执行dlopen命令。只有你的载体应用在iOS8.0或更高的版本中运行时才会指定dlopen命令。进行此调用时,请务必使用Objective-C,而不是Swift。
特定的iOS API经过dlopen命令使用内嵌框架。你必须选择性的使用这些API,就像使用 dlopen 命令时那样。这些API都是 CFBundleRef 的封装类型:
CFBundleGetFunctionPointerForName
CFBundleGetFunctionPointersforNames
还有来自NSBundle
类的方法:
loadloadAndReturnError: classNamed:
由于你通常会将载体应用的运行系统版本配置为较低的版本,因此这些API一般都是在运行时检查,只有确保载体应用在iOS8.0或更高版本中运行时才会使用这些API。