14 KiB
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: Itemvar 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 dismissvar 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>