2021年08月23日, 編集履歴
SwiftUIでDocument-Based Appな画像閲覧アプリを作る その1 プロジェクト作成から画像の表示まで
SwiftUIでDocument-Based Appな画像閲覧アプリを習作した。PNG/JPEG画像を開いて閲覧、スクロール、ピンチジェスチャで拡大縮小、ツールバーにボタン配置等を実装した。その覚書その1。
このシリーズの他のブログは、
- SwiftUIでDocument-Based Appな画像閲覧アプリを作る その2 ジェスチャによる拡大縮小
- SwiftUIでDocument-Based Appな画像閲覧アプリを作る その3 ツールバーの実装
- SwiftUIでDocument-Based Appな画像閲覧アプリを作る その4 メニューコマンドの実装
プロジェクトはGitHubで公開している。
ビルド環境は、
- macOS 11.5.2
- Xcode 12.5.1
である。
Document Appテンプレートでプロジェクト作成
SwiftUIでDocument-Based Appなプロジェクトを作成するには「Document App」テンプレートを使用する。今回はmacOS/iOS両対応で作りたいので、「Multiplatform」の「Document App」テンプレートを選択してプロジェクトを作成する。

Info.plistの設定
Info.plistファイルで、アプリが読み書きできるファイル形式を以下の項目で指定する。
- Document Types(
CFBundleDocumentTypes) - Imported Type Identifiers(
UTImportedTypeDeclarations) - Exported Type Identifiers(
UTExportedTypeDeclarations)
Document Typesは必須、既存のファイル形式を扱う場合は「Imported 〜」を、アプリ独自の形式を定義する場合は「Exported 〜」を使用する。
今回は既存のPNG/JPEG画像を扱いたいので、「Exported 〜」は不使用。

Document Typesでは、PNG/JPEGそれぞれに形式の名称やUTIを指定する。また、アプリは画像の閲覧専用とするので、「Role」は「Viewer」を、「Handler Rank」は「Alternate」を選択した。

Imported Type Identifiersでは、それぞれの形式のUTIや拡張子、MIME Type等を指定する。
macOS/iOSのInfo.plistは独立しているので、両対応させる場合は両方で設定を行う必要がある。
FileDocument/ReferenceFileDocumentで対応形式を宣言
SwiftUI Document-Based Appでは、対応するファイル形式のモデルをFileDocumentあるいはReferenceFileDocumentに適合したオブジェクトで表現する。FileDocumentあるいはReferenceFileDocumentが持つstatic var readableContentTypes: [UTType] { get }プロパティで、対応する形式のUTIを[UTType]で返すようにする。
static var readableContentTypes: [UTType] {
[
UTType(importedAs: "public.png"),
UTType(importedAs: "public.jpeg")
]
}
今回はPNG/JPEG画像を同様に扱いたいので、ひとつのドキュメントモデルで一緒に宣言した。形式によって読み書きの処理等を変えたい場合は、複数のドキュメントモデルを作成する。
ファイルの書き込みにも対応する場合は、static var writableContentTypes: [UTType] { get }を実装する。
NSImage、UIImageを一律に扱う
今回のアプリはmacOSとiOSの両対応にしたい。それぞれの環境で画像を表すオブジェクトであるNSImage、UIImageを一律に扱うため、typealiasを使ってIVImageなる型を定義する。
#if os(macOS)
typealias IVImage = NSImage
#elseif os(iOS)
typealias IVImage = UIImage
#endif
また、後の工程で画像を表示するビューとしてSwiftUIのImageを使うが、IVImageを扱えるようにextensionで拡張する。
extension Image {
init(ivImage: IVImage) {
#if os(macOS)
self.init(nsImage: ivImage)
#elseif os(iOS)
self.init(uiImage: ivImage)
#endif
}
}
ドキュメントモデルで読み書き処理を実装
FileDocument/ReferenceFileDocumentなドキュメントのモデルで画像の読み書きの処理を実装する。次回の工程でモデルのプロパティを@Publishedにしたかったので、今回のアプリではReferenceFileDocumentで実装する。この場合、
typealias Snapshotの定義init(configuration:)の実装func fileWrapper(snapshot:configuration:)の実装func snapshot(contentType:)の実装
が必要である。
class ImageDocument: ReferenceFileDocument {
typealias Snapshot = IVImage
static var readableContentTypes: [UTType] {
[
UTType(importedAs: "public.png"),
UTType(importedAs: "public.jpeg")
]
}
var image: IVImage
init(image: IVImage = IVImage()) {
self.image = image
}
required init(configuration: ReadConfiguration) throws {
guard let data = configuration.file.regularFileContents,
let image = IVImage(data: data)
else {
throw CocoaError(.fileReadCorruptFile)
}
self.image = image
}
func fileWrapper(snapshot: IVImage, configuration: WriteConfiguration) throws -> FileWrapper {
throw CocoaError(.fileWriteUnknown)
}
func snapshot(contentType: UTType) throws -> IVImage {
return self.image
}
}
読み込み処理は、単純に画像を読み込んでIVImage(NSImage、UIImage)型のimageプロパティに格納する。func fileWrapper(snapshot:configuration:)はファイルの書き込みに用いられるが、今回のアプリは閲覧専用なのでエラーを吐かせておく。
init(image:)は後の工程でビューのプレビューを表示させるときに空のドキュメントを供給するときに必要なので実装しておいた。
DocumentGroupの指定
Appプロトコルに適合した構造体の中、DocumentGroupシーンでドキュメントモデルの指定を行う。
struct ImageViewerSwiftUIApp: App {
var body: some Scene {
DocumentGroup(viewing: ImageDocument.self) { file in
ContentView(document: file.document)
}
}
}
今回は閲覧専用なので、init(viewing:viewer:)を使った。第1引数にはドキュメントモデルの型を指定する。複数のドキュメントモデルを用いる場合は、DocumentGroupを複数宣言する。
ビューの作成
DocumentGroup内で指定したビューでドキュメントの内容を表示する(前項のContentView)。
struct ContentView: View {
@ObservedObject var document: ImageDocument
var body: some View {
ScrollView([.horizontal, .vertical]) {
Image(ivImage: document.image)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView(document: ImageDocument())
}
}
今回のドキュメントモデルで採用したReferenceFileDocumentはObservableObjectに適合しているので、プロパティとして保持するときは@ObservedObjectで受ける。大きな画像を開いたときにスクロールできるように、ImageをScrollViewに内包させた。
ここまでで、とりあえずPNG/JPEG画像を閲覧するアプリが動く。

Document-Based Appなので、複数のウィンドウで別々の画像を開いたり、複数開いたウィンドウをタブでひとつのウィンドウにまとめたりできる。
次回はピンチイン/アウトジェスチャによる画像の拡大縮小の実装を行う。