Apple も推していますので、iOS デバイスでも Files 経由で使う ドキュメントベースアプリケーション が普及してくるかと思います。
開発する側視点でも複数のタイプの情報を1つのドキュメントファイルにまとめるよりパッケージ型のドキュメントにしておく方が、様々な処理が容易となります。
Sponsor Link
目次
Packaged Document サンプル
以前、Apple から、Objective-C で記述され、UIKit ベースのサンプルが提供されていました。
Xcode12 でも動きました。ここからダウンロードできます。
今回は、Apple のサンプルと、Xcode12 が Document App として生成するテンプレートコードをベースに作成していきます。
サンプルアプリ概要
写真とテキスト情報を合わせて、ドキュメントに保存するアプリです。テキスト情報は、テキストファイル。写真の情報は、PNGファイルとして保存されます。
テキストファイル、PNGファイルは、パッケージの内部に保存されますので、アプリのデータファイルとしては、1つに見えます。
サンプルアプリ機能
サンプルアプリに実装されている機能は、以下の通りでした。
- ドキュメントリストアップ/作成/削除/名称変更
- テキスト編集
- 写真選択(PhotoPicker を使って)
まだ、Files アプリが提供されていなかった時のサンプルなので、Files アプリの機能も実装されています。
サンプルアプリ内で、ドキュメントの作成や削除、名称変更が行えるようになっていますが、現在の SwiftUI で提供されている DocumentGroup を使って管理機能を提供するほうが、良さそうです。
つまり、SwiftUI アプリ側では、(Package 化された)ドキュメントの読み書きとテキスト編集/写真選択 の機能のみを実装することとします。
SwiftUI アプリ仕様
以下のような仕様で作ります
- Document type は、Apple のサンプルを流用
- DocumentGroup から開かれたファイルは、詳細ビューでテキスト編集と写真の選択が行える
- サンプルアプリのドキュメント編集モードは廃止。(開いた時点で編集モードになっているという前提)
実装
プロジェクト作成
Xcode を起動し、以下の設定でプロジェクトを作成します。
- Product Name: PackageDocSwiftUI
- Interface: SwiftUI
- Life Cycle: SwiftUI App
- Language: Swift
Product Name は、変更可能です。変更した場合は、サンプルコードを適当に読み替えてください。
ドキュメント設定(Info.plist)
Apple のサンプルを参考に、Document と Exported Type ID を設定します。
[Swift] [iOS][MacOS] Document App を作る(その1:UTI と Document Type の定義)
ドキュメント設定(PackageDocSwiftUIDocument.swift)
以下が、完成コードです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 |
// // PackageDocSwiftUIDocument.swift // // Created by : Tomoaki Yagishita on 2020/11/05 // © 2020 SmallDeskSoftware // import SwiftUI import UniformTypeIdentifiers extension UTType { // (1) static var notesDoc: UTType { UTType(exportedAs: "com.apple.dts.notesDoc") } } struct PackageDocSwiftUIDocument: FileDocument { // (2) static let textFileName: String = "Text.txt" static let imageFileName: String = "Image.png" var noteString: String = "" var image: UIImage? = nil // (3) init() { noteString = "" image = nil } // (4) static var readableContentTypes: [UTType] { [.notesDoc] } // (5) init(configuration: ReadConfiguration) throws { let docWrapper = configuration.file guard let noteWrapper = docWrapper.fileWrappers![PackageDocSwiftUIDocument.textFileName] else { fatalError("failed to get notewrapper") } guard let textData = noteWrapper.regularFileContents else { fatalError("failed to get text data") } guard let string = String.init(data: textData, encoding: .utf8) else { fatalError("failed to unarchive text data") } var image: UIImage? = nil if let imageWrapper = docWrapper.fileWrappers![PackageDocSwiftUIDocument.imageFileName] { guard let imageData = imageWrapper.regularFileContents else { fatalError("failed to get image data") } image = UIImage(data: imageData) } self.noteString = string self.image = image } // (6) func fileWrapper(configuration: WriteConfiguration) throws -> FileWrapper { print("----- save -----") let docWrapper = FileWrapper.init(directoryWithFileWrappers: [String:FileWrapper]()) guard let textData = noteString.data(using: .utf8) else { fatalError("failed to get text data") } let noteWrapper = FileWrapper.init(regularFileWithContents: textData) noteWrapper.preferredFilename = PackageDocSwiftUIDocument.textFileName docWrapper.addFileWrapper(noteWrapper) if let image = self.image { guard let imageData = image.pngData() else { fatalError("failed to get png data") } let imageWrapper = FileWrapper.init(regularFileWithContents: imageData) imageWrapper.preferredFilename = PackageDocSwiftUIDocument.imageFileName docWrapper.addFileWrapper(imageWrapper) } return docWrapper } } |
- Info.plist でも定義したタイプを Swift 上でも定義します
- Package 内での保存に使われるファイル名を定義しています
- ドキュメントの initializer
- ドキュメントが Read できるタイプを定義しています。別途定義しなければ、同じタイプに対して Write もできることになります。
- Files アプリ上で ドキュメントをクリックした際に呼ばれます
- 保存するときに使用する FileWrapper を定義しています
ドキュメント読込の詳細(PackageDocSwiftUIDocument.swift)
ドキュメントの読込部分について詳細を説明します。(保存側を先に読んだほうがわかりやすいかもしれません)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
init(configuration: ReadConfiguration) throws { // (1) let docWrapper = configuration.file // (2) guard let noteWrapper = docWrapper.fileWrappers![PackageDocSwiftUIDocument.textFileName] else { fatalError("failed to get notewrapper") } guard let textData = noteWrapper.regularFileContents else { fatalError("failed to get text data") } guard let string = String.init(data: textData, encoding: .utf8) else { fatalError("failed to unarchive text data") } // (3) var image: UIImage? = nil if let imageWrapper = docWrapper.fileWrappers![PackageDocSwiftUIDocument.imageFileName] { guard let imageData = imageWrapper.regularFileContents else { fatalError("failed to get image data") } image = UIImage(data: imageData) } // (4) self.note = Note(notes: string, image: image) } |
- ドキュメントが開かれると ReadConfiguration を引数として init が呼ばれます。.file に ドキュメント全体の FileWrapper がセットされています
- ドキュメント全体の FileWrapper には、保存ファイル名をキーとして、テキストファイル用とイメージファイル用の FileWrapper が設定されています。ここでは、テキストファイル用の FileWrapper を取得し、FileWrapper から、テキストを読み込んでいます。
- テキストと同様に、イメージファイルから UIImage を読み込んでいます。(イメージは、optional としているため、保存されていなケースも考慮しています)
- 取得したテキストとイメージを使って、ドキュメントモデル(Note) を設定しています
ドキュメント保存の詳細(PackageDocSwiftUIDocument.swift)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
func fileWrapper(configuration: WriteConfiguration) throws -> FileWrapper { // (1) let docWrapper = FileWrapper.init(directoryWithFileWrappers: [String:FileWrapper]()) // (2) guard let textData = note.notes.data(using: .utf8) else { fatalError("failed to get text data") } let noteWrapper = FileWrapper.init(regularFileWithContents: textData) noteWrapper.preferredFilename = PackageDocSwiftUIDocument.textFileName docWrapper.addFileWrapper(noteWrapper) // (3) if note.image != nil { guard let imageData = note.image?.pngData() else { fatalError("failed to get png data") } let imageWrapper = FileWrapper.init(regularFileWithContents: imageData) imageWrapper.preferredFilename = PackageDocSwiftUIDocument.imageFileName docWrapper.addFileWrapper(imageWrapper) } // (4) return docWrapper } } |
- Package で保存するため、ドキュメント全体の FileWrapper は、このように初期化して作成します。Package 内に含まれる要素についての FileWrapper を追加していきます。
- テキストデータを保存するための FileWrapper を作成し、ファイル名を設定してから、ドキュメント全体の FileWrapper へ追加します。(後から、このファイル名をキーに取得できます)
- イメージデータを持っているならば、テキストと同様に、FileWrapper を作成し、ドキュメント全体の FileWrapper へ追加します。
- 必要な 下位の FileWrapper を設定した FileWrapper を返します。
途中まとめ: Package Document を使う iOS アプリの設定
- Document Type, Exported Type IDを設定する
- UTType も定義し、FileDocument#readableContentTypes に設定する
- FileDocument#init で、読み込み用の FileWrapper を設定する
- FileDocument#fileWrapper で、保存用の FileWrapper を設定する
説明は以上です。 Happy Coding!
Sponsor Link