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