在 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-- Alembic
- kUTTypeAliasFile-- AliasFile
- kUTTypeAliasRecord-- AliasRecord
- kUTTypeAppleICNS-- AppleICNS
- kUTTypeAppleProtectedMPEG4Audio-- AppleProtectedMPEG4Audio
- kUTTypeAppleProtectedMPEG4Video-- AppleProtectedMPEG4Video
- kUTTypeAppleScript-- AppleScript
- kUTTypeApplication-- Application
- kUTTypeApplicationBundle-- ApplicationBundle
- kUTTypeApplicationFile-- ApplicationFile
- kUTTypeArchive-- Archive
- kUTTypeAssemblyLanguageSource-- AssemblyLanguageSource
- kUTTypeAudio-- Audio
- kUTTypeAudioInterchangeFileFormat-- AudioInterchangeFileFormat
- kUTTypeAudiovisualContent-- AudiovisualContent
- kUTTypeAVIMovie-- AVIMovie
- kUTTypeBinaryPropertyList-- BinaryPropertyList
- kUTTypeBMP-- BMP
- kUTTypeBookmark-- Bookmark
- kUTTypeBundle-- Bundle
- kUTTypeBzip2Archive-- Bzip2Archive
- kUTTypeCalendarEvent-- CalendarEvent
- kUTTypeCHeader-- CHeader
- kUTTypeCommaSeparatedText-- CommaSeparatedText
- kUTTypeCompositeContent-- CompositeContent
- kUTTypeConformsToKey-- ConformsToKey
- kUTTypeContact-- Contact
- kUTTypeContent-- Content
- kUTTypeCPlusPlusHeader-- CPlusPlusHeader
- kUTTypeCPlusPlusSource-- CPlusPlusSource
- kUTTypeCSource-- CSource
- kUTTypeData-- Database
- kUTTypeDelimitedText-- DelimitedText
- kUTTypeDescriptionKey-- DescriptionKey
- kUTTypeDirectory-- Directory
- kUTTypeDiskImage-- DiskImage
- kUTTypeElectronicPublication-- ElectronicPublication
- kUTTypeEmailMessage-- EmailMessage
- kUTTypeExecutable-- Executable
- kUTExportedTypeDeclarationsKey-- ExportedTypeDeclarationsKey
- kUTTypeFileURL-- FileURL
- kUTTypeFlatRTFD-- FlatRTFD
- kUTTypeFolder-- Folder
- kUTTypeFont-- Font
- kUTTypeFramework-- Framework
- kUTTypeGIF-- GIF
- kUTTypeGNUZipArchive-- GNUZipArchive
- kUTTypeHTML-- HTML
- kUTTypeICO-- ICO
- kUTTypeIconFileKey-- IconFileKey
- kUTTypeIdentifierKey-- IdentifierKey
- kUTTypeImage-- Image
- kUTImportedTypeDeclarationsKey-- ImportedTypeDeclarationsKey
- kUTTypeInkText-- InkText
- kUTTypeInternetLocation-- InternetLocation
- kUTTypeItem-- Item
- kUTTypeJavaArchive-- JavaArchive
- kUTTypeJavaClass-- JavaClass
- kUTTypeJavaScript-- JavaScript
- kUTTypeJavaSource-- JavaSource
- kUTTypeJPEG-- JPEG
- kUTTypeJPEG2000-- JPEG2000
- kUTTypeJSON-- JSON
- kUTType3dObject-- k3dObject
- kUTTypeLivePhoto-- LivePhoto
- kUTTypeLog-- Log
- kUTTypeM3UPlaylist-- M3UPlaylist
- kUTTypeMessage-- Message
- kUTTypeMIDIAudio-- MIDIAudio
- kUTTypeMountPoint-- MountPoint
- kUTTypeMovie-- Movie
- kUTTypeMP3-- MP3
- kUTTypeMPEG-- MPEG
- kUTTypeMPEG2TransportStream-- MPEG2TransportStream
- kUTTypeMPEG2Video-- MPEG2Video
- kUTTypeMPEG4-- MPEG4
- kUTTypeMPEG4Audio-- MPEG4Audio
- kUTTypeObjectiveCPlusPlusSource-- ObjectiveCPlusPlusSource
- kUTTypeObjectiveCSource-- ObjectiveCSource
- kUTTypeOSAScript-- OSAScript
- kUTTypeOSAScriptBundle-- OSAScriptBundle
- kUTTypePackage-- Package
- kUTTypePDF-- PDF
- kUTTypePerlScript-- PerlScript
- kUTTypePHPScript-- PHPScript
- kUTTypePICT-- PICT
- kUTTypePKCS12-- PKCS12
- kUTTypePlainText-- PlainText
- kUTTypePlaylist-- Playlist
- kUTTypePluginBundle-- PluginBundle
- kUTTypePNG-- PNG
- kUTTypePolygon-- Polygon
- kUTTypePresentation-- Presentation
- kUTTypePropertyList-- PropertyList
- kUTTypePythonScript-- PythonScript
- kUTTypeQuickLookGenerator-- QuickLookGenerator
- kUTTypeQuickTimeImage-- QuickTimeImage
- kUTTypeQuickTimeMovie-- QuickTimeMovie
- kUTTypeRawImage-- RawImage
- kUTTypeReferenceURLKey-- ReferenceURLKey
- kUTTypeResolvable-- Resolvable
- kUTTypeRTF-- RTF
- kUTTypeRTFD-- RTFD
- kUTTypeRubyScript-- RubyScript
- kUTTypeScalableVectorGraphics-- ScalableVectorGraphics
- kUTTypeScript-- Script
- kUTTypeShellScript-- ShellScript
- kUTTypeSourceCode-- SourceCode
- kUTTypeSpotlightImporter-- SpotlightImporter
- kUTTypeSpreadsheet-- Spreadsheet
- kUTTypeStereolithography-- Stereolithography
- kUTTypeSwiftSource-- SwiftSource
- kUTTypeSymLink-- SymLink
- kUTTypeSystemPreferencesPane-- SystemPreferencesPane
- kUTTypeTabSeparatedText-- TabSeparatedText
- kUTTagClassFilenameExtension-- TagClassFilenameExtension
- kUTTagClassMIMEType-- TagClassMIMEType
- kUTTypeTagSpecificationKey-- TagSpecificationKey
- kUTTypeText-- Text
- kUTType3DContent-- ThreeDContent
- kUTTypeTIFF-- TIFF
- kUTTypeToDoItem-- ToDoItem
- kUTTypeTXNTextAndMultimediaData-- TXNTextAndMultimediaData
- kUTTypeUniversalSceneDescription-- UniversalSceneDescription
- kUTTypeUnixExecutable-- UnixExecutable
- kUTTypeURL-- URL
- kUTTypeURLBookmarkData-- URLBookmarkData
- kUTTypeUTF16ExternalPlainText-- UTF16ExternalPlainText
- kUTTypeUTF16PlainText-- UTF16PlainText
- kUTTypeUTF8PlainText-- UTF8PlainText
- kUTTypeUTF8TabSeparatedText-- UTF8TabSeparatedText
- kUTTypeVCard-- VCard
- kUTTypeVersionKey-- VersionKey
- kUTTypeVideo-- Video
- kUTTypeVolume-- Volume
- kUTTypeWaveformAudio-- WaveformAudio
- kUTTypeWebArchive-- WebArchive
- kUTTypeWindowsExecutable-- WindowsExecutable
- kUTTypeX509Certificate-- X509Certificate
- kUTTypeXML-- XML
- kUTTypeXMLPropertyList-- XMLPropertyList
- kUTTypeXPCService-- XPCService
- kUTTypeZipArchive-- 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 模拟器中测试扩展, 并在最后简要讨论了如何与主机应用通信,以及开发扩展时应采取的一些预防措施和注意事项。


















