Xamarin.iOS での iCloud の使用

iOS 5i の iCloud Storage API を使用すると、アプリケーションでユーザー ドキュメントとアプリケーション固有のデータを中央の場所に保存し、すべてのユーザーのデバイスからそれらの項目にアクセスできます。

使用できるストレージには、次の 4 つの種類があります。

  • キーと値のストレージ - 少量のデータをユーザーの他のデバイス上のアプリケーションと共有します。

  • UIDocument ストレージ - UIDocument のサブクラスを使用して、ドキュメントやその他のデータをユーザーの iCloud アカウントに格納します。

  • CoreData - SQLite データベース ストレージ。

  • 個々のファイルとディレクトリ - ファイル システムで多数の異なるファイルを直接管理できます。

このドキュメントでは、最初の 2 つの種類 (キーと値のペアおよび UIDocument サブクラス) と、Xamarin.iOS でのそれらの使用方法について説明します。

重要

Apple からは、開発者が欧州連合の一般データ保護規則 (GDPR) を適切に処理するためのツールが提供されています。

要件

  • Xamarin.iOS の最新の安定バージョン
  • Xcode 10
  • Visual Studio for Mac または Visual Studio 2019。

iCloud 向けの開発の準備

Apple プロビジョニング ポータルとプロジェクト自体の両方で、iCloud を使用するようにアプリケーションを構成する必要があります。 iCloud 向けの開発を行う (サンプルを試用する) 前に、次の手順に従います。

iCloud にアクセスするようにアプリケーションを正しく構成するには、次の手順を行います。

Check iCloud as an allowed service

  • 新しいプロビジョニング プロファイルを作成する - プロビジョニング プロファイルを作成するには、デバイス プロビジョニング ガイドに説明されている手順に従います。

  • コンテナー識別子を Entitlements.plist に追加する - コンテナー識別子の形式は、TeamID.BundleID です。 詳細については、「権利の使用」ガイドを参照してください。

  • プロジェクトのプロパティを構成する - Info.plist ファイルで、バンドル識別子アプリ ID の作成時に設定されたバンドル ID と一致することを確認します。[iOS] の [バンドル署名] では、iCloud App Service とアプリ ID を含むプロビジョニング プロファイルと、選択された [カスタム エンタイトルメント] ファイルを使用します。 これはすべて、Visual Studio の [プロジェクトのプロパティ] ペインで実行できます。

  • デバイスで iCloud を有効にする - [設定] > [iCloud] の順に移動し、デバイスがログインしていることを確認します。 [ドキュメントとデータ] オプションを選択してオンにします。

  • iCloud をテストするにはデバイスを使用する必要がある - シミュレーターでは動作しません。 実際、iCloud が動作していることを確認するには、2 台以上のデバイスがすべて同じ Apple ID でサインインしている必要があります。

キーと値のストレージ

キーと値のストレージは、ユーザーがデバイス間で保持する可能性のある少量のデータ (本や雑誌で最後に閲覧したページなど) を対象としています。 キーと値のストレージをデータのバックアップに使用しないでください。

キーと値のストレージを使用する場合に注意すべき制限事項がいくつかあります。

  • キーの最大サイズ - キー名の長さは 64 バイトを超えることはできません。

  • 値の最大サイズ - 1 つの値は 64 KB を超えることはできません。

  • アプリのキーと値の最大ストア サイズ - アプリケーションで格納できるキーと値のデータは、合計で最大 64 KB のみです。 この制限を超えてキーを設定しようとすると失敗し、前の値が保持されます。

  • データ型 - 格納できるのは、文字列、数値、ブール値などの基本型のみです。

iCloudKeyValue の例は、そのしくみを示しています。 サンプル コードでは、各デバイスの名前が付けられたキーを作成します。このキーは 1 台のデバイスで設定でき、その値が他のデバイスに伝達されます。 また、任意のデバイスで編集できる "Shared" という名前のキーも作成されます。一度に多数のデバイスで編集した場合、iCloud によって、"勝利した" 値が決定され (変更時のタイムスタンプを使用して)、伝達されます。

このスクリーンショットは、使用中のサンプルを示しています。 iCloud から変更通知を受信すると、その通知が画面の下部にあるスクロール テキスト ビューに出力され、入力フィールドで更新できます。

The flow of messages between devices

データの設定と取得

次のコードは、文字列値の設定方法を示しています。

var store = NSUbiquitousKeyValueStore.DefaultStore;
store.SetString("testkey", "VALUE IN THE CLOUD");  // key and value
store.Synchronize();

Synchronize を呼び出すと、値がローカル ディスク ストレージにのみ保持されることが保証されます。 iCloud への同期はバックグラウンドで行われ、アプリケーション コードで "強制" することはできません。 ネットワーク接続が良好であれば、多くの場合、同期は 5 秒以内に行われますが、ネットワーク品質が低い (または切断されている) 場合、更新には、はるかに長い時間がかかる可能性があります。

値を取得するには、次のコードを使用します。

var store = NSUbiquitousKeyValueStore.DefaultStore;
display.Text = store.GetString("testkey");

値はローカル データ ストアから取得されます。このメソッドは、iCloud サーバーに接続して "最新" の値を取得することを試みません。 iCloud は、独自のスケジュールに従ってローカル データ ストアを更新します。

データの削除

キーと値のペアを完全に削除するには、次のような Remove メソッドを使用します。

var store = NSUbiquitousKeyValueStore.DefaultStore;
store.Remove("testkey");
store.Synchronize();

変更の監視

オブザーバーを NSNotificationCenter.DefaultCenter に追加すると、アプリケーションは、iCloud によって値が変更されたときにも通知を受け取ることができます。 KeyValueViewController.csViewWillAppear メソッドの次のコードは、これらの通知をリッスンし、変更されたキーの一覧を作成する方法を示しています。

keyValueNotification =
NSNotificationCenter.DefaultCenter.AddObserver (
    NSUbiquitousKeyValueStore.DidChangeExternallyNotification, notification => {
    Console.WriteLine ("Cloud notification received");
    NSDictionary userInfo = notification.UserInfo;

    var reasonNumber = (NSNumber)userInfo.ObjectForKey (NSUbiquitousKeyValueStore.ChangeReasonKey);
    nint reason = reasonNumber.NIntValue;

    var changedKeys = (NSArray)userInfo.ObjectForKey (NSUbiquitousKeyValueStore.ChangedKeysKey);
    var changedKeysList = new List<string> ();
    for (uint i = 0; i < changedKeys.Count; i++) {
        var key = changedKeys.GetItem<NSString> (i); // resolve key to a string
        changedKeysList.Add (key);
    }
    // now do something with the list...
});

この後、コードは、変更されたキーの一覧を使用して、何らかのアクションを実行できます。たとえば、変更されたキーのローカル コピーの更新や、新しい値による UI の更新などがあります。

変更の理由としては、ServerChange (0)、InitialSyncChange (1)、または QuotaViolationChange (2) が考えられます。 理由にアクセスし、必要に応じて別の処理を実行できます (たとえば、QuotaViolationChange の結果として、いくつかのキーを削除することが必要な場合などです)。

ドキュメント ストレージ

iCloud ドキュメント ストレージは、アプリ (およびユーザー) にとって重要なデータを管理するように設計されています。 アプリの実行に必要なファイルやその他のデータを管理するために使用でき、同時に、iCloud ベースのバックアップを提供するため、またすべてのユーザーのデバイス間で機能を共有するためにも使用できます。

次の図は、すべてがどのように組み合わされるかを示しています。 各デバイスでは、データがローカル ストレージ (UbiquityContainer) に保存されており、オペレーティング システムの iCloud デーモンがクラウドでのデータの送受信を処理します。 同時アクセスを防止するには、UbiquityContainer へのすべてのファイル アクセスを FilePresenter/FileCoordinator 経由で行う必要があります。 UIDocument クラスはこれらを自動的に実装します。次の例は、UIDocument の使用方法を示しています。

The document storage overview

iCloudUIDoc の例では、1 つのテキスト フィールドを含む単純な UIDocument サブクラスを実装しています。 テキストは UITextView にレンダリングされ、編集内容は iCloud によって他のデバイスに伝達され、通知メッセージが赤色で表示されます。 サンプル コードでは、競合解決などの高度な iCloud 機能は扱っていません。

このスクリーンショットはサンプル アプリケーションを示しています。テキストを変更して UpdateChangeCount を押すと、ドキュメントが iCloud 経由で他のデバイスに同期されます。

This screenshot shows the sample application after changing the text and pressing UpdateChangeCount

iCloudUIDoc サンプルには、次の 5 つの部分があります。

  1. UbiquityContainer へのアクセス - iCloud が有効になっているかどうかを確認し、有効になっている場合は、アプリケーションの iCloud ストレージ領域へのパスを確認します。

  2. UIDocument サブクラスの作成 - iCloud ストレージとモデル オブジェクトとの間の中間クラスを作成します。

  3. iCloud ドキュメントの検索とオープン - NSFileManagerNSPredicate を使用して、iCloud ドキュメントを検索し、開きます。

  4. iCloud ドキュメントの表示 - UI コントロールと対話できるように、UIDocument のプロパティを公開します。

  5. iCloud ドキュメントの保存 - UI で行われた変更がディスクと iCloud に確実に保持されます。

何かが発生するのを待っている間にブロックされないように、すべての iCloud 操作を非同期で実行します (または実行する必要があります)。 このサンプルでは、次の 3 つの異なる方法でこれを実現しています。

スレッド - AppDelegate.FinishedLaunching では、メイン スレッドがブロックされないように、GetUrlForUbiquityContainer の最初の呼び出しが別のスレッドで実行されています。

NotificationCenter - NSMetadataQuery.StartQuery などの非同期操作が完了したときの通知を登録します。

完了ハンドラー - UIDocument.Open などの非同期操作の完了時に実行するメソッドを渡します。

UbiquityContainer へのアクセス

iCloud ドキュメント ストレージを使用する最初のステップは、iCloud が有効になっているかどうかを確認することです。有効になっている場合は、"ユビキティ コンテナー" (デバイス上で iCloud 対応ファイルが保存されているディレクトリ) の場所を確認します。

このコードは、サンプルの AppDelegate.FinishedLaunching メソッド内にあります。

// GetUrlForUbiquityContainer is blocking, Apple recommends background thread or your UI will freeze
ThreadPool.QueueUserWorkItem (_ => {
    CheckingForiCloud = true;
    Console.WriteLine ("Checking for iCloud");
    var uburl = NSFileManager.DefaultManager.GetUrlForUbiquityContainer (null);
    // OR instead of null you can specify "TEAMID.com.your-company.ApplicationName"

    if (uburl == null) {
        HasiCloud = false;
        Console.WriteLine ("Can't find iCloud container, check your provisioning profile and entitlements");

        InvokeOnMainThread (() => {
            var alertController = UIAlertController.Create ("No \uE049 available",
            "Check your Entitlements.plist, BundleId, TeamId and Provisioning Profile!", UIAlertControllerStyle.Alert);
            alertController.AddAction (UIAlertAction.Create ("OK", UIAlertActionStyle.Destructive, null));
            viewController.PresentViewController (alertController, false, null);
        });
    } else { // iCloud enabled, store the NSURL for later use
        HasiCloud = true;
        iCloudUrl = uburl;
        Console.WriteLine ("yyy Yes iCloud! {0}", uburl.AbsoluteUrl);
    }
    CheckingForiCloud = false;
});

Apple は、アプリがフォアグラウンドになるたびに GetUrlForUbiquityContainer を呼び出すように推奨しています。ただし、このサンプルではそうしていません。

UIDocument サブクラスの作成

すべての iCloud ファイルとディレクトリ (つまり、UbiquityContainer ディレクトリに保存されているもの) は、NSFileManager メソッドを使用して管理し、NSFilePresenter プロトコルを実装し、NSFileCoordinator を介して書き込む必要があります。 これらすべてを行う最も簡単な方法は、自分で記述しないで、すべてを自動的に実行する UIDocument のサブクラスを作成することです。

iCloud を操作するために UIDocument サブクラスに実装する必要があるメソッドは、次の 2 つだけです。

  • LoadFromContents - 1 つまたは複数のモデル クラスにアンパックするために、ファイルの内容の NSData を渡します。

  • ContentsForType - ディスク (およびクラウド) に保存する 1 つまたは複数のモデル クラスの NSData 表現を提供するように要求します。

iCloudUIDoc\MonkeyDocument.cs の次のサンプル コードは、UIDocument を実装する方法を示しています。

public class MonkeyDocument : UIDocument
{
    // the 'model', just a chunk of text in this case; must easily convert to NSData
    NSString dataModel;
    // model is wrapped in a nice .NET-friendly property
    public string DocumentString {
        get {
            return dataModel.ToString ();
        }
        set {
            dataModel = new NSString (value);
        }
    }
    public MonkeyDocument (NSUrl url) : base (url)
    {
        DocumentString = "(default text)";
    }
    // contents supplied by iCloud to display, update local model and display (via notification)
    public override bool LoadFromContents (NSObject contents, string typeName, out NSError outError)
    {
        outError = null;

        Console.WriteLine ("LoadFromContents({0})", typeName);

        if (contents != null)
            dataModel = NSString.FromData ((NSData)contents, NSStringEncoding.UTF8);

        // LoadFromContents called when an update occurs
        NSNotificationCenter.DefaultCenter.PostNotificationName ("monkeyDocumentModified", this);
        return true;
    }
    // return contents for iCloud to save (from the local model)
    public override NSObject ContentsForType (string typeName, out NSError outError)
    {
        outError = null;

        Console.WriteLine ("ContentsForType({0})", typeName);
        Console.WriteLine ("DocumentText:{0}",dataModel);

        NSData docData = dataModel.Encode (NSStringEncoding.UTF8);
        return docData;
    }
}

この場合のデータ モデルは非常に単純で、テキスト フィールドは 1 つだけです。 データ モデルは、XML ドキュメントやバイナリ データなど、必要に応じて複雑にすることができます。 UIDocument 実装の主な役割は、モデル クラスと、ディスクに保存または読み込み可能な NSData 表現の間で変換を行うことです。

iCloud ドキュメントの検索とオープン

サンプル アプリで処理されるファイルは 1 つ (test.txt) のみであるため、AppDelegate.cs のコードは、そのファイル名のみを探す NSPredicateNSMetadataQuery を作成します。 NSMetadataQuery は非同期で実行され、完了すると通知を送信します。 DidFinishGathering は、通知オブザーバーによって呼び出され、クエリを停止し、LoadDocument を呼び出します。LoadDocument は、完了ハンドラーで UIDocument.Open メソッドを使用して、ファイルの読み込みと MonkeyDocumentViewController への表示を試みます。

string monkeyDocFilename = "test.txt";
void FindDocument ()
{
    Console.WriteLine ("FindDocument");
    query = new NSMetadataQuery {
        SearchScopes = new NSObject [] { NSMetadataQuery.UbiquitousDocumentsScope }
    };

    var pred = NSPredicate.FromFormat ("%K == %@", new NSObject[] {
        NSMetadataQuery.ItemFSNameKey, new NSString (MonkeyDocFilename)
    });

    Console.WriteLine ("Predicate:{0}", pred.PredicateFormat);
    query.Predicate = pred;

    NSNotificationCenter.DefaultCenter.AddObserver (
        this,
        new Selector ("queryDidFinishGathering:"),
        NSMetadataQuery.DidFinishGatheringNotification,
        query
    );

    query.StartQuery ();
}

[Export ("queryDidFinishGathering:")]
void DidFinishGathering (NSNotification notification)
{
    Console.WriteLine ("DidFinishGathering");
    var metadataQuery = (NSMetadataQuery)notification.Object;
    metadataQuery.DisableUpdates ();
    metadataQuery.StopQuery ();

    NSNotificationCenter.DefaultCenter.RemoveObserver (this, NSMetadataQuery.DidFinishGatheringNotification, metadataQuery);
    LoadDocument (metadataQuery);
}

void LoadDocument (NSMetadataQuery metadataQuery)
{
    Console.WriteLine ("LoadDocument");

    if (metadataQuery.ResultCount == 1) {
        var item = (NSMetadataItem)metadataQuery.ResultAtIndex (0);
        var url = (NSUrl)item.ValueForAttribute (NSMetadataQuery.ItemURLKey);
        doc = new MonkeyDocument (url);

        doc.Open (success => {
            if (success) {
                Console.WriteLine ("iCloud document opened");
                Console.WriteLine (" -- {0}", doc.DocumentString);
                viewController.DisplayDocument (doc);
            } else {
                Console.WriteLine ("failed to open iCloud document");
            }
        });
    } // TODO: if no document, we need to create one
}

iCloud ドキュメントの表示

UIDocument の表示は、他のモデル クラスの場合と違いはありません。プロパティは UI コントロールに表示され、ユーザーによって編集された後、モデルに書き戻すことができます。

iCloudUIDoc\MonkeyDocumentViewController.cs の例では、MonkeyDocument のテキストが UITextView に表示されます。 ViewDidLoad は、MonkeyDocument.LoadFromContents メソッドで送信された通知をリッスンします。 LoadFromContents は、iCloud にファイルの新しいデータが含まれたときに呼び出されるため、通知はドキュメントが更新されたことを示します。

NSNotificationCenter.DefaultCenter.AddObserver (this,
    new Selector ("dataReloaded:"),
    new NSString ("monkeyDocumentModified"),
    null
);

サンプル コードの通知ハンドラーは、UI を更新するメソッドを呼び出します。この場合、競合の検出や解決は行われません。

[Export ("dataReloaded:")]
void DataReloaded (NSNotification notification)
{
    doc = (MonkeyDocument)notification.Object;
    // we just overwrite whatever was being typed, no conflict resolution for now
    docText.Text = doc.DocumentString;
}

iCloud ドキュメントの保存

UIDocument を iCloud に追加するには、UIDocument.Save を直接呼び出すか (新しいドキュメントの場合のみ)、または NSFileManager.DefaultManager.SetUbiquitious を使用して既存のファイルを移動することができます。 コード例では、次のコードを使用して、ユビキティ コンテナーに新しいドキュメントを直接作成します (ここには、2 つの完了ハンドラーがあります。1 つは Save 操作用で、もう 1 つは Open 用です)。

var docsFolder = Path.Combine (iCloudUrl.Path, "Documents"); // NOTE: Documents folder is user-accessible in Settings
var docPath = Path.Combine (docsFolder, MonkeyDocFilename);
var ubiq = new NSUrl (docPath, false);
var monkeyDoc = new MonkeyDocument (ubiq);
monkeyDoc.Save (monkeyDoc.FileUrl, UIDocumentSaveOperation.ForCreating, saveSuccess => {
Console.WriteLine ("Save completion:" + saveSuccess);
if (saveSuccess) {
    monkeyDoc.Open (openSuccess => {
        Console.WriteLine ("Open completion:" + openSuccess);
        if (openSuccess) {
            Console.WriteLine ("new document for iCloud");
            Console.WriteLine (" == " + monkeyDoc.DocumentString);
            viewController.DisplayDocument (monkeyDoc);
        } else {
            Console.WriteLine ("couldn't open");
        }
    });
} else {
    Console.WriteLine ("couldn't save");
}

ドキュメントに対する以降の変更は、直接 "保存" されません。代わりに、UpdateChangeCount を使用して、ドキュメントが変更されたことを UIDocument に通知します。ディスクへの保存操作は自動的にスケジュールされます。

doc.UpdateChangeCount (UIDocumentChangeKind.Done);

iCloud ドキュメントの管理

ユーザーは、[設定] を使用して、アプリケーションの外部にある "ユビキティ コンテナー" の [ドキュメント] ディレクトリ内の iCloud ドキュメントを管理でき、ファイル リストを表示したり、スワイプして削除したりすることができます。 アプリケーション コードでは、ドキュメントがユーザーによって削除された状況に対処できる必要があります。 内部アプリケーション データを [ドキュメント] ディレクトリに保存しないでください。

Managing iCloud Documents workflow

また、ユーザーは、iCloud 対応アプリケーションをデバイスから削除しようとすると、そのアプリケーションに関連する iCloud ドキュメントの状態を通知するさまざまな警告を受け取ります。

Screenshot shows a warning for Document Updates Pending.

Screenshot shows a warning for Delete i Cloud.

iCloud バックアップ

iCloud へのバックアップは、開発者が直接アクセスする機能ではありませんが、アプリケーションの設計方法がユーザー エクスペリエンスに影響を与える可能性があります。 Apple は、開発者が iOS アプリケーションで従う必要がある開発者向けiOS データ ストレージ ガイドラインを提供しています。

最も重要な考慮事項は、アプリで、ユーザーが生成していない大きなファイルを格納するかどうかということです (たとえば、1 号あたり 100 MB を超えるコンテンツを格納する雑誌リーダー アプリケーションなど)。 Apple では、iCloud にバックアップすると、ユーザーの iCloud クォータを不必要に埋めることになる場所に、この種のデータを格納しないことを推奨しています。

このような大量のデータを格納するアプリケーションでは、それを、バックされないユーザー ディレクトリの 1 つ (たとえば、キャッシュや tmp など) に格納するか、または NSFileManager.SetSkipBackupAttribute を使用してそれらのファイルにフラグを適用し、バックアップ操作中に iCloud によって無視されるようにします。

まとめ

この記事では、iOS 5 に含まれる新しい iCloud 機能について説明しました。 ここでは、iCloud を使用するようにプロジェクトを構成するために必要な手順を調べ、iCloud 機能を実装する方法を示す例を提供しました。

キーと値のストレージの例では、NSUserPreferences を格納する方法と同様に、iCloud を使用して少量のデータを格納する方法を示しました。 UIDocument の例では、iCloud を使用して、より複雑なデータを複数のデバイスに格納し、それらのデバイス間で同期する方法を示しました。

最後に、iCloud バックアップの追加がアプリケーションの設計に与える影響について簡単に説明しました。