次の方法で共有


Xamarin.iOS でのハンドオフ

この記事では、Xamarin.iOS アプリでハンドオフを使用して、ユーザーの他のデバイスで実行されているアプリ間でユーザー アクティビティを転送する方法について説明します。

Apple は、iOS 8 と OS X Yosemite (10.10) で、ユーザーが自分のデバイスの 1 つで開始したアクティビティを、同じアプリを実行している別のデバイスまたは同じアクティビティをサポートする別のアプリに転送する共通のメカニズムを提供する Handoff を導入しました。

ハンドオフ操作の実行例

この記事では、Xamarin.iOS アプリでアクティビティ共有を有効にする方法について簡単に説明し、ハンドオフ フレームワークについて詳しく説明します:

ハンドオフについて

Handoff (継続性とも呼ばれる) は、ユーザーがいずれかのデバイス (iOS または Mac) でアクティビティを開始し、その同じアクティビティを別のデバイス (ユーザーの iCloud アカウントによって特定される) で継続する方法として、iOS 8 および OS X Yosemite (10.10) で Apple によって導入されました。

iOS 9 では Handoff が強化され、新しく強化された検索機能もサポートされました。 詳細については、検索の機能強化に関するドキュメントを参照してください。

たとえば、ユーザーは自分の iPhone でメールを開始し、同じメッセージ情報がすべて入力された状態で、カーソルを iOS に残したのと同じ場所に置いて、Mac でシームレスにメールを続行できます。

同じ チーム ID を共有するアプリは、これらのアプリが iTunes App Store 経由で配信されるか、登録済みの開発者 (Mac、エンタープライズ、またはアドホック アプリの場合) によって署名されている限り、ハンドオフを使用してアプリ間でユーザー アクティビティを継続する資格があります。

任意 NSDocument のアプリまたは UIDocument ベースのアプリにはハンドオフ サポートが自動的に組み込まれており、ハンドオフをサポートするために最小限の変更が必要です。

ユーザー アクティビティの継続

NSUserActivity クラス (UIKitAppKit に対するいくつかの小さな変更と共に) は、ユーザーの別のデバイスで継続できる可能性のあるユーザーのアクティビティを定義するためのサポートを提供します。

アクティビティを別のユーザーのデバイスに渡すには、アクティビティをインスタンス NSUserActivity にカプセル化し、現在のアクティビティとしてマークし、ペイロードセット (継続を実行するために使用されるデータ) を設定し、そのアクティビティをそのデバイスに送信する必要があります。

ハンドオフでは、継続するアクティビティをていぎするための最小限の情報が渡され、より大きなデータは iCloud 経由で同期されます。

受信デバイスで、ユーザーはアクティビティが継続可能であることを示す通知を受け取ります。 ユーザーが新しいデバイスでアクティビティを続行することを選択した場合は、指定されたアプリが起動され (まだ実行されていない場合)、NSUserActivity からのペイロードがアクティビティの再起動に使用されます。

ユーザー アクティビティ続行の概要

同じ開発者チーム ID を共有し、特定のアクティビティの種類に応答するアプリのみが継続の対象となります。 アプリは、Info.plist ファイルの NSUserActivityTypes キーの下でサポートするアクティビティの種類を定義します。 この場合、継続デバイスは、チーム ID、アクティビティの種類、および必要に応じてアクティビティ タイトルに基づいて継続を実行するアプリを選択します。

受信側のアプリは、NSUserActivityUserInfo 辞書からの情報を使用してユーザー インターフェイスを構成し、エンド ユーザーへの移行がシームレスに表示されるように、特定のアクティビティの状態を復元します。

継続に、NSUserActivity 経由で効率的に送信できる以上の情報が必要な場合、再開アプリは発信元アプリに呼び出しを送信し、必要なデータを送信するための 1 つ以上のストリームを確立できます。 たとえば、アクティビティが複数の画像を含む大きなテキスト ドキュメントを編集していた場合、受信デバイスでアクティビティを続行するために必要な情報を転送するためにストリーミングが必要になります。 詳細については、以下の「継続ストリームのサポート」セクションを参照してください。

前述のように、NSDocument または UIDocument ベースのアプリには、ハンドオフ サポートが自動的に組み込まれています。 詳細については、以下の「ドキュメントベースのアプリでのハンドオフのサポート」セクションを参照してください。

NSUserActivity クラス

NSUserActivity クラスはハンドオフ交換の主オブジェクトであり、継続可能なユーザー アクティビティの状態をカプセル化するために使用されます。 アプリは、サポートされ、別のデバイスで続行するアクティビティの NSUserActivity のコピーをインスタンス化します。 たとえば、ドキュメント エディターでは、現在開いている各ドキュメントのアクティビティが作成されます。 ただし、最上位のウィンドウまたはタブに表示される最前面のドキュメントのみが現在のアクティビティであるため、継続に使用できます。

NSUserActivity のインスタンスは、その ActivityType プロパティと Title プロパティの両方によって識別されます。 UserInfo 辞書プロパティは、アクティビティの状態に関する情報を伝達するために使用されます。 NSUserActivity のデリゲートを介して状態情報を遅延読み込みする場合は、NeedsSave プロパティを true に設定します。 AddUserInfoEntries メソッドを使用して、アクティビティの状態を維持するために必要に応じて、他のクライアントの新しいデータを UserInfo 辞書にマージします。

NSUserActivityDelegate クラス

NSUserActivityDelegate は、NSUserActivityUserInfo 辞書の情報を最新の状態に保ち、アクティビティの現在の状態と同期するために使用されます。 システムは、アクティビティの情報を更新する必要がある場合 (別のデバイスでの継続前など)、デリゲートの UserActivityWillSave メソッドを呼び出します。

UserActivityWillSave メソッドを実装し、(UserInfoTitle など) NSUserActivity に変更を加えて、現在のアクティビティの状態が確実に反映されるようにする必要があります。 システムが UserActivityWillSave メソッドを呼び出すと、NeedsSave フラグがクリアされます。 アクティビティのいずれかのデータ プロパティを変更する場合は、もう一度 NeedsSavetrue に設定する必要があります。

上記の UserActivityWillSave メソッドを使用する代わりに、必要に応じて、ユーザー アクティビティを自動的に UIKit または AppKit 管理できます。 これを行うには、レスポンダー オブジェクトの UserActivity プロパティを設定し、 UpdateUserActivityState メソッドを実装します。 詳細については、後述の「レスポンダーでのハンドオフのサポート」セクションを参照してください。

アプリ フレームワークのサポート

UIKit (iOS) と AppKit (OS X) の両方で、NSDocument レスポンダー (UIResponder/NSResponder)、および AppDelegate クラスでのハンドオフのサポートが組み込まれています。 各 OS のハンドオフの実装は若干異なりますが、基本的なメカニズムと API は同じです。

ドキュメント ベースのアプリのユーザー アクティビティ

ドキュメント ベースの iOS アプリと OS X アプリには、ハンドオフサポートが自動的に組み込まれています。 このサポートをアクティブにするには、アプリの Info.plist ファイル内の各 CFBundleDocumentTypes エントリに NSUbiquitousDocumentUserActivityType キーと値を追加する必要があります。

このキーが存在する場合、NSDocumentUIDocument の両方で、指定した種類の iCloud ベースのドキュメントの NSUserActivity インスタンスが自動的に作成されます。 アプリがサポートするドキュメントの種類ごとにアクティビティの種類を指定する必要があり、複数のドキュメントの種類で同じアクティビティの種類を使用できます。 NSDocumentUIDocument の両方で、NSUserActivityUserInfo プロパティに FileURL プロパティの値が自動的に設定されます。

OS X では、 AppKit によって管理され、レスポンダーに関連付けられている NSUserActivity は、ドキュメントのウィンドウがメイン ウィンドウになると、自動的に現在のアクティビティになります。 iOS では、UIKit によって管理される NSUserActivity オブジェクトの場合、BecomeCurrent メソッドを明示的に呼び出すか、アプリが前面になったときにドキュメントの UserActivity プロパティを UIViewController に設定する必要があります。

AppKit は、OS X でこのように作成されたすべての UserActivity プロパティを自動的に復元します。これは、ContinueUserActivity メソッドが false を返す場合、または実装されていない場合に発生します。 この場合、ドキュメントは NSDocumentControllerOpenDocument メソッドで開き、RestoreUserActivityState メソッド呼び出しを受け取ります。

詳細については、以下の「ドキュメントベースのアプリでのハンドオフのサポート」セクションを参照してください。

ユーザー アクティビティとレスポンダー

レスポンダー オブジェクトの UserActivity プロパティとして設定すると、UIKitAppKit の両方でユーザー アクティビティを自動的に管理できます。 状態が変更された場合は、レスポンダーの UserActivityNeedsSave プロパティを true に設定する必要があります。 システムは、UpdateUserActivityState メソッドを呼び出してレスポンダーに状態を更新する時間を与えた後、必要に応じて UserActivity を自動的に保存します。

複数のレスポンダーが 1 つの NSUserActivity インスタンスを共有する場合、システムがユーザー アクティビティ オブジェクトを更新すると、UpdateUserActivityState コールバックを受け取ります。 レスポンダーは、 AddUserInfoEntries メソッドを呼び出して、NSUserActivityUserInfo 辞書を更新して、この時点での現在のアクティビティの状態を反映する必要があります。 UserInfo 辞書は、各 UpdateUserActivityState 呼び出しの前にクリアされます。

レスポンダーは、アクティビティとの関連付けを解除するために、UserActivity プロパティを nullに設定できます。 アプリ フレームワークのマネージド NSUserActivity インスタンスにレスポンダーやドキュメントが関連付けられていない場合、自動的に無効になります。

詳細については、後述の「レスポンダーでのハンドオフのサポート」セクションを参照してください。

ユーザー アクティビティと AppDelegate

ハンドオフ継続を処理する場合、アプリの AppDelegate が主要なエントリ ポイントになります。 ユーザーがハンドオフ通知に応答すると、適切なアプリが起動され (まだ実行されていない場合)、AppDelegateWillContinueUserActivityWithType メソッドが呼び出されます。 この時点で、アプリは継続が開始されていることをユーザーに通知する必要があります。

NSUserActivity インスタンスは、AppDelegateContinueUserActivity メソッドが呼び出されたときに配信されます。 この時点で、アプリのユーザー インターフェイスを構成し、特定のアクティビティを続行する必要があります。

詳細については、以下の「ハンドオフの実装」セクションを参照してください。

Xamarin アプリでのハンドオフの有効化

ハンドオフによって課されるセキュリティ要件のため、ハンドオフ フレームワークを使用する Xamarin.iOS アプリは、Apple Developer ポータルと Xamarin.iOS プロジェクト ファイルの両方で適切に構成する必要があります。

次の操作を行います。

  1. Apple Developer ポータルにログインします。

  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 Framework API にアクセスする準備が整いました。 プロビジョニングの詳細については、「デバイス プロビジョニング」ガイド と「アプリのプロビジョニング」ガイドを参照してください。

ハンドオフの実装

ユーザー アクティビティは、同じ開発者チーム ID で署名され、同じアクティビティの種類をサポートするアプリ間で継続できます。 Xamarin.iOS アプリでハンドオフを実装するには、ユーザー アクティビティ オブジェクト (UIKit または AppKit) を作成し、アクティビティを追跡するようにオブジェクトの状態を更新し、受信デバイスでアクティビティを続行する必要があります。

ユーザー アクティビティの識別

ハンドオフを実装する最初の手順は、アプリがサポートするユーザー アクティビティの種類を特定し、それらのアクティビティのうち、別のデバイスでの継続の候補となるものを確認することです。 たとえば、ToDo アプリでは、アイテムの編集を 1 つのユーザー アクティビティの種類としてサポートし、使用可能な項目リストを別のユーザー アクティビティの種類として参照できます。

アプリは、必要な数のユーザー アクティビティの種類を作成できます。1 つは、アプリが提供する任意の関数用です。 ユーザー アクティビティの種類ごとに、アプリは種類のアクティビティの開始と終了を追跡する必要があり、別のデバイスでそのタスクを続行するには、最新の状態情報を維持する必要があります。

ユーザー アクティビティは、同じチーム ID で署名された任意のアプリで続行でき、送受信アプリ間で 1 対 1 のマッピングを行う必要はありません。 たとえば、特定のアプリは、別のデバイス上の異なる個々のアプリによって消費される 4 種類のアクティビティを作成できます。 これは、アプリの Mac バージョン (多くの機能を持つ可能性がある) と iOS アプリの間で一般的に発生します。各アプリは小さく、特定のタスクに重点を置きます。

アクティビティの種類識別子の作成

アクティビティの種類識別子は、特定のユーザー アクティビティの種類を一意に識別するために使用される、アプリの Info.plist ファイルの NSUserActivityTypes 配列に追加される短い文字列です。 配列には、アプリがサポートするアクティビティごとに 1 つのエントリがあります。 Apple では、競合を回避するために、アクティビティの種類識別子に逆引き DNS スタイルの表記を使用することを勧めています。 例えば、特定のアプリ ベースのアクティビティには com.company-name.appname.activity、複数のアプリにまたがって実行できるアクティビティには com.company-name.activity を指定します。

アクティビティの種類識別子は、アクティビティの種類を識別するために NSUserActivity インスタンスを作成するときに使用されます。 アクティビティが別のデバイスで継続されると、アクティビティの種類 (アプリのチーム ID と共に) によって、アクティビティを続行するために起動するアプリが決まります。

例として、MonkeyBrowser という サンプル アプリを作成します。 このアプリには 4 つのタブが表示され、それぞれ異なる URL が Web ブラウザー ビューで開かれます。 ユーザーは、アプリを実行している別の iOS デバイスで任意のタブを続行できます。

この動作をサポートするのに必要なアクティビティの種類識別子を作成するには、Info.plist ファイルを編集し、"ソース" ビューに切り替えます。 NSUserActivityTypes キーを追加し、次の識別子を作成します:

plist エディターの NSUserActivityTypes キーと必要な識別子

例の MonkeyBrowser アプリの タブごとに 1 つずつ、4 つの新しいアクティビティの種類識別子を作成しました。 独自のアプリを作成するときは、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 に格納される値は、NSArrayNSDataNSDateNSDictionaryNSNullNSNumberNSSetNSString、または 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 は、アクティビティが受信デバイスにタイムリーに送信されるように、送信される情報を最小限に抑えておけることをお勧めします。 ドキュメントに添付された画像を編集する必要があるなど、より大きな情報が必要な場合は、継続ストリームを使用する必要があります。 詳細については、以下の「継続ストリームのサポート」セクションを参照してください。

アクティビティの継続

ハンドオフは、発信元デバイスに物理的に近く、同じ 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 を返した場合、UIKit または AppKit はアクティビティを自動的に再開できます。 詳細については、以下の「ドキュメントベースのアプリでのハンドオフのサポート」セクションを参照してください。

ハンドオフの適切な失敗

ハンドオフは、緩やかに接続された 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 プロパティのドメイン部分を要求している場合、システムはそのアプリを継続のハンドルとして使用します。 新しいデバイスは、アクティビティの種類を BrowsingWeb としてマークする NSUserActivity インスタンスを受け取り、WebpageURL にはユーザーがアクセスした URL が含まれ、UserInfo ディクショナリは空になります。

アプリがこの種類のハンドオフに参加するには、形式が <service>:<fully qualified domain name> (例: activity continuation:company.com) の com.apple.developer.associated-domains エンタイトルメントでドメインを要求する必要があります。

指定したドメインが WebpageURL プロパティの値と一致する場合、ハンドオフは、そのドメインの Web サイトから承認済みアプリ ID の一覧をダウンロードします。 Web サイトでは、apple-app-site-association (たとえば、https://company.com/apple-app-site-association) という名前の署名済み JSON ファイルに承認済み ID の一覧を提供する必要があります。

この JSON ファイルには、<team identifier>.<bundle identifier> 形式のアプリ ID の一覧を指定する辞書が含まれています。 次に例を示します。

{
    "activitycontinuation": {
        "apps": [    "YWBN8XTPBJ.com.company.FirstApp",
            "YWBN8XTPBJ.com.company.SecondApp" ]
    }
}

JSON ファイルに署名するには (application/pkcs7-mime の正しい Content-Type が含まれるように)、ターミナル アプリと、iOS によって信頼されている証明機関によって発行された証明書とキーを使用して openssl コマンドを使用します (一覧については、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 コマンドは、Web サイトに配置する署名付き JSON ファイルを、apple-app-site-association URL に出力します。 次に例を示します。

https://example.com/apple-app-site-association.

アプリは、WebpageURL ドメインがその com.apple.developer.associated-domains エンタイトルメントにあるすべてのアクティビティを受け取ります。 http プロトコルと https プロトコルのみがサポートされており、他のプロトコルでは例外が発生します。

ドキュメント ベースのアプリでのハンドオフのサポート

前述のように、iOS および OS X では、アプリの info.plist ファイルに NSUbiquitousDocumentUserActivityTypeCFBundleDocumentTypes キーが含まれている場合、ドキュメント ベースのアプリは iCloud ベースのドキュメントのハンドオフを自動的にサポートします。 次に例を示します。

<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 プロパティを通じて使用可能) は、アプリ内の他のオブジェクトから参照でき、継続時の状態の復元に使用できます。 たとえば、アイテムの選択とドキュメントの位置を追跡します。 このアクティビティ NeedsSave プロパティは、状態が変化するたびに true に設定し、UpdateUserActivityState メソッドで UserInfo 辞書を更新する必要があります。

UserActivity プロパティは、任意のスレッドから使用でき、キー値監視 (KVO) プロトコルに準拠しているため、iCloud との間でドキュメントの移動時にドキュメントを同期するために使用できます。 ドキュメントを閉じると、UserActivity プロパティは無効になります。

詳細については、ドキュメント ベースのアプリの Apple のユーザー アクティビティ サポート ドキュメントを参照してください。

レスポンダーでのハンドオフのサポート

レスポンダー (iOS 上の UIResponder または OS X の NSResponder から継承) をアクティビティに関連付けるには UserActivity プロパティを設定します。 システムは、UserActivity プロパティを適切な時刻に自動的に保存し、レスポンダーの UpdateUserActivityState メソッドを呼び出して、AddUserInfoEntriesFromDictionary メソッドを使用して現在のデータをユーザー アクティビティ オブジェクトに追加します。

継続ストリームのサポート

アクティビティを続行するために必要な情報の量を、最初のハンドオフ ペイロードによって効率的に転送できない場合があります。 このような状況では、受信側のアプリは、それ自体と元のアプリの間に 1 つ以上のストリームを確立してデータを転送できます。

元のアプリは、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 は書き込み専用アクセスを提供します。 ストリームは要求と応答の方法で使用する必要があります。この方法では、受信アプリはより多くのデータを要求し、元のアプリが提供します。 そのため、送信元デバイスの出力ストリームに書き込まれたデータは、継続デバイス上の入力ストリームから読み取られます。その逆も同様です。

継続ストリームが必要な場合でも、2 つのアプリ間の通信は最小限に抑える必要があります。

詳細については、Apple の継続ストリームの使用に関するドキュメントを参照してください。

ハンドオフのベスト プラクティス

ハンドオフによるユーザー アクティビティのシームレスな継続の正常な実装には、関連するすべてのコンポーネントが含まれているため、慎重な設計が必要です。 Apple では、ハンドオフ対応アプリに次のベスト プラクティスを採用することをお勧めします:

  • アクティビティの状態を引き続き関連付けるために、可能な限り最小のペイロードを要求するようにユーザー アクティビティを設計します。 ペイロードが大きいほど、継続の開始にかかる時間が長くなります。
  • 継続を成功させるために大量のデータを転送する必要がある場合は、構成とネットワークのオーバーヘッドに関連するコストを考慮してください。
  • 大規模な Mac アプリでは、iOS デバイス上の複数の小規模なタスク固有のアプリによって処理されるユーザー アクティビティを作成するのが一般的です。 さまざまなアプリと OS のバージョンは、正常に連携するか、適切に失敗するように設計する必要があります。
  • アクティビティの種類を指定するときは、逆引き DNS 表記を使用して競合を回避します。 アクティビティが特定のアプリに固有の場合は、その名前を型定義に含める必要があります (たとえば com.myCompany.myEditor.editing)。 アクティビティが複数のアプリで機能する場合は、定義からアプリ名を削除します (たとえば com.myCompany.editing)。
  • アプリでユーザー アクティビティ (NSUserActivity) の状態を更新する必要がある場合は、NeedsSave プロパティを true に設定します。 必要に応じて、Handoff はデリゲートの UserActivityWillSave メソッドを呼び出して、必要に応じて UserInfo 辞書を更新できるようにします。
  • ハンドオフ プロセスは受信側デバイスですぐに初期化されない可能性があるため、AppDelegateWillContinueUserActivity を実装し、継続が開始されることをユーザーに通知する必要があります。

ハンドオフ アプリの例

Xamarin.iOS アプリでハンドオフを使用する例として、MonkeyBrowser サンプル アプリがあります。 アプリには、ユーザーが Web を閲覧するために使用できる 4 つのタブがあり、それぞれに特定のアクティビティの種類 (天気、お気に入り、コーヒー ブレーク、仕事) があります。

任意のタブで、ユーザーが新しい URL を入力し、[移動] ボタンをタップすると、ユーザーが現在参照している URL を含む新しい NSUserActivity がタブに作成されます:

ハンドオフ アプリの例

別のユーザーのデバイスに MonkeyBrowser アプリがインストールされていて、同じユーザー アカウントを使用して iCloud にサインインしている場合、同じネットワーク上にあり、上記のデバイスに近接している場合は、ホーム画面 (左下隅) にハンドオフ アクティビティが表示されます:

ホーム画面の左下隅に表示されるハンドオフ アクティビティ

ユーザーがハンドオフ アイコン上にドラッグすると、アプリが起動し、NSUserActivity で指定されたユーザー アクティビティが新しいデバイスで続行されます:

新しいデバイスでユーザー アクティビティが続行されました

ユーザー アクティビティが別の Apple デバイスに正常に送信されると、送信側デバイスの NSUserActivity は、その NSUserActivityDelegateUserActivityWasContinued メソッドの呼び出しを受け取り、ユーザー アクティビティが別のデバイスに正常に転送されたことを通知します。

まとめ

この記事では、ユーザーの複数の Apple デバイス間でユーザー アクティビティを続行するために使用されるハンドオフ フレームワークの概要について説明しました。 次に、Xamarin.iOS アプリでハンドオフを有効にして実装する方法について説明しました。 最後に、利用可能なさまざまな種類のハンドオフ継続とハンドオフのベスト プラクティスについて説明しました。