AppExtension编程指南:扩展基础

通过应用扩展提升你的应用

iOS8/OS X v10.10

翻译自苹果官方文档 App Extension Programming Guide–App Extensions Essentials

重要提示: 该文档是一个开发过程中使用API或技术的预发布文档。Apple提供该文档的目的在于让开发者尽快熟悉新技术和新的编程接口,使得开发者针对苹果品牌设备进行开发。但是该文档会不断的更新,使用该文档提供的技术开发的软件应该基于最终操作系统软件和最终的文档进行测试。要时刻关注该文档的更新,以便了解新的API和技术特性。

当iOS 8.0和OS X v10.10发布后,一个全新的概念出现在我们眼前,那就是应用扩展。顾名思义,应用扩展允许开发者扩展应用的自定义功能和内容,能够让用户在使用其他app时使用该项功能。你可以开发一个应用扩展来执行某些特定的任务,用户使用该扩展后就可以在多个上下文环境中执行该任务。比如说,你提供了一个能让用户把内容分享到社交网站的扩展,用户可以在阅读邮件信息或者上网时通过app扩展来发表评论。或者说,如果你提供了一个能展示当前比赛分数的扩展,那么用户可以把它放在通知中心,这样在他们打开Today view时就能看到最新的比分。你甚至可以开发一个提供自定义键盘的扩展,用户可以用它代替iOS系统自带的键盘。

APP扩展的类型

iOS和OS X定义了几种应用扩展的类型,每一种类型的扩展都对应系统中的一块区域,比如分享、通知中心以及键盘等。我们把这些支持扩展的区域称为扩展点。每个扩展点都定义了使用策略并提供了API,你可以用来针对该区域创建扩展。要注意的一点是,当你针某个扩展点开发应用扩展时,该应用扩展的功能必须要符合该扩展点的功能特性。

table 1-1 列出了iOS和OS X中的扩展点,并列举了对应扩展点的示例任务。

由于系统定义了扩展特定领域,所以当你在开发应用扩展时,很重要的一点是,你要选择最能匹配你想在app中提供的功能的区域。比如说你要开发一个分享信息的应用扩展,那么你只能选择分享扩展点,而不能使用Today或Action扩展点。

重要提示:你开发的app扩展要精确地匹配Table 1-1中列出的扩展点。你不能开发一个通用的扩展来匹配一个以上的扩展点,

Xcode和App Store帮助你创建并交付应用扩展

应用扩展不同于应用,虽然你需要基于应用去开发应用扩展,但是每个应用扩展都是独立于应用运行的二进制文件。

当你要创建一个应用扩展时,需要在项目中创建一个新的Target。和其他 Target 一样,扩展 Target 将设置信息和相关文件打包在Products文件下生成一个扩展名为.appex的包。你可以在应用项目中创建多个扩展Target(一个应用程序可以包含一个或多个应用扩展,该应用程序称为主体应用程序)。

开发一个应用扩展最好的切入点是使用 Xcode 在两个平台上为每个扩展点提供的模板去开发。每个模板包含扩展点的具体实现文件和相关设置,并生成独立的二进制文件添加到应用程序的包中。

注意:在iOS中,包含扩展的应用必须提供一个扩展之外的功能。而在OS X中没有这个硬性要求,一个包含扩展的应用不要求必须提供一个额外功能。

为了将扩展分发给使用者,你需要提交一个包含扩展的应用到App Store。用户只要下载并安装了你的应用,那么同时也自动安装了你的应用扩展。

当安装了应用扩展之后,用户首先必须要开启他们。通常,用户可以在他们当前任务的上下文环境中启动扩展。比如,如果你的扩展启用了“Today”通知,那用户可以在通知中心编辑“Today ”view 来添加你的扩展。在其他情况中,用户可以使用 iOS 中的“Settings”或者 OS X 中的“System Preferences”来启用和管理扩展。

用户在不同的情形下体验不同的应用扩展

虽然每种类型的应用扩展的功能都是不同的,但对大部分扩展来说,它们在用户体验上还是有一些共同点的。如果你准备开发一个应用扩展,有一点很重要,那就是要理解在你选择的扩展点中,用户体验是什么样的。从一个更高角度看,对于所有扩展来说,最佳的用户体验是快速、流畅以及只关注单一任务。

通常用户通过与系统提供的用户界面进行交互来开启应用扩展。比如说,用户在app中通过激活系统提供的分享按钮来访问 Share 扩展,并从展示的列表中选择扩展。一个应用扩展必须要提供一个图标,以便用户选择和识别,通常情况下,应用扩展的图标与应用程序的图标是相同的。

虽然大多数的应用程序扩展都提供了一些自定义的UI元素,但一般用户不会看到你的自定义用户界面,除非他们进入到应用程序扩展中。当用户进入应用扩展,你的自定义UI可以让用户知晓他们正进入一个新的上下文环境。由于用户可以把你的扩展和当前应用区别开来,所以他们会欣赏你提供的独特功能。当用户意识到扩展其实是独立运行的实体时,他们也可以确认并移除体验不好或功能不好的扩展。

为了让用户平滑过渡到你的应用程序扩展,你要斟酌自定义界面与扩展点界面的风格,做一个权衡。比如说,一个很好的方法就是让你的插件看起来像是通知中心中原生的Widget,再比如说照片编辑扩展,你应该创建一个和 iOS 中 Photos 应用风格相协调的用户界面。

注意:即使你的应用程序扩展没有展示自定义UI(不包括图标),但用户仍然知道该扩展不同于当前的应用,因为它们需要采用特定的操作来激活。

了解应用程序扩展的工作原理

iOS8/OS X v10.10

应用程序扩展不是一个应用,相反,它是主体应用程序(containing app)中一个单独的包,并能生成单独的二进制文件。与主体应用程序不同,应用扩展实现的是一个特定的、狭义范围内的任务,并且要严格遵循扩展点的协议。

应用程序扩展的生命周期

因为一个应用程序扩展不是一个应用,所以它的生命周期和运行环境也不同于应用。一般情况来说,应用程序扩展的生命周期始于用户从某个应用中启动它。允许用户选择某一种扩展来帮助他们执行某项任务的应用,我们称之为扩展的载体应用(host app)载体应用(host app)定义了激活扩展的上下文环境,并在其发送请求以响应用户操作时开始扩展的生命周期。当应用扩展完成载体应用发送的请求任务后,该扩展的生命周期也随之结束。

比如说,假定这样一个场景,用户在扩展载体应用(host app)中选中一段文字,点击分享按钮,并从分享列表中选择某一个分享扩展,将选中的内容分享至社交网站。此时,载体应用就会向扩展发送一个包含用户所选文本的请求,启动该扩展,从而去响应用户的请求,即分享所选文本的内容。应用扩展基本的生命周期归纳在下图2-1中。

                            基本的扩展生命周期

在图2-1中的第2步中,系统实例化载体应用请求中确定的扩展,并在两者之间建立通信通道。然后扩展在载体应用的上下文场景中显示相关界面,并使用其在载体应用请求中接收到的项目来执行任务(在上述的例子中,扩展接收到的项目是用户选中的文本)

在图2-1的第3步中,用户在扩展中执行或取消某个任务,或者直接可以忽略该扩展。在对用户的操作做出响应的过程中,扩展会根据载体应用的请求立即开始执行任务,但如果有特殊需要时,扩展也会在系统后台执行任务,这种情况下,载体不会显示扩展相关界面,并且用户返回到其先前在载体应用中的上下文环境。当应用扩展执行完任务后,不管是立刻返回还是延迟返回,但最后其执行结果都会返回给载体应用。

当扩展开始执行任务之后(或者开始在系统后台执行任务),系统就会终止扩展,比如图2-1中的第4步。

应用扩展是如何进行通信的

当应用扩展在运行的时候,它只会直接和载体应用(host app)进行通信,而扩展和主体应用containing app之间不会直接通信。在扩展运行的过程中,主体应用(containing app)甚至都不运行。另外,载体应用(host app)主体应用(containing app)之间也不会进行通信。图2-2中描述了运行中的应用扩展载体应用(host app)以及主体应用(containing app)这三者之间的关系。

                        运行中的扩展仅和载体应用进行通信

当一个扩展需要和主体应用containing app进行通信时,只能在载体应用确定的上下文环境中进行间接通信。比如,一个应用扩展有可能会运行调用它的应用程序。在这个例子中,扩展使用了不能与 containing app 传递消息的API。此外,应用扩展与其 containing app 可以在一个定义为私有的共享容器中访问数据。图2-3描述了扩展和 containing app 之间的间接通信类型。

                    运行中的扩展可以与其containing app进行间接通信

注意:在底层,系统使用进程间通信来确保应用扩展和扩展载体应用共同来开启无缝结合体验。在你的代码中,你无需考虑这些底层的通信机制,因为你使用的是扩展点和系统提供的高级API。

创建应用扩展

iOS8/OS X v10.10

当你准备好开发一个应用扩展时,可以从选择一个支持你想执行的任务的扩展点开始。然后选择适用于你所选的扩展点的Xcode模板,如果需要的话,可使用自定义代码和自定义用户界面来增强默认文件。最后,在你调试并优化你的应用扩展之后,就可以打包进你的应用程序给用户使用了。

从选择正确的扩展点开始

每个扩展点都针对一个定义明确的用户场景,你首先要清楚这个应用能为用户提供什么样的功能,所以要选择一个支持该功能的扩展点。这是非常关键的,因为每个扩展点定义了不同的API,为你提供不同的功能开发,所以如果选错了扩展点,那么很有可能无法实现想要的功能。表1-1列出了iOS和OS X中支持的所有扩展点。

选定了一个适用于应用扩展的扩展点后,在你的containing app中会有一个新的Target。添加应用扩展Target最简单的方式就是使用Xcode提供的模板,这些模板为扩展点提供了预配置。

你可以通过菜单选择 File > New > Target在你的Xcode工程中添加一个新Target。在左边的侧边栏中,选择iOS或OS X的Application Extension选项,然后在右边面板呈现的Xcode 模板中选择你想要创建的应用扩展模板,如下图所示:

选择好模板,并在工程中添加Target,在自定义扩展代码之前,你可以试着编译并运行一下项目。如果你是基于Xcode提供的模板创建的扩展,那么当编译成功后,就会生成一个扩展名为 .appex的应用扩展包

注意64位的架构:
一个应用程序扩展在Architectures设置中必须要包含arm64的架构,否则在上传App Store时会被拒绝。创建完一个应用扩展后,你可以在Xcode的“Standard architectures”设置选项中设置arm64架构。

如果你的包含扩展的应用程序使用了一些嵌入框架,那么你的应用程序也必须要包含arm64架构,否则上传App Store时也会被拒绝。

关于64位架构开发环境的更多信息,请根据你的target平台参阅64-Bit Transition Guide for Cocoa Touch或者64-Bit Transition Guide for Cocoa。

在大多数情况下,你可以通过在System Preferences或Settings中启用应用扩展或者授予权限来测试默认的应用扩展,然后可以通过其他应用来访问它。比如你可以在OS X系统中通过Safari中打开一个页面来测试分享扩展,点击分享按钮,然后选择你要测试的扩展即可。

检查默认的扩展模板

一般情况下,每个扩展模板都包含一个属性列表文件(就是Info.plist文件),一个View Controller类和一个默认的UI,这些都是扩展点定义的。默认的View Controller类(或principal class)都含有扩展点对应功能的方法,需要我们去实现。

应用扩展Target的Info.plist文件除了识别扩展点外还罗列了应用扩展的详细信息。该文件至少包括NSExtension字典以及扩展点指定的其他键值字典。。比如NSExtensionPointIdentifier的key值就是扩展点的反向DNS名称,比如com.apple.widget-extension。在应用扩展的NSExtension字典中还有其他的Key和值:

NSExtensionAttributes:这是一个描述扩展点具体属性的字典,就像照片编辑扩展中的PHSupportedMediaTypes一样。
NSExtensionPrincipalClass:这是扩展模板创建的主体视图控制器类,比如SharingViewController。当载体应用程序(host app)调用扩展时,扩展点会实例化这个类。
NSExtensionMainStoryboard(只适用于iOS):扩展默认的Storyboard文件,一般名为MainInterface。

除了在属性列表中设置以外,扩展模板还可以设置一些默认的功能。每个扩展点可以定义扩展点支持的适用于某个类型任务的功能。比如,一个iOS的Document Provider扩展就包含com.apple.security.application-groups的功能。

所有的OS X扩展模板都默认包含应用程序沙箱和com.apple.security.files.user-selected.read-only功能。如果你开发的扩展需要适用网络,或者访问用户的相册,再或者需要访问用户的通讯录,那么你就需要额外定义这些功能。

注意:通常情况下,如果用户允许主应用程序(containing app)访问他们的私有数据,那么主程序里的扩展也同样拥有该权限。

响应“载体程序”(Host App)的请求

正如你在Understand How an Extension Works这篇文档中了解的,当用户在host app选择一个扩展,并使host app向扩展发出请求时,就会打开扩展。说的再详细一点,你的扩展会根据用户的操作接收到请求,帮用户执行任务,然后完成或者关闭请求。比如说,一个分享扩展收到了来自host app的请求,然后该扩展会打开相应视图来响应请求。然后用户在该界面中编辑要分享的内容,用户可以选择发送或者不发送,最后扩展根据用户的行为响应完成还是关闭请求。

当host app向扩展发出请求时,一般都会指明扩展运行的上下文。对于很多扩展来说,最重要的一部分就是要设置一个工作项,这个工作项就是用户在使用这个扩展时要处理的工作项。比如说,一个分享扩展的上下文可能就包含用户选择的想要分享的一段文字。

当host app发出一个请求(通常就是调用beginRequestWithExtensionContext:方法),你的扩展就可以用主试图控制器中的 extensionContext 属性来获得上下文,然后使用 NSExtensionContext 类解析上下文并获得工作项。通常,在视图控制器的 loadView 方法中解析上下文并获得工作项,这样在加载完视图后就可以将信息显示在视图界面中了。获取扩展上下文可以使用如下代码:

NSExtensionContext *myExtensionContext = [self extensionContext]; 

有意思的是内容对象的 inputItems 属性,它包含了应用扩展需要使用的工作项。inputItems 属性包含一个 NSExtensionItem 类型的数组,数组的每一个成员都包含一个可执行的工作项。从上下文中获取工作项可以使用如下代码:

NSArray *inputItems = [myExtensionContext inputItems]; 

每个 NSExtensionItem 对象都包含若干个描述工作项的属性,比如标题、文本内容、附件信息、用户信息。

注意 attachments 属性,它包含一个与工作项相关联的媒体数据数组。比如说一个分享请求的工作项,那么 attachments 属性可能就包含用户想要分享网页中的信息。

当用户工作项处理完后,应用扩展通常会给用户两个选择,完成任务或取消任务。根据用户的选择,扩展会调用 completeRequestReturningItems:expirationHandler:completion: 方法,把工作项返回给 host app,或者会调用 cancelRequestWithError: 方法,返回一个错误代码。

在iOS中,你的应用程序扩展可能需要更多的时间去处理潜在的需长时间处理的任务,比如说往网上上传内容。这种情况下,你就要使用 NSURLSession 类将该任务转为后台处理的任务。因为转换到后台处理任务需要用一个单独的线程,所以在扩展完成主应用请求并关闭后仍然可以处理。想了解更多关于扩展中NSURLSession类的用法,请参阅:Performing Uploads and Downloads。

重要:虽然你可以设置一个后台URL来上传或下载任务,但是有一些类型的后台任务,比如支持 VoIp 或者在后台播放音乐的任务,是不能通过扩展来实现的。

如果你应用扩展的Info.plist文件中含有 UIBackgroundModes 关键字,那么在上传App Store时会被拒绝。(想了解更多关于 UIBackgroundModes 关键字的内容,请参阅 Information Property List Key Reference 中的 UIBackgroundModes)

优化效率和性能

应用扩展在内存使用优先级上要明显低于当前运行的应用程序。不管是 iOS 还是 OS X,系统都会毫不犹豫地终止扩展,因为用户想返回到host app中的主要目标中。但是也有一些应用扩展的内存使用优先级要高于其他扩展,比如说widgets就要求要高一些,因为它要实时的显示一些信息,因为一般用户更倾向于同时开启多个widgets。

你的应用扩展并不拥有主循环线程,你要遵循这一规则,以便让扩展在主循环线程中发挥最好的性能。比如说,如果你的应用扩展阻止了主循环线程,那么在用户使用主应用程序的过程中会造成非常糟糕的用户体验效果。
我们需要记住的一点是,GPU在系统中是一个共享的资源,所以应用扩展不会得到很高的优先级照顾。比如说,如果你正在玩一个对GPU消耗很高的游戏,那么由于内存压力比较大,它就有可能会选择关闭Today widget。

设计一个精简的用户界面

大多数的扩展点都要求你向用户提供一些自定义的界面,它在用户打开你的应用扩展时呈现给用户。通常情况下,应用扩展的界面要尽可能的简约、内敛,并主要关注一个单一任务。为了提高性能和用户体验效果,你要避免与该扩展功能无关的界面出现。

大多数Xcode 提供的应用扩展模板都包含一个初始界面文件,你可以从这个文件中设计界面开始。

在用户的惯性思维中,一般他们都是通过应用扩展的图标来辨识扩展功能的。通常情况下,应用扩展的图标和它的主体应用的图标是一致的。使用主体应用的图标作为应用扩展的图标有利于用户去判断这个扩展的来源,也就是说让用户确信这个扩展是来源于他们安装的主体应用。当然也有一些例外。

在iOS中,自定义的Action扩展的图标使用其主体应用的图标。
在OS X中,如果一个扩展的主体程序只是用来安装扩展的封装包,那么该扩展要提供一个单独的图标,否则都会使用主体应用的图标。

应用扩展要使用一个简短,语义明确的名字,这能让帮助用户把扩展和你的主应用程序联系起来,并且能让他们在系统中更好的管理应用扩展。通过应用扩展 TargetCFBundleDisplayName 属性来设置它的名称,你可以在Info.plist文件中修改它。如果你没有给 CFBundleDisplayName设置值,也就是没有给扩展设置名称,那么应用扩展会使用其主体应用的名称,也就是CFBundleName属性中的值。

同时一些应用扩展也需要一个简短的说明。比如说,OS X中的 Widget 扩展就会显示一个简单的描述,这能帮助用户更好的选择他们想要显示在今日通知中的Widget扩展。扩展的描述可以在 InfoPlist.strings 文件的widget.description 属性中设置。

确保您的iOS App扩展适用于所有设备

您必须确保提交的应用扩展程序是通用的:它必须适用于iPhone,iPod touch和iPad。无论您为包含的应用选择哪个目标设备系列,此要求均适用。Xcode中的应用程序扩展模板已针对通用目标设备系列进行了正确配置。

要声明您的应用扩展程序是通用的,请使用Xcode中的目标设备系列构建设置,指定“iPhone / iPad”值。

确保您的应用扩展程序具有通用性

  1. 在keyboard project的Xcode项目导航器中,选择项目文件。
    如果隐藏了项目编辑器中的project和targets列表,请显示它。为此,请单击project编辑器选项卡栏左侧的按钮。
  2. 在project 和 targets列表的targets组中,选择应用扩展程序的targets。
  3. 在project编辑器中选择Build Settings选项卡。
    确保选中Basic 和 Combined按钮,以便于您更轻松地找到所需的设置。
  4. 在project编辑器的Deployment group中,查看“Targeted Device Family”设置。对于Debug和Release配置,值应为“iPhone / iPad”。
    如果您找到不同的值,请将其更正为“iPhone / iPad”。

在设计和构建应用扩展时 使用Auto Layout和size classes类。测试您的应用扩展程序,以确保其符合您对所有设备大小和方向的预期行为。如“ Simulator User Guide所述,在iOS模拟器中执行此操作,如果可能,还可以在两个方向上对物理设备进行测试。

请记住,即使您的主体应用(containing app)仅针对iPad设备系列,您所包含的应用扩展程序也会以兼容模式运行显示在的iPhone应用中。

重要
要通过App Review,您必须将“iPhone / iPad”(有时称为*universal)*)指定为应用扩展程序的目标设备系列,无论您为主体应用(containing app)选择哪个目标设备系列。

在以后的iOS更新中,应用扩展程序仅在扩展程序主体应用本机支持的设备(或设备兼容模式)上运行。例如,在兼容模式下使用iPhone应用程序时,在只有iPad的主体应用(containing app)提供的扩展程序将不可见。为确保获得最佳用户体验,我们建议您的主体应用(containing app)和其应用扩展程序是通用的。

调试,配置和测试你的应用扩展

注意:要确保主体应用中的所有扩展都要使用相同签名方式的代码。
Xcode项目中的所有target都必须以相同的方式进行代码签名。例如,在测试期间,您可以使用临时代码签名或使用开发人员证书,但必须对项目中的所有target使用相同的方法。要提交到App Store,请使用您的分发证书来获取所有目标。

使用 Xcode 调试应用扩展和调试其他程序基本是一样的,但唯一点不同的是:你要选择一个能访问扩展的载体应用。当你编译运行应用扩展后,Xcode 会运行载体应用,等待你去使用扩展并触发调试点来调试扩展。你要在 scheme 中要为扩展指定一个载体应用(一个 scheme 封装了 Target 编译的说明)。

当你在主体应用工程中添加一个应用扩展的Target时,Xcode 就会为应用扩展默认创建一个 scheme。应用扩展的 scheme 可以让你指定在调试时由哪个应用程序来调用你的扩展,也就是指定一个调试时的载体应用。默认情况下,当你编译运行扩展时,会询问你使用哪个载体应用来调用该扩展。

在你编译运行应用扩展之前,你要确保你的扩展已经选择了一个 scheme。你可以通过 Product > Scheme > MyExtensionName 或者使用 Xcode 菜单栏呼出 scheme 菜单并选择 MyExtensionName 来设置应用扩展的 scheme。

注意:如果你运行主体应用的 scheme 代替应用扩展的 scheme,那么你在编译工程时Xcode会告诉你它正在等待调试应用扩展。

当你编译运行应用扩展时,Xcode会为你列出允许调用该扩展的载体应用程序。当你选择一个载体应用程序并且运行后,调试器就准备开始工作了,并准备好在你打的断点处进行拦截。当你在载体应用程序中使用扩展时,就可以对应用扩展进行Debug调试了。调试应用扩展的方式和使用Xcode调试其他进程一样。

在OS X中,你在载体应用程序中访问扩展之前,要确保该扩展是允许被使用的。一般情况下,在System Preferences的扩展面板中开启或关闭扩展(你也可以在共享或Action菜单中打开应用扩展面板)。这里要注意一点,在 OS X 中使用 Widget 模拟器调试 Widget扩展时,是不需要对其进行开启操作的。当你要调试键盘扩展时,必须要开启该扩展(你可以通过Settings > General > Keyboard > Keyboards开启键盘扩展)。

在调试时,Xcode会在OS X中创建一个持续的编译应用扩展的会话。这意味着,如果你要使用OS X系统下的扩展,你需要使用Finder把它从构建处拷贝到类似 Applications folder的地方。

注意:在Xcode的调试控制台日志中,应用扩展的二进制值可能是和 CFBundleIdentifier 属性关联,而不是 CFBundleDisplayName 属性。

由于应用扩展必须具有响应性和高效性,因此当运行应用扩展时,最好在调试导航器中查看调试指标( the debug gauges)。调试指标显示扩展在运行时如何使用CPU,内存和其他系统资源。当你发现类似占用CPU资源出现异常的性能问题时,例如CPU使用率出现异常高峰,您可以使用Instruments来分析您的扩展,并确定需要改进的地方。通过在任何调试仪表报告中单击Instruments中的配置文件,您可以在调试会话期间打开Instruments(要查看调试仪报告,请单击调试区域中的仪表)。想学习了解调试监控器,请查阅Debug Your App;想学习了解Instruments,请查阅Instruments User Guide。

注意:在Xcode中选择 Product > Profile可以直接在Instruments中编译并运行应用扩展。Instruments使用方案的Profile部分中的可执行文件集作为扩展的载体。

如果要使用Xcode提供的测试框架(比如XCTest APIs)测试应用扩展,你需要在主体应用程序中写一些测试用例代码。想了解更多XCTest的知识,请参阅Testing with Xcode。

分发扩展主体应用程序

你无法直接将应用扩展上传至App Store,除非它包含在主体应用程序中,并且你不能将应用扩展从一个应用程序中转到另一个应用程序。

如果想让用户使用你的应用扩展,你必须提交一个主体应用程序到App Store中,并且主体应用程序如要有其他的功能,不能只包含应用扩展。

如果你想提交 OS X 应用程序扩展,推荐你将主体应用程序提交至App Store,但这也不是唯一的途径。在OS X中,主体应用程序就可以只包含应用扩展,而不需要提供扩展外的其他功能。

注意:如果你不使用App Store来将主体应用程序和OS X 扩展交付给用户,那么在主体应用程序通过审核前,Gatekeeper是不会允许应用扩展生效的。同时,如果你不将主体应用程序上传至App Store,那么该主体应用程序也不能签署你的开发者ID名称,所以用户必须明确从主体应用程序中重载Gatekeeper,才可以让应用扩展生效。

常见问题的处理方案

当编写自定义代码以执行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的容器是不同的。

重要提示:如果你的应用扩展使用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。