Apple も推していますので、iOS デバイスでも Files 経由で使う ドキュメントベースアプリケーション が普及してくるかと思います。
開発する側視点でも複数のタイプの情報を1つのドキュメントファイルにまとめるよりパッケージ型のドキュメントにしておく方が、様々な処理が容易となります。
Sponsor Link
シリーズ記事
Document-based app で PackagedDocument を扱う
[SwiftUI] [Swift] iOS で Packaged Document を使う (その1 Packaged Document を定義して実装する)
[SwiftUI] [Swift] iOS で Packaged Document を使う (その2 Packaged Document をベースにしたモデルを定義して表示する)
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 を設定します。
ドキュメント設定(PackageDocSwiftUIDocument.swift)
以下が、完成コードです。
//
// 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)
ドキュメントの読込部分について詳細を説明します。(保存側を先に読んだほうがわかりやすいかもしれません)
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)
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