Xamarin.iOS 中的 iOS 扩展

在 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 10iOS 12 中添加了其他扩展点。 可以在 iOS 应用扩展编程指南中找到所有受支持类型的完整表格。

限制

扩展具有许多限制,其中一些限制适用于所有类型(例如,任何扩展类型都无法访问相机或麦克风),而其他类型的扩展可能在其使用方面有特定限制(例如,自定义键盘不能用于安全数据输入字段,如密码)。

通用限制包括:

有关个别限制,请参阅 Apple 的应用扩展编程指南

分发、安装和运行扩展

扩展是从容器应用内分发的,容器应用又通过 App Store 进行提交和分发。 此时会安装随应用一起分发的扩展,但用户必须显式启用每个扩展。 不同类型的扩展以不同的方式启用;有几个类型要求用户导航到“设置”应用并从那里启用它们。 而其他类型在使用时便会启用,例如在发送照片时启用共享扩展。

在其中使用扩展(即用户遇到扩展点)的应用称为“主机应用”,因为它是在执行扩展时托管扩展的应用。 安装扩展的应用是“容器应用”,因为它是在安装扩展时包含该扩展的应用

通常,容器应用描述扩展,并引导用户完成启用扩展的过程。

调试和发布扩展版本

运行应用扩展的内存限制明显低于应用于前台应用的内存限制。 运行 iOS 的模拟器对扩展的限制较少,你可以在没有任何问题的情况下执行扩展。 但是,在设备上运行同一扩展可能会导致意外结果,包括扩展崩溃或系统主动终止。 因此,在传送扩展之前,请确保在设备上生成和测试扩展。

应确保将以下设置应用于容器项目和所有引用的扩展:

  1. 在“发布”配置中生成应用程序包
  2. 在“iOS 生成”项目设置中,将“链接器行为”选项设置为“仅链接 Framework SDK”或“全部链接”
  3. 在“iOS 调试”项目设置中,取消选中“启用调试”和“启用分析”选项

扩展生命周期

扩展可以像单个 UIViewController 那样简单,也可以是呈现多个 UI 屏幕的更为复杂的扩展。 当用户遇到扩展点时(例如共享图像时),他们将有机会从为该扩展点注册的扩展中进行选择

如果他们选择应用的其中一个扩展,相应的 UIViewController 将会实例化并开始正常的视图控制器生命周期。 然而,与普通应用不同的是,当用户完成交互时,普通应用会暂停但通常并不终止,而扩展则会反复加载、执行、再终止。

扩展可以通过 NSExtensionContext 对象与其主机应用通信。 某些扩展具有的操作是接收异步回调及结果。 这些回调将在后台线程上执行,扩展必须考虑到这一点;例如,如果想要更新用户界面,请使用 NSObject.InvokeOnMainThread。 有关更多详细信息,请参阅下面的与主机应用通信部分。

默认情况下,尽管扩展及其容器应用是一起安装的,但二者无法通信。 在某些情况下,容器应用实质上是一个空的“传送”容器,扩展安装之后其目的便已达到。 但是,如果形势需要,容器应用和扩展可能会共享来自公共区域的资源。 此外,“今日扩展”可能会请求其容器应用打开 URL。 此行为显示在事件倒计时小组件中。

创建扩展

扩展(及其容器应用)必须是 64 位二进制文件,并使用 Xamarin.iOS Unified API 生成。 开发扩展时,解决方案将至少包含两个项目:容器应用和容器提供的每个扩展的一个项目。

容器应用项目要求

用于安装扩展的容器应用具有以下要求:

  • 必须保持对扩展项目的引用。
  • 必须是一个完整的应用(必须能够成功启动和运行),即便它只是提供一种扩展安装方法。
  • 必须有一个作为扩展项目的捆绑标识符基础的捆绑标识符(有关详细信息,请参阅下面的部分)。

扩展项目要求

此外,扩展的项目具有以下要求:

  • 必须有一个以容器应用的捆绑标识符开头的捆绑标识符。 例如,如果容器应用的捆绑标识符为 com.myCompany.ContainerApp,则扩展的标识符可能是 com.myCompany.ContainerApp.MyExtension

    Bundle identifiers

  • 必须在其 Info.plist 文件中使用适当的值定义 NSExtensionPointIdentifier 键(例如,com.apple.widget-extension 代表“今日”通知中心小组件)。

  • 还必须在其 Info.plist 文件中使用适当的值定义 NSExtensionMainStoryboard 键或 NSExtensionPrincipalClass

    • 使用 NSExtensionMainStoryboard 键指定呈现扩展主 UI 的情节提要的名称(减去 .storyboard)。 例如,Main 代表 Main.storyboard 文件。
    • 使用 NSExtensionPrincipalClass 键指定在启动扩展时将初始化的类。 值必须与 UIViewController 的 Register 值匹配

    Principal class registration

特定类型的扩展可能有其他要求。 例如,“今日”或“通知中心”扩展的主体类必须实现 INCWidgetProviding

重要

如果使用 Visual Studio for Mac 提供的一个扩展模板启动项目,则该模板将自动提供并满足大多数要求(如果不是全部的话)。

演练

在以下演练中,你将创建一个示例“今日”小组件,用于计算一年中的第几天和年份中的剩余天数

An example Today widget that calculates the day and number of days remaining in the year

创建解决方案

若要创建所需的解决方案,请执行以下操作:

  1. 首先,创建新的 iOS“单视图应用”项目,然后单击“下一步”按钮

    First, create a new iOS, Single View App project and click the Next button

  2. 将项目命名为 TodayContainer 并单击“下一步”按钮

    Call the project TodayContainer and click the Next button

  3. 验证“项目名称”和“解决方案名称”,然后单击“创建”按钮以创建解决方案

    Verify the Project Name and SolutionName and click the Create button to create the solution

  4. 接下来,在“解决方案资源管理器”中,右键单击该解决方案,然后从“今日扩展”模板添加新的“iOS 扩展”项目

    Next, in the Solution Explorer, right-click on the Solution and add a new iOS Extension project from the Today Extension template

  5. 将项目命名为 DaysRemaining 并单击“下一步”按钮

    Call the project DaysRemaining and click the Next button

  6. 查看项目,然后单击“创建”按钮进行创建:

    Review the project and click the Create button to create it

生成的解决方案现在应有两个项目,如下所示:

The resulting Solution should now have two projects, as shown here

创建扩展用户界面

接下来,需要为“今日”小组件设计界面。 该操作可以使用情节提要完成,也可以通过使用代码创建 UI 来完成。 下面将详细介绍这两种方法。

使用情节提要

若要使用情节提要生成 UI,请执行以下操作:

  1. 在“解决方案资源管理器”中,双击扩展项目的 Main.storyboard 文件,将其打开进行编辑

    Double-click the Extension projects Main.storyboard file to open it for editing

  2. 选择模板自动添加到 UI 的标签,然后在“属性资源管理器”的“小组件”选项卡中为其指定“名称”TodayMessage

    Select the Label that was automatically added to the UI by template and give it the Name TodayMessage in the Widget tab of the Properties Explorer

  3. 保存对情节提要所做的更改。

使用代码

若要使用代码生成 UI,请执行以下操作:

  1. 在“解决方案资源管理器”中,选择 DaysRemaining 项目,添加新类并将其命名为 CodeBasedViewController

    Aelect the DaysRemaining project, add a new class and call it CodeBasedViewController

  2. 再次在“解决方案资源管理器”中双击扩展的 Info.plist 文件,将其打开进行编辑

    Double-click Extensions Info.plist file to open it for editing

  3. 选择“源视图”(从屏幕底部)并打开 NSExtension 节点

    Select the Source View from the bottom of the screen and open the NSExtension node

  4. 移除 NSExtensionMainStoryboard 键并添加值为 CodeBasedViewControllerNSExtensionPrincipalClass

    Remove the NSExtensionMainStoryboard key and add a NSExtensionPrincipalClass with the value CodeBasedViewController

  5. 保存所做更改。

接下来,编辑 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.csCodeBasedViewController.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 文件,并添加一些文本来定义扩展的功能及其安装方式:

Edit the TodayContainers Main.storyboard file and add some text defining the Extensions function and how to install it

保存对情节提要所做的更改。

测试扩展

若要在 iOS 模拟器中测试扩展,请运行 TodayContainer 应用。 将显示容器的主视图:

The containers main view will be displayed

接下来,在模拟器中点击“主页”按钮,从屏幕顶部向下轻扫以打开“通知中心”,选择“今日”选项卡并单击“编辑”按钮

Hit the Home button in the Simulator, swipe down from the top of the screen to open the Notification Center, select the Today tab and click the Edit button

向“今日”视图中添加 DaysRemaining 扩展,然后单击“完成”按钮

Add the DaysRemaining Extension to the Today view and click the Done button

将会在“今日”视图中添加新的小组件,并显示结果

The new widget will be added to the Today view and the results will be displayed

与主机应用通信

上面创建的示例“今日”扩展不会与其主机应用(“今日”屏幕)通信。 如果通信,它将使用 TodayViewControllerCodeBasedViewController 类的 ExtensionContext 属性。

对于将从主机应用接收数据的扩展,数据以 NSExtensionItem 对象数组的形式存储在扩展 UIViewControllerExtensionContextInputItems 属性中。

其他扩展(如照片编辑扩展)可以区分用户完成或取消使用情况。 这将以信号的形式通过 ExtensionContext 属性的 CompleteRequestCancelRequest 方法发送回主机应用。

有关详细信息,请参阅 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 模拟器中测试扩展, 并在最后简要讨论了如何与主机应用通信,以及开发扩展时应采取的一些预防措施和注意事项。