常见问题的处理方案 -- iOS8/OS X v10.10应用扩展编程指南

当你在开发一个应用扩展,让其执行某个任务时,你可能会遇到一些常见的,或者说在多种扩展中都会出现的通用的问题,并且必须要处理这些问题。在这一章节中,我们将帮助你如何应对和处理这些常见的问题。

使用内嵌框架共享代码

你可以创建一个内嵌框架,用于在应用扩展和它的载体应用之间共享代码。比如,你开发了一个图片滤镜的照片编辑扩展,同时该扩展的载体应用也有这个功能,那么你就可以将实现该功能的代码封装成一个框架,让扩展target和载体应用target都使用这个框架。

你要确保你创建的内嵌框架不包含应用扩展不能使用的API。这类API一般使用unavailability宏来标记,比如像NS_EXTENSION_UNAVAILABLE

如果你创建的内嵌框架中包含应用扩展不能使用的API,那么你将其Link到载体应用没关系,载体应用可以正常使用框架中的API,但是不能与应用扩展共享代码,也就是应用扩展不能使用该框架提供的所有API,继而无法做到代码共享。如果你上传App Store的应用扩展中有这种框架,或者应用扩展中使用了不允许使用的API,那么审核时会被拒绝。

如果我们要想应用扩展使用内嵌框架,那么首先要配置一下。将targetRequire Only App-Extension-Safe API选项设置为Yes。如果你不这样设置,那么Xcode会向你提示警告:linking against dylib not safe for use in application extensions

重要提示:如果载体应用要使用内嵌框架,那么必须要支持arm64架构,否则在上传App Store时会被拒绝。(如“创建应用扩展”章节中介绍的,所有应用扩展都要支持arm64架构。)

在配置你的Xcode项目时,在Build Phases选项卡的Copy Files项中一定要将Destination设置为Frameworks

重要提示:我们要始终将Destination设置为Frameworks,如果你将其设置为SharedFramework,那么上传App Store时会被拒绝的。

你可以让载体应用支持iOS7或更早的版本,但当在iOS8或更新的版本中运行时,要特别注意内嵌框架的安全性。详细内容可以参阅Deploying a Containing App to Older Versions of iOS

有关创建和使用内嵌框架的更多内容,请观看WWDC 2014的视频“Building Modern Frameworks”。

与载体应用共享数据

虽然应用扩展的包内嵌在载体应用中,但是应用扩展和载体应用的安全域还是不一样的。默认情况下,应用扩展和载体应用是不能直接访问对方内容的。

不过你可以通过数据共享来实现这个愿望。比如,你希望应用扩展和它的载体应用共享一个单一的大数据集。比如prerendered assets

要实现数据共享,我们要使用Xcode或者开发者门户网站允许应用扩展和它的载体应用成为一个应用组,然后在开发者门户网站中注册应用组,并指明在载体应用中使用该应用组。关于应用组的知识请查阅Entitlement Key Reference文档的Adding an App to an App Group章节。

当你设置好应用组后,应用扩展和它的载体应用就可以通过NSUserDefaultsAPI共享访问用户的信息。我们可以使用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"];

下图向我们展示了应用扩展和它的载体应用是如何通过共享容器实现数据共享的:

结构

重要提示:如果你的应用扩展使用NSURLSession类执行后台的上传下载任务时,你必须要设置一个共享容器。这样扩展和载体应用就可以访问到转换传输的数据。后台上传下载的更多知识请参阅 Performing Uploads and Downloads

如果你设置了共享容器,那么载体应用和它包含的扩展就可以对共享容器里的内容进行读写操作了。同时你还必须要对数据的操作进行同步,以避免数据损坏或出错。使用UIDocument类、Core Data或者SQLite可以帮助定位、协调操作共享容器中的内容。

访问网页

在分享扩展(iOS与OS X平台)和Action扩展(iOS平台)中,一般都允许用户使用Safari浏览器执访问网页并通过执行JavaScript脚本将结果返回到扩展中。你也可以在你的扩展运行之前或执行完任务之后通过JavaScript文件修改网页内容。比如分享扩展,它可以帮助用户将网页上的内容分享到用户的社交网站上,或者iOS上的Action扩展可能会显示当前网页的指定翻译内容。

如果要通过应用扩展访问网页并操作网页内容,那么需要遵循下面几个步骤:

  • 创建一个JavaScript文件,并申明一个全局对象,名为ExtensionPreprocessingJS并将其实例化。
  • 在应用扩展的属性列表文件中添加关键字NSExtensionJavaScriptPreprocessingFile,给Safari浏览器指明使用哪个JavaScript文件。
  • NSExtensionActivationRule字典中,将NSExtensionActivationSupportsWebURLWithMaxCount赋值一个非零的值。(更多关于NSExtensionActivationRule字典的知识请参阅Declaring Supported Data Types for a Share or Action Extension。)
  • 当你的应用扩展开始运行时,使用NSItemProvider类从JavaScript文件的返回值中获取结果。
  • 在iOS系统的应用扩展中,如果你希望当应用扩展执行完任务后实时更新网页内容,那么你要向JavaScript文件中传入内容。(在这一步中也使用NSItemProvider类。)

为了告知Safari你的应用扩展中包含一个JavaScript文件,你需要在应用扩展的Info.plist文件中,向NSExtensionAttributes字典添加NSExtensionJavaScriptPreprocessingFile关键字来指明你的JavaScript文件。这个关键字的值就是你希望当你的应用扩展运行前,Safari要加载的JavaScript文件的名称。比如:

<key>NSExtensionAttributes</key>
    <dict>
        <key>NSExtensionJavaScriptPreprocessingFile</key>
        <string>MyJavaScriptFile</string>
    </dict>

在iOS和OS X平台中,在你自定义的JavaScript类中可以定义一个run()函数,该函数就是Safari加载JavaScript文件的入口。在run()函数中,Safari提供了一个名为completionFunction的参数,你可以使用键值对象的形式将结果传给应用扩展。

在iOS平台中,你还可以定义一个finalize()函数,当应用扩展在任务结束阶段调用completeRequestReturningItems:expirationHandler:completion:方法时Safari会调用finalize()函数。在该函数中,可以通过向completeRequestReturningItems:expirationHandler:completion:方法传值,来改变网页内容。

比如,你的iOS应用扩展需要基于一个网页URI启动,并且当它结束运行时改变网页的背景色,那么你需要这样写JavaScript代码:


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:expirationHandler: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平台中,你需要编写代码来处理fun()函数返回的值,为获取到字典中的值,我们需要指定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()函数是在当应用扩展执行完任务后传参并调用的,创建一个含有我们需要处理的值的字典,然后用NSItemProviderinitWithItem:typeIdentifier:方法来封装该字典。比如当扩展执行完任务后我们想让网页变为红色,我们可以这样写:


NSExtensionItem *extensionItem = [[NSExtensionItem alloc] init];
extensionItem.attachments = @[[[NSItemProvider alloc] initWithItem: @{NSExtensionJavaScriptFinalizeArgumentKey: @{@"bgColor":@"red"}} typeIdentifier:(NSString *)kUTTypePropertyList]];
[[self extensionContext] completeRequestReturningItems:@[extensionItem] expirationHandler:nil 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扩展申明支持的数据类型

在你的分享或Action扩展中,在它们的工作中可能会使用到一些数据,并且这些数据的类型各不相同。为了确保只有当用户在主叫应用中选择了你的扩展支持的数据类型时,才会展示你的扩展功能。你需要在扩展的属性列表文件中添加NSExtensionActivationRule关键字。你也可以使用该关键字指定扩展处理每种类型的最大数目。当你的应用扩展运行时,系统会用改关键字的值与扩展数据项的attachments属性值作比较。关于NSExtensionActivationRule关键字的详细信息可以参阅Action Extension Keys文档中的Information Property List Key Reference章节。

比如,你可以申明你的分享扩展支持最大处理10张图片,一部影片和一个网站URL。你可以参考下面的写法:

<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.urlpublic.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 == 1).@count == 1

开发过程中,在你创建断言语句之前你可以使用TRUEPREDICATE常量(结果为true)测试你的代码路径。更多断言语句的语法知识请参阅Predicate Format String Syntax

重要提示:在将你的载体应用上传App Store之前,要确保所有的TRUEPREDICATE常量已经替换为指定的断言语句或NSExtensionActivationRule关键字,不然载体应用会被App Store拒绝。

配置载体应用以适用于老版本的iOS系统

如果你在载体应用中使用了内嵌框架,那么它就可以在iOS8.0之后的版本中使用,即便内嵌框架不支持老版本的系统也没关系。

使载体应用能做到上述这一点的是dlopen命令,它可以使你使用条件链接和加载框架包的机制。你可以使用这个命令来代替编译时链接,你可以在Xcode的General 选项或Build Phases选项中对该命令进行编辑。其原理就是只有当载体应用在iOS8.0或更高的版本中运行时,才会链接使用内嵌框架。

重要提示:如果你的载体应用使用了内嵌框架,那么就必须要支持arm64架构,否则会被App Store拒绝。

设置Xcode项目中应用扩展的条件链接

  1. 将每一个应用扩展的运行系统版本设置为iOS8.0或更高,通常选中Xcode中的target,在General选项中设置Deployment info
  2. 将你载体应用的运行系统版本设置为你想支持的最低iOS版本。
  3. 在你的载体应用中,通过systemVersion方法,在运行时检查判断iOS的版本,并判断是否执行dlopen命令。只有你的载体应用在iOS8.0或更高的版本中运行时才会指定dlopen命令。

特定的iOS API通过dlopen命令使用内嵌框架。你必须选择性的使用这些API,就像使用dlopen命令时那样。这些API都是CFBundleRef的封装类型:

还有来自NSBundle类的方法:

因为你一般会将载体应用的运行系统版本配置为较低的版本,所以这些API通常都是在运行时检查,只有确保载体应用在iOS8.0或更高版本中运行时才会使用这些API。

原文地址:Handling Common Scenarios

分享到: