[译]AppExtension编程指南:扩展基础4

961 阅读15分钟

App Extension Programming Guide-App Extension Essentials AppExtension编程指南:扩展基础4

Handling Common Scenarios 常见问题的处理方案

iOS8/OS X v10.10

常见问题的处理方案

当编写自定义代码以执行app扩展任务时,你可能需要处理一些其他多种类型扩展也会出现的情况。在这一章节中,我们将帮助你如何应对和处理这些常见的问题。

使用内嵌框架共享代码

你可以创建一个内嵌框架,用于在应用扩展和它的主应用程序(containing app)之间共享代码。比如,你在照片编辑扩展中开发了图片滤镜功能,那么同时该扩展的主应用程序containing app也有这个功能,那么你可以将实现该功能的代码封装成一个框架,并在扩展target和主应用程序target中嵌入这个框架。

你要确保你创建的内嵌框架不包含应用扩展不能使用的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架构。)

在配置配置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包中的。默认情况下,应用扩展和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的容器是不同的。

image

重要提示:如果你的应用扩展使用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() 函数是在当应用扩展执行完任务后传参并调用的,创建一个含有我们需要处理的值的字典,然后用NSItemProviderinitWithItem: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扩展申明支持的数据类型

在你的分享或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.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 == $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拒绝。

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

如果你在主体应用中使用了内嵌框架,那么它就可以在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。