在 iOS 视频中创建扩展
iOS 8 中引入的扩展是专用的 UIViewControllers,由 iOS 显示在标准上下文,例如“通知中心”内、用户请求的用于执行专用输入的自定义键盘类型和其他上下文(例如编辑照片时,扩展可以提供特殊效果筛选器)。
所有扩展都与容器应用(两个元素均使用 64 位 Unified API 编写)一起安装,并从主机应用中的特定扩展点激活。 由于它们将用作对现有系统功能的补充,因此必须高性能、精简且可靠。
扩展点
| 类型 | 描述 | 扩展点 | 主机应用 |
|---|---|---|---|
| 操作 | 专用于特定媒体类型的编辑器或查看器 | com.apple.ui-services |
任意 |
| 文档提供程序 | 允许应用使用远程文档存储 | com.apple.fileprovider-ui |
使用 UIDocumentPickerViewController 的应用 |
| 键盘 | 备用键盘 | com.apple.keyboard-service |
任意 |
| 照片编辑 | 照片操作和编辑 | com.apple.photo-editing |
Photos.app 编辑器 |
| 共享 | 与社交网络、消息服务等共享数据。 | com.apple.share-services |
任意 |
| Today | 显示在“今日”屏幕或通知中心上的“小组件” | com.apple.widget-extensions |
“今日”和通知中心 |
iOS 10 和 iOS 12 中添加了其他扩展点。 可以在 iOS 应用扩展编程指南中找到所有受支持类型的完整表格。
限制
扩展具有许多限制,其中一些限制适用于所有类型(例如,任何扩展类型都无法访问相机或麦克风),而其他类型的扩展可能在其使用方面有特定限制(例如,自定义键盘不能用于安全数据输入字段,如密码)。
通用限制包括:
- 运行状况工具包和事件工具包 UI 框架不可用
- 扩展不能使用扩展后台模式
- 扩展无法访问设备的相机或麦克风(尽管它们可以访问现有的媒体文件)
- 扩展无法接收 Air Drop 数据(尽管它们可以通过 Air Drop 传输数据)
- UIActionSheet 和 UIAlertView 不可用;扩展必须使用 UIAlertController
- UIApplication 的多个成员不可用:UIApplication.SharedApplication、UIApplication.OpenUrl、UIApplication.BeginIgnoringInteractionEvents 和 UIApplication.EndIgnoringInteractionEvents
- iOS 对“今日”的扩展强制实施 16 MB 内存使用限制。
- 默认情况下,键盘扩展无法访问网络。 这会影响设备上的调试(模拟器中未强制实施限制),因为 Xamarin.iOS 需要网络访问权限才能正常进行调试。 可以通过将项目 Info.plist 中的
Requests Open Access值设置为Yes来请求网络访问。 有关键盘扩展限制的详细信息,请参阅 Apple 的自定义键盘指南。
有关个别限制,请参阅 Apple 的应用扩展编程指南。
分发、安装和运行扩展
扩展是从容器应用内分发的,容器应用又通过 App Store 进行提交和分发。 此时会安装随应用一起分发的扩展,但用户必须显式启用每个扩展。 不同类型的扩展以不同的方式启用;有几个类型要求用户导航到“设置”应用并从那里启用它们。 而其他类型在使用时便会启用,例如在发送照片时启用共享扩展。
在其中使用扩展(即用户遇到扩展点)的应用称为“主机应用”,因为它是在执行扩展时托管扩展的应用。 安装扩展的应用是“容器应用”,因为它是在安装扩展时包含该扩展的应用。
通常,容器应用描述扩展,并引导用户完成启用扩展的过程。
调试和发布扩展版本
运行应用扩展的内存限制明显低于应用于前台应用的内存限制。 运行 iOS 的模拟器对扩展的限制较少,你可以在没有任何问题的情况下执行扩展。 但是,在设备上运行同一扩展可能会导致意外结果,包括扩展崩溃或系统主动终止。 因此,在传送扩展之前,请确保在设备上生成和测试扩展。
应确保将以下设置应用于容器项目和所有引用的扩展:
- 在“发布”配置中生成应用程序包。
- 在“iOS 生成”项目设置中,将“链接器行为”选项设置为“仅链接 Framework SDK”或“全部链接”。
- 在“iOS 调试”项目设置中,取消选中“启用调试”和“启用分析”选项。
扩展生命周期
扩展可以像单个 UIViewController 那样简单,也可以是呈现多个 UI 屏幕的更为复杂的扩展。 当用户遇到扩展点时(例如共享图像时),他们将有机会从为该扩展点注册的扩展中进行选择。
如果他们选择应用的其中一个扩展,相应的 UIViewController 将会实例化并开始正常的视图控制器生命周期。 然而,与普通应用不同的是,当用户完成交互时,普通应用会暂停但通常并不终止,而扩展则会反复加载、执行、再终止。
扩展可以通过 NSExtensionContext 对象与其主机应用通信。 某些扩展具有的操作是接收异步回调及结果。 这些回调将在后台线程上执行,扩展必须考虑到这一点;例如,如果想要更新用户界面,请使用 NSObject.InvokeOnMainThread。 有关更多详细信息,请参阅下面的与主机应用通信部分。
默认情况下,尽管扩展及其容器应用是一起安装的,但二者无法通信。 在某些情况下,容器应用实质上是一个空的“传送”容器,扩展安装之后其目的便已达到。 但是,如果形势需要,容器应用和扩展可能会共享来自公共区域的资源。 此外,“今日扩展”可能会请求其容器应用打开 URL。 此行为显示在事件倒计时小组件中。
创建扩展
扩展(及其容器应用)必须是 64 位二进制文件,并使用 Xamarin.iOS Unified API 生成。 开发扩展时,解决方案将至少包含两个项目:容器应用和容器提供的每个扩展的一个项目。
容器应用项目要求
用于安装扩展的容器应用具有以下要求:
- 必须保持对扩展项目的引用。
- 必须是一个完整的应用(必须能够成功启动和运行),即便它只是提供一种扩展安装方法。
- 必须有一个作为扩展项目的捆绑标识符基础的捆绑标识符(有关详细信息,请参阅下面的部分)。
扩展项目要求
此外,扩展的项目具有以下要求:
必须有一个以容器应用的捆绑标识符开头的捆绑标识符。 例如,如果容器应用的捆绑标识符为
com.myCompany.ContainerApp,则扩展的标识符可能是com.myCompany.ContainerApp.MyExtension:
必须在其
Info.plist文件中使用适当的值定义NSExtensionPointIdentifier键(例如,com.apple.widget-extension代表“今日”通知中心小组件)。还必须在其
Info.plist文件中使用适当的值定义NSExtensionMainStoryboard键或NSExtensionPrincipalClass键:- 使用
NSExtensionMainStoryboard键指定呈现扩展主 UI 的情节提要的名称(减去.storyboard)。 例如,Main代表Main.storyboard文件。 - 使用
NSExtensionPrincipalClass键指定在启动扩展时将初始化的类。 值必须与UIViewController的 Register 值匹配:

- 使用
特定类型的扩展可能有其他要求。 例如,“今日”或“通知中心”扩展的主体类必须实现 INCWidgetProviding。
重要
如果使用 Visual Studio for Mac 提供的一个扩展模板启动项目,则该模板将自动提供并满足大多数要求(如果不是全部的话)。
演练
在以下演练中,你将创建一个示例“今日”小组件,用于计算一年中的第几天和年份中的剩余天数:
创建解决方案
若要创建所需的解决方案,请执行以下操作:
首先,创建新的 iOS“单视图应用”项目,然后单击“下一步”按钮:
将项目命名为
TodayContainer并单击“下一步”按钮:验证“项目名称”和“解决方案名称”,然后单击“创建”按钮以创建解决方案:
接下来,在“解决方案资源管理器”中,右键单击该解决方案,然后从“今日扩展”模板添加新的“iOS 扩展”项目:
将项目命名为
DaysRemaining并单击“下一步”按钮:查看项目,然后单击“创建”按钮进行创建:
生成的解决方案现在应有两个项目,如下所示:
创建扩展用户界面
接下来,需要为“今日”小组件设计界面。 该操作可以使用情节提要完成,也可以通过使用代码创建 UI 来完成。 下面将详细介绍这两种方法。
使用情节提要
若要使用情节提要生成 UI,请执行以下操作:
在“解决方案资源管理器”中,双击扩展项目的
Main.storyboard文件,将其打开进行编辑:按模板选择自动添加到 UI 的标签,并在“属性资源管理器”的小组件选项卡中为其指定名称
TodayMessage:保存对情节提要所做的更改。
使用代码
若要使用代码生成 UI,请执行以下操作:
在“解决方案资源管理器”中,选择 DaysRemaining 项目,添加新类并将其命名为
CodeBasedViewController:再次在“解决方案资源管理器”中双击扩展的
Info.plist文件,将其打开进行编辑:选择“源视图”(从屏幕底部)并打开
NSExtension节点:移除
NSExtensionMainStoryboard键并添加值为CodeBasedViewController的NSExtensionPrincipalClass:保存所做更改。
接下来,编辑 CodeBasedViewController.cs 文件,使其如下所示:
using System;
using Foundation;
using UIKit;
using NotificationCenter;
using CoreGraphics;
namespace DaysRemaining
{
[Register("CodeBasedViewController")]
public class CodeBasedViewController : UIViewController, INCWidgetProviding
{
public CodeBasedViewController ()
{
}
public override void ViewDidLoad ()
{
base.ViewDidLoad ();
// Add label to view
var TodayMessage = new UILabel (new CGRect (0, 0, View.Frame.Width, View.Frame.Height)) {
TextAlignment = UITextAlignment.Center
};
View.AddSubview (TodayMessage);
// Insert code to power extension here...
}
}
}
请注意,[Register("CodeBasedViewController")] 与为上述 NSExtensionPrincipalClass 指定的值匹配。
编写扩展的代码
创建用户界面后,打开 TodayViewController.cs 或 CodeBasedViewController.cs 文件(基于上述创建用户界面所使用的方法),更改 ViewDidLoad 方法,使其如下所示:
public override void ViewDidLoad ()
{
base.ViewDidLoad ();
// Calculate the values
var dayOfYear = DateTime.Now.DayOfYear;
var leapYearExtra = DateTime.IsLeapYear (DateTime.Now.Year) ? 1 : 0;
var daysRemaining = 365 + leapYearExtra - dayOfYear;
// Display the message
if (daysRemaining == 1) {
TodayMessage.Text = String.Format ("Today is day {0}. There is one day remaining in the year.", dayOfYear);
} else {
TodayMessage.Text = String.Format ("Today is day {0}. There are {1} days remaining in the year.", dayOfYear, daysRemaining);
}
}
如果使用基于代码的用户界面方法,请将 // Insert code to power extension here... 注释替换为上述新代码。 调用基本实现(并插入基于代码的版本的标签)后,此代码会进行简单的计算,以获取一年中的第几天和剩余天数。 然后,它会将消息显示在 UI 设计中所创建的标签 (TodayMessage) 中。
请注意此过程与编写应用的正常过程有多相似。 扩展的 UIViewController 与应用中的视图控制器具有相同的生命周期,不同之处是扩展没有后台模式,而且在用户使用完后不会暂停。 相反,扩展会根据需要重复初始化和取消分配。
创建容器应用用户界面
在本演练中,容器应用只是用作传送和安装扩展的一种方法,自身无任何功能。 编辑 TodayContainer 的 Main.storyboard 文件,并添加一些文本来定义扩展的功能及其安装方式:
保存对情节提要所做的更改。
测试扩展
若要在 iOS 模拟器中测试扩展,请运行 TodayContainer 应用。 将显示容器的主视图:
接下来,在模拟器中点击“主页”按钮,从屏幕顶部向下轻扫以打开“通知中心”,选择“今日”选项卡并单击“编辑”按钮:
向“今日”视图中添加 DaysRemaining 扩展,然后单击“完成”按钮:
将会在“今日”视图中添加新的小组件,并显示结果:
与主机应用通信
上面创建的示例“今日”扩展不会与其主机应用(“今日”屏幕)通信。 如果通信,它将使用 TodayViewController 或 CodeBasedViewController 类的 ExtensionContext 属性。
对于将从主机应用接收数据的扩展,数据以 NSExtensionItem 对象数组的形式存储在扩展 UIViewController 的 ExtensionContext 的 InputItems 属性中。
其他扩展(如照片编辑扩展)可以区分用户完成或取消使用情况。 这将以信号的形式通过 ExtensionContext 属性的 CompleteRequest 和 CancelRequest 方法发送回主机应用。
有关详细信息,请参阅 Apple 的应用扩展编程指南。
与父应用通信
应用组允许不同的应用程序(或一个应用程序及其扩展)访问共享文件存储位置。 应用组可以用于如下所示的数据:
有关详细信息,请参阅“使用功能”文档中的应用组部分。
MobileCoreServices
使用扩展时,使用统一类型标识符 (UTI) 创建和操作在应用、其他应用和/或服务之间交换的数据。
MobileCoreServices.UTType 静态类定义以下与 Apple kUTType... 定义相关的帮助程序属性:
kUTTypeAlembic-AlembickUTTypeAliasFile-AliasFilekUTTypeAliasRecord-AliasRecordkUTTypeAppleICNS-AppleICNSkUTTypeAppleProtectedMPEG4Audio-AppleProtectedMPEG4AudiokUTTypeAppleProtectedMPEG4Video-AppleProtectedMPEG4VideokUTTypeAppleScript-AppleScriptkUTTypeApplication-ApplicationkUTTypeApplicationBundle-ApplicationBundlekUTTypeApplicationFile-ApplicationFilekUTTypeArchive-ArchivekUTTypeAssemblyLanguageSource-AssemblyLanguageSourcekUTTypeAudio-AudiokUTTypeAudioInterchangeFileFormat-AudioInterchangeFileFormatkUTTypeAudiovisualContent-AudiovisualContentkUTTypeAVIMovie-AVIMoviekUTTypeBinaryPropertyList-BinaryPropertyListkUTTypeBMP-BMPkUTTypeBookmark-BookmarkkUTTypeBundle-BundlekUTTypeBzip2Archive-Bzip2ArchivekUTTypeCalendarEvent-CalendarEventkUTTypeCHeader-CHeaderkUTTypeCommaSeparatedText-CommaSeparatedTextkUTTypeCompositeContent-CompositeContentkUTTypeConformsToKey-ConformsToKeykUTTypeContact-ContactkUTTypeContent-ContentkUTTypeCPlusPlusHeader-CPlusPlusHeaderkUTTypeCPlusPlusSource-CPlusPlusSourcekUTTypeCSource-CSourcekUTTypeData-DatabasekUTTypeDelimitedText-DelimitedTextkUTTypeDescriptionKey-DescriptionKeykUTTypeDirectory-DirectorykUTTypeDiskImage-DiskImagekUTTypeElectronicPublication-ElectronicPublicationkUTTypeEmailMessage-EmailMessagekUTTypeExecutable-ExecutablekUTExportedTypeDeclarationsKey-ExportedTypeDeclarationsKeykUTTypeFileURL-FileURLkUTTypeFlatRTFD-FlatRTFDkUTTypeFolder-FolderkUTTypeFont-FontkUTTypeFramework-FrameworkkUTTypeGIF-GIFkUTTypeGNUZipArchive-GNUZipArchivekUTTypeHTML-HTMLkUTTypeICO-ICOkUTTypeIconFileKey-IconFileKeykUTTypeIdentifierKey-IdentifierKeykUTTypeImage-ImagekUTImportedTypeDeclarationsKey-ImportedTypeDeclarationsKeykUTTypeInkText-InkTextkUTTypeInternetLocation-InternetLocationkUTTypeItem-ItemkUTTypeJavaArchive-JavaArchivekUTTypeJavaClass-JavaClasskUTTypeJavaScript-JavaScriptkUTTypeJavaSource-JavaSourcekUTTypeJPEG-JPEGkUTTypeJPEG2000-JPEG2000kUTTypeJSON-JSONkUTType3dObject-k3dObjectkUTTypeLivePhoto-LivePhotokUTTypeLog-LogkUTTypeM3UPlaylist-M3UPlaylistkUTTypeMessage-MessagekUTTypeMIDIAudio-MIDIAudiokUTTypeMountPoint-MountPointkUTTypeMovie-MoviekUTTypeMP3-MP3kUTTypeMPEG-MPEGkUTTypeMPEG2TransportStream-MPEG2TransportStreamkUTTypeMPEG2Video-MPEG2VideokUTTypeMPEG4-MPEG4kUTTypeMPEG4Audio-MPEG4AudiokUTTypeObjectiveCPlusPlusSource-ObjectiveCPlusPlusSourcekUTTypeObjectiveCSource-ObjectiveCSourcekUTTypeOSAScript-OSAScriptkUTTypeOSAScriptBundle-OSAScriptBundlekUTTypePackage-PackagekUTTypePDF-PDFkUTTypePerlScript-PerlScriptkUTTypePHPScript-PHPScriptkUTTypePICT-PICTkUTTypePKCS12-PKCS12kUTTypePlainText-PlainTextkUTTypePlaylist-PlaylistkUTTypePluginBundle-PluginBundlekUTTypePNG-PNGkUTTypePolygon-PolygonkUTTypePresentation-PresentationkUTTypePropertyList-PropertyListkUTTypePythonScript-PythonScriptkUTTypeQuickLookGenerator-QuickLookGeneratorkUTTypeQuickTimeImage-QuickTimeImagekUTTypeQuickTimeMovie-QuickTimeMoviekUTTypeRawImage-RawImagekUTTypeReferenceURLKey-ReferenceURLKeykUTTypeResolvable-ResolvablekUTTypeRTF-RTFkUTTypeRTFD-RTFDkUTTypeRubyScript-RubyScriptkUTTypeScalableVectorGraphics-ScalableVectorGraphicskUTTypeScript-ScriptkUTTypeShellScript-ShellScriptkUTTypeSourceCode-SourceCodekUTTypeSpotlightImporter-SpotlightImporterkUTTypeSpreadsheet-SpreadsheetkUTTypeStereolithography-StereolithographykUTTypeSwiftSource-SwiftSourcekUTTypeSymLink-SymLinkkUTTypeSystemPreferencesPane-SystemPreferencesPanekUTTypeTabSeparatedText-TabSeparatedTextkUTTagClassFilenameExtension-TagClassFilenameExtensionkUTTagClassMIMEType-TagClassMIMETypekUTTypeTagSpecificationKey-TagSpecificationKeykUTTypeText-TextkUTType3DContent-ThreeDContentkUTTypeTIFF-TIFFkUTTypeToDoItem-ToDoItemkUTTypeTXNTextAndMultimediaData-TXNTextAndMultimediaDatakUTTypeUniversalSceneDescription-UniversalSceneDescriptionkUTTypeUnixExecutable-UnixExecutablekUTTypeURL-URLkUTTypeURLBookmarkData-URLBookmarkDatakUTTypeUTF16ExternalPlainText-UTF16ExternalPlainTextkUTTypeUTF16PlainText-UTF16PlainTextkUTTypeUTF8PlainText-UTF8PlainTextkUTTypeUTF8TabSeparatedText-UTF8TabSeparatedTextkUTTypeVCard-VCardkUTTypeVersionKey-VersionKeykUTTypeVideo-VideokUTTypeVolume-VolumekUTTypeWaveformAudio-WaveformAudiokUTTypeWebArchive-WebArchivekUTTypeWindowsExecutable-WindowsExecutablekUTTypeX509Certificate-X509CertificatekUTTypeXML-XMLkUTTypeXMLPropertyList-XMLPropertyListkUTTypeXPCService-XPCServicekUTTypeZipArchive-ZipArchive
请参阅以下示例:
using MobileCoreServices;
...
NSItemProvider itemProvider = new NSItemProvider ();
itemProvider.LoadItem(UTType.PropertyList ,null, (item, err) => {
if (err == null) {
NSDictionary results = (NSDictionary )item;
NSString baseURI =
results.ObjectForKey("NSExtensionJavaScriptPreprocessingResultsKey");
}
});
有关详细信息,请参阅“使用功能”文档中的应用组部分。
预防措施和注意事项
扩展的可用内存明显少于应用可用的内存。 预计其执行速度更快,对用户和主机应用的入侵程度最低。 然而,扩展还应通过品牌 UI 为使用中的应用提供独特且有用的功能,以便用户能够确定其所属的扩展开发者或容器应用。
鉴于上述严苛要求,应仅部署在性能和内存使用方面经过全面测试和优化的扩展。
总结
本文档介绍了扩展、扩展概念、扩展点的类型以及 iOS 对扩展施加的已知限制。 文中讨论了如何创建、分发、安装和运行扩展以及扩展的生命周期, 并提供了一个创建简单“今日”小组件的演练,用于演示如何使用情节提要或代码创建小组件 UI 的两种方法。 之后,该文档介绍了如何在 iOS 模拟器中测试扩展, 并在最后简要讨论了如何与主机应用通信,以及开发扩展时应采取的一些预防措施和注意事项。


















