SwiftUI macOS文档应用:如何自定义默认保存对话框?
在macOS SwiftUI文档应用中自定义保存对话框
要给保存对话框添加标签输入框,确实需要手动创建NSSavePanel并自定义保存流程——因为SwiftUI的DocumentGroup默认提供的保存对话框没有直接API支持添加自定义控件。下面提供两种可行的实现方案:
方案一:基于NSDocument集成系统保存流程
这种方案通过继承NSDocument(macOS原生文档类)重写保存逻辑,能更好地适配系统默认的菜单(比如Cmd+S)和保存流程:
import SwiftUI import AppKit class CustomDocument: NSDocument, FileDocument { static var readableContentTypes: [UTType] { [.plainText] } var content: String = "" var tags: String = "" // 存储标签的属性 init() { super.init() } required init(configuration: ReadConfiguration) throws { guard let data = configuration.file.regularFileContents, let string = String(data: data, encoding: .utf8) else { throw CocoaError(.fileReadCorruptFile) } content = string // 从文件扩展属性读取已保存的标签 if let tagData = try configuration.file.resourceValues(forKeys: [.extendedAttributesKey]).extendedAttributes?["com.yourapp.tags"], let tagString = String(data: tagData, encoding: .utf8) { tags = tagString } } func fileWrapper(configuration: WriteConfiguration) throws -> FileWrapper { let data = content.data(using: .utf8)! let fileWrapper = FileWrapper(regularFileWithContents: data) // 将标签写入文件扩展属性 if !tags.isEmpty { try fileWrapper.setExtendedAttribute(data: tags.data(using: .utf8)!, name: "com.yourapp.tags") } return fileWrapper } override func save(_ sender: Any?) { let savePanel = NSSavePanel() savePanel.allowedContentTypes = [.plainText] savePanel.title = "保存文档" // 创建自定义标签输入控件 let tagLabel = NSTextField(labelWithString: "标签:") let tagTextField = NSTextField(frame: NSRect(x: 0, y: 0, width: 300, height: 22)) tagTextField.stringValue = tags tagTextField.placeholderString = "输入标签" // 组装辅助视图并设置布局 let accessoryView = NSView(frame: NSRect(x: 0, y: 0, width: 300, height: 40)) accessoryView.addSubview(tagLabel) accessoryView.addSubview(tagTextField) tagLabel.translatesAutoresizingMaskIntoConstraints = false tagTextField.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ tagLabel.topAnchor.constraint(equalTo: accessoryView.topAnchor, constant: 8), tagLabel.leadingAnchor.constraint(equalTo: accessoryView.leadingAnchor, constant: 8), tagTextField.topAnchor.constraint(equalTo: tagLabel.bottomAnchor, constant: 4), tagTextField.leadingAnchor.constraint(equalTo: accessoryView.leadingAnchor, constant: 8), tagTextField.trailingAnchor.constraint(equalTo: accessoryView.trailingAnchor, constant: -8), accessoryView.heightAnchor.constraint(equalToConstant: 40) ]) savePanel.accessoryView = accessoryView // 弹出对话框并处理保存逻辑 savePanel.beginSheetModal(for: NSApp.keyWindow!) { [weak self] response in guard let self = self, response == .OK else { return } guard let url = savePanel.url else { return } self.tags = tagTextField.stringValue self.save(to: url, ofType: savePanel.allowedContentTypes.first!.identifier.rawValue, completionHandler: { error in if let error = error { NSAlert(error: error).runModal() } }) } } } @main struct CustomDocumentApp: App { var body: some Scene { DocumentGroup(newDocument: CustomDocument()) { file in TextEditor(text: file.$content) } } }
关键说明
- 继承
NSDocument后,重写save(_:)方法替换默认保存对话框,通过accessoryView添加自定义标签输入框。 - 标签数据可以存储在文件的扩展属性中,不会污染文档本身的内容;也可根据需求嵌入到文档内容里(比如用JSON格式存储内容和标签)。
- 该方案会自动适配系统的保存菜单和快捷键,无需额外处理。
方案二:纯SwiftUI FileDocument手动处理保存
如果想保持纯FileDocument的跨平台特性,可以手动添加保存按钮并自定义保存流程,但需要自己处理系统菜单的替换:
import SwiftUI import AppKit struct CustomFileDocument: FileDocument { static var readableContentTypes: [UTType] { [.plainText] } var content: String var tags: String init(content: String = "", tags: String = "") { self.content = content self.tags = tags } init(configuration: ReadConfiguration) throws { guard let data = configuration.file.regularFileContents, let string = String(data: data, encoding: .utf8) else { throw CocoaError(.fileReadCorruptFile) } // 读取文件扩展属性中的标签 if let tagData = try configuration.file.resourceValues(forKeys: [.extendedAttributesKey]).extendedAttributes?["com.yourapp.tags"], let tagString = String(data: tagData, encoding: .utf8) { tags = tagString } else { tags = "" } content = string } func fileWrapper(configuration: WriteConfiguration) throws -> FileWrapper { let data = content.data(using: .utf8)! let fileWrapper = FileWrapper(regularFileWithContents: data) // 写入标签到扩展属性 if !tags.isEmpty { try fileWrapper.setExtendedAttribute(data: tags.data(using: .utf8)!, name: "com.yourapp.tags") } return fileWrapper } } struct ContentView: View { @Binding var document: CustomFileDocument @Environment(\.dismissWindow) private var dismissWindow var body: some View { VStack { TextEditor(text: $document.content) TextField("标签", text: $document.tags) .padding(.horizontal) Button("保存文档") { saveDocument() } .padding() } .toolbar { ToolbarItem(placement: .primaryAction) { Button("保存") { saveDocument() } } } // 替换系统默认的保存命令 .commands { CommandGroup(replacing: .saveItem) { Button("保存") { saveDocument() } .keyboardShortcut("s") } } } private func saveDocument() { let savePanel = NSSavePanel() savePanel.allowedContentTypes = [.plainText] savePanel.title = "保存文档" savePanel.beginSheetModal(for: NSApp.keyWindow!) { response in guard response == .OK, let url = savePanel.url else { return } do { try document.write(to: url, contentType: .plainText) dismissWindow() } catch { NSAlert(error: error).runModal() } } } } @main struct CustomDocumentApp: App { var body: some Scene { DocumentGroup(newDocument: CustomFileDocument()) { file in ContentView(document: file.$document) } } }
关键说明
- 完全基于
FileDocument实现,保留跨平台潜力,但需要手动添加保存按钮和替换系统默认的保存命令。 - 标签的存储逻辑和方案一一致,可选择扩展属性或嵌入文档内容。
- 需要通过
CommandGroup替换系统默认的保存菜单,确保Cmd+S快捷键触发自定义保存逻辑。
内容的提问来源于stack exchange,提问作者sam0701
相关产品推荐
相关产品推荐

