Skip to content

macOS Views & Components Reference

macOS-specific SwiftUI views, file operations, drag & drop, and AppKit interop. Covers HSplitView, VSplitView, Table, PasteButton, file dialogs, cross-app drag & drop, and NSViewRepresentable.


API Availability macOS-Only? Usage
HSplitView macOS 10.15+ Yes Horizontal resizable split layout with user-draggable dividers
VSplitView macOS 10.15+ Yes Vertical resizable split layout with user-draggable dividers
Table macOS 12.0+ No Full multi-column layout with sorting; on iOS compact, columns collapse
PasteButton macOS 10.15+ No System button that reads clipboard; does NOT auto-validate on macOS
CopyButton macOS 15.0+ Yes System button that copies Transferable content to clipboard
API Availability macOS-Only? Usage
fileImporter() macOS 11.0+ No Native NSOpenPanel with column/list/gallery view, sidebar, tags, QuickLook
fileExporter() macOS 11.0+ No Native NSSavePanel with format dropdown, tags field
fileMover() macOS 11.0+ No Native macOS move panel with Finder-like navigation
fileDialogMessage(_:) macOS 13.0+ Yes Custom message text in file dialogs
fileDialogConfirmationLabel(_:) macOS 13.0+ Yes Custom confirm button text in file dialogs
fileExporterFilenameLabel(_:) macOS 13.0+ Yes Custom filename field label in file exporter
API Availability macOS-Only? Usage
onDrag(_:) / draggable(_:) macOS 11.0+ No Drag image follows cursor; items draggable between apps
onDrop(of:delegate:) / dropDestination(for:action:) macOS 11.0+ No Accepts drops from any macOS app including Finder
API Availability macOS-Only? Usage
NSViewRepresentable macOS 10.15+ Yes Wrap an AppKit NSView in SwiftUI
NSViewControllerRepresentable macOS 10.15+ Yes Wrap an AppKit NSViewController in SwiftUI
NSHostingController macOS 10.15+ Yes Host SwiftUI inside an AppKit view controller
NSHostingView macOS 10.15+ Yes Host SwiftUI inside an AppKit NSView hierarchy

Resizable split layouts with user-draggable dividers. Use for IDE-style panes where all panels are equal peers. VSplitView works identically but splits vertically (use minHeight instead).

HSplitView {
FileTreeView()
.frame(minWidth: 200)
CodeEditorView()
.frame(minWidth: 400)
PreviewPane()
.frame(minWidth: 200)
}

When to use which:

  • NavigationSplitView — sidebar-based navigation (sidebar drives content/detail)
  • HSplitView/VSplitView — IDE-style layouts where all panes are equal peers

For Table basics (creation, selection, sorting, adaptive compact layout), see list-patterns.md. This section covers macOS-specific table styling.

// Bordered with visible grid lines (macOS-only)
Table(people) { /* columns */ }
.tableStyle(.bordered)
// Bordered with alternating row backgrounds
Table(people) { /* columns */ }
.tableStyle(.bordered(alternatesRowBackgrounds: true))
// Inset (no borders)
Table(people) { /* columns */ }
.tableStyle(.inset)
// Hide column headers
Table(people) { /* columns */ }
.tableColumnHeaders(.hidden)

System button that reads clipboard content via Transferable. On macOS, it does NOT auto-validate pasteboard changes (unlike iOS).

struct ClipboardView: View {
@State private var pastedText = ""
var body: some View {
HStack {
PasteButton(payloadType: String.self) { strings in
pastedText = strings[0]
}
Divider()
Text(pastedText)
Spacer()
}
}
}

System button that copies Transferable content to the clipboard.

struct CopyableContent: View {
let shareableText = "Hello, world!"
var body: some View {
HStack {
Text(shareableText)
CopyButton(item: shareableText)
}
}
}

On macOS, presents a native NSOpenPanel with column/list/gallery view, sidebar favorites, tags, and QuickLook.

.fileImporter(
isPresented: $showImporter,
allowedContentTypes: [.pdf],
allowsMultipleSelection: false
) { result in
if case .success(let urls) = result, let url = urls.first {
guard url.startAccessingSecurityScopedResource() else { return }
defer { url.stopAccessingSecurityScopedResource() }
// use url
}
}

Important: Always call startAccessingSecurityScopedResource() on returned URLs, and stopAccessingSecurityScopedResource() when done. These are security-scoped bookmarks — access fails without this.

On macOS, presents a native NSSavePanel with format dropdown and tags.

.fileExporter(
isPresented: $showExporter,
document: document,
contentType: .plainText,
defaultFilename: "MyFile.txt"
) { result in
// handle Result<URL, Error>
}

Customize text in file dialogs with these macOS-specific modifiers:

// Custom message and confirm button on file importer
.fileImporter(
isPresented: $showImporter,
allowedContentTypes: [.image]
) { result in
// handle result
}
.fileDialogMessage("Select an image to use as your profile photo")
.fileDialogConfirmationLabel("Use This Photo")
// Custom filename label on file exporter
.fileExporter(
isPresented: $showExporter,
document: myDocument,
contentType: .png
) { result in
// handle result
}
.fileExporterFilenameLabel("Export As:")

On macOS, drag and drop works across applications (e.g., drag from your app to Finder, Mail, or other apps).

// Drag source
struct DraggableCard: View {
let item: MyItem
var body: some View {
Text(item.title)
.draggable(item) // Requires Transferable conformance
}
}
// Drop target
struct DropZone: View {
@State private var droppedItems: [MyItem] = []
var body: some View {
VStack {
ForEach(droppedItems) { item in
Text(item.title)
}
}
.dropDestination(for: MyItem.self) { items, location in
droppedItems.append(contentsOf: items)
return true
}
.frame(width: 300, height: 200)
.border(.secondary)
}
}
// Drag source
Image(systemName: "doc")
.onDrag {
NSItemProvider(object: fileURL as NSURL)
}
// Drop target
Text("Drop files here")
.onDrop(of: [.fileURL], isTargeted: nil) { providers in
// handle providers
return true
}

Wraps an AppKit NSView for use in SwiftUI. Implement makeNSView(context:) and updateNSView(_:context:).

struct WebView: NSViewRepresentable {
let url: URL
func makeNSView(context: Context) -> WKWebView { WKWebView() }
func updateNSView(_ nsView: WKWebView, context: Context) {
nsView.load(URLRequest(url: url))
}
}

Use a Coordinator to forward delegate/target-action callbacks to SwiftUI.

struct SearchField: NSViewRepresentable {
@Binding var text: String
func makeNSView(context: Context) -> NSSearchField {
let field = NSSearchField()
field.delegate = context.coordinator
return field
}
func updateNSView(_ nsView: NSSearchField, context: Context) {
nsView.stringValue = text
}
func makeCoordinator() -> Coordinator { Coordinator(text: $text) }
class Coordinator: NSObject, NSSearchFieldDelegate {
var text: Binding<String>
init(text: Binding<String>) { self.text = text }
func controlTextDidChange(_ obj: Notification) {
if let field = obj.object as? NSSearchField {
text.wrappedValue = field.stringValue
}
}
}
}

Warning: Never set frame/bounds directly on the managed NSView — SwiftUI owns the layout.

NSViewControllerRepresentable (macOS-only)

Section titled “NSViewControllerRepresentable (macOS-only)”

Wraps an AppKit NSViewController for use in SwiftUI.

struct MapViewWrapper: NSViewControllerRepresentable {
func makeNSViewController(context: Context) -> MapViewController {
MapViewController()
}
func updateNSViewController(_ nsViewController: MapViewController, context: Context) {
// Update the controller when SwiftUI state changes
}
}

NSHostingController & NSHostingView (macOS-only)

Section titled “NSHostingController & NSHostingView (macOS-only)”

Host SwiftUI content inside AppKit (reverse direction — AppKit app embedding SwiftUI views).

// Host SwiftUI as a view controller
let hostingController = NSHostingController(rootView: MySwiftUIView())
window.contentViewController = hostingController
// Host SwiftUI directly as an NSView
let hostingView = NSHostingView(rootView: MySwiftUIView())
someNSView.addSubview(hostingView)

  • Use NavigationSplitView for sidebar-driven navigation — reserve HSplitView/VSplitView for IDE-style equal peer panes
  • Make Table adaptive — handle compact size classes by showing combined info in the first column
  • Always call startAccessingSecurityScopedResource() on URLs from fileImporter — they are security-scoped
  • Use Transferable for drag & drop (modern) — fall back to NSItemProvider only for legacy compatibility
  • Use NSViewRepresentable with Coordinator when you need delegate callbacks from AppKit views
  • Never set frame/bounds directly on views managed by NSViewRepresentable — SwiftUI owns the layout
  • Prefer native SwiftUI over AppKit interop when possible — only use NSViewRepresentable for features SwiftUI doesn’t provide