Dunfey · Hotel WWDC as data, est. 1983
Front desk everything
Years
Topics

2022 EssentialsSwiftUI & UI Frameworks

WWDC22 · 19 min · Essentials / SwiftUI & UI Frameworks

Use SwiftUI with AppKit

Discover how the Shortcuts app uses both SwiftUI and AppKit to create a top-tier experience on macOS. Follow along with the Shortcuts team as we explore how you can host SwiftUI views in AppKit code, handle layout and sizing, participate in the responder chain, enable navigational focus, and more. We’ll also show you how to host AppKit views, helping you migrate existing code into a SwiftUI layout within your app.

Watch at developer.apple.com ↗

Transcript all transcripts

Code shown on screen · 13 snippets

SidebarView and SidebarItem swift · at 1:29 ↗
struct SidebarView: View {
    @State private var selectedItem: SidebarItem
    
    var body: some View {
        List(selection: $selectedItem) {
            ...
            Section("Shortcuts") { ... }
            Section("Folders") { ... }
        }
    }
}

enum SidebarItem: Hashable {
    case gallery
    case allShortcuts
    ...
    case folder(Folder)
}
Hosting SwiftUI sidebar swift · at 1:53 ↗
let splitViewController = NSSplitViewController()

let sidebar = NSHostingController(rootView: SidebarView(...))
let splitViewItem = NSSplitViewItem(viewController: sidebar)
splitViewController.addSplitViewItem(splitViewItem)
Sidebar selection model swift · at 3:06 ↗
class SelectionModel: ObservableObject {

    @Published var selectedItem: SidebarItem = .allShortcuts

}

// AppKit Window Controller
cancellable = selectionModel.$selectedItem.sink { newItem in
    // update the NSSplitViewController detail
}
Collection view item hosting SwiftUI swift · at 4:37 ↗
class ShortcutItemView: NSCollectionViewItem {
    private var hostingView: NSHostingView<ShortcutView>?

    func displayShortcut(_ shortcut: Shortcut) {
        let shortcutView = ShortcutView(shortcut: shortcut)

        if let hostingView = hostingView {
            hostingView.rootView = shortcutView
        } else {
            let newHostingView = NSHostingView(rootView: shortcutView)
            view.addSubview(newHostingView)
            setupConstraints(for: newHostingView)
            self.hostingView = newHostingView
        }
    }
}
Popover presentation swift · at 7:55 ↗
viewController.present(NSHostingController(rootView: ...), 
    asPopoverRelativeTo: rect, of: view, 
    preferredEdge: .maxY, behavior: .transient)
Sheet presentation swift · at 8:15 ↗
viewController.presentAsSheet(NSHostingController(rootView: ...))
Modal window presentation swift · at 8:22 ↗
let hostingController = NSHostingController(rootView: ModalView())
hostingController.title = "Window Title"
viewController.presentAsModalWindow(hostingController)
Sizing options swift · at 8:45 ↗
hostingController.sizingOptions = [.minSize, .intrinsicContentSize, .maxSize]
Copy, Cut, and Paste commands swift · at 10:47 ↗
Image(...)
    .focusable()
    .copyable { ... }
    .cuttable { ... }
    .pasteDestination(payloadType: Image.self) { ... }
Respond to standard commands swift · at 11:02 ↗
struct ShortcutsEditorView: View {
    var body: some View {
        ScrollView { ... }
            .onMoveCommand { moveSelection(direction: $0) }
            .onExitCommand { cancelOperations() }
            .onCommand(#selector(NSResponder.selectAll(_:)) { selectAllActions() }
            .onCommand(#selector(moveActionUp(_:)) { moveSelectedAction(.up) }
            .onCommand(#selector(moveActionDown(_:)) { moveSelectedAction(.down) }
    }
}
Script editor swift · at 15:18 ↗
class ScriptEditorView: NSView {
    var sourceCode: String
    var isEditable: Bool
    weak var delegate: ScriptEditorViewDelegate?
}

protocol ScriptEditorViewDelegate: AnyObject {
    func sourceCodeDidChange(in view: ScriptEditorView) -> Void
}
Script editor container swift · at 15:40 ↗
struct ScriptEditorContainerView: View {
    @State var sourceCode: String = ""

    var body: some View {
        VStack {
            CompileButton { compile(code: sourceCode) }
            Divider()
            ScriptEditorRepresentable(sourceCode: $sourceCode)
        }
    }
}
Script editor representable swift · at 16:13 ↗
struct ScriptEditorRepresentable: NSViewRepresentable {
    @Binding var sourceCode: String

    func makeNSView(context: Context) -> ScriptEditorView {
        let scriptEditor = ScriptEditorView(frame: .zero)
        scriptEditor.delegate = context.coordinator
        return scriptEditor
    }

    func updateNSView(_ nsView: ScriptEditorView, context: Context) {
        if sourceCode != scriptEditor.sourceCode {
            scriptEditor.sourceCode = sourceCode
        }
        scriptEditor.isEditable = context.environment.isEnabled
        // Make sure coordinator has a reference to the current value 
        // of the binding:
        context.coordinator.representable = self
    }

    func makeCoordinator() -> Coordinator {
        Coordinator(representable: self)
    }
}

class Coordinator: NSObject, ScriptEditorViewDelegate {
    var representable: ScriptEditorRepresentable

    init(representable: ScriptEditorRepresentable) { ... }

    func sourceCodeDidChange(in view: ScriptEditorView) {
        representable.sourceCode = view.sourceCode
    }
}

Resources