Files
gh-glittercowboy-taches-cc-…/skills/expertise/macos-apps/references/macos-polish.md
2025-11-29 18:28:37 +08:00

14 KiB

macOS Polish

Details that make apps feel native and professional.

<keyboard_shortcuts> <standard_shortcuts>

import SwiftUI

struct AppCommands: Commands {
    var body: some Commands {
        // File operations
        CommandGroup(replacing: .saveItem) {
            Button("Save") { save() }
                .keyboardShortcut("s", modifiers: .command)

            Button("Save As...") { saveAs() }
                .keyboardShortcut("s", modifiers: [.command, .shift])
        }

        // Edit operations (usually automatic)
        // ⌘Z Undo, ⌘X Cut, ⌘C Copy, ⌘V Paste, ⌘A Select All

        // View menu
        CommandMenu("View") {
            Button("Zoom In") { zoomIn() }
                .keyboardShortcut("+", modifiers: .command)

            Button("Zoom Out") { zoomOut() }
                .keyboardShortcut("-", modifiers: .command)

            Button("Actual Size") { resetZoom() }
                .keyboardShortcut("0", modifiers: .command)

            Divider()

            Button("Toggle Sidebar") { toggleSidebar() }
                .keyboardShortcut("s", modifiers: [.command, .control])

            Button("Toggle Inspector") { toggleInspector() }
                .keyboardShortcut("i", modifiers: [.command, .option])
        }

        // Custom menu
        CommandMenu("Actions") {
            Button("Run") { run() }
                .keyboardShortcut("r", modifiers: .command)

            Button("Build") { build() }
                .keyboardShortcut("b", modifiers: .command)
        }
    }
}

</standard_shortcuts>

<view_shortcuts>

struct ContentView: View {
    var body: some View {
        MainContent()
            .onKeyPress(.space) {
                togglePlay()
                return .handled
            }
            .onKeyPress(.delete) {
                deleteSelected()
                return .handled
            }
            .onKeyPress(.escape) {
                clearSelection()
                return .handled
            }
            .onKeyPress("f", modifiers: .command) {
                focusSearch()
                return .handled
            }
    }
}

</view_shortcuts> </keyboard_shortcuts>

<menu_bar>

@main
struct MyApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
        .commands {
            // Replace standard items
            CommandGroup(replacing: .newItem) {
                Button("New Project") { newProject() }
                    .keyboardShortcut("n", modifiers: .command)

                Button("New from Template...") { newFromTemplate() }
                    .keyboardShortcut("n", modifiers: [.command, .shift])
            }

            // Add after existing group
            CommandGroup(after: .importExport) {
                Button("Import...") { importFile() }
                    .keyboardShortcut("i", modifiers: [.command, .shift])

                Button("Export...") { exportFile() }
                    .keyboardShortcut("e", modifiers: [.command, .shift])
            }

            // Add entire menu
            CommandMenu("Project") {
                Button("Build") { build() }
                    .keyboardShortcut("b", modifiers: .command)

                Button("Run") { run() }
                    .keyboardShortcut("r", modifiers: .command)

                Divider()

                Button("Clean") { clean() }
                    .keyboardShortcut("k", modifiers: [.command, .shift])
            }

            // Add to Help menu
            CommandGroup(after: .help) {
                Button("Keyboard Shortcuts") { showShortcuts() }
                    .keyboardShortcut("/", modifiers: .command)
            }
        }
    }
}

</menu_bar>

<context_menus>

struct ItemRow: View {
    let item: Item

    var body: some View {
        Text(item.name)
            .contextMenu {
                Button("Open") { open(item) }

                Button("Open in New Window") { openInNewWindow(item) }

                Divider()

                Button("Duplicate") { duplicate(item) }
                    .keyboardShortcut("d", modifiers: .command)

                Button("Rename") { rename(item) }

                Divider()

                Button("Delete", role: .destructive) { delete(item) }
            }
    }
}

</context_menus>

<window_management> <multiple_windows>

@main
struct MyApp: App {
    var body: some Scene {
        // Main document window
        DocumentGroup(newDocument: MyDocument()) { file in
            DocumentView(document: file.$document)
        }

        // Auxiliary windows
        Window("Inspector", id: "inspector") {
            InspectorView()
        }
        .windowResizability(.contentSize)
        .defaultPosition(.trailing)
        .keyboardShortcut("i", modifiers: [.command, .option])

        // Floating utility
        Window("Quick Entry", id: "quick-entry") {
            QuickEntryView()
        }
        .windowStyle(.hiddenTitleBar)
        .windowResizability(.contentSize)

        Settings {
            SettingsView()
        }
    }
}

// Open window from view
struct ContentView: View {
    @Environment(\.openWindow) private var openWindow

    var body: some View {
        Button("Show Inspector") {
            openWindow(id: "inspector")
        }
    }
}

</multiple_windows>

<window_state>

// Save and restore window state
class WindowStateManager {
    static func save(_ window: NSWindow, key: String) {
        let frame = window.frame
        UserDefaults.standard.set(NSStringFromRect(frame), forKey: "window.\(key).frame")
    }

    static func restore(_ window: NSWindow, key: String) {
        guard let frameString = UserDefaults.standard.string(forKey: "window.\(key).frame"),
              let frame = NSRectFromString(frameString) as NSRect? else { return }
        window.setFrame(frame, display: true)
    }
}

// Window delegate
class WindowDelegate: NSObject, NSWindowDelegate {
    func windowWillClose(_ notification: Notification) {
        guard let window = notification.object as? NSWindow else { return }
        WindowStateManager.save(window, key: "main")
    }
}

</window_state> </window_management>

<dock_menu>

class AppDelegate: NSObject, NSApplicationDelegate {
    func applicationDockMenu(_ sender: NSApplication) -> NSMenu? {
        let menu = NSMenu()

        menu.addItem(NSMenuItem(
            title: "New Project",
            action: #selector(newProject),
            keyEquivalent: ""
        ))

        menu.addItem(NSMenuItem.separator())

        // Recent items
        let recentProjects = RecentProjectsManager.shared.projects
        for project in recentProjects.prefix(5) {
            let item = NSMenuItem(
                title: project.name,
                action: #selector(openRecent(_:)),
                keyEquivalent: ""
            )
            item.representedObject = project.url
            menu.addItem(item)
        }

        return menu
    }

    @objc private func newProject() {
        NSDocumentController.shared.newDocument(nil)
    }

    @objc private func openRecent(_ sender: NSMenuItem) {
        guard let url = sender.representedObject as? URL else { return }
        NSDocumentController.shared.openDocument(
            withContentsOf: url,
            display: true
        ) { _, _, _ in }
    }
}

</dock_menu>

```swift struct ItemRow: View { let item: Item
var body: some View {
    HStack {
        Image(systemName: item.icon)
        VStack(alignment: .leading) {
            Text(item.name)
            Text(item.date.formatted())
                .font(.caption)
        }
    }
    .accessibilityElement(children: .combine)
    .accessibilityLabel("\(item.name), \(item.date.formatted())")
    .accessibilityHint("Double-tap to open")
    .accessibilityAddTraits(.isButton)
}

}

</voiceover>

<custom_rotors>
```swift
struct NoteListView: View {
    let notes: [Note]
    @State private var selectedNote: Note?

    var body: some View {
        List(notes, selection: $selectedNote) { note in
            NoteRow(note: note)
        }
        .accessibilityRotor("Pinned Notes") {
            ForEach(notes.filter { $0.isPinned }) { note in
                AccessibilityRotorEntry(note.title, id: note.id) {
                    selectedNote = note
                }
            }
        }
        .accessibilityRotor("Recent Notes") {
            ForEach(notes.sorted { $0.modifiedAt > $1.modifiedAt }.prefix(10)) { note in
                AccessibilityRotorEntry("\(note.title), modified \(note.modifiedAt.formatted())", id: note.id) {
                    selectedNote = note
                }
            }
        }
    }
}

</custom_rotors>

<reduced_motion>

struct AnimationHelper {
    static var prefersReducedMotion: Bool {
        NSWorkspace.shared.accessibilityDisplayShouldReduceMotion
    }

    static func animation(_ animation: Animation) -> Animation? {
        prefersReducedMotion ? nil : animation
    }
}

// Usage
withAnimation(AnimationHelper.animation(.spring())) {
    isExpanded.toggle()
}

</reduced_motion>

<user_defaults>

extension UserDefaults {
    enum Keys {
        static let theme = "theme"
        static let fontSize = "fontSize"
        static let recentFiles = "recentFiles"
        static let windowFrame = "windowFrame"
    }

    var theme: String {
        get { string(forKey: Keys.theme) ?? "system" }
        set { set(newValue, forKey: Keys.theme) }
    }

    var fontSize: Double {
        get { double(forKey: Keys.fontSize).nonZero ?? 14.0 }
        set { set(newValue, forKey: Keys.fontSize) }
    }

    var recentFiles: [URL] {
        get {
            guard let data = data(forKey: Keys.recentFiles),
                  let urls = try? JSONDecoder().decode([URL].self, from: data)
            else { return [] }
            return urls
        }
        set {
            let data = try? JSONEncoder().encode(newValue)
            set(data, forKey: Keys.recentFiles)
        }
    }
}

extension Double {
    var nonZero: Double? { self == 0 ? nil : self }
}

// Register defaults at launch
func registerDefaults() {
    UserDefaults.standard.register(defaults: [
        UserDefaults.Keys.theme: "system",
        UserDefaults.Keys.fontSize: 14.0
    ])
}

</user_defaults>

<error_presentation>

struct ErrorPresenter: ViewModifier {
    @Binding var error: AppError?

    func body(content: Content) -> some View {
        content
            .alert(
                "Error",
                isPresented: Binding(
                    get: { error != nil },
                    set: { if !$0 { error = nil } }
                ),
                presenting: error
            ) { _ in
                Button("OK", role: .cancel) {}
            } message: { error in
                Text(error.localizedDescription)
            }
    }
}

extension View {
    func errorAlert(_ error: Binding<AppError?>) -> some View {
        modifier(ErrorPresenter(error: error))
    }
}

// Usage
ContentView()
    .errorAlert($appState.error)

</error_presentation>

```swift struct OnboardingView: View { @AppStorage("hasSeenOnboarding") private var hasSeenOnboarding = false @Environment(\.dismiss) private var dismiss
var body: some View {
    VStack(spacing: 24) {
        Image(systemName: "star.fill")
            .font(.system(size: 64))
            .foregroundStyle(.accentColor)

        Text("Welcome to MyApp")
            .font(.largeTitle)

        VStack(alignment: .leading, spacing: 16) {
            FeatureRow(icon: "doc.text", title: "Create Documents", description: "Organize your work in documents")
            FeatureRow(icon: "folder", title: "Stay Organized", description: "Use folders and tags")
            FeatureRow(icon: "cloud", title: "Sync Everywhere", description: "Access on all your devices")
        }

        Button("Get Started") {
            hasSeenOnboarding = true
            dismiss()
        }
        .buttonStyle(.borderedProminent)
    }
    .padding(40)
    .frame(width: 500)
}

}

struct FeatureRow: View { let icon: String let title: String let description: String

var body: some View {
    HStack(spacing: 12) {
        Image(systemName: icon)
            .font(.title2)
            .frame(width: 40)
            .foregroundStyle(.accentColor)

        VStack(alignment: .leading) {
            Text(title).fontWeight(.medium)
            Text(description).foregroundStyle(.secondary)
        }
    }
}

}

</onboarding>

<sparkle_updates>
```swift
// Add Sparkle package for auto-updates
// https://github.com/sparkle-project/Sparkle

import Sparkle

class UpdaterManager {
    private var updater: SPUUpdater?

    func setup() {
        let controller = SPUStandardUpdaterController(
            startingUpdater: true,
            updaterDelegate: nil,
            userDriverDelegate: nil
        )
        updater = controller.updater
    }

    func checkForUpdates() {
        updater?.checkForUpdates()
    }
}

// In commands
CommandGroup(after: .appInfo) {
    Button("Check for Updates...") {
        updaterManager.checkForUpdates()
    }
}

</sparkle_updates>

<app_lifecycle>

class AppDelegate: NSObject, NSApplicationDelegate {
    func applicationDidFinishLaunching(_ notification: Notification) {
        // Register defaults
        registerDefaults()

        // Setup services
        setupServices()

        // Check for updates
        checkForUpdates()
    }

    func applicationWillTerminate(_ notification: Notification) {
        // Save state
        saveApplicationState()
    }

    func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
        // Return false for document-based or menu bar apps
        return false
    }

    func applicationShouldHandleReopen(_ sender: NSApplication, hasVisibleWindows flag: Bool) -> Bool {
        if !flag {
            // Reopen main window
            NSDocumentController.shared.newDocument(nil)
        }
        return true
    }
}

</app_lifecycle>