协作头脑风暴
brainstorming
在实现前澄清需求、探索设计选择,并形成清晰的开发方向。
desktop-swiftui【开发与工程】Use when the user asks about SwiftUI for macOS, SwiftUI desktop patterns, AppKit bridging, SwiftUI app lifecycle, or SwiftUI multiplatform apps. Do NOT use for: AppKit-only (desktop-appkit), or iOS SwiftUI (mobile skill).
独立 GitHub 快照 · 自动分类 · 以原作者文档为准
Build native macOS applications using SwiftUI — Apple's declarative UI framework for all Apple platforms. SwiftUI on macOS provides automatic support for Dark Mode, Dynamic Type, Accessibility, and native window management with less code than AppKit.
Exact user phrases: "SwiftUI macOS", "SwiftUI App", "macOS SwiftUI", "SwiftUI multiplatform", "SwiftUI scene", "NSViewRepresentable", "SwiftUI window", "SwiftUI settings", "SwiftUI commands".
SwiftUI macOS architecture with scene structure, view hierarchy, state management, and platform-specific patterns.
250 lines.
What type of macOS app?
├── Simple single-window (settings, player, quick tool)
│ → WindowGroup with NavigationStack
│ → @AppStorage for preferences
├── Multi-window MDI (editor, dashboard, browser)
│ → WindowGroup with ID-based windows
│ → WindowScene for custom window management
├── Document-based (file editor, image viewer)
│ → DocumentGroup with file types
│ → SwiftData or Codable for document model
├── Menu bar app (status bar, background)
│ → MenuBarExtra scene
│ → NSApplication activation policy: .accessory
└── Preferences window
→ Settings scene (macOS 13+)
→ TabView with form-based controls
@main struct MyApp: App {
var body: some Scene {
WindowGroup { ContentView() } // Main window
DocumentGroup(...) // Document-based window
MenuBarExtra(...) // Menu bar app
Settings { SettingsView() } // Preferences
Window("Name", id: "custom") { ... } // Custom window (macOS 13+)
WindowGroup("Name", id: "group") { ... }
}
}
// MyApp.swift
import SwiftUI
@main
struct MyApp: App {
@State private var appState = AppState()
var body: some Scene {
WindowGroup {
ContentView()
.environment(appState)
.frame(minWidth: 800, minHeight: 600)
}
.windowResizability(.contentSize)
.windowToolbarStyle(.unified)
.defaultSize(width: 1000, height: 700)
Settings {
SettingsView()
.environment(appState)
}
}
}
@Observable
class AppState {
var selectedItemID: UUID?
var items: [Item] = []
var isEditing = false
}
// ContentView.swift - Three-column macOS navigation
import SwiftUI
struct ContentView: View {
@Environment(AppState.self) private var appState
var body: some View {
NavigationSplitView {
// Sidebar
List(appState.items, selection: $appState.selectedItemID) { item in
Text(item.name)
.tag(item.id)
}
.navigationSplitViewColumnWidth(min: 180, ideal: 200, max: 300)
} content: {
// Content list (middle column)
if let id = appState.selectedItemID,
let item = appState.items.first(where: { $0.id == id }) {
ItemListView(item: item)
} else {
ContentUnavailableView(
"Select an Item",
systemImage: "sidebar.left",
description: Text("Choose an item from the sidebar")
)
}
} detail: {
// Detail view (right column)
if let id = appState.selectedItemID,
let item = appState.items.first(where: { $0.id == id }) {
DetailView(item: item)
} else {
ContentUnavailableView(
"No Selection",
systemImage: "rectangle.and.text.magnifyingglass",
description: Text("Select an item to view details")
)
}
}
}
}
// Toolbar configuration
struct ContentView: View {
@Environment(AppState.self) private var appState
@State private var isShowingSheet = false
var body: some View {
NavigationSplitView {
// Sidebar content
} content: {
// Content
} detail: {
// Detail
}
.toolbar {
ToolbarItem {
Button(action: { isShowingSheet = true }) {
Label("Add Item", systemImage: "plus")
}
}
ToolbarItem(placement: .primaryAction) {
Button(action: appState.refresh) {
Label("Refresh", systemImage: "arrow.clockwise")
}
}
}
.sheet(isPresented: $isShowingSheet) {
AddItemSheet()
}
}
}
// Menu commands
struct MyCommands: Commands {
@FocusedBinding(\.selectedItem) private var selectedItem
var body: some Commands {
CommandGroup(replacing: .newItem) {
Button("New Item") {
// Create new item
}
.keyboardShortcut("n", modifiers: .command)
}
CommandMenu("Item") {
Button("Duplicate") {
// Duplicate selected item
}
.keyboardShortcut("d", modifiers: .command)
.disabled(selectedItem == nil)
Divider()
Button("Delete") {
// Delete selected item
}
.keyboardShortcut(.delete, modifiers: .command)
}
}
}
import SwiftData
@Model
final class Item {
var name: String
var detail: String
var creationDate: Date
var isFavorite: Bool
@Attribute(.externalStorage) var thumbnailData: Data?
init(name: String, detail: String) {
self.name = name
self.detail = detail
self.creationDate = .now
self.isFavorite = false
}
}
// Usage in app
@main
struct MyApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
.modelContainer(for: Item.self)
}
}
// Usage in view
struct ContentView: View {
@Environment(\.modelContext) private var modelContext
@Query(sort: \Item.creationDate, order: .reverse) private var items: [Item]
var body: some View {
List {
ForEach(items) { item in
Text(item.name)
}
.onDelete(perform: deleteItems)
}
}
private func deleteItems(offsets: IndexSet) {
for index in offsets {
modelContext.delete(items[index])
}
}
}
// Wrapping NSTableView for complex table behavior
struct TableViewWrapper: NSViewRepresentable {
@Binding var items: [Item]
var selectedItem: Binding<UUID?>
func makeNSView(context: Context) -> NSTableView {
let tableView = NSTableView()
tableView.delegate = context.coordinator
tableView.dataSource = context.coordinator
tableView.columnAutoresizingStyle = .uniformColumnAutoresizingStyle
let column = NSTableColumn(identifier: .init("name"))
column.title = "Name"
column.width = 200
tableView.addTableColumn(column)
return tableView
}
func updateNSView(_ nsView: NSTableView, context: Context) {
nsView.reloadData()
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
class Coordinator: NSObject, NSTableViewDelegate, NSTableViewDataSource {
var parent: TableViewWrapper
init(_ parent: TableViewWrapper) {
self.parent = parent
}
func numberOfRows(in tableView: NSTableView) -> Int {
parent.items.count
}
func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? {
let cell = tableView.makeView(withIdentifier: .init("cell"), owner: nil)
as? NSTableCellView ?? NSTableCellView()
let item = parent.items[row]
cell.textField?.stringValue = item.name
return cell
}
}
}
List {
ForEach(viewModel.items) { item in
Text(item.name)
.onDrag {
NSItemProvider(object: item.id.uuidString as NSString)
}
.onDrop(of: [.text], delegate: ItemDropDelegate(item: item, viewModel: viewModel))
}
}
struct ItemDropDelegate: DropDelegate {
let item: Item
let viewModel: ViewModel
func performDrop(info: DropInfo) -> Bool {
guard let provider = info.itemProviders(for: [.text]).first else { return false }
provider.loadObject(ofClass: NSString.self) { text, _ in
guard let idString = text as? String,
let id = UUID(uuidString: idString) else { return }
Task { @MainActor in
viewModel.moveItem(id: id, to: item.id)
}
}
return true
}
}
| Pitfall | Description | Prevention |
|---|---|---|
| iOS patterns on macOS | TabView where NavigationSplitView belongs | Use NavigationSplitView for sidebar/content/detail |
| Missing keyboard shortcuts | No keyboard navigation for menus | Use CommandMenu and .keyboardShortcut() |
| No focus management | Keyboard tab order broken | Use @FocusState, .focused() modifier |
| Single window assumption | App can't open multiple windows | Use WindowGroup, not fixed single window |
| Heavy View.body | Frequent recomputation | Extract sub-views, use computed properties |
| @Published in @StateObject | Using Combine when @Observable works | Prefer @Observable (iOS 17/macOS 14+) |
| Missing Settings scene | No preferences window | Add Settings scene, use TabView + Form |
| Ignoring @Environment values | Platform-specific behavior | Use @Environment(.colorScheme), .controlSize, etc. |
| No drag and drop | Users expect drag from lists | Implement .onDrag + .onDrop on List items |
| Missing minimum window size | Window can shrink to unusable size | Use .frame(minWidth:minHeight:) |
| Practice | Rationale |
|---|---|
| NavigationSplitView for 3-panel | macOS standard Finder/Xcode pattern |
| @Observable over ObservableObject | Simpler, less boilerplate, value semantics |
| SwiftData over Core Data for new projects | Native Swift, macros, automatic iCloud |
| Extract sub-views extensively | SwiftUI performance depends on small view bodies |
| Use ViewThatFits for adaptive layout | Automatically selects best-fitting layout variant |
| Menus and commands in a Commands struct | Separated from views, reusable, testable |
| Prefer NSViewRepresentable over fighting SwiftUI | Some things (NSTableView perf) are best in AppKit |
| Default size and minimum size always set | Prevents unusable tiny windows |
| Test with VoiceOver | Accessibility is expected on macOS |
| Use PreviewProvider in DEBUG | SwiftUI previews accelerate development |
WindowGroup(id: "editor") {
EditorView()
.environment(appState)
}
.windowResizability(.contentSize)
// Open new editor window
@Environment(\.openWindow) private var openWindow
openWindow(id: "editor")
MenuBarExtra("MyApp", systemImage: "star.fill") {
Button("Show Window") { showWindow() }
Divider()
Button("Quit") { NSApplication.shared.terminate(nil) }
}
// Configure activation policy
// In AppDelegate or Info.plist:
// LSUIElement = true
struct SettingsView: View {
@AppStorage("showSidebar") private var showSidebar = true
@AppStorage("recentFiles") private var recentFiles = 10
@AppStorage("theme") private var theme = "system"
var body: some View {
TabView {
Form {
Toggle("Show Sidebar", isOn: $showSidebar)
Picker("Recent Files", selection: $recentFiles) {
Text("5").tag(5)
Text("10").tag(10)
Text("20").tag(20)
}
}
.tabItem { Label("General", systemImage: "gearshape") }
Form {
Picker("Theme", selection: $theme) {
Text("System").tag("system")
Text("Light").tag("light")
Text("Dark").tag("dark")
}
}
.tabItem { Label("Appearance", systemImage: "paintpalette") }
}
.frame(width: 400, height: 250)
}
}
Hand off to desktop-appkit for AppKit bridge details. Hand off to design-accessibility for VoiceOver testing.
` interface EventObserver { onEvent(event: T): Promise; }
class EventBus { private observers: Set<EventObserver> = new Set(); subscribe(observer: EventObserver): void { this.observers.add(observer); } unsubscribe(observer: EventObserver): void { this.observers.delete(observer); } async emit(event: T): Promise { const results = Array.from(this.observers).map(o => o.onEvent(event)); await Promise.allSettled(results); } } `
config: defaults: timeout: 30s retryCount: 3 overrides: production: timeout: 60s retryCount: 5 development: timeout: 300s retryCount: 1
| Metric | Threshold | Severity | Action |
|---|---|---|---|
| Error rate | > 1% over 5min | Critical | Page on-call |
| p99 latency | > 2s over 5min | Warning | Investigate |
| Throughput drop | > 50% over 1min | Critical | Check upstream |
| Queue depth | > 1000 over 1min | Warning | Scale consumers |
| Disk usage | > 85% | Warning | Clean or expand |
| Memory usage | > 90% heap | Critical | Restart or scale |
| Anti-Pattern | Symptom | Root Cause | Solution |
|---|---|---|---|
| Premature optimization | Complex code for no measured benefit | Guessing instead of profiling | Measure first, optimize based on data |
| Copy-paste reuse | Duplicate code across codebase | Lack of abstraction | Extract shared logic into libraries |
| Gold-plating | Features with no current requirement | Over-engineering | YAGNI — build what's needed now |
| Magical thinking | Assumptions without validation | Skipping error handling | Handle all failure modes explicitly |
Cache hierarchy: L1 (in-memory local) → L2 (distributed Redis/Memcached) → L3 (CDN/Edge). Cache invalidation: TTL-based (simple, stale), event-based (complex, fresh), write-through (consistent, higher write latency), write-behind (fast writes, eventual consistency).
| Decision | SwiftUI App (@main) | UIKit App Delegate |
|---|---|---|
| Boilerplate | Minimal (< 10 lines) | Significant (SceneDelegate) |
| Lifecycle | Automatic | Manual control |
| UIKit interop | UIHostingController | Full UIKit access |
| Scene support | Built-in WindowGroup | UISceneConfiguration |
| Minimum iOS | iOS 14+ / macOS 11+ | iOS 2+ |
| Best for | New SwiftUI projects | UIKit migration, complex lifecycle |
| Aspect | @StateObject | @ObservedObject | @EnvironmentObject |
|---|---|---|---|
| Ownership | View creates and owns | Parent owns | Ancestor injects |
| Lifetime | View's lifetime | Parent controls | Environment tree |
| Re-creation | Persists across view updates | Re-created if parent rebuilds | Stable |
| Testability | Direct init | Inject mock | Environment injection |
| Use case | View's own data model | Shared model, parent-owned | Global app state |