Sponsor Link
環境&対象
- macOS Big Sur 11.1
- Xcode 12.3
Xcode の生成コード
Xcode 12.3 では、Document App を選択してプロジェクトを作成すると、テキストファイルをドキュメントとしてもつアプリケーションがテンプレートとして作成されます。
複雑なデータを処理するアプリでは、Document Package をドキュメントとして持ちたいケースがあります。
テンプレートのコードを修正し、Document Package を扱うことができるようにする方法を説明します。
[Swift] [iOS][MacOS] Document App を作る(その1:UTI と Document Type の定義)
全体のステップ
以下のステップで説明していきます。
- 作るアプリの概要
- Doc Type の定義
- Document Package コード実装
- アプリ実装
- 動作確認
作るアプリの概要
以下のような外観のアプリを作ります。
画像をタップすると、画像が切り替わります。
保存すると、その時表示されている テキスト と 画像 が Document Pacakge に保存されます。
Doc Type の定義
以下のように Document Package を定義します。
- Document Package として以下のファイルを内部に持つ
- テキストファイル (ファイル名 txtfile.txt)
- イメージファイル (ファイル名 imagefile.png)
- イメージ名称 (ファイル名 imagename.txt)
Type Identifier 定義 (Info.plist)
ドキュメントの定義としては、以下とします。
- Description
- Text, Image, ImageName
- Identifier
- com.smalldesksoftware.bundletxtimage
- Conform to
- com.apple.package
- extension
- bundletxtimg
以下のように、Exported Type Identifier として定義しています。
Document Package コード実装
Type Identifier 定義 (コード)
Document 定義に、UTType を追加/置換し、readableContentTypes に設定します。
extension UTType {
static var bundletxtimg: UTType {
UTType(exportedAs: "com.smalldesksoftware.bundletxtimage")
}
}
上記の定義は、Info.plist で定義した Identifier と一致している必要があります。
次に、ドキュメントとして、読み書きできるタイプを設定します。
struct BundleDocDocument: FileDocument {
// .. snip ...
// (1)
static var readableContentTypes: [UTType] { [.bundletxtimg] }
// .. snip ...
- Document の readableContentTypes を設定することで、読み書き対象のタイプを指定することができます
FileDocument.init に実装追加
.init を実装することで、実際の読み込み操作を実装することになります。
let imageNames = ["dog", "cat"]
enum DocContent:String {
case TextFile = "txtfile.txt"
case ImageFile = "imagefile.png"
case ImageNameFile = "imagename.txt"
}
struct BundleDocDocument: FileDocument {
var text: String = "Hello world"
var image: NSImage = NSImage.init(named: imageNames[0])!
var imageName: String = imageNames[0]
init() {
// initialization code here
}
static var readableContentTypes: [UTType] { [.bundletxtimg] }
init(configuration: ReadConfiguration) throws {
// (1)
guard configuration.file.isDirectory else { fatalError("invalid filewrapper") }
guard let fileWrappers = configuration.file.fileWrappers else { fatalError("invalid filewrapper") }
// (2)
if let txtFileWrapper = fileWrappers[DocContent.TextFile.rawValue] {
if let data = txtFileWrapper.regularFileContents,
let string = String(data: data, encoding: .utf8) {
text = string
}
}
// (3)
if let imgFileWrapper = fileWrappers[DocContent.ImageFile.rawValue] {
if let data = imgFileWrapper.regularFileContents,
let img = NSImage(data: data) {
self.image = img
}
}
// (4)
if let imgNameFileWrapper = fileWrappers[DocContent.ImageNameFile.rawValue] {
if let data = imgNameFileWrapper.regularFileContents,
let imgName = String(data: data, encoding: .utf8) {
self.imageName = imgName
}
}
}
// .. snip ..
}
- configuration.file に保存されている FileWrapper をチェックして取得
- TextFile 用の FileWrapper を使用してテキストデータを取得
- ImageFile 用の FileWrapper を使用してイメージデータを取得
- ImageNameFile 用の FileWrapper を使用してテキストデータを取得
FileDocument.fileWrapper に実装追加
.fileWrapper を実装することで、実際の書き出し操作を実装することになります。
let imageNames = ["dog", "cat"]
enum DocContent:String {
case TextFile = "txtfile.txt"
case ImageFile = "imagefile.png"
case ImageNameFile = "imagename.txt"
}
struct BundleDocDocument: FileDocument {
var text: String = "Hello world"
var image: NSImage = NSImage.init(named: imageNames[0])!
var imageName: String = imageNames[0]
init() {
// initialization code here
}
static var readableContentTypes: [UTType] { [.bundletxtimg] }
// .. snip ..
func fileWrapper(configuration: WriteConfiguration) throws -> FileWrapper {
// (1)
let txtData = text.data(using: .utf8)!
let txtFileWrapper = FileWrapper.init(regularFileWithContents: txtData)
// (2)
let imgData = image.tiffRepresentation!
let imgFileWrapper = FileWrapper.init(regularFileWithContents: imgData)
// (3)
let imgNameData = imageName.data(using: .utf8)!
let imgNameFileWrapper = FileWrapper.init(regularFileWithContents: imgNameData)
// (4)
let rootFileWrapper = FileWrapper.init(directoryWithFileWrappers: [DocContent.TextFile.rawValue: txtFileWrapper,
DocContent.ImageFile.rawValue: imgFileWrapper,
DocContent.ImageNameFile.rawValue: imgNameFileWrapper])
return rootFileWrapper
}
}
- テキストを data 化して、FileWrapper に渡します
- イメージを data 化して、FileWrapper に渡します
- イメージ名(テキスト) data 化して、FileWrapper に渡します
- 3つの FileWrapper をまとめた FileWrapper をドキュメントの FileWrapper として返します
アプリ実装
Document が保持している、txt, image, imageName を表示するようにします。
Image は、タップされた時に、別のイメージに切り替えるようにします。
# 表示用のイメージは、”cat”, “dog” という名前で、リソースに登録済みです
struct ContentView: View {
@Binding var document: BundleDocDocument
var body: some View {
VStack {
TextEditor(text: $document.text)
Image(nsImage: document.image)
.onTapGesture {
document.toggleImage()
}
}
.frame(width: 600, height: 600)
}
}
画像を toggle させるメソッドを ドキュメントに追加しています
extension BundleDocDocument {
mutating func toggleImage() {
if self.imageName == imageNames[0] {
self.image = NSImage(named: imageNames[1])!
self.imageName = imageNames[1]
} else {
self.image = NSImage(named: imageNames[0])!
self.imageName = imageNames[0]
}
}
}
動作確認
以下のような動作のアプリになります。
アプリの動作
ドキュメント内部
作成したドキュメントを保存し、Finder から確認してみると、1つのファイルに見えます。
そのファイルを「パッケージの内容を確認」で開いてみると、以下のように構成されていることが確認できます。
各ファイルの中身も期待通りになっていることも確認できます。
これで、単一ファイルではなく、複数ファイルを束ねたフォルダをドキュメントとして認識させることができていることになります。
まとめ:Document App で Document Package を扱う方法
- Document App をベースにすると簡単
- UTI を定義する
- FileDocument に準拠する Document を 定義した UTI に対応させる
- FileDocument に準拠する Document の init, fileWrapper を対応させる
説明は以上です。
不明な点やおかしな点ありましたら、こちらまで。
Sponsor Link