Xamarin.iOS 中的 Handoff
本文介绍如何在 Xamarin.iOS 应用中使用 Handoff,以在用户的其他设备上运行的应用之间传输用户活动。
Apple 在 iOS 8 和 OS X Yosemite (10.10) 中引入了接力功能,为用户提供一种通用机制,可将某个设备上启动的活动传输到运行同一应用的另一台设备或支持相同活动的其他应用。
本文将简要介绍如何在 Xamarin.iOS 应用中启用活动共享,并详细介绍 Handoff 框架:
关于 Handoff
Apple 在 iOS 8 和 OS X Yosemite (10.10) 中引入了接力(也称为连续互通),它可让用户在其一台设备(iOS 或 Mac)上启动活动,并在其另一台设备(由用户的 iCloud 帐户标识)上继续执行相同的活动。
iOS 9 中对接力进行了扩展,还支持新增强的搜索功能。 有关详细信息,请参阅我们的搜索增强文档。
例如,用户可以在其 iPhone 上启动电子邮件,并在 Mac 上无缝地继续发送电子邮件,填写所有相同的邮件信息,并且光标位于他们在 iOS 中离开的相同位置。
共享同一团队 ID 的任何应用都有资格使用 Handoff 继续跨应用的用户活动,前提是这些应用是通过 iTunes App Store 交付的,或者由注册的开发人员(适用于 Mac、Enterprise 或 Ad Hoc 应用)签名。
任何基于 NSDocument
或 UIDocument
的应用都自动内置了 Handoff 支持,并且只需最少更改即可支持 Handoff。
继续用户活动
NSUserActivity
类(以及对 UIKit
和 AppKit
的一些小更改)支持定义用户的活动,这些活动可能会在另一个用户的设备上继续。
若要将活动传递给另一个用户设备,该活动必须封装在实例 NSUserActivity
中,标记为“当前活动”,设置了有效负载(用于执行延续的数据),然后必须将活动传输到该设备。
Handoff 传递最少量的信息来定义要继续的活动,并通过 iCloud 同步更大的数据包。
在接收设备上,用户将收到一条通知,指出活动可用于继续。 如果用户选择在新设备上继续活动,则会启动指定的应用(如果尚未运行),并且 NSUserActivity
中的有效负载用于重启活动。
只有共享同一开发人员团队 ID 并响应给定活动类型的应用才有资格继续。 应用在其 Info.plist 文件的 NSUserActivityTypes
键下定义其支持的活动类型。 鉴于此情况,继续设备会根据团队 ID、活动类型和活动标题选择应用来执行继续。
接收应用使用 NSUserActivity
的 UserInfo
字典中的信息来配置其用户界面并还原给定活动的状态,以便最终用户可以无缝地进行转换。
如果继续需要的信息比通过 NSUserActivity
有效发送的信息更多,则恢复应用可以向发起应用发送呼叫,并建立一个或多个流来传输所需的数据。 例如,如果活动是编辑具有多个图像的大型文本文档,则需要流式传输来传输在接收设备上继续活动所需的信息。 有关详细信息,请参阅下面的支持延续流部分。
如上所述,基于 NSDocument
或 UIDocument
的应用自动内置了 Handoff 支持。 有关详细信息,请参阅下面的在基于文档的应用中支持 Handoff 部分。
NSUserActivity 类
NSUserActivity
类是 Handoff 交换中的主要对象,用于封装可用于继续的用户活动的状态。 应用将实例化它支持的任何活动的 NSUserActivity
副本,并希望在另一台设备上继续。 例如,文档编辑器将为当前打开的每个文档创建一个活动。 但是,只有最前面的文档(显示在最前面的窗口或选项卡中)是“当前活动”,因此可以继续。
NSUserActivity
实例由其 ActivityType
和 Title
属性标识。 UserInfo
字典属性用于传送有关活动状态的信息。 如果要通过 NSUserActivity
的委托延迟加载状态信息,请将 NeedsSave
属性设置为 true
。 根据需要,使用 AddUserInfoEntries
方法将来自其他客户端的新数据合并到 UserInfo
字典中,以保留活动的状态。
NSUserActivityDelegate 类
NSUserActivityDelegate
用于使 NSUserActivity
的 UserInfo
字典中的信息保持最新,并与活动的当前状态同步。 当系统需要更新活动中的信息时(例如在其他设备上继续之前),它会调用委托的 UserActivityWillSave
方法。
你需要实现 UserActivityWillSave
方法,并对 NSUserActivity
(如 UserInfo
、Title
等)进行任何更改,以确保它仍然反映当前活动的状态。 当系统调用 UserActivityWillSave
方法时,NeedsSave
标志将被清除。 如果修改活动的任何数据属性,则需要再次将 NeedsSave
设置为 true
。
可以选择让 UIKit
或 AppKit
自动管理用户活动,而不是使用上述 UserActivityWillSave
方法。 为此,请设置响应方对象的 UserActivity
属性并实现 UpdateUserActivityState
方法。 有关详细信息,请参阅下面的在响应方中支持 Handoff 部分。
应用框架支持
UIKit
(iOS) 和 AppKit
(OS X) 都在 NSDocument
、Responder (UIResponder
/NSResponder
) 和 AppDelegate
类中为 Handoff 提供内置支持。 虽然每个操作系统实现 Handoff 的方式略有不同,但基本机制和 API 是相同的。
基于文档的应用中的用户活动
基于文档的 iOS 和 OS X 应用自动内置 Handoff 支持。 若要激活此支持,需要为应用的 Info.plist 文件中的每个 CFBundleDocumentTypes
条目添加 NSUbiquitousDocumentUserActivityType
键和值。
如果存在此键,则 NSDocument
和 UIDocument
都会自动为指定类型的基于 iCloud 的文档创建 NSUserActivity
实例。 你需要为应用支持的每种文档类型提供一个活动类型,并且多个文档类型可以使用相同的活动类型。 NSDocument
和 UIDocument
都会自动使用 FileURL
属性的值填充 NSUserActivity
的 UserInfo
属性。
在 OS X 上,当文档的窗口变为主窗口时,由 AppKit
管理并与响应方关联的 NSUserActivity
会自动变为“当前活动”。 在 iOS 上,对于由 UIKit
管理的 NSUserActivity
对象,当应用进入前台时,必须显式调用 BecomeCurrent
方法,或者在 UIViewController
上设置文档的 UserActivity
属性。
AppKit
将自动还原 OS X 上以这种方式创建的任何 UserActivity
属性。如果 ContinueUserActivity
方法返回 false
或未实现,则会出现这种情况。 在这种情况下,将使用 NSDocumentController
的 OpenDocument
方法打开文档,然后它将收到 RestoreUserActivityState
方法调用。
有关详细信息,请参阅下面的在基于文档的应用中支持 Handoff 部分。
用户活动和响应方
如果将 UIKit
和 AppKit
设置为响应方对象的 UserActivity
属性,则它们可以自动管理用户活动。 如果状态已修改,则需要将响应方的 UserActivity
的 NeedsSave
属性设置为 true
。 在通过调用其 UpdateUserActivityState
方法为响应方提供更新状态的时间后,系统在需要时会自动保存 UserActivity
。
如果多个响应方共享单个 NSUserActivity
实例,则当系统更新用户活动对象时,它们会收到 UpdateUserActivityState
回调。 响应方需要调用 AddUserInfoEntries
方法来更新 NSUserActivity
的 UserInfo
字典,以反映当前活动状态。 每个 UpdateUserActivityState
调用之前,将清除 UserInfo
字典。
若要将自身与活动取消关联,响应方可以将其 UserActivity
属性设置为 null
。 当应用框架托管的 NSUserActivity
实例没有更多关联的响应方或文档时,它将自动失效。
有关详细信息,请参阅下面的在响应方中支持 Handoff 部分。
用户活动和 AppDelegate
处理 Handoff 继续时,应用的 AppDelegate
是它的主要入口点。 当用户响应 Handoff 通知时,将启动相应的应用(如果尚未运行),并调用 AppDelegate
的 WillContinueUserActivityWithType
方法。 此时,应用应通知用户开始继续。
调用 AppDelegate
的 ContinueUserActivity
方法时,将传递 NSUserActivity
实例。 此时,应配置应用的用户界面并继续给定活动。
有关详细信息,请参阅下面的实现 Handoff 部分。
在 Xamarin 应用中启用 Handoff
由于 Handoff 实施了安全性要求,因此必须在 Apple 开发人员门户和 Xamarin.iOS 项目文件中正确配置使用 Handoff 框架的 Xamarin.iOS 应用。
请执行以下操作:
登录到 Apple 开发人员门户。
单击“证书、标识符和配置文件”。
如果尚未执行此操作,请单击“标识符”并为应用创建 ID(例如
com.company.appname
),否则请编辑现有 ID。确保已检查 iCloud 服务是否有给定 ID:
保存所做更改。
单击“预配配置文件”>“开发”并为应用创建新的开发预配配置文件:
下载并安装新的预配配置文件,或使用 Xcode 下载并安装配置文件。
编辑 Xamarin.iOS 项目选项,并确保使用刚刚创建的预配配置文件:
接下来,编辑 Info.plist 文件,并确保使用用于创建预配配置文件的应用 ID:
滚动到“后台模式”部分,并检查以下项:
保存对所有文件的更改。
进行这些设置后,应用程序即可访问 Handoff 框架 API。 有关预配的详细信息,请参阅我们的设备预配和预配应用指南。
实现 Handoff
可以在使用同一开发人员团队 ID 进行签名的应用中继续用户活动,并支持相同的活动类型。 在 Xamarin.iOS 应用中实现 Handoff 需要创建用户活动对象(在 UIKit
或 AppKit
中),更新对象的状态以跟踪活动,并继续接收设备上的活动。
确定用户活动
实现 Handoff 的第一步是确定应用支持的用户活动类型,并查看哪些活动是在其他设备上继续的良好候选项。 例如:ToDo 应用可能支持将项目编辑为一个用户活动类型,并支持以另一种形式浏览可用项列表。
应用可以根据需要创建任意数量的用户活动类型,一个用于应用提供的任何函数。 对于每种用户活动类型,应用需要跟踪该类型的活动何时开始和结束,并且需要维护最新的状态信息才能在另一台设备上继续执行该任务。
可以在使用同一团队 ID 签名的任何应用上继续执行用户活动,而无需在发送和接收应用之间进行任何一对一映射。 例如,给定的应用可以创建四种不同类型的活动,这些活动由其他设备上的不同单个应用使用。 这是 Mac 版应用(可能具有许多特性和功能)和 iOS 应用(其中每个应用较小且侧重于特定任务)之间的常见情况。
创建活动类型标识符
活动类型标识符是添加到应用的 Info.plist 文件的 NSUserActivityTypes
数组中的一个短字符串,用于唯一识别给定的用户活动类型。 对于应用支持的每个活动,数组中都会有一个条目。 Apple 建议对活动类型标识符使用反向 DNS 样式表示法以避免冲突。 例如:com.company-name.appname.activity
表示基于特定应用的活动,com.company-name.activity
表示可以跨多个应用运行的活动。
创建 NSUserActivity
实例时使用活动类型标识符来标识活动类型。 在另一台设备上继续执行活动时,活动类型(以及应用的团队 ID)将确定启动哪个应用来继续执行该活动。
例如,我们将创建一个名为 MonkeyBrowser 的示例应用。 此应用将显示四个选项卡,每个选项卡在 Web 浏览器视图中打开不同的 URL。 用户将能够继续运行应用的其他 iOS 设备上的任意选项卡。
若要创建支持此行为所需的活动类型标识符,请编辑 Info.plist 文件并切换到“源”视图。 添加 NSUserActivityTypes
键并创建以下标识符:
我们创建了四个新的活动类型标识符,示例 MonkeyBrowser 应用中的每个选项卡一个。 创建你自己的应用时,请将 NSUserActivityTypes
数组的内容替换为特定于应用支持的活动的活动类型标识符。
跟踪用户活动更改
创建 NSUserActivity
类的新实例时,我们将指定一个 NSUserActivityDelegate
实例来跟踪活动状态的更改。 例如,以下代码可用于跟踪状态更改:
using System;
using CoreGraphics;
using Foundation;
using UIKit;
namespace MonkeyBrowse
{
public class UserActivityDelegate : NSUserActivityDelegate
{
#region Constructors
public UserActivityDelegate ()
{
}
#endregion
#region Override Methods
public override void UserActivityReceivedData (NSUserActivity userActivity, NSInputStream inputStream, NSOutputStream outputStream)
{
// Log
Console.WriteLine ("User Activity Received Data: {0}", userActivity.Title);
}
public override void UserActivityWasContinued (NSUserActivity userActivity)
{
Console.WriteLine ("User Activity Was Continued: {0}", userActivity.Title);
}
public override void UserActivityWillSave (NSUserActivity userActivity)
{
Console.WriteLine ("User Activity will be Saved: {0}", userActivity.Title);
}
#endregion
}
}
当延续流从发送设备接收数据时,将调用 UserActivityReceivedData
方法。 有关详细信息,请参阅下面的支持延续流部分。
当另一个设备接管当前设备的活动时,将调用 UserActivityWasContinued
方法。 根据活动类型(例如,向 ToDo 列表添加新项),应用可能需要中止发送设备上的活动。
在本地可用设备上保存和同步对活动的任何更改之前,将调用 UserActivityWillSave
方法。 可以使用此方法在发送 NSUserActivity
实例之前对其 UserInfo
属性进行最后一分钟更改。
创建 NSUserActivity 实例
应用希望提供在另一台设备上继续的可能性的每个活动都必须封装在 NSUserActivity
实例中。 应用可以根据需要创建任意数量的活动,这些活动的性质取决于相关应用的功能和特性。 例如,电子邮件应用可能会创建一个用于创建新邮件的活动,另一个活动用于阅读邮件。
对于示例应用,每次用户在选项卡式 Web 浏览器视图中输入新 URL 时都会创建一个新的 NSUserActivity
。 以下代码存储给定选项卡的状态:
public NSString UserActivityTab1 = new NSString ("com.xamarin.monkeybrowser.tab1");
public NSUserActivity UserActivity { get; set; }
...
UserActivity = new NSUserActivity (UserActivityTab1);
UserActivity.Title = "Weather Tab";
UserActivity.Delegate = new UserActivityDelegate ();
// Update the activity when the tab's URL changes
var userInfo = new NSMutableDictionary ();
userInfo.Add (new NSString ("Url"), new NSString (url));
UserActivity.AddUserInfoEntries (userInfo);
// Inform Activity that it has been updated
UserActivity.BecomeCurrent ();
它使用上面创建的用户活动类型之一创建新的 NSUserActivity
,并为活动提供可读的游戏。 它附加到上面创建的 NSUserActivityDelegate
实例,以监视状态更改,并通知 iOS 此用户活动是“当前活动”。
填充 UserInfo 字典
如上所述,NSUserActivity
类的 UserInfo
属性是用于定义给定活动的状态的键值对的 NSDictionary
。 存储在 UserInfo
中的值必须是下列类型之一:NSArray
、NSData
、NSDate
、NSDictionary
、NSNull
、NSNumber
、NSSet
、NSString
或 NSURL
。 指向 iCloud 文档的 NSURL
数据值将自动调整,以便它们指向接收设备上的相同文档。
在上面的示例中,我们创建了一个 NSMutableDictionary
对象,并使用单个键填充了该对象,其中提供了用户当前在给定选项卡上查看的 URL。用户活动的 AddUserInfoEntries
方法用于使用将用于在接收设备上还原活动的数据更新活动:
// Update the activity when the tab's URL changes
var userInfo = new NSMutableDictionary ();
userInfo.Add (new NSString ("Url"), new NSString (url));
UserActivity.AddUserInfoEntries (userInfo);
Apple 建议将发送的信息保持在最低水平,以确保活动及时发送到接收设备。 如果需要更多信息(例如,需要发送附加到要编辑的文档的图像),则应使用延续流。 有关详细信息,请参阅下面的支持延续流部分。
继续活动
Handoff 将自动通知与发起设备物理接近并登录到同一 iCloud 帐户的本地 iOS 和 OS X 设备可继续的用户活动是否可用。 如果用户选择在新设备上继续活动,系统将启动适当的应用(基于团队 ID 和活动类型),并向其 AppDelegate
提供继续所需的信息。
首先,调用 WillContinueUserActivityWithType
方法,以便应用可以通知用户继续即将开始。 我们在示例应用的 AppDelegate.cs 文件中使用以下代码来处理延续开始:
public NSString UserActivityTab1 = new NSString ("com.xamarin.monkeybrowser.tab1");
public NSString UserActivityTab2 = new NSString ("com.xamarin.monkeybrowser.tab2");
public NSString UserActivityTab3 = new NSString ("com.xamarin.monkeybrowser.tab3");
public NSString UserActivityTab4 = new NSString ("com.xamarin.monkeybrowser.tab4");
...
public FirstViewController Tab1 { get; set; }
public SecondViewController Tab2 { get; set;}
public ThirdViewController Tab3 { get; set; }
public FourthViewController Tab4 { get; set; }
...
public override bool WillContinueUserActivity (UIApplication application, string userActivityType)
{
// Report Activity
Console.WriteLine ("Will Continue Activity: {0}", userActivityType);
// Take action based on the user activity type
switch (userActivityType) {
case "com.xamarin.monkeybrowser.tab1":
// Inform view that it's going to be modified
Tab1.PreparingToHandoff ();
break;
case "com.xamarin.monkeybrowser.tab2":
// Inform view that it's going to be modified
Tab2.PreparingToHandoff ();
break;
case "com.xamarin.monkeybrowser.tab3":
// Inform view that it's going to be modified
Tab3.PreparingToHandoff ();
break;
case "com.xamarin.monkeybrowser.tab4":
// Inform view that it's going to be modified
Tab4.PreparingToHandoff ();
break;
}
// Inform system we handled this
return true;
}
在上面的示例中,每个视图控制器都向 AppDelegate
注册,并具有一个公共 PreparingToHandoff
方法,用于显示活动指示器和一条消息,告知用户活动即将移交给当前设备。 示例:
private void ShowBusy(string reason) {
// Display reason
BusyText.Text = reason;
//Define Animation
UIView.BeginAnimations("Show");
UIView.SetAnimationDuration(1.0f);
Handoff.Alpha = 0.5f;
//Execute Animation
UIView.CommitAnimations();
}
...
public void PreparingToHandoff() {
// Inform caller
ShowBusy ("Continuing Activity...");
}
将调用 AppDelegate
的 ContinueUserActivity
,以实际继续给定的活动。 同样,在我们的示例应用中:
public override bool ContinueUserActivity (UIApplication application, NSUserActivity userActivity, UIApplicationRestorationHandler completionHandler)
{
// Report Activity
Console.WriteLine ("Continuing User Activity: {0}", userActivity.ToString());
// Get input and output streams from the Activity
userActivity.GetContinuationStreams ((NSInputStream arg1, NSOutputStream arg2, NSError arg3) => {
// Send required data via the streams
// ...
});
// Take action based on the Activity type
switch (userActivity.ActivityType) {
case "com.xamarin.monkeybrowser.tab1":
// Preform handoff
Tab1.PerformHandoff (userActivity);
completionHandler (new NSObject[]{Tab1});
break;
case "com.xamarin.monkeybrowser.tab2":
// Preform handoff
Tab2.PerformHandoff (userActivity);
completionHandler (new NSObject[]{Tab2});
break;
case "com.xamarin.monkeybrowser.tab3":
// Preform handoff
Tab3.PerformHandoff (userActivity);
completionHandler (new NSObject[]{Tab3});
break;
case "com.xamarin.monkeybrowser.tab4":
// Preform handoff
Tab4.PerformHandoff (userActivity);
completionHandler (new NSObject[]{Tab4});
break;
}
// Inform system we handled this
return true;
}
每个视图控制器的公共 PerformHandoff
方法实际上执行移交,并还原当前设备上的活动。 在本示例中,它在给定选项卡中显示用户在不同设备上浏览的相同 URL。 示例:
private void HideBusy() {
//Define Animation
UIView.BeginAnimations("Hide");
UIView.SetAnimationDuration(1.0f);
Handoff.Alpha = 0f;
//Execute Animation
UIView.CommitAnimations();
}
...
public void PerformHandoff(NSUserActivity activity) {
// Hide busy indicator
HideBusy ();
// Extract URL from dictionary
var url = activity.UserInfo ["Url"].ToString ();
// Display value
URL.Text = url;
// Display the give webpage
WebView.LoadRequest(new NSUrlRequest(NSUrl.FromString(url)));
// Save activity
UserActivity = activity;
UserActivity.BecomeCurrent ();
}
ContinueUserActivity
方法包括一个 UIApplicationRestorationHandler
,可以调用它来恢复基于文档或响应方的活动。 调用时,需要将 NSArray
或可还原对象传递给还原处理程序。 例如:
completionHandler (new NSObject[]{Tab4});
对于传递的每个对象,将调用其 RestoreUserActivityState
方法。 然后,每个对象都可以使用 UserInfo
字典中的数据还原其自己的状态。 例如:
public override void RestoreUserActivityState (NSUserActivity activity)
{
base.RestoreUserActivityState (activity);
// Log activity
Console.WriteLine ("Restoring Activity {0}", activity.Title);
}
对于基于文档的应用,如果不实现 ContinueUserActivity
方法或该方法返回 false
,则 UIKit
或 AppKit
可以自动恢复活动。 有关详细信息,请参阅下面的在基于文档的应用中支持 Handoff 部分。
正常失败移交
由于 Handoff 依赖于松散连接的 iOS 和 OS X 设备之间的信息传输,因此传输过程有时可能会失败。 应设计应用来正常处理这些失败,并通知用户出现的任何情况。
如果失败,则将调用 AppDelegate
的 DidFailToContinueUserActivitiy
方法。 例如:
public override void DidFailToContinueUserActivitiy (UIApplication application, string userActivityType, NSError error)
{
// Log information about the failure
Console.WriteLine ("User Activity {0} failed to continue. Error: {1}", userActivityType, error.LocalizedDescription);
}
应使用提供的 NSError
向用户提供有关失败的信息。
本机应用到 Web 浏览器移交
用户可能希望在不将适当的本机应用安装在所需设备上的情况下继续执行某活动。 在某些情况下,基于 Web 的界面可以提供所需的功能,并且活动仍然可以继续。 例如,用户的电子邮件帐户可以提供用于撰写和阅读邮件的 Web 基础 UI。
如果发起的本机应用知道 Web 界面的 URL(以及标识要继续的给定项所需的语法),则可以在 NSUserActivity
实例的 WebpageURL
属性中对此信息进行编码。 如果接收设备未安装适当的本机应用来处理继续操作,则可以调用提供的 Web 界面。
Web 浏览器到本机应用移交
如果用户在发起设备上使用基于 Web 的界面,并且接收设备上的本机应用声明 WebpageURL
属性的域部分,则系统将使用该应用处理继续操作。 新设备将收到一个 NSUserActivity
实例,该实例会将活动类型标记为 BrowsingWeb
,WebpageURL
将包含用户正在访问的 URL,UserInfo
字典将为空。
若要让应用参与这种类型的 Handoff,它必须在 com.apple.developer.associated-domains
权利中声明域,格式为 <service>:<fully qualified domain name>
(例如:activity continuation:company.com
)。
如果指定的域与 WebpageURL
属性的值匹配,则 Handoff 将从该域的网站下载已批准的应用 ID 列表。 网站必须在名为 apple-app-site-association 的签名 JSON 文件(例如 https://company.com/apple-app-site-association
)中提供批准的 ID 列表。
此 JSON 文件包含一个字典,该字典以 <team identifier>.<bundle identifier>
形式指定应用 ID 的列表。 例如:
{
"activitycontinuation": {
"apps": [ "YWBN8XTPBJ.com.company.FirstApp",
"YWBN8XTPBJ.com.company.SecondApp" ]
}
}
若要对 JSON 文件进行签名(使其具有正确的 Content-Type
application/pkcs7-mime
),请使用终端应用和 openssl
命令以及由 iOS 信任的证书颁发机构颁发的证书和密钥(请参阅 https://support.apple.com/kb/ht5012 获取列表)。 例如:
echo '{"activitycontinuation":{"apps":["YWBN8XTPBJ.com.company.FirstApp",
"YWBN8XTPBJ.com.company.SecondApp"]}}' > json.txt
cat json.txt | openssl smime -sign -inkey company.com.key
-signer company.com.pem
-certfile intermediate.pem
-noattr -nodetach
-outform DER > apple-app-site-association
openssl
命令会输出一个已签名的 JSON 文件,该文件位于网站上的 apple-app-site-association URL 处。 例如:
https://example.com/apple-app-site-association.
该应用将接收其 WebpageURL
域在其 com.apple.developer.associated-domains
权利中的任何活动。 仅支持 http
和 https
协议,任何其他协议都会引发异常。
在基于文档的应用中支持 Handoff
如上所述,在 iOS 和 OS X 上,如果应用的 Info.plist 文件包含 NSUbiquitousDocumentUserActivityType
的 CFBundleDocumentTypes
键,则基于文档的应用将自动支持基于 iCloud 的文档 Handoff。 例如:
<key>CFBundleDocumentTypes</key>
<array>
<dict>
<key>CFBundleTypeName</key>
<string>NSRTFDPboardType</string>
. . .
<key>LSItemContentTypes</key>
<array>
<string>com.myCompany.rtfd</string>
</array>
. . .
<key>NSUbiquitousDocumentUserActivityType</key>
<string>com.myCompany.myEditor.editing</string>
</dict>
</array>
在此示例中,字符串是一个反向 DNS 应用指示符,其名称追加了活动。 如果以这种方式输入,则不需要在 Info.plist 文件的 NSUserActivityTypes
数组中重复活动类型条目。
自动创建的用户活动对象(通过文档的 UserActivity
属性提供)可由应用中的其他对象引用,并用于在继续时还原状态。 例如,跟踪项目选择和文档位置。 每当状态更改并在 UpdateUserActivityState
方法中更新 UserInfo
字典时,都需要将此活动 NeedsSave
属性设置为 true
。
UserActivity
属性可以从任何线程使用,并且符合键值观察 (KVO) 协议,因此可用于在文档移入和移出 iCloud 时使文档保持同步。 关闭文档时,UserActivity
属性将失效。
有关详细信息,请参阅 Apple 的基于文档的应用中的用户活动支持文档。
在响应方中支持 Handoff
通过设置响应方的 UserActivity
属性,可以将响应方(继承自 iOS 上的 UIResponder
或 OS X 上的 NSResponder
)与活动相关联。 系统会在适当的时间自动保存 UserActivity
属性,调用响应方的 UpdateUserActivityState
方法,使用 AddUserInfoEntriesFromDictionary
方法将当前数据添加到用户活动对象中。
支持延续流
在某些情况下,初始传递有效负载无法有效地传输继续执行活动所需的信息量。 在这些情况下,接收应用可以在自身与发起应用之间建立一个或多个流来传输数据。
发起应用将 NSUserActivity
实例的 SupportsContinuationStreams
属性设置为 true
。 例如:
// Create a new user Activity to support this tab
UserActivity = new NSUserActivity (ThisApp.UserActivityTab1){
Title = "Weather Tab",
SupportsContinuationStreams = true
};
UserActivity.Delegate = new UserActivityDelegate ();
// Update the activity when the tab's URL changes
var userInfo = new NSMutableDictionary ();
userInfo.Add (new NSString ("Url"), new NSString (url));
UserActivity.AddUserInfoEntries (userInfo);
// Inform Activity that it has been updated
UserActivity.BecomeCurrent ();
然后,接收应用可以在其 AppDelegate
中调用 NSUserActivity
的 GetContinuationStreams
方法来建立流。 例如:
public override bool ContinueUserActivity (UIApplication application, NSUserActivity userActivity, UIApplicationRestorationHandler completionHandler)
{
// Report Activity
Console.WriteLine ("Continuing User Activity: {0}", userActivity.ToString());
// Get input and output streams from the Activity
userActivity.GetContinuationStreams ((NSInputStream arg1, NSOutputStream arg2, NSError arg3) => {
// Send required data via the streams
// ...
});
// Take action based on the Activity type
switch (userActivity.ActivityType) {
case "com.xamarin.monkeybrowser.tab1":
// Preform handoff
Tab1.PerformHandoff (userActivity);
completionHandler (new NSObject[]{Tab1});
break;
case "com.xamarin.monkeybrowser.tab2":
// Preform handoff
Tab2.PerformHandoff (userActivity);
completionHandler (new NSObject[]{Tab2});
break;
case "com.xamarin.monkeybrowser.tab3":
// Preform handoff
Tab3.PerformHandoff (userActivity);
completionHandler (new NSObject[]{Tab3});
break;
case "com.xamarin.monkeybrowser.tab4":
// Preform handoff
Tab4.PerformHandoff (userActivity);
completionHandler (new NSObject[]{Tab4});
break;
}
// Inform system we handled this
return true;
}
在发起设备上,用户活动委托通过调用其 DidReceiveInputStream
方法来接收流,以提供为在恢复设备上继续用户活动所请求的数据。
你将使用 NSInputStream
提供对流数据的只读访问权限,NSOutputStream
提供只写访问权限。 流应以请求和响应方式使用,其中接收应用会请求更多数据,而发起应用提供这些数据。 因此,从继续设备上的输入流读取写入到发起设备上的输出流的数据,反之亦然。
即使在需要延续流的情况下,两个应用之间的来回通信也应最少。
有关详细信息,请参阅 Apple 的使用延续流文档。
Handoff 最佳做法
通过 Handoff 成功实现用户活动的无缝延续需要精心设计,因为涉及到各种组件。 Apple 建议对已启用 Handoff 的应用采用以下最佳做法:
- 将用户活动设计为需要尽可能小的有效负载来关联要继续的活动的状态。 有效负载越大,开始延续所需的时间就越长。
- 如果必须传输大量数据才能成功延续,请考虑配置和网络开销所涉及的成本。
- 大型 Mac 应用通常会创建由 iOS 设备上的多个较小的任务特定应用处理的用户活动。 不同应用和操作系统版本应设计为很好地协同工作,否则会正常失败。
- 指定活动类型时,请使用反向 DNS 表示法以避免冲突。 如果活动特定于给定应用,则其名称应包含在类型定义中(例如
com.myCompany.myEditor.editing
)。 如果活动可以跨多个应用工作,请从定义中删除应用名称(例如com.myCompany.editing
)。 - 如果应用需要更新用户活动 (
NSUserActivity
) 的状态,请将NeedsSave
属性设置为true
。 在适当的时间,Handoff 将调用委托的UserActivityWillSave
方法,以便可以根据需要更新UserInfo
字典。 - 由于 Handoff 过程可能不会在接收设备上立即初始化,因此应实现
AppDelegate
的WillContinueUserActivity
,并通知用户继续即将开始。
示例 Handoff 应用
在 Xamarin.iOS 应用中使用 Handoff 的一个示例是 MonkeyBrowser 示例应用。 该应用有四个选项卡,用户可以使用它们浏览 Web,每个选项卡都有一个给定的活动类型:“天气”、“收藏”、“茶歇”和“工作”。
在任何选项卡上,当用户输入新 URL 并点击“定位”按钮时,将为该选项卡创建一个新的 NSUserActivity
,其中包含用户当前正在浏览的 URL:
如果用户的另一个设备安装了 MonkeyBrowser 应用,使用相同的用户帐户登录到 iCloud,位于同一网络上,并且靠近上述设备,则“Handoff 活动”将显示在主屏幕上(左下角):
如果用户在 Handoff 图标上向上拖动,则应用将启动,并且新设备上将继续 NSUserActivity
中指定的用户活动:
当用户活动已成功发送到另一个 Apple 设备时,发送设备的 NSUserActivity
将在其 NSUserActivityDelegate
上收到对 UserActivityWasContinued
方法的调用,以使其知道用户活动已成功传输到另一台设备。
总结
本文介绍了用于在用户的多个 Apple 设备之间继续执行用户活动的 Handoff 框架。 接下来,它演示了如何在 Xamarin.iOS 应用中启用和实现 Handoff。 最后,它讨论了可用的不同类型的 Handoff 延续和 Handoff 最佳做法。