当您编写执行应用扩展程序任务的自定义代码时,您可能需要处理许多类型扩展所共有的一些方案。使用本章中的代码和建议来帮助您实施解决方案。
使用嵌入式框架共享代码
您可以创建一个嵌入式框架,以在您的应用扩展程序及其包含应用之间共享代码。例如,如果您开发了一个图像过滤器以用于您的照片编辑扩展以及其包含的应用程序,请将过滤器的代码放在框架中并将框架嵌入到两个目标中。
确保您的嵌入式框架不包含应用程序扩展不可用的API,如某些API不适用于App Extensions中所述。如果您有一个包含此类API的自定义框架,您可以安全地从包含的应用程序链接到它,但不能与应用程序包含的扩展共享该代码。App Store拒绝链接到此类框架或以其他方式使用不可用API的任何应用程序扩展。
要配置应用程序扩展目标以使用嵌入式框架,请将目标的“仅限应用程序扩展安全API”构建设置设置为“是”。如果不这样做,Xcode会提醒您这样做,方法是显示警告“链接到dylib不安全,无法在应用程序扩展中使用”。
重要
链接到嵌入式框架的包含应用程序必须包含arm64(iOS)或x86_64(OS X)体系结构构建设置,否则App Store将拒绝该应用程序。(如创建应用程序扩展中所述,所有应用程序扩展必须包含相应的64位体系结构构建设置。)
配置Xcode项目时,必须在“复制文件”构建阶段选择“框架”作为嵌入式框架的目标。
重要
始终选择“框架”作为“复制文件”构建阶段目标。如果您改为选择“SharedFramework”目的地,App Store将拒绝您的提交。
您可以为运行iOS 7或更早版本的用户提供包含应用程序,但在iOS 8或更高版本中运行时必须采取预防措施以安全地链接嵌入式框架。有关详细信息,请阅读将包含应用程序部署到旧版iOS的内容。
有关创建和使用嵌入式框架的更多信息,请观看WWDC 2014视频“构建现代框架”,网址为https://developer.apple.com/videos/wwdc/2014。
与您的应用程序共享数据
即使应用程序扩展包嵌套在其包含的应用程序包中,运行的应用程序扩展和包含应用程序也无法直接访问彼此的容器。
背景
要了解容器,阅读关于iOS的文件系统中的文件系统编程指南。
但是,您可以启用数据共享。例如,您可能希望允许您的应用扩展程序及其包含的应用程序共享一组大量数据,例如预渲染资产。
要启用数据共享,请使用Xcode或Developer门户为包含应用及其包含的应用扩展启用应用组。接下来,在门户中注册应用程序组,并指定要在包含的应用程序中使用的应用程序组。要了解如何使用应用程序组,请参阅向应用程序组添加应用程序。
启用应用程序组后,应用程序扩展及其包含的应用程序都可以使用[NSUserDefaults](https://developer.apple.com/library/archive/documentation/LegacyTechnologies/WebObjects/WebObjects_3.5/Reference/Frameworks/ObjC/Foundation/Classes/NSUserDefaults/Description.html#//apple_ref/occ/cl/NSUserDefaults)
API共享对用户首选项的访问权限。要启用此共享,请使用该[initWithSuiteName:](https://developer.apple.com/documentation/foundation/userdefaults/1409957-init)
方法实例化新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"];
图4-1显示了扩展及其包含的应用程序如何使用共享容器来共享数据。
图4-1应用程序扩展的容器与其包含应用程序的容器不同[图片上传中...(image-b6cfaa-1559570465434-0)]
重要
如果您的应用扩展程序使用[NSURLSession](https://developer.apple.com/documentation/foundation/urlsession)
该类执行后台上载或下载,则必须设置共享容器,以便扩展程序及其包含的应用程序都可以访问传输的数据。要了解如何在后台执行上载或下载,请参阅执行上载和下载。
设置共享容器时,包含应用程序 - 以及允许参与数据共享的每个包含的应用程序扩展 - 都具有对共享容器的读写权限。要避免数据损坏,必须同步数据访问。
使用Core Data,SQLite或Posix锁来帮助协调共享容器中的数据访问。
版本说明
在iOS 8.2及更高版本中,您也可以使用[UIDocument](https://developer.apple.com/documentation/uikit/uidocument)
该类来协调共享数据访问。
在iOS 9及更高版本中,您可以[NSFileCoordinator](https://developer.apple.com/documentation/foundation/nsfilecoordinator)
直接使用该类进行共享数据访问,但是如果您这样做,则必须[NSFilePresenter](https://developer.apple.com/documentation/foundation/nsfilepresenter)
在应用扩展转换为后台时删除对象。
访问网页
在共享扩展(在两个平台上)和动作扩展(仅限iOS)中,您可以通过要求Safari运行JavaScript文件并将结果返回到扩展名来授予用户访问Web内容的权限。您还可以在扩展程序运行之前(在两个平台上)使用JavaScript文件访问网页,或者在扩展程序完成其任务后访问或修改网页(仅限iOS)。例如,共享扩展可以帮助用户共享来自网页的内容,或者iOS中的Action扩展可以显示用户当前网页的翻译。
要向您的应用扩展程序添加网页访问和操作,请执行以下步骤:
创建一个包含名为的全局对象的JavaScript文件
ExtensionPreprocessingJS
。将自定义JavaScript类的新实例分配给此对象。在
NSExtensionActivationRule
应用扩展程序Info.plist
文件的字典中,为NSExtensionActivationSupportsWebPageWithMaxCount
密钥指定非零值。(要了解有关激活规则字典的更多信息,请参阅为共享或操作扩展声明支持的数据类型。)当您的应用扩展程序启动时,使用
[NSItemProvider](https://developer.apple.com/documentation/foundation/nsitemprovider)
该类来获取执行JavaScript文件返回的结果。在iOS应用扩展程序中,如果您希望Safari在您的扩展程序完成其任务时修改网页,请将值传递给JavaScript文件。(您也可以
NSItemProvider
在此步骤中使用该类。)
要告诉Safari您的应用扩展程序包含JavaScript文件,请将该NSExtensionJavaScriptPreprocessingFile
键添加到NSExtensionAttributes
字典中。密钥的值应该是您希望Safari在扩展开始之前加载的文件。例如:
<key>NSExtensionAttributes</key>
<dict>
<key>NSExtensionJavaScriptPreprocessingFile</key>
<string>MyJavaScriptFile</string>
</dict>
在这两个平台上,您的自定义JavaScript类可以定义run()
Safari在加载JavaScript文件后立即调用的函数。在该run()
函数中,Safari提供了一个名为的参数completionFunction
,您可以使用该参数以键值对象的形式将结果传递给您的应用扩展。
在iOS中,您还可以定义finalize()
Safari completeRequestReturningItems:completion:
在其任务结束时调用时调用的函数。一个finalize()
函数可以使用的物品的延长线在completeRequestReturningItems:completion:
根据需要改变的网页。
例如,如果您的iOS应用扩展程序在启动时需要网页的基本URI,并且在停止时更改网页的背景颜色,您可以编写如清单4-1所示的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;
在这两个平台上,您需要编写代码来处理从run()
函数传回的值。要获取结果字典,请kUTTypePropertyList
在NSItemProvider
方法中指定类型标识符[loadItemForTypeIdentifier:options:completionHandler:](https://developer.apple.com/documentation/foundation/nsitemprovider/1403900-loaditemfortypeidentifier)
。在字典中,使用NSExtensionJavaScriptPreprocessingResultsKey
键来获取结果项。例如,要获取清单4-1中的run()
函数传递的基URI ,可以使用如下代码:
[imageProvider loadItemForTypeIdentifier:kUTTypePropertyList options:nil completionHandler:^(NSDictionary *item, NSError *error) {
NSDictionary *results = (NSDictionary *)item;
NSString *baseURI = [[results objectForKey:NSExtensionJavaScriptPreprocessingResultsKey] objectForKey:@"baseURI"];
}];
要finalize()
在iOS应用扩展程序完成其任务时将值传递给函数,请使用该NSItemProvider``initWithItem:typeIdentifier:
方法将字符串中的值打包为[NSExtensionJavaScriptFinalizeArgumentKey](https://developer.apple.com/documentation/foundation/nsextensionjavascriptfinalizeargumentkey)
密钥。例如,要为清单4-1中的finalize()
函数中使用的背景颜色指定红色,您的扩展可能会使用如下代码:
NSExtensionItem *extensionItem = [[NSExtensionItem alloc] init];
extensionItem.attachments = @[[[NSItemProvider alloc] initWithItem: @{NSExtensionJavaScriptFinalizeArgumentKey: @{@"bgColor":@"red"}} typeIdentifier:(NSString *)kUTTypePropertyList]];
[[self extensionContext] completeRequestReturningItems:@[extensionItem] completion:nil];
执行上传和下载
用户在应用扩展程序中完成任务后,会立即返回主机应用。如果任务涉及可能冗长的上载或下载,则需要确保在您的扩展终止后它可以完成。要执行上载或下载,请使用[NSURLSession](https://developer.apple.com/documentation/foundation/urlsession)
该类创建URL会话并启动后台上载或下载任务。
注意
回想一下,app扩展无法使用其他类型的后台任务,例如支持VoIP或播放后台音频。有关更多信息,请参阅响应主机应用程序的请求。
在您的应用扩展程序启动上载或下载任务后,扩展程序可以完成主机应用程序的请求并终止,而不会影响任务的结果。要了解有关扩展如何处理来自主机应用程序的请求的详细信息,请参阅响应主机应用程序的请求。在iOS中,如果后台任务完成后您的扩展程序未运行,系统将在后台启动您的包含应用程序并调用[application:handleEventsForBackgroundURLSession:completionHandler:](https://developer.apple.com/documentation/uikit/uiapplicationdelegate/1622941-application)
应用程序委托方法。
重要
如果您的应用扩展程序启动后台[NSURLSession](https://developer.apple.com/documentation/foundation/urlsession)
任务,您还必须设置扩展程序及其包含应用程序都可以访问的共享容器。使用类的[sharedContainerIdentifier](https://developer.apple.com/documentation/foundation/nsurlsessionconfiguration/1409450-sharedcontaineridentifier)
属性NSURLSessionConfiguration
指定共享容器的标识符,以便以后可以访问它。
有关设置共享容器的指导, 请参阅与您的应用程序共享数据。
清单4-2显示了配置URL会话并使用它来启动下载的一种方法。
清单4-2配置NSURLSession
对象并开始下载的示例
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:](https://developer.apple.com/documentation/foundation/nsextensioncontext/1411301-completerequest)
以告知主机应用程序其请求已完成后,系统可以随时终止您的扩展程序。
声明共享或操作扩展的支持数据类型
在您的“共享”或“操作”扩展程序中,您可能可以使用某些类型的数据,但不能处理其他类型的数据。要确保主机应用仅在用户选择了您支持的类型的数据时才提供您的扩展,请将该NSExtensionActivationRule
密钥添加到您的扩展的Info.plist
属性列表文件中。您还可以使用此键指定扩展程序可以处理的每种类型的最大项目数。
当您的分机运行时,系统会将NSExtensionActivationRule
密钥的值与分机项[attachments](https://developer.apple.com/documentation/foundation/nsextensionitem/1416690-attachments)
属性中的信息进行比较。有关可以使用此键的完整键列表,请参阅操作扩展键。
例如,要声明您的共享扩展程序最多可以支持十个图像,一个电影和一个网页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操作扩展程序需要访问网页,则必须包含NSExtensionActivationSupportsWebPageWithMaxCount
非零值的密钥。(要了解如何使用JavaScript从您的扩展程序访问网页,请参阅访问网页。)
NSExtensionActivationRule
字典中 的键足以满足典型应用扩展的过滤需求。如果您需要执行更复杂或更具体的过滤,例如区分public.url
和public.image
,则可以创建谓词语句。然后,使用表示谓词的裸字符串作为NSExtensionActivationRule
键的值。(在运行时,系统将此字符串编译为[NSPredicate](https://developer.apple.com/documentation/foundation/nspredicate)
对象。)
例如,应用扩展程序项的attachments
属性可以指定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](https://developer.apple.com/documentation/foundation/nsextensionitem)
对象数组,其次[attachments](https://developer.apple.com/documentation/foundation/nsextensionitem/1416690-attachments)
是每个扩展项中的数组。对于每个附件,谓词评估附件中每个表示的统一类型标识符(UTI)。当附件表示UTI符合两个不同的指定UTI中的任何一个(您在每个UTI-CONFORMS-TO
操作员的右侧看到)时,收集该UTI以进行最终比较测试。TRUE
如果应用程序扩展名仅提供了一个支持UTI的扩展项附件,则返回最后一行。
仅在开发期间,您可以使用TRUEPREDICATE
常量(始终求值为true
)作为存根谓词语句,以在实现谓词语句之前测试代码路径。
重要
在将包含应用程序提交到App Store之前,请确保使用TRUEPREDICATE
功能谓词语句或NSExtensionActivationRule
键替换存根谓词的所有使用。如果您的包含应用中的任何应用扩展程序包含该字符串TRUEPREDICATE
,则该应用将被拒绝。
要了解更多关于断言语句的语法,请参阅谓词格式化字符串的语法在谓词编程指南。
将包含的应用程序部署到iOS的旧版本
如果从包含的应用程序链接到嵌入式框架,您仍然可以将其部署到8.0之前的iOS版本,即使这些版本中没有嵌入式框架。
允许您执行此操作的机制是dlopen命令,您可以使用该命令有条件地链接和加载框架包。您可以使用此命令替代您可以在Xcode General或Build Phases目标编辑器中指定的构建时链接。主要思想是仅在iOS 8.0或更高版本中运行时将嵌入式框架链接到包含应用程序。
您必须在有条件地加载框架包的代码语句中使用Objective-C而不是Swift。您的应用程序的其余部分可以用任何一种语言编写,嵌入式框架本身也可以用任何一种语言编写。
调用之后dlopen
,使用以下类型的语句访问嵌入式框架类:
MyLoadedClass *loadedClass = [[NSClassFromString (@"MyClass") alloc] init];
重要
如果您的包含应用目标链接到嵌入式框架,它必须包含arm64架构,否则它将被App Store拒绝。
设置应用扩展Xcode项目以利用条件链接
-
对于每个包含的应用扩展程序,像往常一样将部署目标设置为iOS 8.0或更高版本。
在Xcode目标编辑器的“常规”选项卡的“部署信息”部分中执行此操作。
对于包含应用程序,请将部署目标设置为您要支持的最旧版本的iOS。
-
在您的包含应用程序中,
dlopen
使用该[systemVersion](https://developer.apple.com/documentation/uikit/uidevice/1620043-systemversion)
方法在运行时检查iOS版本的条件下调用该命令。dlopen
仅当您的包含应用程序在iOS 8.0或更高版本中运行时才 调用该命令。进行此调用时,请务必使用Objective-C,而不是Swift。
某些iOS API通过dlopen
命令使用嵌入式框架。您必须像dlopen
直接调用一样使用这些API 。这些API来自[CFBundleRef](https://developer.apple.com/documentation/corefoundation/cfbundle)
opaque类型:
[CFBundleGetFunctionPointerForName](https://developer.apple.com/documentation/corefoundation/1537143-cfbundlegetfunctionpointerfornam)
[CFBundleGetFunctionPointersforNames](https://developer.apple.com/documentation/corefoundation/1537109-cfbundlegetfunctionpointersforna)
从[NSBundle](https://developer.apple.com/library/archive/documentation/LegacyTechnologies/WebObjects/WebObjects_3.5/Reference/Frameworks/ObjC/Foundation/Classes/NSBundle/Description.html#//apple_ref/occ/cl/NSBundle)
课堂上讲:
[load](https://developer.apple.com/library/archive/documentation/LegacyTechnologies/WebObjects/WebObjects_3.5/Reference/Frameworks/ObjC/Foundation/Classes/NSBundle/Description.html#//apple_ref/occ/instm/NSBundle/load)
[loadAndReturnError:](https://developer.apple.com/documentation/foundation/nsbundle/1411819-loadandreturnerror)
[classNamed:](https://developer.apple.com/library/archive/documentation/LegacyTechnologies/WebObjects/WebObjects_3.5/Reference/Frameworks/ObjC/Foundation/Classes/NSBundle/Description.html#//apple_ref/occ/instm/NSBundle/classNamed:)
在您正在部署到8.0之前的iOS版本的应用程序中,仅在运行时检查中调用这些API,以确保您在iOS 8.0或更高版本中运行,并使用Objective-C调用这些API。