2021年09月06日, 編集履歴
SwiftUI macOSアプリでファイルを開く
SwiftUIの非Document AppなmacOSアプリにおいて、ファイルを開く処理を実装する。今回は以下の3つの手法を試した。
NSOpenPanel
fileImporter()
onDrop()
成果物をGitHubに公開している。
ビルド環境は、
- macOS 11.5.2
- Xcode 12.5.1
である。
NSOpenPanel
まずは、これまで通りNSOpenPanel
を使う方法から。以下のようなビューがあったとする。
Image
とButton
が並んでおり、Button
押下でNSOpenPanel
を表示、選択された画像をImage
に表示する場合を考える。対応する画像形式はPNGとJPEG、Image
のサイズは一定とする。
import SwiftUI
import UniformTypeIdentifiers
struct NSOpenPanelView: View {
@State private var image: NSImage? = nil
var body: some View {
VStack {
Image(nsImage: image ?? NSImage())
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 500, height: 500)
Button("Open") {
let openPanel = NSOpenPanel()
openPanel.allowsMultipleSelection = false
openPanel.canChooseDirectories = false
openPanel.canChooseFiles = true
openPanel.allowedFileTypes = [UTType.png.identifier, UTType.jpeg.identifier]
if openPanel.runModal() == .OK {
guard let url = openPanel.url,
let newImage = NSImage(contentsOf: url)
else { return }
image = newImage
}
}
.padding()
}
}
}
Button
のaction
コールバック内でNSOpenPanel
を生成し、runModal()
してやればよい。その返り値が.OK
の場合はファイル選択が成功し、NSOpenPanel
のurl
プロパティに選択されたファイルのURLが入っている。
対応形式にUTIが求められるときはUTType
型を使うとよい。UTType
は新しい型で、UTIを使うAPIによってはUTType
型を求められたり、String
型で求められたりが混在している。String
型を求められているAPIならUTType
のidentifier
を使う。UTType
型を使うときはimport UniformTypeIdentifiers
が必要である。後の項でも同様。
fileImporter()
SwiftUIらしいやり方として、fileImporter(isPresented:allowedContentTypes:onCompletion:)
やそのヴァリエーションを使う方法がある。前項と同様のビューがあったとする。
今度もButton
押下でファイル選択のパネルを出す。
import SwiftUI
import UniformTypeIdentifiers
struct FileImporterView: View {
@State private var image: NSImage? = nil
@State private var importerPresented = false
var body: some View {
VStack {
Image(nsImage: image ?? NSImage())
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 500, height: 500)
Button("Open") {
importerPresented = true
}
.padding()
}
.fileImporter(isPresented: $importerPresented, allowedContentTypes: [.png, .jpeg]) { result in
switch result {
case .success(let url):
guard let newImage = NSImage(contentsOf: url) else { return }
image = newImage
case .failure:
print("failure")
}
}
}
}
fileImporter()
は、その第1引数isPresented
に指定する@State
なプロパティがtrue
になったときにファイル選択パネルを表示する。Button
押下時のaction
としてそのプロパティをtrue
にする処理を入れてやればよい。isPresented
に指定したプロパティは、ファイル選択パネルでの操作が終わったときに自動的にfalse
に戻る。
fileImporter()
のコールバックの引数はResult<URL, Error>
型なのでswitch
文でURLを取り出し、画像を生成する。
ファイル選択パネルでキャンセルしても.failure
にはならないようである。
onDrop()
画像のドラッグ&ドロップも試してみる。以下のようなImage
が全面に配置されたビューがあったとする。
ドラッグ&ドロップによってImage
に画像を表示する場合を考える。また、ドラッグ中に画像がビューの領域に入った場合は、Text
をオーバーレイ表示させるとする。
import SwiftUI
import UniformTypeIdentifiers
struct DropView: View {
@State private var image: NSImage? = nil
@State private var isDropTargeted = false
var body: some View {
ZStack {
Image(nsImage: image ?? NSImage())
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 500, height: 500)
if isDropTargeted {
Rectangle()
.fill(Color(.windowBackgroundColor))
.opacity(0.8)
.overlay(
Text("Drop Here")
.font(.system(size: 64).bold())
)
}
}
.onDrop(of: [.png, .jpeg, .url, .fileURL], isTargeted: $isDropTargeted) { providers in
guard let provider = providers.first
else { return false }
if provider.hasItemConformingToTypeIdentifier(UTType.image.identifier) {
provider.loadItem(forTypeIdentifier: UTType.image.identifier, options: nil) { data, error in
guard let imageData = data as? Data,
let newImage = NSImage(data: imageData)
else { return }
image = newImage
}
}
else if provider.hasItemConformingToTypeIdentifier(UTType.url.identifier) {
provider.loadItem(forTypeIdentifier: UTType.url.identifier, options: nil) { data, error in
guard let urlData = data as? Data,
let url = URL(dataRepresentation: urlData, relativeTo: nil),
let newImage = NSImage(contentsOf: url)
else { return }
image = newImage
}
}
return true
}
}
}
ドロップに対応させるにはonDrop(of:isTargeted:perform:)
やそのヴァリエーションを使う。
第1引数で対応形式を指定するが、今回は[.png, .jpeg, .url, .fileURL]
とした。
他のアプリから画像データそのものがドロップされた場合.png
や.jpeg
に対応する。Music.appの「情報を見る」の「アートワーク」タブから画像をドロップした場合などがこれである。
Safariで画像のURLを開き、アドレス欄のファビコン部分をドラッグ&ドロップした場合は.url
に対応する。このとき、データの取得にインターネットアクセスを必要とするので、App Sandboxの設定で「Outgoing Connections (Client)」のチェックを入れておく。
Finderから画像ファイルをドロップした場合や、プレビューアプリのタイトルバー部分のアイコンをドロップした場合は.fileURL
に対応する。
また、Safariで表示されている画像そのものをドロップした場合は、.png
等の画像形式と.url
形式に両対応する。
ドロップされたデータの読み込みはloadItem(forTypeIdentifier:options:completionHandler:)
やそのヴァリエーションを使う。今回は.png
と.jpeg
はその親形式であるUTType.image
としてまとめて処理し、.fileURL
は親形式の.url
とまとめて処理した。.url
と.fileURL
に関して、今回は読み込み後の形式チェックはしていないので、PNG/JPEG以外でもNSImage
が対応している形式であれば読み込みが成功するようになっている。
onDrop(of:isTargeted:perform:)
の第2引数isTargeted
に@State
なプロパティを指定しておけば、ドロップ領域に入った場合にtrue
、外に出たときにfalse
が入るので、オーバレイ表示するビューの表示・非表示の条件に用いた。
2023年4月3日追記
(たぶん)macOS 13から.onDrop()
とloadItem()
の挙動が変わって、上記コードでは画像読み込みができなくなった。macOS 13に対応したコードを以下に示す。
//(省略)
.onDrop(of: [.image], isTargeted: $isDropTargeted) { providers in
guard let provider = providers.first else { return false }
if provider.hasItemConformingToTypeIdentifier(UTType.image.identifier) {
provider.loadItem(forTypeIdentifier: UTType.image.identifier) { item, error in
guard error == nil,
let url = item as? URL,
let loadedImage = NSImage(contentsOf: url)
else { return }
image = loadedImage
}
}
return true
}
これで、
- Finderに表示している画像ファイル
- プレビュー.appのタイトルバーアイコン
- Safariに表示された画像
- ミュージック.appの「情報を見る」の「アートワーク」の画像
等々からのドラッグ&ドロップで画像を読み込み、表示することができる。
ただし、Safariで画像そのもののURLを開いて、アドレス欄のURLをドラッグ&ドロップした場合は読み込めない。この場合の読み込み方法は謎である。
また、loadItem()
の引数でUTType.image.identifier
を指定しているのに、取得できるのがURL
型になるのも変な感じがする。直感的にはData
型になりそうなものだが……(NSImage(data:)
で画像データを作れるイメージ)。
最後に
今回は説明のためにそれぞれの読み込み方法を別々のビューに実装したが、実際に使用する場合は前二者の方法とonDrop()
の方法を組み合わせたり、メニューからの読み込み処理の呼び出しを実装したりするとよい。