macOS 视图与组件参考
macOS 特定的 SwiftUI 视图、文件操作、拖放以及 AppKit 互操作。涵盖
HSplitView、VSplitView、Table、PasteButton、文件对话框、跨应用拖放和NSViewRepresentable。
目录
- 快速查找表
- HSplitView 与 VSplitView(macOS 专属)
- Table
- PasteButton 与 CopyButton
- 文件操作
- 拖放与剪贴板
- AppKit 互操作
- 最佳实践
快速查找表
视图
| API | 可用性 | 仅 macOS? | 用途 |
|---|---|---|---|
HSplitView | macOS 10.15+ | 是 | 带用户可拖动分隔线的水平可调整分割布局 |
VSplitView | macOS 10.15+ | 是 | 带用户可拖动分隔线的垂直可调整分割布局 |
Table | macOS 12.0+ | 否 | 带排序的完整多列布局;在 iOS 紧凑模式下,列会折叠 |
PasteButton | macOS 10.15+ | 否 | 读取剪贴板的系统按钮;在 macOS 上不会自动验证 |
CopyButton | macOS 15.0+ | 是 | 将Transferable内容复制到剪贴板的系统按钮 |
文件操作
| API | 可用性 | 仅 macOS? | 用途 |
|---|---|---|---|
fileImporter() | macOS 11.0+ | 否 | 带列/列表/画廊视图、侧边栏、标签和 QuickLook 的原生 NSOpenPanel |
fileExporter() | macOS 11.0+ | 否 | 带格式下拉框和标签字段的原生 NSSavePanel |
fileMover() | macOS 11.0+ | 否 | 带 Finder 式导航的原生 macOS 移动面板 |
fileDialogMessage(_:) | macOS 13.0+ | 是 | 文件对话框中的自定义消息文本 |
fileDialogConfirmationLabel(_:) | macOS 13.0+ | 是 | 文件对话框中自定义的确认按钮文本 |
fileExporterFilenameLabel(_:) | macOS 13.0+ | 是 | 文件导出器中自定义的文件名字段标签 |
拖放与剪贴板
| API | 可用性 | 仅 macOS? | 用途 |
|---|---|---|---|
onDrag(_:)/draggable(_:) | macOS 11.0+ | 否 | 拖动图像跟随光标;项目可在应用之间拖动 |
onDrop(of:delegate:)/dropDestination(for:action:) | macOS 11.0+ | 否 | 接受来自任何 macOS 应用(包括 Finder)的拖放 |
AppKit 互操作
| API | 可用性 | 仅 macOS? | 用途 |
|---|---|---|---|
NSViewRepresentable | macOS 10.15+ | 是 | 将 AppKitNSView包裹在 SwiftUI 中 |
NSViewControllerRepresentable | macOS 10.15+ | 是 | 将 AppKitNSViewController包裹在 SwiftUI 中 |
NSHostingController | macOS 10.15+ | 是 | 在 AppKit 视图控制器中承载 SwiftUI |
NSHostingView | macOS 10.15+ | 是 | 在 AppKitNSView层级中承载 SwiftUI |
HSplitView 与 VSplitView(macOS 专属)
带用户可拖动分隔线的可调整分割布局。用于所有面板都是同级对等的 IDE 风格窗格。VSplitView工作方式相同,但垂直分割(改用minHeight)。
HSplitView{FileTreeView().frame(minWidth:200)CodeEditorView().frame(minWidth:400)PreviewPane().frame(minWidth:200)}何时使用哪种:
NavigationSplitView—— 基于侧边栏的导航(侧边栏驱动内容/详情)HSplitView/VSplitView—— 所有窗格都是同级对等的 IDE 风格布局
Table
关于Table的基础(创建、选择、排序、自适应紧凑布局),请参阅list-patterns.md。本节涵盖 macOS 特定的表格样式。
Table 样式
// 带可见网格线的边框样式(macOS 专属)Table(people){/* columns */}.tableStyle(.bordered)// 带交替行背景的边框样式Table(people){/* columns */}.tableStyle(.bordered(alternatesRowBackgrounds:true))// Inset(无边框)Table(people){/* columns */}.tableStyle(.inset)// 隐藏列标题Table(people){/* columns */}.tableColumnHeaders(.hidden)PasteButton 与 CopyButton
PasteButton
通过Transferable读取剪贴板内容的系统按钮。在 macOS 上,它不会自动验证剪贴板变化(与 iOS 不同)。
structClipboardView:View{@StateprivatevarpastedText=""varbody:someView{HStack{PasteButton(payloadType:String.self){stringsinpastedText=strings[0]}Divider()Text(pastedText)Spacer()}}}CopyButton(macOS 15.0+、macOS 专属)
将Transferable内容复制到剪贴板的系统按钮。
structCopyableContent:View{letshareableText="Hello, world!"varbody:someView{HStack{Text(shareableText)CopyButton(item:shareableText)}}}文件操作
fileImporter
在 macOS 上,呈现带列/列表/画廊视图、侧边栏收藏、标签和 QuickLook 的原生NSOpenPanel。
.fileImporter(isPresented:$showImporter,allowedContentTypes:[.pdf],allowsMultipleSelection:false){resultinifcase.success(leturls)=result,leturl=urls.first{guardurl.startAccessingSecurityScopedResource()else{return}defer{url.stopAccessingSecurityScopedResource()}// use url}}重要:始终对返回的 URL 调用
startAccessingSecurityScopedResource(),并在完成后调用stopAccessingSecurityScopedResource()。这些是安全作用域书签——没有它,访问会失败。
fileExporter
在 macOS 上,呈现带格式下拉框和标签的原生NSSavePanel。
.fileExporter(isPresented:$showExporter,document:document,contentType:.plainText,defaultFilename:"MyFile.txt"){resultin// handle Result<URL, Error>}文件对话框自定义(macOS 专属)
使用这些 macOS 特定修饰符自定义文件对话框中的文本:
// 文件导入器上的自定义消息和确认按钮.fileImporter(isPresented:$showImporter,allowedContentTypes:[.image]){resultin// handle result}.fileDialogMessage("Select an image to use as your profile photo").fileDialogConfirmationLabel("Use This Photo")// 文件导出器上的自定义文件名标签.fileExporter(isPresented:$showExporter,document:myDocument,contentType:.png){resultin// handle result}.fileExporterFilenameLabel("Export As:")拖放与剪贴板
在 macOS 上,拖放跨应用工作(例如,从你的应用拖到 Finder、Mail 或其他应用)。
现代方法(Transferable)
// 拖动源structDraggableCard:View{letitem:MyItemvarbody:someView{Text(item.title).draggable(item)// 需要 Transferable 遵循}}// 拖放目标structDropZone:View{@StateprivatevardroppedItems:[MyItem]=[]varbody:someView{VStack{ForEach(droppedItems){iteminText(item.title)}}.dropDestination(for:MyItem.self){items,locationindroppedItems.append(contentsOf:items)returntrue}.frame(width:300,height:200).border(.secondary)}}传统方法(NSItemProvider)
// 拖动源Image(systemName:"doc").onDrag{NSItemProvider(object:fileURLasNSURL)}// 拖放目标Text("Drop files here").onDrop(of:[.fileURL],isTargeted:nil){providersin// handle providersreturntrue}AppKit 互操作
NSViewRepresentable(macOS 专属)
将 AppKitNSView包裹起来以在 SwiftUI 中使用。实现makeNSView(context:)和updateNSView(_:context:)。
structWebView:NSViewRepresentable{leturl:URLfuncmakeNSView(context:Context)->WKWebView{WKWebView()}funcupdateNSView(_nsView:WKWebView,context:Context){nsView.load(URLRequest(url:url))}}带 Coordinator 的 NSViewRepresentable
使用 Coordinator 将 delegate/target-action 回调转发给 SwiftUI。
structSearchField:NSViewRepresentable{@Bindingvartext:StringfuncmakeNSView(context:Context)->NSSearchField{letfield=NSSearchField()field.delegate=context.coordinatorreturnfield}funcupdateNSView(_nsView:NSSearchField,context:Context){nsView.stringValue=text}funcmakeCoordinator()->Coordinator{Coordinator(text:$text)}classCoordinator:NSObject,NSSearchFieldDelegate{vartext:Binding<String>init(text:Binding<String>){self.text=text}funccontrolTextDidChange(_obj:Notification){ifletfield=obj.objectas?NSSearchField{text.wrappedValue=field.stringValue}}}}警告:绝不要直接在被管理的
NSView上设置frame/bounds——SwiftUI 拥有布局。
NSViewControllerRepresentable(macOS 专属)
将 AppKitNSViewController包裹起来以在 SwiftUI 中使用。
structMapViewWrapper:NSViewControllerRepresentable{funcmakeNSViewController(context:Context)->MapViewController{MapViewController()}funcupdateNSViewController(_nsViewController:MapViewController,context:Context){// Update the controller when SwiftUI state changes}}NSHostingController 与 NSHostingView(macOS 专属)
在 AppKit 内部承载 SwiftUI 内容(反向——AppKit 应用嵌入 SwiftUI 视图)。
// 将 SwiftUI 作为视图控制器承载lethostingController=NSHostingController(rootView:MySwiftUIView())window.contentViewController=hostingController// 将 SwiftUI 直接作为 NSView 承载lethostingView=NSHostingView(rootView:MySwiftUIView())someNSView.addSubview(hostingView)最佳实践
- 侧边栏驱动的导航使用
NavigationSplitView—— 将HSplitView/VSplitView保留给 IDE 风格的对等窗格 - 让
Table具备自适应性—— 通过在第一列显示组合信息来处理紧凑尺寸类别 - 对
fileImporter返回的 URL 始终调用startAccessingSecurityScopedResource()—— 它们是安全作用域的 - 拖放使用
Transferable(现代)—— 仅出于遗留兼容性才回退到NSItemProvider - 当你需要 AppKit 视图的 delegate 回调时,使用带 Coordinator 的
NSViewRepresentable - 绝不在
NSViewRepresentable管理的视图上直接设置frame/bounds—— SwiftUI 拥有布局 - 尽可能优先使用原生 SwiftUI而不是 AppKit 互操作 —— 仅对 SwiftUI 不提供的功能使用
NSViewRepresentable