[SwiftUI] Document App で Document Package を扱う

SwiftUI

     

TAGS:

⌛️ 3 min.
Xcode で Document App を選択したアプリで Document Package を扱う方法を説明します

環境&対象

以下の環境で動作確認を行なっています。

  • macOS Big Sur 11.1
  • Xcode 12.3

Xcode の生成コード

Xcode 12.3 では、Document App を選択してプロジェクトを作成すると、テキストファイルをドキュメントとしてもつアプリケーションがテンプレートとして作成されます。

複雑なデータを処理するアプリでは、Document Package をドキュメントとして持ちたいケースがあります。

テンプレートのコードを修正し、Document Package を扱うことができるようにする方法を説明します。

SwiftUI [Swift] [iOS][MacOS] Document App を作る(その1:UTI と Document Type の定義)

全体のステップ

以下のステップで説明していきます。

  1. 作るアプリの概要
  2. Doc Type の定義
  3. Document Package コード実装
  4. アプリ実装
  5. 動作確認

作るアプリの概要

以下のような外観のアプリを作ります。

アプリ外観
アプリ外観

画像をタップすると、画像が切り替わります。
保存すると、その時表示されている テキスト と 画像 が 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 として定義しています。

Exported Type Identifier
Exported Type Identifier

Document Package コード実装

Type Identifier 定義 (コード)

Document 定義に、UTType を追加/置換し、readableContentTypes に設定します。

BundleDocDocument


extension UTType {
    static var bundletxtimg: UTType {
        UTType(exportedAs: "com.smalldesksoftware.bundletxtimage")
    }
}

上記の定義は、Info.plist で定義した Identifier と一致している必要があります。

次に、ドキュメントとして、読み書きできるタイプを設定します。

BundleDocDocument


struct BundleDocDocument: FileDocument {
    // .. snip ...
    // (1)
    static var readableContentTypes: [UTType] { [.bundletxtimg] }

    // .. snip ...
コード解説
  1. Document の readableContentTypes を設定することで、読み書き対象のタイプを指定することができます

FileDocument.init に実装追加

.init を実装することで、実際の読み込み操作を実装することになります。

BundleDocDocument


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 .. 
}
コード解説
  1. configuration.file に保存されている FileWrapper をチェックして取得
  2. TextFile 用の FileWrapper を使用してテキストデータを取得
  3. ImageFile 用の FileWrapper を使用してイメージデータを取得
  4. ImageNameFile 用の FileWrapper を使用してテキストデータを取得

FileDocument.fileWrapper に実装追加

.fileWrapper を実装することで、実際の書き出し操作を実装することになります。

BundleDocDocument


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
    }
}
コード解説
  1. テキストを data 化して、FileWrapper に渡します
  2. イメージを data 化して、FileWrapper に渡します
  3. イメージ名(テキスト) data 化して、FileWrapper に渡します
  4. 3つの FileWrapper をまとめた FileWrapper をドキュメントの FileWrapper として返します

アプリ実装

Document が保持している、txt, image, imageName を表示するようにします。

Image は、タップされた時に、別のイメージに切り替えるようにします。

# 表示用のイメージは、”cat”, “dog” という名前で、リソースに登録済みです

ContentView


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 させるメソッドを ドキュメントに追加しています

BundleDocDocument extension


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 Package の内容を確認
Document Package の内容を確認

各ファイルの中身も期待通りになっていることも確認できます。

これで、単一ファイルではなく、複数ファイルを束ねたフォルダをドキュメントとして認識させることができていることになります。

まとめ:Document App で Document Package を扱う方法

Document App で Document Package を扱う方法
  • Document App をベースにすると簡単
  • UTI を定義する
  • FileDocument に準拠する Document を 定義した UTI に対応させる
  • FileDocument に準拠する Document の init, fileWrapper を対応させる

説明は以上です。
不明な点やおかしな点ありましたら、こちらまで。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です