はじめに
iCloud Document対応Mac Appを実装する上で割とハマったので、以下備忘録として残します。暗黙知を含みます。
iCloudとドキュメントベースドアプリケーションに関する資料
これらの資料で概要を学ぶことができます。少々古いものですが日本語訳版が公式で提供されているので日本人としてはとても嬉しいです。
iCloud設計ガイド
このガイドでは、アプリケーションをiCloud対応にする工程についての概要を学ぶことができます。
iCloudでは主に次の4種類のストレージAPIを目的に応じて選択することができます。
-
KVS(キー値ストレージ)
- 環境設定や単純で軽量な情報の保存と同期に適しているストレージ
-
iCloud Document
- ドキュメントベースドアプリケーションのドキュメント(拡張子のついたファイル)の保存と同期を行えるストレージ
- iCloud Drive対応のアプリケーション(KeynoteやSketchなど)はこれを採用している
-
Core Data ストレージ
- Core Dataデータベースの保存と同期を行えるストレージ
-
CloudKit
- CloudKitが提供するデータベースの保存と同期を行えるストレージ
- CloudKit Dashboardによってwebブラウザで閲覧可能な管理画面も提供される
CloudKitクイックスタート
CloudKitを利用したアプリケーションの実装方法を学ぶことができます。
今回iCloud Document対応アプリケーションでは利用しないため詳細は割愛します。
ファイルシステムプログラミングガイド
このガイドでは、macOS, iOSにおけるファイルやドキュメントの扱い方の基礎を学ぶことができます。
以下代表的な章を抜き出していますが、全体的に有益な解説がありますので適宜参照してみてください。
ファイルコーディネータとファイルプレゼンタの役割 では、NSDocument, UIDocument, Autosave に関する基本が解説されています。
iCloudのファイル管理 では、iCloudドキュメントの保存方法と扱い方について概要をつかむことができます。
ファイルラッパーのファイルコンテナとしての使用 では、NSFileWrapper を使ったドキュメントベースドアプリケーションのCRUD操作の基本を学ぶことができます。設計次第ではありますが、iCloudドキュメントではこの方法を使うことになるかもしれません。
Packaged Document for OS X
テキストや画像など複数のリソースを単一のパッケージドキュメントに収めるための方法、それをiCloud Documentに対応する方法のサンプルコードになります。
パッケージに関しては Bundle Programming Guide を参照してください。
iCloud Documentストレージを有効化する
まず、XcodeプロジェクトのCapabilitiesペインでiCloud Documentを有効にします。必要なiCloud Containerの定義やApp IDの設定などは自動的に行われると思いますが、再設定が必要であれば Developer ポータルの Certificates に関する設定画面(macOS)にアクセスしてください。
次に、Info.plist に次のような NSUbiquitousContainers
の記述を追加します。「iCloud.com.example.MyApp」部分はアプリケーションのBundle Identifierに揃えてください。$(PRODUCT_NAME)
部分はストレージの名称になります。普通はアプリケーション名と同じだと思われるので、変数で参照する形で良いかと思います。直接文字列を記述することもできます。
個々のキーに関する意味については Information Property List Key Reference / Cocoa Keys を参照してください。
<key>NSUbiquitousContainers</key>
<dict>
<key>iCloud.com.example.MyApp</key>
<dict>
<key>NSUbiquitousContainerIsDocumentScopePublic</key>
<true/>
<key>NSUbiquitousContainerSupportedFolderLevels</key>
<string>Any</string>
<key>NSUbiquitousContainerName</key>
<string>$(PRODUCT_NAME)</string>
</dict>
</dict>
Document Types と Exported UTIs を定義する
ドキュメントの種類を定義します。Xcodeプロジェクトの Info タブで例えば次のように設定します。
さわりだけ書いておきますが、詳しくはドキュメントベースドアプリケーションに関する資料を参照してください。
Document Types
扱うドキュメントごとに定義します。
-
Name
- ドキュメントの名称。Finderの情報ウインドウなどに表示される名称
-
Class
- このドキュメントを扱うモデルクラス名を指定する
- デフォルトでは「
$(PRODUCT_MODULE_NAME).Document
」などになっているはず
-
Extensions
- 拡張子
-
Icon
- ドキュメント用のアイコンファイル(.icns)名
-
Identifier
- ドキュメントを示すUTI
- ここで独自に定義しても良い
-
Role
- このアプリケーションがいずれの役割を果たすか。通常はEditorかと
- Editor, Viewer, None
-
Bundle
- ドキュメントは単一のファイルか、Bundle Document かを指示する
- パッケージの場合は一つのフォルダの中に様々な書類が格納されるが、Finder上では単一書類のように振る舞う。「パッケージの内容を表示」によって開くことができる
Exported UTIs
Document Types に対応する定義をこちらでも行います。
-
Description
- 説明文
-
Identifier
- ドキュメントを示すUTI
-
Icon
- ドキュメント用のアイコンファイル(.icns)名
-
Conforms To
- このドキュメントがどのUTIを継承するかを指示する
- プレーンテキスト系の場合は
public.text
- Bundle Documentの場合は
com.apple.package
-
Extensions
- 拡張子
-
Mime Types
- 任意でMIMEタイプ
-
Pboard Types
- 任意でペーストボードタイプ
- ドラッグ&ドロップに対応する場合などに必要
-
OS Types
- 基本的には不要
このままではうまく反映されないため、Info.plist のビルド番号を変更する
ここまではドキュメント通りのやり方なので間違っていないはずなのですが、なぜかiCloud Driveにストレージが現れないことがあります。その対策として「Info.plist の CFBundleVersion を書き換えて再ビルドする」ということを行ってこのバグのような現象を回避することができます。
合わせてクリーンビルドすると良いかもしれません。
参照: https://stackoverflow.com/questions/26865643/icloud-drive-folder
うまくいくと、次のようにアプリケーションのiCloudストレージが現れるようになります。現れないようなら上記の方法を試してみてください。

Documentクラスの実装
Xcodeでドキュメントベースドアプリケーションとして新規プロジェクトを作成すると初めからNSDocumentのサブクラスがあるかと思います。ここに必要な実装を行なっていきます。
自動保存に対応しておく
次のようにしてプロパティをオーバーライドします。
override class var autosavesInPlace: Bool {
return true
}
iCloud共同編集に(とりあえず)対応する
次のようにしてプロパティをオーバーライドすれば、とりあえず「人を追加」メニューは利用可能になります。
詳しくはまだ調べられていないためこの程度にとどめておきます。
override var allowsDocumentSharing: Bool {
return true
}

FileWrapperのI/O関係メソッドを実装する
NSDocumentのファイルの読み込みと書き出しにはいくつかの方法がありますが、FileWrapper (NSFileWrapper) を利用する方法なら Bundle Document にも対応することができるようです。これらを実装するなら readFromData
系は必要ないかと思います。
詳しい実装方法は Packaged Document for OS X や各種ガイドを参考にしてください。
// typeName にはドキュメントの UTI が入るので、適宜分岐処理を行う
override func read(from fileWrapper: FileWrapper, ofType typeName: String) throws {
...
}
// 保存するドキュメントを表すFileWrapperオブジェクトを用意して返す
override func fileWrapper(ofType typeName: String) throws -> FileWrapper {
...
}