概要
- AppKitではファイルを開く/保存する際に
NSOpenPanel/NSSavePanel
を使用していましたが、SwiftUIではfileImporter/fileExporter
が利用できます。
参考
- SwiftUI macOSアプリでファイルを開く
- How to export files using fileExporter()
- fileExporter(isPresented:document:contentType:defaultFilename:onCompletion:)
- fileImporter(isPresented:allowedContentTypes:allowsMultipleSelection:onCompletion:)
-
fileImporter
でデフォルトのディレクトリ指定ができないなど、NSOpenPanelと比べて色々とパラメータが少ないように思います。細かい設定をしたい場合はそちらを使うのがいいかもしれません。
実装
- まずファイルのアクセス権が必要となるので、Sandboxの
User Selected File
をRead/Write
に設定します。
- また保存するファイルの形式に合わせて、FileDocumentのサブクラスを作る必要があります。
import SwiftUI
import UniformTypeIdentifiers
/// refs: https://www.hackingwithswift.com/quick-start/swiftui/how-to-export-files-using-fileexporter
struct TextFile: FileDocument {
// tell the system we support only plain text
static var readableContentTypes = [UTType.plainText]
// by default our document is empty
var text = ""
// a simple initializer that creates new, empty documents
init(initialText: String = "") {
text = initialText
}
// this initializer loads data that has been saved previously
init(configuration: ReadConfiguration) throws {
if let data = configuration.file.regularFileContents {
text = String(decoding: data, as: UTF8.self)
}
}
// this will be called when the system wants to write our data to disk
func fileWrapper(configuration: WriteConfiguration) throws -> FileWrapper {
let data = Data(text.utf8)
return FileWrapper(regularFileWithContents: data)
}
}
- 呼び出し例は以下の通りとなります。
import SwiftUI
struct ContentView: View {
@State private var text = ""
@State private var isSaveButtonDisable = true
@State private var exporterPresented = false
@State private var importerPresented = false
var body: some View {
VStack {
textField()
HStack {
Spacer()
saveButton()
openButton()
}
}
.padding()
.fileExporter(isPresented: $exporterPresented,
document: TextFile(initialText: text),
contentType: .plainText,
defaultFilename: "Untitled.txt",
onCompletion: { result in
switch result {
case .success(let url):
print("\(url)に保存が完了しました!")
case .failure(let error):
print(error.localizedDescription)
}
})
.fileImporter(isPresented: $importerPresented,
allowedContentTypes: [.plainText],
allowsMultipleSelection: false) { result in
switch result {
case .success(let urls):
guard let url = urls.first else { return }
do {
let contents = try String(contentsOf: url)
text = contents
} catch {
print(error.localizedDescription)
}
case .failure(let error):
print(error.localizedDescription)
}
}
}
}
// MARK: - ViewBuilder
extension ContentView {
@ViewBuilder
private func textField() -> some View {
TextField("Enter here!", text: $text, axis: .vertical)
.lineLimit(3, reservesSpace: true)
.textFieldStyle(.roundedBorder)
.onChange(of: text) { newValue in
isSaveButtonDisable = newValue.isEmpty
}
}
@ViewBuilder
private func saveButton() -> some View {
Button {
exporterPresented.toggle()
} label: {
Text("Save...")
}
.disabled(isSaveButtonDisable)
}
@ViewBuilder
private func openButton() -> some View {
Button {
importerPresented.toggle()
} label: {
Text("Open...")
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}