协作头脑风暴
brainstorming
在实现前澄清需求、探索设计选择,并形成清晰的开发方向。
desktop-appkit【开发与工程】Use when the user asks about macOS app development with AppKit, Cocoa, Objective-C, Swift AppKit, macOS UI framework, or native Mac application architecture. Do NOT use for: SwiftUI (desktop-swiftui), or cross-platform desktop (desktop-electron, desktop-tauri).
独立 GitHub 快照 · 自动分类 · 以原作者文档为准
Build native macOS applications using AppKit — Apple's mature, Objective-C-originated UI framework. AppKit provides deep OS integration, native look and feel, and access to macOS-specific features (menus, toolbars, sheets, drawers, NSTableView, NSCollectionView, document architecture) that SwiftUI cannot fully replace.
Exact user phrases: "AppKit", "macOS app", "Cocoa framework", "NSView", "NSViewController", "NSTableView", "NSDocument", "native Mac app", "Mac Catalyst", "Objective-C Mac", "Swift AppKit".
AppKit architecture plan with view controller hierarchy, window management, delegation patterns, and data flow design.
250 lines.
What type of macOS app?
├── Document-based (files are the primary data unit)
│ → NSDocument + NSDocumentController architecture
│ → Auto-save, versions, NSFileWrapper, UTType declaration
├── Single-window utility (calculator, preferences, toolbar)
│ → NSWindowController + single NSViewController
│ → NSWindow level behavior (floating, modal panel)
├── Multi-window browser/inspector (Xcode, Finder-like)
│ → NSWindowController per window, NSWindowController + delegate
│ → NSWindow restoration, tabs (NSWindowTabGroup)
└── Menu bar app (status item, background process)
→ NSStatusItem + NSMenu (no dock icon if LSUIElement)
→ NSApplication delegate, activateIgnoringOtherApps
Model (NSManagedObject / Codable structs)
↕ Bindings or KVO
Controller (NSViewController / NSWindowController)
↕ Outlets and Actions
View (NSView subclasses: NSTableView, NSButton, NSTextField)
loadView() // Create view hierarchy programmatically (or load from nib)
↓
viewDidLoad() // Setup initial state, register observers, configure views
↓
viewWillAppear() // View about to appear (update from model)
↓
viewDidAppear() // View appeared (start animations, set first responder)
↓
viewWillDisappear() // View about to disappear (save state)
↓
viewDidDisappear() // View disappeared (stop timers, remove observers)
| Pattern | Description | Best For |
|---|---|---|
| MVC (Traditional) | Model-View-Controller, Cocoa native | Simple apps, document-based apps |
| MVVM + Bindings | NSObjectController, ArrayController, bindings in IB | Form-heavy, preferences, inspector panels |
| Presenter | NSViewController as presenter, passive views | Testable logic, moderate complexity |
| Coordinator | AppKit + child view controller coordination | Multi-screen flows, wizards |
| Combine + AppKit | Reactive pipelines with NSObject subscriptions | Modern Swift, async data handling |
Modern approach: Use Combine for binding-like data flow without IB bindings, which are fragile and hard to debug.
class DocumentViewController: NSViewController {
let viewModel = DocumentViewModel()
private var cancellables = Set<AnyCancellable>()
override func viewDidLoad() {
super.viewDidLoad()
viewModel.$documentTitle
.sink { [weak self] title in
self?.titleLabel.stringValue = title
}
.store(in: &cancellables)
viewModel.$isDirty
.sink { [weak self] dirty in
self?.view.window?.isDocumentEdited = dirty
}
.store(in: &cancellables)
}
}
// AppDelegate with programmatic window creation
class AppDelegate: NSObject, NSApplicationDelegate {
var window: NSWindow!
func applicationDidFinishLaunching(_ notification: Notification) {
let windowSize = NSSize(width: 1000, height: 700)
let screenFrame = NSScreen.main?.visibleFrame ?? NSRect.zero
let windowOrigin = NSPoint(
x: (screenFrame.width - windowSize.width) / 2,
y: (screenFrame.height - windowSize.height) / 2
)
window = NSWindow(
contentRect: NSRect(origin: windowOrigin, size: windowSize),
styleMask: [.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView],
backing: .buffered,
defer: false
)
window.title = "My App"
window.contentViewController = MainSplitViewController()
window.makeKeyAndOrderFront(nil)
}
func applicationWillTerminate(_ notification: Notification) {
// Save state, close resources
}
}
Menu System:
// Validating menu items dynamically
override func validateMenuItem(_ menuItem: NSMenuItem) -> Bool {
switch menuItem.action {
case #selector(deleteSelectedItem):
return tableView.selectedRow >= 0
case #selector(exportDocument):
return document != nil
default:
return super.validateMenuItem(menuItem)
}
}
NSTableView (Virtualized List):
class MyTableViewController: NSViewController, NSTableViewDataSource, NSTableViewDelegate {
@IBOutlet weak var tableView: NSTableView!
var items: [Item] = []
// NSTableViewDataSource
func numberOfRows(in tableView: NSTableView) -> Int { items.count }
func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? {
let cell = tableView.makeView(withIdentifier: tableColumn!.identifier, owner: self) as! NSTableCellView
cell.textField?.stringValue = items[row].name
return cell
}
// NSTableViewDelegate
func tableViewSelectionDidChange(_ notification: Notification) {
let selectedRow = tableView.selectedRow
// Update detail view
}
}
Performance Tips for Large NSTableView:
tableView:heightOfRow: only if variable heights are required (expensive)tableView.beginUpdates() / tableView.endUpdates()NSTableViewDataSource instead of Cocoa Bindings for >10K rowsNSAnimationContext to batch layout changesNSCollectionView (Grid/Layout):
let layout = NSCollectionViewFlowLayout()
layout.itemSize = NSSize(width: 200, height: 150)
layout.minimumInteritemSpacing = 16
layout.minimumLineSpacing = 16
layout.sectionInset = NSEdgeInsets(top: 16, left: 16, bottom: 16, right: 16)
let collectionView = NSCollectionView()
collectionView.collectionViewLayout = layout
collectionView.register(MyCollectionViewItem.self, forItemWithIdentifier: .myItem)
// Pasteboard data types
extension NSPasteboard.PasteboardType {
static let myCustomType = NSPasteboard.PasteboardType("com.example.myapp.mytype")
}
// Drag source
override func mouseDragged(with event: NSEvent) {
let pasteboardItem = NSPasteboardItem()
pasteboardItem.setString(serializedData, forType: .myCustomType)
let draggingItem = NSDraggingItem(pasteboardWriter: pasteboardItem)
draggingItem.setDraggingFrame(dragImageRect, contents: dragImage)
beginDraggingSession(with: [draggingItem], event: event, source: self)
}
// Drop destination
override func draggingEntered(_ sender: NSDraggingInfo) -> NSDragOperation {
return sender.draggingPasteboard.canReadObject(forClasses: [NSString.self], options: nil) ? .copy : []
}
override func performDragOperation(_ sender: NSDraggingInfo) -> Bool {
guard let strings = sender.draggingPasteboard.readObjects(forClasses: [NSString.self], options: nil) as? [String] else { return false }
handleDroppedStrings(strings)
return true
}
Dark Mode support is automatic if you use NSColor system colors:
// System colors that adapt automatically
view.layer?.backgroundColor = NSColor.controlBackgroundColor.cgColor
textField.textColor = NSColor.labelColor
placeholderField.textColor = NSColor.secondaryLabelColor
// Custom adaptative colors
let dynamicColor = NSColor(name: "customAccent") { appearance in
let isDark = appearance.name == .darkAqua || appearance.name == .vibrantDark
return isDark ? NSColor(red: 0.4, green: 0.6, blue: 1.0, alpha: 1.0) : NSColor(red: 0.0, green: 0.3, blue: 0.8, alpha: 1.0)
}
Accessibility:
// Every NSView subclass must be an NSAccessibilityElement
override func isAccessibilityElement() -> Bool { true }
override func accessibilityRole() -> NSAccessibility.Role { .button }
override func accessibilityLabel() -> String? { "Save document" }
override func accessibilityPerformPress() -> Bool {
performClick()
return true
}
<!-- Entitlements for sandboxed apps -->
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.files.user-selected.read-write</key>
<true/>
<key>com.apple.security.network.client</key>
<true/>
<key>com.apple.security.files.bookmarks.app-scope</key>
<true/>
class MainSplitViewController: NSSplitViewController {
override func viewDidLoad() {
let sidebarVC = SidebarViewController()
let contentVC = ContentViewController()
let sidebarItem = NSSplitViewItem(sidebarWithViewController: sidebarVC)
sidebarItem.minimumThickness = 180
sidebarItem.maximumThickness = 300
let contentItem = NSSplitViewItem(viewController: contentVC)
contentItem.minimumThickness = 400
addSplitViewItem(sidebarItem)
addSplitViewItem(contentItem)
}
}
class MyDocument: NSDocument {
@objc dynamic var content = MyDocumentData()
override class var autosavesInPlace: Bool { true }
override func makeWindowControllers() {
let storyboard = NSStoryboard(name: "Main", bundle: nil)
let wc = storyboard.instantiateController(withIdentifier: "Document Window Controller") as! NSWindowController
addWindowController(wc)
// Pass data to view controller
(wc.contentViewController as? DocumentViewController)?.document = self
}
override func data(ofType typeName: String) throws -> Data {
return try JSONEncoder().encode(content)
}
override func read(from data: Data, ofType typeName: String) throws {
content = try JSONDecoder().decode(MyDocumentData.self, from: data)
}
}
The responder chain enables flexible event handling:
@IBAction methods on any responder for automatic menu validation| Pitfall | Description | Prevention |
|---|---|---|
| Retain cycles with delegates | Strong reference from view to delegate | Always use weak references for delegates and dataSources |
| Stale bindings | IB bindings break silently when model changes | Use Combine or willSet/didSet observers instead |
| Main thread blocking | Heavy computation on main thread freezes UI | Dispatch to global queue, update UI on main |
| Improper view lifecycle | Crash when accessing outlets before loadView() | Only access outlets after viewDidLoad() |
| Dark mode breakage | Hardcoded colors look wrong in dark mode | Use NSColor.systemColors or dynamic NSColor always |
| Sandbox denial | App rejected for missing entitlement | Test with sandbox enabled from day one |
| Missing menu validation | Menu items enabled when they shouldn't be | Always implement validateMenuItem: |
| Window state loss | Window position/size not restored | Implement window autosave name, encode restorable state |
| Sheet on wrong window | Sheet on modal panel instead of key window | Call beginSheet: on the correct window |
| Practice | Rationale |
|---|---|
| Use storyboard for menu + windows, programmatic for complex views | Maintains clarity without fighting IB limitations |
| Prefer NSViewController containment over custom container | Built-in child management, responder chain support |
| Use Cocoa Bindings sparingly | Works in IB but breaks silently — replace with Combine |
| Always implement validateMenuItem: | Correct menu state is expected macOS behavior |
| Use NSView's wantsLayer = true for complex rendering | Hardware-accelerated compositing |
| Test with VoiceOver enabled | Accessibility is a requirement, not a feature |
| Use autosave for window state | Users expect restored window positions |
| Prefer Swift over Objective-C for new code | Memory safety, Swift concurrency, Combine |
| Use structured concurrency (async/await) | Avoids callback pyramids, integrates with AppKit |
| Version your Core Data model | Irreversible migration is a support nightmare |
let sidebar = NSSplitViewItem(sidebarWithViewController: sidebarVC)
let content = NSSplitViewItem(viewController: contentVC)
let detail = NSSplitViewItem(viewController: detailVC)
// Content gets more space: hold splitter position with NSSplitViewItem behavior
let sheetVC = SheetViewController()
presentAsSheet(sheetVC)
// Or for critical operations:
presentAsModalWindow(sheetVC)
let popoverVC = PopoverContentViewController()
let popover = NSPopover()
popover.contentViewController = popoverVC
popover.behavior = .transient // Dismisses on click outside
popover.show(relativeTo: button.bounds, of: button, preferredEdge: .maxY)
// Use NSTabView in a separate window controller
// NSWindowController with window level .normal (not modal)
// Register defaults via UserDefaults.standard.register(defaults:)
Hand off to desktop-swiftui for SwiftUI migration strategy. Hand off to design-accessibility for VoiceOver compliance 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 | AppKit | SwiftUI |
|---|---|---|
| Minimum OS | macOS 10.0+ | macOS 11.0+ |
| Flexibility | Full control, any UI | Limited to SwiftUI capabilities |
| Performance | Native AppKit views | SwiftUI rendering overhead |
| Interop | — | NSHostingView for embedding |
| Development speed | Slower (manual layout) | Faster (declarative) |
| Future-proof | Stable (legacy) | Apple's focus |
| Best for | Complex, legacy macOS apps | New development, simple UIs |
| Aspect | Storyboard (XIB) | Programmatic |
|---|---|---|
| Visualization | Visual editor | Runtime only |
| Merge conflicts | Frequent (XML) | Rare (code) |
| Reusability | NIB loading | Custom NSView subclasses |
| Debugging | Hard (runtime connections) | Easy (breakpoints) |
| Team preference | Design-oriented | Code-oriented |