Sponsor Link
環境&対象
- macOS14.2 Beta
- Xcode 15.1 beta
- iOS 17
- Swift 5.9
Document App
アプリケーションのタイプの1つに、Document App があります。
Xcode でプロジェクトを作成する時に選べるテンプレートの1つでもあります。
Document App として作成することで、アプリの内部にデータを持つのではなく、データを外部ファイルとして持つことができるようになります。
AppKit/UIKit では、NSDocument や UIDocument を使って実装していましたが、SwiftUI になって、macOS / iOS のどちらでも FileDocument や ReferenceFileDocument を使って同じコードで 実装できるようになりました。
基本的には同じコードで macOS / iOS どちらにも対応できるのですが、例によって(?) 少しづつ振る舞いが異なるケースが見えてきましたので、まとめてみます。
以下のケースでの相違を見ていきます。
・新規ファイルを作る時
・既存ファイルを開く時
・保存する時
・Finder(相当)への行き来
相違を見る時に使うコード
以下のコードで、振る舞いの相違を見ていきます。
できるだけテンプレートにコードを追加しないようにしています。
# FileDocument はテンプレートコードのままです
App
//
// DocAppTestApp.swift
//
// Created by : Tomoaki Yagishita on 2023/10/30
// © 2023 SmallDeskSoftware
//
import SwiftUI
@main
struct DocAppTestApp: App {
var body: some Scene {
DocumentGroup(newDocument: DocAppTestDocument()) { file in
ContentView(document: file.$document,
fileURL: file.fileURL)
}
}
}
App では、渡ってくる fileURL がわかるように、ContentView に fileURL を渡しています。
ContentView
//
// ContentView.swift
//
// Created by : Tomoaki Yagishita on 2023/10/30
// © 2023 SmallDeskSoftware
//
import SwiftUI
struct ContentView: View {
@Binding var document: DocAppTestDocument
let fileURL: URL?
var body: some View {
VStack {
Text("fileURL: \(fileURL?.relativePath ?? "nil")")
TextEditor(text: $document.text)
}
.navigationTitle("File: " + (fileURL?.lastPathComponent ?? "-"))
}
}
#Preview {
ContentView(document: .constant(DocAppTestDocument()), fileURL: nil)
}
上位から渡された fileURL の内容がわかるように表示しています。Optional で渡されるため nil の時には、nil だったとわかるようにしています。
FileDocument
//
// DocAppTestDocument.swift
//
// Created by : Tomoaki Yagishita on 2023/10/30
// © 2023 SmallDeskSoftware
//
import SwiftUI
import UniformTypeIdentifiers
extension UTType {
static var exampleText: UTType {
UTType(importedAs: "com.example.plain-text")
}
}
struct DocAppTestDocument: 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)
}
}
FileDocument は、無変更です。
DocumentGroup の動作については、以下の記事で説明しています。
[SwiftUI] ReferenceFileDocument を使った DocumentBased App の作り方
新規ファイルを作る時
新規ファイルは、以下の操作で作成することができます。
・macOS では、メニュー “File”-“New” を選択する 等
・iOS では、File Browser 上で、”+” ボタンを押下する 等
同じ動作に見えますが、以下の点で異なります。
macOS: fileURL には、nil が渡される
iOS: fileURL には ファイルの URL が渡される
macOS / AppKit での Document-based App では、ファイル位置を確定する前(ファイルを保存する前)から Document を扱うことができましたが、SwiftUI/DocumentGroup でも同じように扱っていることがわかります。
iOS / UIKit では、新規ドキュメント作成時に Document の fileURL は、確定するようです。
つまり、macOS への対応が必要なケースでは fileURL が nil のケースも考慮しないといけません。
既存ファイルを開く時
既存ファイルは、以下の操作で作成することができます。
・macOS では、メニュー “File”-“Open” を選択し、その後のダイアログでファイルを選択することで開くことができます。
・iOS では、File Browser 上で、該当ファイルをタップすることで、開くことができます。
既存ファイルを開く時には、いずれも fileURL に 該当ファイルの URL が渡されてきます。
iOS / macOS で 特に相違点はありません。
保存する時
ファイル保存を行うための UI は、デフォルトでは、macOS でのみ用意されています。
・macOS では、メニュー “File”-“Save” を選択することで 明示的に保存することができます。
・iOS では、Save するための UI は用意されていません。
SwiftUI では、自動 Save が行われるようになっています。
上記の相違はありますが、macOS / iOS のどちらでも FileDocument / ReferenceFileDocument を使用しまして実装できますし、それぞれに 保存するための メソッドが定義されていますので、同じものを使用することが可能です。
単に、macOS では ユーザーが明示的に保存することができるという違いです。
使った感じですが(汗)、iOS では、すくなくとも アプリ切り替えやファイル切り替えのタイミングでは保存されている気がします。
# 調べた範囲では 自動 Save するタイミングは明記されていないと思います。
Finder/File Browserとの行き来
macOS での Finder、iOS での ファイル選択画面(File Browser) との関係性についてです。
iOS では、DocumentGroup を使用することで ファイル選択画面(File Browser) もアプリに組み込まれることになります。
![FileBrowser@iOS](https://software.small-desk.com/wp-content/uploads/2023/10/FileBrowser@iOS.png)
Finder/FileBrowser との行き来
・macOS では、Finder は常に存在しています。Finder ウィンドウや デスクトップをクリックすることで、Finder をアクティブにすることができ、Finder 上で ファイルをダブルクリックする 等で該当ファイルをアプリで開くことが可能です。
・iOS では(テンプレートで作成されたコードだけでは)、開いたファイルを閉じることができません。
iOS では、例示したコードのように、.navigationTitle を指定しないと ナビゲーションバーが表示されず、File Browser との行き来ができません。
![NavigationBar](https://software.small-desk.com/wp-content/uploads/2023/10/NavigationBar.png)
NavigationTitle の不思議
File Browser と行き来できるために設定した navigationTitle だったのですが、少し試していると不思議な動作になっていることに気づきました。
以下のドキュメントが説明してくれると期待したのですが、今回の不思議な動作を説明してくれるものではありませんでした。
Apple のドキュメント “configure-your-apps-navigation-titles” は、こちら。
navigationTitle の不思議 @ macOS
fileURL に nil が渡されると navigationTitle に “-” を設定するようにしたのですが、実際に動かしてみると、ウィンドウタイトルには “Untitled” 等のような 仮のファイル名が表示されます。
これは、navigationTitle に指定している文字列ではありません。
つまり、指定した文字列が表示されているのではなく、ファイル名が表示されているということです・・・
複数ファイルを開いた時の Tab 表示での Tab のタイトルもファイル名となっています。
結局 macOS では .navigationTitle で指定した文字列がどこに表示されているのかは、みつかりませんでした・・・
navigationTitle @ iOS
iOS では、navigationTitle に指定した文字列がきちんと(?) タイトル位置に表示されます。
瑣末なことですが、このことから DocumentGroup は、NavigationStack を使用して、ドキュメント表示用のビューを表示しているとわかります。
まとめ
FileDocument/ReferenceFileDocument と DocumentGroup を使用する Document App での挙動を説明しました。
- DocumentGroup を使用すると基本的な機能は提供される
- macOS では新規ドキュメントの URL に nil が渡される
- iOS では、明示的な 保存の UI は提供されていない
- iOS では、.navigationTitle 指定しないと、File Browser との行き来ができなくなる
- macOS では、.navigationTitle を指定しても無視され、ファイル名が表示される
説明は以上です。
不明な点やおかしな点ありましたら、こちらまで。
SwiftUI おすすめ本
SwiftUI を理解するには、以下の本がおすすめです。
SwiftUI ViewMatery
SwiftUI で開発していくときに、ViewやLayoutのための適切なmodifierを探すのが大変です。
英語での説明になってしまいますが、以下の”SwiftUI Views Mastery Bundle”という本がビジュアル的に確認して探せるので、便利です。
英語ではありますが、1ページに コードと画面が並んでいるので、非常にわかりやすいです。
View に適用できる modifier もわかりやすく説明されているので、ビューの理解だけではなく、どのような装飾ができるかも簡単にわかります。
超便利です
販売元のページは、こちらです。
SwiftUI 徹底入門
# SwiftUI は、毎年大きく改善されていますので、少し古くなってしまいましたが、いまでも 定番本です。
Swift学習におすすめの本
詳解Swift
Swift の学習には、詳解 Swift という書籍が、おすすめです。
著者は、Swift の初期から書籍を出していますし、Swift の前に主力言語だった Objective-C という言語についても同様の書籍を出しています。
最新版を購入するのがおすすめです。
現時点では、上記の Swift 5 に対応した第5版が最新版です。
Sponsor Link