[SwiftUI] ReferenceFileDocument を使った DocumentBased App の作り方

SwiftUI2021

     
⌛️ 2 min.
SwiftUI で ReferenceFileDocument を使った DocumentBasedApp の作り方を確認します。

環境&対象

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

  • macOS Sonoma Beta 3
  • Xcode 15 Beta 3
  • iOS 17 Beta
  • Swift 5.9

DocumentBasedApp

ワープロアプリ (Page のようなアプリ)のように、アプリを操作したデータは アプリ内部ではなく、ドキュメント内部に保存されるアプリケーションがあります。DocumentBased App と呼ばれます。

Pages, Numbers 等のアプリを使ったことがあればイメージできると思います。

SwiftUI でも DocumentBased App を作ることができ、Xcode に そのためのテンプレートも用意されています。

Xcode のテンプレートで用意されているのは、Document が FileDocument であるケース向けです。

これは、Document が Value-type であるときに使用できるテンプレートです。
つまり、Document を struct で定義して管理している時に使用できます。

ですが、仕様によっては、Value-type ではなく Reference-type の Document を使いたくなります。

この記事では、ReferenceType の Document = ReferenceFileDocument を Document にもつ SwiftUI アプリの作り方を説明します。

FileDocument

FileDocument は、content が Value-type であるようなドキュメントを扱うための Protocol です。

Apple のドキュメントは、こちら

value-type であるために、FileDocument を使用して作る Document は、UNDO/REDOが作りやすかったりします。

以下が、Xcode の テンプレートで作成される FileDocument です。

struct DocBasedAppDocument: FileDocument {
    var text: String

    init(text: String = "Hello, world!") {
        self.text = text
    }

    static var readableContentTypes: [UTType] { [.exampleText] }

    init(configuration: ReadConfiguration) throws {
        guard let data = configuration.file.regularFileContents,
              let string = String(data: data, encoding: .utf8)
        else {
            throw CocoaError(.fileReadCorruptFile)
        }
        text = string
    }
    
    func fileWrapper(configuration: WriteConfiguration) throws -> FileWrapper {
        let data = text.data(using: .utf8)!
        return .init(regularFileWithContents: data)
    }
}

init は、新規ドキュメント作成時に呼ばれます。

init(configuration:) は、既存ファイルを読み込む時に呼ばれます。

fileWrapper(configuration: ) は、保存時に呼ばれます。

ReferenceFileDocument

ReferenceFileDocument は、reference-type (i.e. class) で定義される Document です。

FileDocument とのわかりやすい相違点は、保存の処理です。

ReferenceFileDocument は、定義時に、Snapshot という型を定義することが必要です。
保存時には、ReferenceFileDocument から Snapshot を出力し、この Snapshot を保存対象として処理していきます。
こうすることで、保存中に別の処理が実行され不整合が発生することを抑止しています。

以下は、ReferenceFileDocument の実装をした Document の例です。

class MyReferenceDoc: ReferenceFileDocument {
    var text: String
    
    init() {
        self.text = "Hello world"
    }
    
    static var readableContentTypes: [UTType] { [.exampleText] }
    
    required init(configuration: ReadConfiguration) throws {
        guard let data = configuration.file.regularFileContents,
              let string = String(data: data, encoding: .utf8)
        else {
            throw CocoaError(.fileReadCorruptFile)
        }
        text = string
    }
    
    typealias Snapshot = String
    func snapshot(contentType: UTType) throws -> String {
        text
    }
    
    func fileWrapper(snapshot: String, configuration: WriteConfiguration) throws -> FileWrapper {
        let data = snapshot.data(using: .utf8)!
        return .init(regularFileWithContents: data)
    }
}

FileDocument を継承した DocBasedAppDocument との相違点は以下です。

・ class で実装されている
・init に required が付与されている
・Snapshot が定義されている。保存時の不整合を防ぐために save 時に snapshot が利用されます。
・fileWrapper(snapshot:, configuration:) は、snapshot を受け取る

この例では、snapshot として String 型を使用しています。

MEMO

この例では、Document の中身は、String なので、Value-type / Reference-type の違いが分かりにくいです。

DocumentGroup の振る舞い

ReferenceFileDocument は、 FileDocument と同様に、DocumentGroup と組み合わせて使用されます。

Apple のドキュメントは、こちら

この記事では、もう少し詳細に DocumentGroup と ReferenceFileDocument のやり取りを確認します。

特に、以下のケースでの振る舞いを確認します。
1) 新規ドキュメントが作成されて開かれる
2) 既存ドキュメントが指定されて開かれる
3) 新規ドキュメントが保存される

新規ドキュメント作成

新規ドキュメントが開かれる i.e. ユーザーが、アプリで “File” – “New” を選んだ時の振る舞いです。
以下の様な動作をします。

sequenceDiagram
      actor UI
	participant App
      participant DocumentGroup
      participant ReferenceFileDocument
 	UI ->> App: "New (document)"
      App ->> DocumentGroup: newDocument
      DocumentGroup ->> ReferenceFileDocument: init
	App ->> DocumentGroup: editor

既存ドキュメント開く

既存ファイルを開かれる i.e. ユーザーが “File” – “Open…” で既存のファイルを選んだ時には、以下の様な動作になります。

sequenceDiagram
      actor UI
      participant App
      participant DocumentGroup
      participant ReferenceFileDocument
      UI ->> App: "File"-"Open..."
      App ->> DocumentGroup: newDocument
      DocumentGroup ->> ReferenceFileDocument: init
        DocumentGroup ->> ReferenceFileDocument: init(config:)
	App ->> DocumentGroup: editor

新規ドキュメント作成と似てますよね。違いは、ReferenceFileDocument.init(config:) が追加で呼ばれている点です。

最初に init された ReferenceFileDocument はどこにも使われない気がしますが、不明です。

ですが、実際に指定されたファイルが読み込まれるのは、ReferenceFileDocument.init(config:) であることは間違いありません。

新規ドキュメント保存

ちょっと不思議かもしれませんが、ドキュメント保存時にも DocumentGroup は関わってきます。

sequenceDiagram
      actor UI
	participant App
      participant DocumentGroup
      participant ReferenceFileDocument
 	UI ->> App: "Save" (then specify filename)
      App ->> ReferenceFileDocument: snapshot
      App ->> ReferenceFileDocument: fileWrapper
      App ->> DocumentGroup: editor

推測ですが、editor に渡される引数 configuration が変更される(fileURL が確定します)ので、その変更を反映するために editor が呼ばれているのかもしれません。(特にドキュメント等に言及はありません)

まとめ

ReferenceFileDocument を使った DocumentBasedApp の作り方を確認しました。

ReferenceFileDocument を使った DocumentBasedApp の作り方
  • 大きな流れは FileDocument と同じ
  • 保存処理の時、ReferenceFileDocument は snapshot を作成し保存する
  • ReferenceFileDocument の保存先は、ReferenceFileConfiguration<Document> でしか 参照できない
  • 上記の ReferenceFileConfiguration は、DocumentGroup.editor 呼び出し時にのみ参照できる

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

SwiftUI おすすめ本

SwiftUI を理解するには、以下の本がおすすめです。

SwiftUI ViewMatery

SwiftUI で開発していくときに、ViewやLayoutのための適切なmodifierを探すのが大変です。
英語での説明になってしまいますが、以下の”SwiftUI Views Mastery Bundle”という本がビジュアル的に確認して探せるので、便利です。

英語ではありますが、1ページに コードと画面が並んでいるので、非常にわかりやすいです。

View に適用できる modifier もわかりやすく説明されているので、ビューの理解だけではなく、どのような装飾ができるかも簡単にわかります。

超便利です

SwiftUIViewsMastery

販売元のページは、こちらです。

SwiftUI 徹底入門

# SwiftUI は、毎年大きく改善されていますので、少し古くなってしまいましたが、いまでも 定番本です。

Swift学習におすすめの本

詳解Swift

Swift の学習には、詳解 Swift という書籍が、おすすめです。

著者は、Swift の初期から書籍を出していますし、Swift の前に主力言語だった Objective-C という言語についても同様の書籍を出しています。

最新版を購入するのがおすすめです。

現時点では、上記の Swift 5 に対応した第5版が最新版です。

コメントを残す

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