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 应用)签名。

任何基于 NSDocumentUIDocument 的应用都自动内置了 Handoff 支持,并且只需最少更改即可支持 Handoff。

继续用户活动

NSUserActivity 类(以及对 UIKitAppKit 的一些小更改)支持定义用户的活动,这些活动可能会在另一个用户的设备上继续。

若要将活动传递给另一个用户设备,该活动必须封装在实例 NSUserActivity 中,标记为“当前活动”,设置了有效负载(用于执行延续的数据),然后必须将活动传输到该设备。

Handoff 传递最少量的信息来定义要继续的活动,并通过 iCloud 同步更大的数据包。

在接收设备上,用户将收到一条通知,指出活动可用于继续。 如果用户选择在新设备上继续活动,则会启动指定的应用(如果尚未运行),并且 NSUserActivity 中的有效负载用于重启活动。

继续用户活动的概述

只有共享同一开发人员团队 ID 并响应给定活动类型的应用才有资格继续。 应用在其 Info.plist 文件的 NSUserActivityTypes 键下定义其支持的活动类型。 鉴于此情况,继续设备会根据团队 ID、活动类型和活动标题选择应用来执行继续。

接收应用使用 NSUserActivityUserInfo 字典中的信息来配置其用户界面并还原给定活动的状态,以便最终用户可以无缝地进行转换。

如果继续需要的信息比通过 NSUserActivity 有效发送的信息更多,则恢复应用可以向发起应用发送呼叫,并建立一个或多个流来传输所需的数据。 例如,如果活动是编辑具有多个图像的大型文本文档,则需要流式传输来传输在接收设备上继续活动所需的信息。 有关详细信息,请参阅下面的支持延续流部分。

如上所述,基于 NSDocumentUIDocument 的应用自动内置了 Handoff 支持。 有关详细信息,请参阅下面的在基于文档的应用中支持 Handoff 部分。

NSUserActivity 类

NSUserActivity 类是 Handoff 交换中的主要对象,用于封装可用于继续的用户活动的状态。 应用将实例化它支持的任何活动的 NSUserActivity 副本,并希望在另一台设备上继续。 例如,文档编辑器将为当前打开的每个文档创建一个活动。 但是,只有最前面的文档(显示在最前面的窗口或选项卡中)是“当前活动”,因此可以继续。

NSUserActivity 实例由其 ActivityTypeTitle 属性标识。 UserInfo 字典属性用于传送有关活动状态的信息。 如果要通过 NSUserActivity 的委托延迟加载状态信息,请将 NeedsSave 属性设置为 true。 根据需要,使用 AddUserInfoEntries 方法将来自其他客户端的新数据合并到 UserInfo 字典中,以保留活动的状态。

NSUserActivityDelegate 类

NSUserActivityDelegate 用于使 NSUserActivityUserInfo 字典中的信息保持最新,并与活动的当前状态同步。 当系统需要更新活动中的信息时(例如在其他设备上继续之前),它会调用委托的 UserActivityWillSave 方法。

你需要实现 UserActivityWillSave 方法,并对 NSUserActivity(如 UserInfoTitle 等)进行任何更改,以确保它仍然反映当前活动的状态。 当系统调用 UserActivityWillSave 方法时,NeedsSave 标志将被清除。 如果修改活动的任何数据属性,则需要再次将 NeedsSave 设置为 true

可以选择让 UIKitAppKit 自动管理用户活动,而不是使用上述 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 键和值。

如果存在此键,则 NSDocumentUIDocument 都会自动为指定类型的基于 iCloud 的文档创建 NSUserActivity 实例。 你需要为应用支持的每种文档类型提供一个活动类型,并且多个文档类型可以使用相同的活动类型。 NSDocumentUIDocument 都会自动使用 FileURL 属性的值填充 NSUserActivityUserInfo 属性。

在 OS X 上,当文档的窗口变为主窗口时,由 AppKit 管理并与响应方关联的 NSUserActivity 会自动变为“当前活动”。 在 iOS 上,对于由 UIKit 管理的 NSUserActivity 对象,当应用进入前台时,必须显式调用 BecomeCurrent 方法,或者在 UIViewController 上设置文档的 UserActivity 属性。

AppKit 将自动还原 OS X 上以这种方式创建的任何 UserActivity 属性。如果 ContinueUserActivity 方法返回 false 或未实现,则会出现这种情况。 在这种情况下,将使用 NSDocumentControllerOpenDocument 方法打开文档,然后它将收到 RestoreUserActivityState 方法调用。

有关详细信息,请参阅下面的在基于文档的应用中支持 Handoff 部分。

用户活动和响应方

如果将 UIKitAppKit 设置为响应方对象的 UserActivity 属性,则它们可以自动管理用户活动。 如果状态已修改,则需要将响应方的 UserActivityNeedsSave 属性设置为 true。 在通过调用其 UpdateUserActivityState 方法为响应方提供更新状态的时间后,系统在需要时会自动保存 UserActivity

如果多个响应方共享单个 NSUserActivity 实例,则当系统更新用户活动对象时,它们会收到 UpdateUserActivityState 回调。 响应方需要调用 AddUserInfoEntries 方法来更新 NSUserActivityUserInfo 字典,以反映当前活动状态。 每个 UpdateUserActivityState 调用之前,将清除 UserInfo 字典。

若要将自身与活动取消关联,响应方可以将其 UserActivity 属性设置为 null。 当应用框架托管的 NSUserActivity 实例没有更多关联的响应方或文档时,它将自动失效。

有关详细信息,请参阅下面的在响应方中支持 Handoff 部分。

用户活动和 AppDelegate

处理 Handoff 继续时,应用的 AppDelegate 是它的主要入口点。 当用户响应 Handoff 通知时,将启动相应的应用(如果尚未运行),并调用 AppDelegateWillContinueUserActivityWithType 方法。 此时,应用应通知用户开始继续。

调用 AppDelegateContinueUserActivity 方法时,将传递 NSUserActivity 实例。 此时,应配置应用的用户界面并继续给定活动。

有关详细信息,请参阅下面的实现 Handoff 部分。

在 Xamarin 应用中启用 Handoff

由于 Handoff 实施了安全性要求,因此必须在 Apple 开发人员门户和 Xamarin.iOS 项目文件中正确配置使用 Handoff 框架的 Xamarin.iOS 应用。

请执行以下操作:

  1. 登录到 Apple 开发人员门户

  2. 单击“证书、标识符和配置文件”。

  3. 如果尚未执行此操作,请单击“标识符”并为应用创建 ID(例如 com.company.appname),否则请编辑现有 ID。

  4. 确保已检查 iCloud 服务是否有给定 ID:

    为给定 ID 启用 iCloud 服务

  5. 保存所做更改。

  6. 单击“预配配置文件”>“开发”并为应用创建新的开发预配配置文件:

    为应用创建新开发预配配置文件

  7. 下载并安装新的预配配置文件,或使用 Xcode 下载并安装配置文件。

  8. 编辑 Xamarin.iOS 项目选项,并确保使用刚刚创建的预配配置文件:

    选择刚刚创建的预配配置文件

  9. 接下来,编辑 Info.plist 文件,并确保使用用于创建预配配置文件的应用 ID:

    设置应用 ID

  10. 滚动到“后台模式”部分,并检查以下项:

    启用所需后台模式

  11. 保存对所有文件的更改。

进行这些设置后,应用程序即可访问 Handoff 框架 API。 有关预配的详细信息,请参阅我们的设备预配预配应用指南。

实现 Handoff

可以在使用同一开发人员团队 ID 进行签名的应用中继续用户活动,并支持相同的活动类型。 在 Xamarin.iOS 应用中实现 Handoff 需要创建用户活动对象(在 UIKitAppKit 中),更新对象的状态以跟踪活动,并继续接收设备上的活动。

确定用户活动

实现 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 键并创建以下标识符:

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 中的值必须是下列类型之一:NSArrayNSDataNSDateNSDictionaryNSNullNSNumberNSSetNSStringNSURL。 指向 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...");
}

将调用 AppDelegateContinueUserActivity,以实际继续给定的活动。 同样,在我们的示例应用中:

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,则 UIKitAppKit 可以自动恢复活动。 有关详细信息,请参阅下面的在基于文档的应用中支持 Handoff 部分。

正常失败移交

由于 Handoff 依赖于松散连接的 iOS 和 OS X 设备之间的信息传输,因此传输过程有时可能会失败。 应设计应用来正常处理这些失败,并通知用户出现的任何情况。

如果失败,则将调用 AppDelegateDidFailToContinueUserActivitiy 方法。 例如:

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 实例,该实例会将活动类型标记为 BrowsingWebWebpageURL 将包含用户正在访问的 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-Typeapplication/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 权利中的任何活动。 仅支持 httphttps 协议,任何其他协议都会引发异常。

在基于文档的应用中支持 Handoff

如上所述,在 iOS 和 OS X 上,如果应用的 Info.plist 文件包含 NSUbiquitousDocumentUserActivityTypeCFBundleDocumentTypes 键,则基于文档的应用将自动支持基于 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 中调用 NSUserActivityGetContinuationStreams 方法来建立流。 例如:

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 过程可能不会在接收设备上立即初始化,因此应实现 AppDelegateWillContinueUserActivity,并通知用户继续即将开始。

示例 Handoff 应用

在 Xamarin.iOS 应用中使用 Handoff 的一个示例是 MonkeyBrowser 示例应用。 该应用有四个选项卡,用户可以使用它们浏览 Web,每个选项卡都有一个给定的活动类型:“天气”、“收藏”、“茶歇”和“工作”。

在任何选项卡上,当用户输入新 URL 并点击“定位”按钮时,将为该选项卡创建一个新的 NSUserActivity,其中包含用户当前正在浏览的 URL:

示例 Handoff 应用

如果用户的另一个设备安装了 MonkeyBrowser 应用,使用相同的用户帐户登录到 iCloud,位于同一网络上,并且靠近上述设备,则“Handoff 活动”将显示在主屏幕上(左下角):

左下角主屏幕上显示的 Handoff 活动

如果用户在 Handoff 图标上向上拖动,则应用将启动,并且新设备上将继续 NSUserActivity 中指定的用户活动:

在新设备上继续的用户活动

当用户活动已成功发送到另一个 Apple 设备时,发送设备的 NSUserActivity 将在其 NSUserActivityDelegate 上收到对 UserActivityWasContinued 方法的调用,以使其知道用户活动已成功传输到另一台设备。

总结

本文介绍了用于在用户的多个 Apple 设备之间继续执行用户活动的 Handoff 框架。 接下来,它演示了如何在 Xamarin.iOS 应用中启用和实现 Handoff。 最后,它讨论了可用的不同类型的 Handoff 延续和 Handoff 最佳做法。