Sponsor Link
環境&対象
- macOS Big Sur 11.2
- Xcode 12.4
概要
AppKit が提供する NSSavePanel は、そのままでも フォルダ作成等の機能が使えますが、複数フォーマットをサポートしようとすると追加実装が必要になります。
Apple のドキュメントは、こちら。
具体的には以下の点について 不足を感じると思います。
- 保存フォーマットの選択
- 保存ファイル名へのファイル拡張子の設定
NSSavePanel で上記の対応を行うための追加実装を説明します。
保存フォーマットの選択
デフォルトの NSSavePanel には、保存フォーマットを選択するような機能はついていません。
ユーザーの入力したファイル名の拡張子から判断するという手もありますが、あまりスマートではありません。
NSSavePanel に 保存フォーマットを選択できるようなプルダウンメニューを追加するのが一般的です。
accessoryView
NSSavePanel の accessoryView に ビューを設定すると 以下の場所に追加でビューを表示させることができます。
このアクセサリビューを使って、フォーマット選択を行えるようにします。
accessoryView の実装
画像を保存するという設定で、JPG ファイル/ PNG ファイルを選択できるようにしてみます。
//
// ContentView.swift
//
// Created by : Tomoaki Yagishita on 2021/02/04
// © 2021 SmallDeskSoftware
//
import SwiftUI
struct ContentView: View {
@StateObject private var appModel: MyAppModel = MyAppModel()
var body: some View {
VStack {
Text("Hello, world!")
Button(action: {
self.appModel.savePanel()
}, label: {
Text("SavePanel")
})
}
.padding()
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
class MyAppModel: ObservableObject{
var nsSavePanel: NSSavePanel? = nil
func savePanel() {
let label = NSTextField(frame: CGRect(x: 20, y: 15, width: 80, height: 18))
label.stringValue = "type: "
label.alignment = .right
label.isBordered = false
label.isSelectable = false
label.isEditable = false
label.backgroundColor = .clear
let popup = NSPopUpButton(frame: CGRect(x: 100, y: 10, width: 80, height: 25))
popup.addItems(withTitles: ["JPG", "PNG"])
let accessoryView = NSView(frame: CGRect(x: 0, y: 0, width: 200, height: 50))
accessoryView.addSubview(popup)
accessoryView.addSubview(label)
self.nsSavePanel = NSSavePanel()
guard let nsSavePanel = self.nsSavePanel else { return }
nsSavePanel.canCreateDirectories = true
nsSavePanel.showsTagField = false
nsSavePanel.isExtensionHidden = false
nsSavePanel.allowedFileTypes = ["jpg", "png"]
nsSavePanel.nameFieldStringValue = "FileNameBase"
nsSavePanel.accessoryView = accessoryView
nsSavePanel.level = NSWindow.Level.modalPanel
nsSavePanel.beginSheetModal(for: NSApp.mainWindow!) { (result) in
if result == .OK {
guard let saveFileURL = nsSavePanel.url else { return }
let selected = popup.selectedItem?.title ?? "JPG"
if selected == "JPG" {
print("you tried to save file: \(saveFileURL)")
} else if selected == "PNG" {
print("you tried to save file: \(saveFileURL)")
}
}
}
}
}
上記アプリを起動すると以下のような動作になります。
保存ファイル名へのファイル拡張子の設定
NSSavePanel を開いたときに、保存ファイル名を設定しておくことができます。表示された後に、ユーザーが自分で変更することもできますが、ある程度自動で設定してくれると便利です。
複数フォーマットを選択できるようにしているときには、フォーマットに応じた拡張子を自動でつけてくれると便利です。
保存ファイル名の設定
NSSavePanel の nameFieldStringValue を設定することで、NSSavePanel が開かれたときに入力されている値を設定することができます。
先の例では、nameFieldStringValue に設定している “FileNameBase” という値と、allowedFileTypes で最初に設定されている jpg が組み合わされて、”FileNameBase.jpg” という値が表示されています。
選択されたフォーマットの検知
ユーザーが保存ファイルフォーマットを変更したことは、NSPopupButton の Target/Action を通じて取得することができます。
Target/Action を用いて、以下のような形になります。
popup.target = self
popup.action = #selector(saveTypeIsChanged)
// target/action で呼ばれる関数
@objc
func saveTypeIsChanged(_ any:Any) {
guard let popupButton = any as? NSPopUpButton else { return }
guard let selected = popupButton.selectedItem?.title else { return }
...
}
上記コードで何を選択されたかという情報を取得することはできるのですが、それを NSSavePanel に反映しようとすると問題が発生します。
具体的には、nameFieldStringValue を更新しても、すでに開かれている NSSavePanel で表示されているファイル名は、更新されません。
選択されたフォーマットのファイル名への反映
allowedFileTypes をうまく使うことで、保存ファイル名の拡張子をアップデートすることができました。
allowedFileType を変更すると、表示されているファイル名の拡張子も更新されるようです。
”ようです”と書いているのはこの辺りの動作についてはドキュメントには記載されておらず、実際の動作から推測しているためです。
先ほどの target/action で呼ばれる関数を以下のようにすると期待の動作になります。
@objc
func saveTypeIsChanged(_ any:Any) {
guard let popupButton = any as? NSPopUpButton else { return }
guard let selected = popupButton.selectedItem?.title else { return }
if selected == "JPG" {
self.nsSavePanel?.allowedFileTypes = ["jpg"]
} else if selected == "PNG" {
self.nsSavePanel?.allowedFileTypes = ["png"]
}
}
以下のような動作になります。
全コード
//
// ContentView.swift
//
// Created by : Tomoaki Yagishita on 2021/02/04
// © 2021 SmallDeskSoftware
//
import SwiftUI
struct ContentView: View {
@StateObject private var appModel: MyAppModel = MyAppModel()
var body: some View {
VStack {
Text("Hello, world!")
Button(action: {
self.appModel.savePanel()
}, label: {
Text("SavePanel")
})
}
.padding()
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
class MyAppModel: ObservableObject{
var nsSavePanel: NSSavePanel? = nil
func savePanel() {
let label = NSTextField(frame: CGRect(x: 20, y: 15, width: 80, height: 18))
label.stringValue = "type: "
label.alignment = .right
label.isBordered = false
label.isSelectable = false
label.isEditable = false
label.backgroundColor = .clear
let popup = NSPopUpButton(frame: CGRect(x: 100, y: 10, width: 80, height: 25))
popup.addItems(withTitles: ["JPG", "PNG"])
let accessoryView = NSView(frame: CGRect(x: 0, y: 0, width: 200, height: 50))
accessoryView.addSubview(popup)
accessoryView.addSubview(label)
popup.target = self
popup.action = #selector(saveTypeIsChanged)
self.nsSavePanel = NSSavePanel()
guard let nsSavePanel = self.nsSavePanel else { return }
nsSavePanel.canCreateDirectories = true
nsSavePanel.showsTagField = false
nsSavePanel.isExtensionHidden = false
nsSavePanel.allowedFileTypes = ["jpg"]
nsSavePanel.nameFieldStringValue = "FileNameBase"
nsSavePanel.accessoryView = accessoryView
nsSavePanel.level = NSWindow.Level.modalPanel
nsSavePanel.beginSheetModal(for: NSApp.mainWindow!) { (result) in
if result == .OK {
guard let saveFileURL = nsSavePanel.url else { return }
let selected = popup.selectedItem?.title ?? "JPG"
if selected == "JPG" {
print("you tried to save file: \(saveFileURL)")
} else if selected == "PNG" {
print("you tried to save file: \(saveFileURL)")
}
}
}
}
@objc
func saveTypeIsChanged(_ any:Any) {
guard let popupButton = any as? NSPopUpButton else { return }
guard let selected = popupButton.selectedItem?.title else { return }
if selected == "JPG" {
self.nsSavePanel?.allowedFileTypes = ["jpg"]
} else if selected == "PNG" {
self.nsSavePanel?.allowedFileTypes = ["png"]
}
}
}
まとめ:NSSavePanel でフォーマット選択できるようにする拡張
- accessoryView を使うと、追加 UI を表示できる
- allowedFileTypes を使うと、表示されているファイル名の拡張子も更新される
説明は以上です。
不明な点やおかしな点ありましたら、こちらまで。
Sponsor Link