Xamarin.iOS の Document Picker
ドキュメント ピッカーを使うと、アプリ間でドキュメントを共有できます。 これらのドキュメントは、iCloud または別のアプリのディレクトリに保存することができます。 ドキュメントは、ユーザーが自分のデバイスにインストールした一連の Document Provider 拡張機能を介して共有されます。
アプリとクラウド間でドキュメントの同期を維持するのが難しいため、一定の複雑さが生じます。
要件
この記事で説明する手順を完了するには、次のものが必要です:
- Xcode 7 および iOS 8 以降 - Apple の Xcode 7 および iOS 8 以降の API を開発者のコンピューターにインストールして構成する必要があります。
- Visual Studio または Visual Studio for Mac - 最新バージョンの Visual Studio for Mac をインストールする必要があります。
- iOS デバイス - iOS 8 以降を実行している iOS デバイス。
iCloud の変更
Document Picker の新機能を実装するために、Apple の iCloud サービスに次の変更が加えられています。
- iCloud デーモンが、CloudKit を使用して完全に書き換えられました。
- 既存の iCloud 機能の名前が iCloud Drive に変更されました。
- Microsoft Windows OS のサポートが iCloud に追加されました。
- Mac OS Finder に iCloud フォルダーが追加されました。
- iOS デバイスは、Mac OS iCloud フォルダーの内容にアクセスできます。
重要
Apple からは、開発者が欧州連合の一般データ保護規則 (GDPR) を適切に処理するためのツールが提供されています。
ドキュメントとは
iCloud でドキュメントを参照する場合、これは単一のスタンドアロン エンティティであり、ユーザーもそのように認識する必要があります。 ユーザーは、ドキュメントを変更したり、(電子メールなどを使用して) 他のユーザーと共有したりできます。
ユーザーがすぐにドキュメントとして認識するファイルには、Pages、Keynote、Numbers などのいくつかの種類のファイルがあります。 ただし、iCloud はこの概念に限定されません。 たとえば、ゲーム (チェスの試合など) の状態をドキュメントとして扱い、iCloud に格納できます。 このファイルを、ユーザーのデバイス間で受け渡し、別のデバイスで中断したゲームを途中から受け取ることができます。
ドキュメントの扱い
Xamarin で Document Picker を使用するために必要なコードについて説明する前に、この記事では、iCloud ドキュメントを操作するためのベスト プラクティスと、Document Picker をサポートするために必要な既存の API に加えられたいくつかの変更について説明します。
ファイル調整の使用
ファイルは複数の異なる場所から変更できるため、データ損失を防ぐために調整を使用する必要があります。
上の図を見てみましょう。
- ファイル調整を使用する iOS デバイスでは、新しいドキュメントが作成され、iCloud フォルダーに保存されます。
- iCloud は、変更したファイルをクラウドに保存して、すべてのデバイスに配布します。
- 接続された Mac は、変更されたファイルを iCloud フォルダーで表示し、ファイル調整を使用してファイルへの変更をコピーします。
- ファイル調整を使用していないデバイスは、ファイルに変更を行い、それを iCloud フォルダーに保存します。 これらの変更は、他のデバイスに即座にレプリケートされます。
たとえば、元の iOS デバイスまたは Mac によってファイルが編集された後、変更内容が反映されないまま、調整されていないデバイスによるファイルのバージョンで上書きされたとします。 データの損失を防ぐには、クラウドベースのドキュメントを操作する際にファイル調整が必要です。
UIDocument の使用
開発者にとっての重労働を UIDocument
(または macOS の場合は NSDocument
) で代わりに行えば、単純化できます。 これは、バックグラウンド キューを使用して組み込みのファイル調整を実行し、アプリケーションの UI をブロックしないようにします。
UIDocument
は、開発者が必要とするさまざまな用途の Xamarin アプリケーションの開発作業を容易にする、いくつもの高度な API を公開します。
次のコードは、iCloud からのテキストを格納、取得するために使用できる、汎用テキストベースのドキュメントを実装するための UIDocument
のサブクラスを作成します。
using System;
using Foundation;
using UIKit;
namespace DocPicker
{
public class GenericTextDocument : UIDocument
{
#region Private Variable Storage
private NSString _dataModel;
#endregion
#region Computed Properties
public string Contents {
get { return _dataModel.ToString (); }
set { _dataModel = new NSString(value); }
}
#endregion
#region Constructors
public GenericTextDocument (NSUrl url) : base (url)
{
// Set the default document text
this.Contents = "";
}
public GenericTextDocument (NSUrl url, string contents) : base (url)
{
// Set the default document text
this.Contents = contents;
}
#endregion
#region Override Methods
public override bool LoadFromContents (NSObject contents, string typeName, out NSError outError)
{
// Clear the error state
outError = null;
// Were any contents passed to the document?
if (contents != null) {
_dataModel = NSString.FromData( (NSData)contents, NSStringEncoding.UTF8 );
}
// Inform caller that the document has been modified
RaiseDocumentModified (this);
// Return success
return true;
}
public override NSObject ContentsForType (string typeName, out NSError outError)
{
// Clear the error state
outError = null;
// Convert the contents to a NSData object and return it
NSData docData = _dataModel.Encode(NSStringEncoding.UTF8);
return docData;
}
#endregion
#region Events
public delegate void DocumentModifiedDelegate(GenericTextDocument document);
public event DocumentModifiedDelegate DocumentModified;
internal void RaiseDocumentModified(GenericTextDocument document) {
// Inform caller
if (this.DocumentModified != null) {
this.DocumentModified (document);
}
}
#endregion
}
}
上記の GenericTextDocument
クラスは、Xamarin.iOS 8 アプリケーションで Document Picker と外部ドキュメントを操作するときに、この記事全体で使用します。
非同期ファイル調整
iOS 8 は、新しいファイル調整 API を使用して、いくつかの新しい非同期ファイル調整機能を提供します。 iOS 8 より前、既存のすべてのファイル調整 API は完全に同期していました。 つまり、ファイル調整によってアプリケーションの UI がブロックされないように、開発者は独自のバックグラウンド キューを実装する必要がありました。
新しい NSFileAccessIntent
クラスには、ファイルを指す URL と、必要な調整の種類を制御するためのいくつかのオプションが含まれています。 次のコードは、インテントを使用してファイルをある場所から別の場所に移動する方法を示しています。
// Get source options
var srcURL = NSUrl.FromFilename ("FromFile.txt");
var srcIntent = NSFileAccessIntent.CreateReadingIntent (srcURL, NSFileCoordinatorReadingOptions.ForUploading);
// Get destination options
var dstURL = NSUrl.FromFilename ("ToFile.txt");
var dstIntent = NSFileAccessIntent.CreateReadingIntent (dstURL, NSFileCoordinatorReadingOptions.ForUploading);
// Create an array
var intents = new NSFileAccessIntent[] {
srcIntent,
dstIntent
};
// Initialize a file coordination with intents
var queue = new NSOperationQueue ();
var fileCoordinator = new NSFileCoordinator ();
fileCoordinator.CoordinateAccess (intents, queue, (err) => {
// Was there an error?
if (err!=null) {
Console.WriteLine("Error: {0}",err.LocalizedDescription);
}
});
ドキュメントの検出と一覧表示
ドキュメントを検出して一覧表示する方法は、既存の NSMetadataQuery
API を使用することです。 このセクションでは、NSMetadataQuery
に追加された、ドキュメントの操作を以前よりも簡単にする新機能について説明します。
既存の動作
iOS 8 より前、NSMetadataQuery
はローカル ファイルの変更 (削除、作成、名前変更など) を取得するのに時間がかかっていました。
上の図の説明です。
- アプリケーション コンテナーに既に存在するファイルの場合、
NSMetadataQuery
には既存のNSMetadata
レコードが事前に作成され、スプールされているため、アプリケーションですぐに使用できます。 - アプリケーションは、アプリケーション コンテナーに新しいファイルを作成します。
NSMetadataQuery
がアプリケーション コンテナーの変更を認識し、必要なNSMetadata
レコードを作成するまでには遅延があります。
NSMetadata
レコードの作成が遅れるため、アプリケーションでは、ローカル ファイルの変更用とクラウドベースの変更用の 2 つのデータ ソースを開く必要がありました。
ステッチング
iOS 8 では、NSMetadataQuery
は、ステッチングと呼ばれる新機能で直接使用する方が簡単です。
上の図にあるステッチングの使用方法:
- 前と同様に、アプリケーション コンテナーに既に存在するファイルの場合、
NSMetadataQuery
には既存のNSMetadata
レコードが事前に作成されており、スプールされています。 - アプリケーションは、ファイル調整を使用して、アプリケーション コンテナーに新しいファイルを作成します。
- アプリケーション コンテナーのフックは、変更を認識し、
NSMetadataQuery
を呼び出して必要なNSMetadata
レコードを作成します。 NSMetadata
レコードがファイルの直後に作成され、アプリケーションで使用できるようになります。
ステッチングを使用することで、ローカルやクラウドベースのファイルの変更を監視するためにデータ ソースを開く必要がなくなります。 これで、アプリケーションは NSMetadataQuery
に直接依存できます。
重要
ステッチングは、上記のセクションで説明したように、アプリケーションがファイル調整を使用している場合にのみ機能します。 ファイル調整が使用されていない場合、API は既定で既存の iOS 8 より前の動作になります。
iOS 8 の新しいメタデータ機能
iOS 8 では、NSMetadataQuery
に次の新機能が追加されています。
NSMetatadataQuery
は、クラウドに格納されているローカル以外のドキュメントを一覧表示できるようになりました。- クラウドベースのドキュメントのメタデータ情報にアクセスするための新しい API が追加されました。
- ファイルのコンテンツをローカルで使用できるかどうかに関係なく、そのファイルの属性にアクセスできる新しい
NSUrl_PromisedItems
API があります。 GetPromisedItemResourceValue
メソッドを使用して特定のファイルに関する情報を取得するか、GetPromisedItemResourceValues
メソッドを使用して一度に複数のファイルに関する情報を取得します。
メタデータを処理するための 2 つの新しいファイル調整フラグが追加されました。
NSFileCoordinatorReadImmediatelyAvailableMetadataOnly
NSFileCoordinatorWriteContentIndependentMetadataOnly
上記のフラグを使用すると、ドキュメント ファイルの内容をローカルで使用できるようにする必要がなくなります。
次のコード セグメントは、NSMetadataQuery
を使用して特定のファイルの存在を照会し、存在しない場合はファイルをビルドする方法を示しています。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using Foundation;
using UIKit;
using ObjCRuntime;
using System.IO;
#region Static Properties
public const string TestFilename = "test.txt";
#endregion
#region Computed Properties
public bool HasiCloud { get; set; }
public bool CheckingForiCloud { get; set; }
public NSUrl iCloudUrl { get; set; }
public GenericTextDocument Document { get; set; }
public NSMetadataQuery Query { get; set; }
#endregion
#region Private Methods
private void FindDocument () {
Console.WriteLine ("Finding Document...");
// Create a new query and set it's scope
Query = new NSMetadataQuery();
Query.SearchScopes = new NSObject [] {
NSMetadataQuery.UbiquitousDocumentsScope,
NSMetadataQuery.UbiquitousDataScope,
NSMetadataQuery.AccessibleUbiquitousExternalDocumentsScope
};
// Build a predicate to locate the file by name and attach it to the query
var pred = NSPredicate.FromFormat ("%K == %@"
, new NSObject[] {
NSMetadataQuery.ItemFSNameKey
, new NSString(TestFilename)});
Query.Predicate = pred;
// Register a notification for when the query returns
NSNotificationCenter.DefaultCenter.AddObserver (this,
new Selector("queryDidFinishGathering:"), NSMetadataQuery.DidFinishGatheringNotification,
Query);
// Start looking for the file
Query.StartQuery ();
Console.WriteLine ("Querying: {0}", Query.IsGathering);
}
[Export("queryDidFinishGathering:")]
public void DidFinishGathering (NSNotification notification) {
Console.WriteLine ("Finish Gathering Documents.");
// Access the query and stop it from running
var query = (NSMetadataQuery)notification.Object;
query.DisableUpdates();
query.StopQuery();
// Release the notification
NSNotificationCenter.DefaultCenter.RemoveObserver (this
, NSMetadataQuery.DidFinishGatheringNotification
, query);
// Load the document that the query returned
LoadDocument(query);
}
private void LoadDocument (NSMetadataQuery query) {
Console.WriteLine ("Loading Document...");
// Take action based on the returned record count
switch (query.ResultCount) {
case 0:
// Create a new document
CreateNewDocument ();
break;
case 1:
// Gain access to the url and create a new document from
// that instance
NSMetadataItem item = (NSMetadataItem)query.ResultAtIndex (0);
var url = (NSUrl)item.ValueForAttribute (NSMetadataQuery.ItemURLKey);
// Load the document
OpenDocument (url);
break;
default:
// There has been an issue
Console.WriteLine ("Issue: More than one document found...");
break;
}
}
#endregion
#region Public Methods
public void OpenDocument(NSUrl url) {
Console.WriteLine ("Attempting to open: {0}", url);
Document = new GenericTextDocument (url);
// Open the document
Document.Open ( (success) => {
if (success) {
Console.WriteLine ("Document Opened");
} else
Console.WriteLine ("Failed to Open Document");
});
// Inform caller
RaiseDocumentLoaded (Document);
}
public void CreateNewDocument() {
// Create path to new file
// var docsFolder = Environment.GetFolderPath (Environment.SpecialFolder.Personal);
var docsFolder = Path.Combine(iCloudUrl.Path, "Documents");
var docPath = Path.Combine (docsFolder, TestFilename);
var ubiq = new NSUrl (docPath, false);
// Create new document at path
Console.WriteLine ("Creating Document at:" + ubiq.AbsoluteString);
Document = new GenericTextDocument (ubiq);
// Set the default value
Document.Contents = "(default value)";
// Save document to path
Document.Save (Document.FileUrl, UIDocumentSaveOperation.ForCreating, (saveSuccess) => {
Console.WriteLine ("Save completion:" + saveSuccess);
if (saveSuccess) {
Console.WriteLine ("Document Saved");
} else {
Console.WriteLine ("Unable to Save Document");
}
});
// Inform caller
RaiseDocumentLoaded (Document);
}
public bool SaveDocument() {
bool successful = false;
// Save document to path
Document.Save (Document.FileUrl, UIDocumentSaveOperation.ForOverwriting, (saveSuccess) => {
Console.WriteLine ("Save completion: " + saveSuccess);
if (saveSuccess) {
Console.WriteLine ("Document Saved");
successful = true;
} else {
Console.WriteLine ("Unable to Save Document");
successful=false;
}
});
// Return results
return successful;
}
#endregion
#region Events
public delegate void DocumentLoadedDelegate(GenericTextDocument document);
public event DocumentLoadedDelegate DocumentLoaded;
internal void RaiseDocumentLoaded(GenericTextDocument document) {
// Inform caller
if (this.DocumentLoaded != null) {
this.DocumentLoaded (document);
}
}
#endregion
ドキュメントのサムネイル
Apple では、アプリケーションのドキュメントを一覧表示するときの最適なユーザー エクスペリエンスは、プレビューを使用することと考えています。 これにより、エンド ユーザーのコンテキストが得られるため、操作するドキュメントをすばやく識別できます。
iOS 8 より前では、ドキュメントのプレビューを表示するにはカスタム実装が必要でした。 iOS 8 で新しくなったのはファイル システム属性で、これにより開発者がドキュメント サムネイルをすばやく操作できるようになりました。
ドキュメント サムネイルの取得
GetPromisedItemResourceValue
または GetPromisedItemResourceValues
メソッドを呼び出すことで、NSUrl_PromisedItems
API、NSUrlThumbnailDictionary
が返されます。 現在、このディクショナリ内の唯一のキーは、NSThumbnial1024X1024SizeKey
とそれに対応する UIImage
です。
ドキュメント サムネイルの保存
サムネイルを保存する最も簡単な方法は、UIDocument
を使用することです。 GetFileAttributesToWrite
の UIDocument
メソッドを呼び出し、サムネイルを設定することで、ドキュメント ファイルがある場合は自動的に保存されます。 iCloud デーモンがこの変更を認識し、iCloud に伝達します。 Mac OS X では、Quick Look プラグインによって、開発者向けにサムネイルが自動的に生成されます。
iCloud ベースのドキュメント操作の基本と、既存の API の変更により、Xamarin iOS 8 モバイル アプリケーションで Document Picker View Controller を実装する準備ができました。
Xamarin での iCloud の有効化
Document Picker を Xamarin.iOS アプリケーションで使用するには、アプリケーションと Apple の両方で iCloud サポートを有効にする必要があります。
次の手順では、iCloud のプロビジョニング プロセスについて説明します。
- iCloud コンテナーを作成します。
- iCloud App Service を含む App ID を作成します。
- この App ID が含まれるプロビジョニング プロファイルを作成します。
「機能の使用」ガイドでは、最初の 2 つの手順について説明します。 プロビジョニング プロファイルを作成するには、「プロファイルのプロビジョニング」ガイドの手順に従います。
以下の手順では、iCloud 用にアプリケーションを構成するプロセスについて説明します。
次の操作を行います。
Visual Studio for Mac または Visual Studio でプロジェクトを開きます。
ソリューション エクスプローラーで、プロジェクトを右クリックし、[オプション] を選択します。
[オプション] ダイアログ ボックスで [iOS アプリケーション] を選択し、[バンドル識別子] がアプリケーション用に上記で作成した App ID で定義されたものと一致していることを確認します。
[iOS バンドル署名] を選択し、上記で作成した開発者 ID とプロビジョニング プロファイルを選択します。
[OK] ボタンをクリックして変更内容を保存し、ダイアログ ボックスを閉じます。
ソリューション エクスプローラーで
Entitlements.plist
を右クリックし、エディターで開きます。重要
Visual Studio では、エンタイトルメント エディターを開く (右クリックして [プログラムから開く] を選択し、[プロパティ リスト エディター] を選択) ことが必要になる場合があります。
[iCloud の有効化]、[iCloud ドキュメント]、[キーと値のストレージ]、[CloudKit] をオンにします。
(上記で作成した) アプリケーションのコンテナーが存在することを確認します。 例:
iCloud.com.your-company.AppName
変更をファイルに保存します。
権利の詳細については、「権利の使用」ガイドを参照してください。
上記の設定を行うことで、アプリケーションでは、クラウドベースのドキュメントと新しい Document Picker View Controller を使用できるようになりました。
一般的なセットアップ コード
Document Picker View Controller の使用を開始する前に、いくつかの標準的なセットアップ コードが必要です。 まず、アプリケーションの AppDelegate.cs
ファイルを変更し、次のようにします:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using Foundation;
using UIKit;
using ObjCRuntime;
using System.IO;
namespace DocPicker
{
[Register ("AppDelegate")]
public partial class AppDelegate : UIApplicationDelegate
{
#region Static Properties
public const string TestFilename = "test.txt";
#endregion
#region Computed Properties
public override UIWindow Window { get; set; }
public bool HasiCloud { get; set; }
public bool CheckingForiCloud { get; set; }
public NSUrl iCloudUrl { get; set; }
public GenericTextDocument Document { get; set; }
public NSMetadataQuery Query { get; set; }
public NSData Bookmark { get; set; }
#endregion
#region Private Methods
private void FindDocument () {
Console.WriteLine ("Finding Document...");
// Create a new query and set it's scope
Query = new NSMetadataQuery();
Query.SearchScopes = new NSObject [] {
NSMetadataQuery.UbiquitousDocumentsScope,
NSMetadataQuery.UbiquitousDataScope,
NSMetadataQuery.AccessibleUbiquitousExternalDocumentsScope
};
// Build a predicate to locate the file by name and attach it to the query
var pred = NSPredicate.FromFormat ("%K == %@",
new NSObject[] {NSMetadataQuery.ItemFSNameKey
, new NSString(TestFilename)});
Query.Predicate = pred;
// Register a notification for when the query returns
NSNotificationCenter.DefaultCenter.AddObserver (this
, new Selector("queryDidFinishGathering:")
, NSMetadataQuery.DidFinishGatheringNotification
, Query);
// Start looking for the file
Query.StartQuery ();
Console.WriteLine ("Querying: {0}", Query.IsGathering);
}
[Export("queryDidFinishGathering:")]
public void DidFinishGathering (NSNotification notification) {
Console.WriteLine ("Finish Gathering Documents.");
// Access the query and stop it from running
var query = (NSMetadataQuery)notification.Object;
query.DisableUpdates();
query.StopQuery();
// Release the notification
NSNotificationCenter.DefaultCenter.RemoveObserver (this
, NSMetadataQuery.DidFinishGatheringNotification
, query);
// Load the document that the query returned
LoadDocument(query);
}
private void LoadDocument (NSMetadataQuery query) {
Console.WriteLine ("Loading Document...");
// Take action based on the returned record count
switch (query.ResultCount) {
case 0:
// Create a new document
CreateNewDocument ();
break;
case 1:
// Gain access to the url and create a new document from
// that instance
NSMetadataItem item = (NSMetadataItem)query.ResultAtIndex (0);
var url = (NSUrl)item.ValueForAttribute (NSMetadataQuery.ItemURLKey);
// Load the document
OpenDocument (url);
break;
default:
// There has been an issue
Console.WriteLine ("Issue: More than one document found...");
break;
}
}
#endregion
#region Public Methods
public void OpenDocument(NSUrl url) {
Console.WriteLine ("Attempting to open: {0}", url);
Document = new GenericTextDocument (url);
// Open the document
Document.Open ( (success) => {
if (success) {
Console.WriteLine ("Document Opened");
} else
Console.WriteLine ("Failed to Open Document");
});
// Inform caller
RaiseDocumentLoaded (Document);
}
public void CreateNewDocument() {
// Create path to new file
// var docsFolder = Environment.GetFolderPath (Environment.SpecialFolder.Personal);
var docsFolder = Path.Combine(iCloudUrl.Path, "Documents");
var docPath = Path.Combine (docsFolder, TestFilename);
var ubiq = new NSUrl (docPath, false);
// Create new document at path
Console.WriteLine ("Creating Document at:" + ubiq.AbsoluteString);
Document = new GenericTextDocument (ubiq);
// Set the default value
Document.Contents = "(default value)";
// Save document to path
Document.Save (Document.FileUrl, UIDocumentSaveOperation.ForCreating, (saveSuccess) => {
Console.WriteLine ("Save completion:" + saveSuccess);
if (saveSuccess) {
Console.WriteLine ("Document Saved");
} else {
Console.WriteLine ("Unable to Save Document");
}
});
// Inform caller
RaiseDocumentLoaded (Document);
}
/// <summary>
/// Saves the document.
/// </summary>
/// <returns><c>true</c>, if document was saved, <c>false</c> otherwise.</returns>
public bool SaveDocument() {
bool successful = false;
// Save document to path
Document.Save (Document.FileUrl, UIDocumentSaveOperation.ForOverwriting, (saveSuccess) => {
Console.WriteLine ("Save completion: " + saveSuccess);
if (saveSuccess) {
Console.WriteLine ("Document Saved");
successful = true;
} else {
Console.WriteLine ("Unable to Save Document");
successful=false;
}
});
// Return results
return successful;
}
#endregion
#region Override Methods
public override void FinishedLaunching (UIApplication application)
{
// Start a new thread to check and see if the user has iCloud
// enabled.
new Thread(new ThreadStart(() => {
// Inform caller that we are checking for iCloud
CheckingForiCloud = true;
// Checks to see if the user of this device has iCloud
// enabled
var uburl = NSFileManager.DefaultManager.GetUrlForUbiquityContainer(null);
// Connected to iCloud?
if (uburl == null)
{
// No, inform caller
HasiCloud = false;
iCloudUrl =null;
Console.WriteLine("Unable to connect to iCloud");
InvokeOnMainThread(()=>{
var okAlertController = UIAlertController.Create ("iCloud Not Available", "Developer, please check your Entitlements.plist, Bundle ID and Provisioning Profiles.", UIAlertControllerStyle.Alert);
okAlertController.AddAction (UIAlertAction.Create ("Ok", UIAlertActionStyle.Default, null));
Window.RootViewController.PresentViewController (okAlertController, true, null);
});
}
else
{
// Yes, inform caller and save location the Application Container
HasiCloud = true;
iCloudUrl = uburl;
Console.WriteLine("Connected to iCloud");
// If we have made the connection with iCloud, start looking for documents
InvokeOnMainThread(()=>{
// Search for the default document
FindDocument ();
});
}
// Inform caller that we are no longer looking for iCloud
CheckingForiCloud = false;
})).Start();
}
// This method is invoked when the application is about to move from active to inactive state.
// OpenGL applications should use this method to pause.
public override void OnResignActivation (UIApplication application)
{
}
// This method should be used to release shared resources and it should store the application state.
// If your application supports background execution this method is called instead of WillTerminate
// when the user quits.
public override void DidEnterBackground (UIApplication application)
{
// Trap all errors
try {
// Values to include in the bookmark packet
var resources = new string[] {
NSUrl.FileSecurityKey,
NSUrl.ContentModificationDateKey,
NSUrl.FileResourceIdentifierKey,
NSUrl.FileResourceTypeKey,
NSUrl.LocalizedNameKey
};
// Create the bookmark
NSError err;
Bookmark = Document.FileUrl.CreateBookmarkData (NSUrlBookmarkCreationOptions.WithSecurityScope, resources, iCloudUrl, out err);
// Was there an error?
if (err != null) {
// Yes, report it
Console.WriteLine ("Error Creating Bookmark: {0}", err.LocalizedDescription);
}
}
catch (Exception e) {
// Report error
Console.WriteLine ("Error: {0}", e.Message);
}
}
// This method is called as part of the transition from background to active state.
public override void WillEnterForeground (UIApplication application)
{
// Is there any bookmark data?
if (Bookmark != null) {
// Trap all errors
try {
// Yes, attempt to restore it
bool isBookmarkStale;
NSError err;
var srcUrl = new NSUrl (Bookmark, NSUrlBookmarkResolutionOptions.WithSecurityScope, iCloudUrl, out isBookmarkStale, out err);
// Was there an error?
if (err != null) {
// Yes, report it
Console.WriteLine ("Error Loading Bookmark: {0}", err.LocalizedDescription);
} else {
// Load document from bookmark
OpenDocument (srcUrl);
}
}
catch (Exception e) {
// Report error
Console.WriteLine ("Error: {0}", e.Message);
}
}
}
// This method is called when the application is about to terminate. Save data, if needed.
public override void WillTerminate (UIApplication application)
{
}
#endregion
#region Events
public delegate void DocumentLoadedDelegate(GenericTextDocument document);
public event DocumentLoadedDelegate DocumentLoaded;
internal void RaiseDocumentLoaded(GenericTextDocument document) {
// Inform caller
if (this.DocumentLoaded != null) {
this.DocumentLoaded (document);
}
}
#endregion
}
}
重要
上記のコードには、上記の「ドキュメントの検出と一覧表示」セクションのコードが含まれています。 これは、実際のアプリケーションに表示されるのと同じようにここに表示されます。 わかりやすくするために、この例は 1 つのハードコーディングされたファイル (test.txt
) でのみ機能します。
上記のコードは、アプリケーションの残りの部分で作業しやすくするために、いくつかの iCloud Drive ショートカットを公開しています。
次に、Document Picker を使用するか、クラウドベースのドキュメントを操作するビューまたはビュー コンテナーに次のコードを追加します。
using CloudKit;
...
#region Computed Properties
/// <summary>
/// Returns the delegate of the current running application
/// </summary>
/// <value>The this app.</value>
public AppDelegate ThisApp {
get { return (AppDelegate)UIApplication.SharedApplication.Delegate; }
}
#endregion
これにより、AppDelegate
にアクセスし、上記で作成した iCloud ショートカットにアクセスするためのショートカットが追加されます。
このコードを配置して、Xamarin iOS 8 アプリケーションで Document Picker View Controller を実装する方法を見てみましょう。
Document Picker View Controller の使用
iOS 8 より前では、アプリ内でアプリケーション外のドキュメントを検出する方法がなかったため、別のアプリケーションからドキュメントにアクセスすることは非常に困難でした。
既存の動作
iOS 8 より前で、外部ドキュメントにアクセスする方法を見てみましょう。
- 最初に、ユーザーは、最初にドキュメントを作成したアプリケーションを開く必要があります。
- ドキュメントが選択され、そのドキュメントを新しいアプリケーションに送信するために
UIDocumentInteractionController
が使用されます。 - 最後に、元のドキュメントのコピーが新しいアプリケーションのコンテナーに配置されます。
そこから、2 番目のアプリケーションでドキュメントを開いて編集できます。
アプリのコンテナー外でのドキュメントの検出
iOS 8 では、アプリケーションはアプリケーション コンテナーの外部にあるドキュメントに簡単にアクセスできます。
新しい iCloud Document Picker (UIDocumentPickerViewController
) を使用すると、iOS アプリケーションはアプリケーション コンテナー外に対し、直接検出してアクセスできます。 UIDocumentPickerViewController
は、ユーザーが、検出されたドキュメントに対して権限を介してアクセス権を付与し、編集を行うためのメカニズムを提供します。
アプリケーションは、そのドキュメントが iCloud Document Picker に表示され、他のアプリケーションがそのドキュメントを検出して操作できるようにオプトインする必要があります。 Xamarin iOS 8 アプリケーションでアプリケーション コンテナーを共有するには、標準テキスト エディターで Info.plist
ファイルを編集し、次の 2 行をディクショナリの下部 (<dict>...</dict>
タグ間) に追加します。
<key>NSUbiquitousContainerIsDocumentScopePublic</key>
<true/>
UIDocumentPickerViewController
は、ユーザーによるドキュメントの選択を実現する、優れた新しい UI を提供します。 Xamarin iOS 8 アプリケーションで Document Picker View Controller を表示するには、次の操作を行います。
using MobileCoreServices;
...
// Allow the Document picker to select a range of document types
var allowedUTIs = new string[] {
UTType.UTF8PlainText,
UTType.PlainText,
UTType.RTF,
UTType.PNG,
UTType.Text,
UTType.PDF,
UTType.Image
};
// Display the picker
//var picker = new UIDocumentPickerViewController (allowedUTIs, UIDocumentPickerMode.Open);
var pickerMenu = new UIDocumentMenuViewController(allowedUTIs, UIDocumentPickerMode.Open);
pickerMenu.DidPickDocumentPicker += (sender, args) => {
// Wireup Document Picker
args.DocumentPicker.DidPickDocument += (sndr, pArgs) => {
// IMPORTANT! You must lock the security scope before you can
// access this file
var securityEnabled = pArgs.Url.StartAccessingSecurityScopedResource();
// Open the document
ThisApp.OpenDocument(pArgs.Url);
// IMPORTANT! You must release the security lock established
// above.
pArgs.Url.StopAccessingSecurityScopedResource();
};
// Display the document picker
PresentViewController(args.DocumentPicker,true,null);
};
pickerMenu.ModalPresentationStyle = UIModalPresentationStyle.Popover;
PresentViewController(pickerMenu,true,null);
UIPopoverPresentationController presentationPopover = pickerMenu.PopoverPresentationController;
if (presentationPopover!=null) {
presentationPopover.SourceView = this.View;
presentationPopover.PermittedArrowDirections = UIPopoverArrowDirection.Down;
presentationPopover.SourceRect = ((UIButton)s).Frame;
}
重要
開発者は、外部ドキュメントにアクセスする前に、NSUrl
の StartAccessingSecurityScopedResource
メソッドを呼び出す必要があります。 ドキュメントが読み込まれたらすぐにセキュリティ ロックを解放するには、StopAccessingSecurityScopedResource
メソッドを呼び出す必要があります。
出力例
以下は、上記のコードを iPhone デバイスで実行した場合に Document Picker がどのように表示されるかを示す例です。
ユーザーがアプリケーションを起動し、メイン インターフェイスが表示されます。
ユーザーは画面の上部にある [アクション] ボタンをタップし、使用可能なプロバイダーの一覧からドキュメント プロバイダーを選択するように求められます。
選択したドキュメント プロバイダーの Document Picker View Controller が表示されます。
ユーザーがドキュメント フォルダーをタップして内容を表示します。
ユーザーがドキュメントを選択し、Document Picker が閉じられます。
メイン インターフェイスが再表示され、ドキュメントが外部コンテナーから読み込まれ、その内容が表示されます。
Document Picker View Controller の実際の表示は、ユーザーがデバイスにインストールしたドキュメント プロバイダーと、実装されている Document Picker モードによって異なります。 上記の例ではオープン モードを使用しています。もう 1 つのモードの種類については、以下で詳しく説明します。
外部ドキュメントの管理
前述のように、iOS 8 より前のバージョンでは、アプリケーションはアプリケーション コンテナーの一部であるドキュメントにのみアクセスできました。 iOS 8 では、アプリケーションは外部ソースからドキュメントにアクセスできます。
ユーザーが外部ソースからドキュメントを選択すると、元のドキュメントを指す参照ドキュメントがアプリケーション コンテナーに書き込まれます。
この新しい機能を既存のアプリケーションに追加するために、いくつかの新機能が NSMetadataQuery
API に追加されました。 通常、アプリケーションはユビキタス ドキュメント スコープを使用して、アプリケーション コンテナー内にあるドキュメントを一覧表示します。 このスコープを使用すると、アプリケーション コンテナー内のドキュメントのみが引き続き表示されます。
新しいユビキタス外部ドキュメント スコープを使用すると、アプリケーション コンテナー外にあるドキュメントが返され、それらのメタデータが返されます。 NSMetadataItemUrlKey
は、ドキュメントが実際に配置されている URL を指します。
参照されているドキュメントをアプリケーションで扱いたくない場合もあります。 代わりに、アプリで参照ドキュメントを直接扱いたい場合です。 たとえば、UI でアプリケーションのフォルダー内のドキュメントを表示したり、ユーザーがフォルダー内で参照を移動できるようにしたりします。
iOS 8 では、参照ドキュメントに直接アクセスするための新しい NSMetadataItemUrlInLocalContainerKey
が用意されています。 このキーは、アプリケーション コンテナー内の外部ドキュメントへの実際の参照を指します。
NSMetadataUbiquitousItemIsExternalDocumentKey
は、ドキュメントがアプリケーションのコンテナーの外部にあるかどうかをテストするために使用されます。 NSMetadataUbiquitousItemContainerDisplayNameKey
は、外部ドキュメントの元のコピーを格納しているコンテナーの名前にアクセスするために使用されます。
ドキュメント参照が必要な理由
iOS 8 が参照を使用して外部ドキュメントにアクセスする主な理由はセキュリティです。 それ以外のアプリケーションのコンテナーへのアクセス権は、どのアプリケーションにも付与されません。 これを行うことができるのは、アウトオブプロセスを実行し、システム全体にアクセスできる Document Picker のみです。
アプリケーション コンテナー外のドキュメントにアクセスする唯一の方法は、Document Picker を使用することであり、ピッカーによって返される URL がセキュリティ スコープ指定されている場合です。 セキュリティ スコープ指定された URL には、選択されたドキュメントに必要な情報と、ドキュメントへのアクセス権をアプリケーションに付与するために必要なスコープ付き権限が含まれています。
セキュリティ スコープ指定された URL が文字列にシリアル化されてから逆シリアル化されると、セキュリティ情報は失われ、ファイルは URL からアクセスできなくなることに注意してください。 ドキュメント参照機能は、これらの URL が指すファイルに戻るためのメカニズムを提供します。
そのため、アプリケーションがいずれかの参照ドキュメントから NSUrl
を取得した場合は、セキュリティ スコープが既にアタッチされており、ファイルへのアクセスに使用できます。 このため、この情報をすべて扱い、処理する UIDocument
を使用することを開発者に強くお勧めします。
ブックマークを使用する
アプリケーションのドキュメントを列挙して特定のドキュメントに戻ることが常に適切とは限りません。たとえば、状態の復元を実行する場合などです。 iOS 8 には、特定のドキュメントを直接対象とするブックマークを作成するメカニズムが用意されています。
次のコードは、UIDocument
の FileUrl
プロパティからブックマークを作成します。
// Trap all errors
try {
// Values to include in the bookmark packet
var resources = new string[] {
NSUrl.FileSecurityKey,
NSUrl.ContentModificationDateKey,
NSUrl.FileResourceIdentifierKey,
NSUrl.FileResourceTypeKey,
NSUrl.LocalizedNameKey
};
// Create the bookmark
NSError err;
Bookmark = Document.FileUrl.CreateBookmarkData (NSUrlBookmarkCreationOptions.WithSecurityScope, resources, iCloudUrl, out err);
// Was there an error?
if (err != null) {
// Yes, report it
Console.WriteLine ("Error Creating Bookmark: {0}", err.LocalizedDescription);
}
}
catch (Exception e) {
// Report error
Console.WriteLine ("Error: {0}", e.Message);
}
既存のブックマーク API は、既存の NSUrl
に対して、外部ファイルへの直接アクセスを実現するために保存、読み込みが可能なブックマークを作成するために使用されます。 次のコードは、上記で作成したブックマークを復元します。
if (Bookmark != null) {
// Trap all errors
try {
// Yes, attempt to restore it
bool isBookmarkStale;
NSError err;
var srcUrl = new NSUrl (Bookmark, NSUrlBookmarkResolutionOptions.WithSecurityScope, iCloudUrl, out isBookmarkStale, out err);
// Was there an error?
if (err != null) {
// Yes, report it
Console.WriteLine ("Error Loading Bookmark: {0}", err.LocalizedDescription);
} else {
// Load document from bookmark
OpenDocument (srcUrl);
}
}
catch (Exception e) {
// Report error
Console.WriteLine ("Error: {0}", e.Message);
}
}
オープン/インポート モードと Document Picker
Document Picker View Controller 機能には、次の 2 つの異なる動作モードがあります。
オープン モード - このモードでは、ユーザーが外部ドキュメントを選択すると、Document Picker によってアプリケーション コンテナーにセキュリティ スコープ指定されたブックマークが作成されます。
インポート モード - このモードでは、ユーザーが外部ドキュメントを選択しても、Document Picker はブックマークを作成せず、代わりにファイルが一時的な場所にコピーされ、その場所にあるドキュメントへのアクセス権がアプリケーションに付与されます。
何らかの理由でアプリケーションが終了すると、一時的な場所が空になり、ファイルが削除されます。 アプリケーションでファイルへのアクセスを管理する必要がある場合は、コピーを作成し、アプリケーション コンテナーに配置する必要があります。
オープン モードは、アプリケーションが別のアプリケーションと共同作業を行い、ドキュメントに加えられた変更をそのアプリケーションと共有する場合に便利です。 インポート モードは、ドキュメントに対する変更を他のアプリケーションと共有しない場合に使用されます。
ドキュメントを外部ドキュメントにする
前述のように、iOS 8 アプリケーションは、自身のアプリケーション コンテナーの外部にあるコンテナーにはアクセスできません。 アプリケーションは、自身のローカル コンテナーや一時的な場所に書き込むことはできます。その後、特殊なドキュメント モードを使用して、最終的なドキュメントをアプリケーション コンテナーの外部のユーザーが選択した場所に移動できます。
ドキュメントを外部の場所に移動するには、次の操作を行います。
- 最初に、ローカルまたは一時的な場所に新しいドキュメントを作成します。
- 新しいドキュメントを指す
NSUrl
を作成します。 - 新しい Document Picker View Controller を開き、
NSUrl
をMoveToService
モードで渡します。 - ユーザーが新しい場所を選択すると、ドキュメントは現在の場所から新しい場所に移動されます。
- 参照ドキュメントがアプリのアプリケーション コンテナーに書き込まれるので、ファイルには、作成するアプリケーションから引き続きアクセスできます。
次のコードを使用して、ドキュメントを外部の場所に移動できます: var picker = new UIDocumentPickerViewController (srcURL, UIDocumentPickerMode.MoveToService);
上記のプロセスによって返される参照ドキュメントは、Document Picker のオープン モードで作成されたものとまったく同じです。 ただし、アプリケーションでは、ドキュメントへの参照を保持せずにそのドキュメントを移動したい場合もあります。
参照を生成せずにドキュメントを移動するには、ExportToService
モードを使用します。 例: var picker = new UIDocumentPickerViewController (srcURL, UIDocumentPickerMode.ExportToService);
ExportToService
モードを使用する場合、ドキュメントは外部コンテナーにコピーされ、既存のコピーは元の場所に残ります。
ドキュメント プロバイダー拡張機能
Apple では iOS 8 以降、エンド ユーザーが、実際に存在する場所に関係なくクラウドベースのあらゆるドキュメントにアクセスできるようにしたいと考えています。 この目標を達成するために、iOS 8 には新しいドキュメント プロバイダー拡張機能のメカニズムが用意されています。
ドキュメント プロバイダー拡張機能とは
簡単に言うと、ドキュメント プロバイダー拡張機能は、アプリケーションの代替ドキュメント ストレージの場所を開発者またはサード パーティに提供する方法であり、これには、既存の iCloud ストレージとまったく同じ方法でアクセスできます。
ユーザーは、これらの代替ストレージの場所のいずれかを Document Picker から選択でき、まったく同じアクセス モード (オープン、インポート、移動、エクスポート) を使用して、その場所のファイルを操作できます。
これは、次の 2 つの異なる拡張機能を使用して実装されます。
- Document Picker 拡張機能 - ユーザーが代替ストレージの場所からドキュメントを選択するためのグラフィカル インターフェイスを提供する
UIViewController
サブクラスを提供します。 このサブクラスは、Document Picker View Controller の一部として表示されます。 - ファイル プロバイダー拡張機能 - これは、実際にファイル コンテンツを提供する非 UI 型の拡張機能です。 これらの拡張機能は、ファイル調整 (
NSFileCoordinator
) を通じて提供されます。 これは、ファイル調整を必要とするもう 1 つの重要なケースです。
次の図は、ドキュメント プロバイダー拡張機能を使用する場合の一般的なデータ フローを示しています。
次の処理が行われます。
- アプリケーションは、Document Picker Controller を表示して、操作するファイルをユーザーが選択できるようにします。
- ユーザーが代替のファイルの場所を選択すると、カスタムの
UIViewController
拡張機能が呼び出され、ユーザー インターフェイスが表示されます。 - ユーザーがこの場所からファイルを選択すると、URL が Document Picker に返されます。
- Document Picker は、ファイルの URL を選択し、ユーザーが作業できるようにアプリケーションに返します。
- URL がファイル コーディネーターに渡され、ファイルの内容がアプリケーションに返されます。
- ファイル コーディネーターは、カスタムのファイル プロバイダー拡張機能を呼び出してファイルを取得します。
- ファイルの内容がファイル コーディネーターに返されます。
- ファイルの内容がアプリケーションに返されます。
セキュリティとブックマーク
このセクションでは、セキュリティと、ブックマークを使用した永続的なファイル アクセスがドキュメント プロバイダー拡張機能とどのように連携するかを簡単に説明します。 iCloud ドキュメント プロバイダーは、アプリケーション コンテナーにセキュリティとブックマークを自動的に保存しますが、ドキュメント プロバイダー拡張機能は、ドキュメント参照システムの一部ではないためこれを行いません。
たとえば、セキュリティで保護された独自の全社的なデータストアがあるエンタープライズ環境では、企業の機密情報へのアクセスや、パブリック iCloud サーバーによる処理を管理者は望みません。 したがって、組み込みのドキュメント参照システムを使用することはできません。
ブックマーク システムは引き続き使用できます。ブックマークされた URL を正しく処理し、それが指すドキュメントの内容を返すのは、ファイル プロバイダー拡張機能の役割です。
セキュリティ上の理由から、iOS 8 には分離レイヤーがあり、どのアプリケーションがどのファイル プロバイダー内のどの識別子にアクセスできるかに関する情報を保持します。 すべてのファイル アクセスがこの分離レイヤーによって制御されていることに注意してください。
次の図は、ブックマークとドキュメント プロバイダー拡張機能を使用する場合のデータ フローを示しています。
次の処理が行われます。
- アプリケーションがバックグラウンドに入ります。この状態を保持する必要があります。 これが
NSUrl
を呼び出して、代替ストレージ内のファイルへのブックマークを作成します。 NSUrl
がファイル プロバイダー拡張機能を呼び出して、ドキュメントへの永続的な URL を取得します。- ファイル プロバイダー拡張機能が URL を文字列として
NSUrl
に返します。 NSUrl
が URL をブックマークにバンドルし、アプリケーションに返します。- アプリケーションがバックグラウンドで起動し、状態を復元する必要があるときは、ブックマークが
NSUrl
に渡されます。 NSUrl
は、ファイルの URL を使用してファイル プロバイダー拡張機能を呼び出します。- ファイル プロバイダー拡張機能がファイルにアクセスし、ファイルの場所を
NSUrl
に返します。 - ファイルの場所はセキュリティ情報にバンドルされ、アプリケーションに返されます。
ここから、アプリケーションはファイルにアクセスし、通常どおりに操作できます。
ファイルへの書き込み
このセクションでは、ドキュメント プロバイダー拡張機能を使用して代替の場所にファイルを書き込む方法について簡単に説明します。 iOS アプリケーションは、ファイル調整を使用して、アプリケーション コンテナー内のディスクに情報を保存します。 ファイルへの正常な書き込みの直後、ファイル プロバイダー拡張機能に変更が通知されます。
この時点で、ファイル プロバイダー拡張機能は、代替の場所にファイルのアップロードを開始できます (または、ファイルをダーティとしてマークし、アップロードを要求します)。
新しいドキュメント プロバイダー拡張機能の作成
新しいドキュメント プロバイダー拡張機能の作成は、この入門記事の範囲外です。 この情報をここで提供するのは、ユーザーが iOS デバイスに読み込んだ拡張機能に基づいて、アプリケーションが、Apple が提供する iCloud の場所以外のドキュメント ストレージの場所にアクセスできる場合があることを示すためです。
開発者は、Document Picker を使用して外部ドキュメントを操作するときに、この事実に注意する必要があります。 これらのドキュメントに iCloud が対応しているとは想定しないでください。
ストレージ プロバイダーまたは Document Picker 拡張機能の作成の詳細については、アプリ拡張機能の概要に関するドキュメントを参照してください。
iCloud Drive への移行
iOS 8 では、ユーザーは iOS 7 (および以前のシステム) で使用されている既存の iCloud ドキュメント システムを引き続き使用するか、既存のドキュメントを新しい iCloud Drive メカニズムに移行するかを選択できます。
Mac OS X Yosemite では、Apple による下位互換性への対応がないため、すべてのドキュメントを iCloud Drive に移行する必要があります。そうしないと、デバイス間での更新が行われなくなります。
ユーザーのアカウントが iCloud Drive に移行されると、iCloud Drive を使用するデバイスのみが、それらのデバイス間でドキュメントに変更を反映できます。
重要
開発者は、この記事で説明する新機能が、ユーザーのアカウントが iCloud Drive に移行された場合にのみ使用できることを理解する必要があります。
まとめ
この記事では、iCloud Drive をサポートするために必要な既存の iCloud API への変更点と、新しい Document Picker View Controller について説明しました。 ファイル調整と、それがクラウドベースのドキュメントを操作する際になぜ重要かについても説明しました。 Xamarin.iOS アプリケーションでクラウドベースのドキュメントを有効にするために必要なセットアップについて説明し、Document Picker View Controller を使用してアプリのアプリケーション コンテナー外でドキュメントを操作する方法について説明しました。
さらに、この記事では、ドキュメント プロバイダー拡張機能について簡単に説明しました。また、クラウドベースのドキュメントを処理できるアプリケーションを作成するときに、開発者がそれらを認識する必要がある理由について説明しました。