macos-programmer skill
Provides expertise in macOS-specific development patterns, platform APIs, and decision frameworks for building native Mac applications.
- GitHub stars
- 9
- Files
- 1
- Last commit
- Oct 13, 2026
- License
- EPL-2.0
- Allowed tools
- —
What does the macos-programmer skill do?
macos-programmer provides expertise in macOS-specific development patterns, platform APIs, and decision frameworks for building native Mac applications.
Summary generated from the skill's SKILL.md.
About the macos-programmer skill
Developing native Mac applications requires navigating the specific complexities of the macOS ecosystem, which differs significantly from iOS. This skill provides developers with the expertise needed to manage macOS-specific development patterns, platform APIs, and decision frameworks. It is particularly useful when deciding between SwiftUI and AppKit, managing complex window lifecycles with NSWindow, or implementing document-based architectures using NSDocument.
An agent should load this skill when a developer needs to bridge frameworks using NSHostingController or NSViewRepresentable, or when troubleshooting platform-specific issues like coordinate system differences, layer-backing requirements, or TextKit 2 integration. The skill helps engineers move beyond simple porting of mobile patterns to build professional tools like IDEs or design applications that respect macOS conventions such as menu bars, multiple windows, and precise mouse tracking. It provides technical guidance on sandboxing, code signing, and notarization to ensure applications meet platform standards.
What an agent can do with it
- 1Implementing hybrid application architecturesCreate a professional Mac application that uses AppKit for window lifecycle management while using SwiftUI for view content.
Written from the skill's SKILL.md. Every use case is checked against a quote from it.
How to install the macos-programmer skill
Each command copies the skill folder into the client's skills directory. Restart the client or start a new session to load it.Install the macos-programmer skill in Plugin
/plugin marketplace add Pyroxin/opinionated-claude-skills /plugin install opinionated-apple-development@opinionated-claude-skills
Install the macos-programmer skill in Claude Code
~/.claude/skills/macos-programmer/SKILL.md
$ npx degit Pyroxin/opinionated-claude-skills/opinionated-apple-development/skills/macos-programmer ~/.claude/skills/macos-programmerInstall the macos-programmer skill in Claude Code (project)
.claude/skills/macos-programmer/SKILL.md
$ npx degit Pyroxin/opinionated-claude-skills/opinionated-apple-development/skills/macos-programmer .claude/skills/macos-programmerInstall the macos-programmer skill in Codex
~/.agents/skills/macos-programmer/SKILL.md
$ npx degit Pyroxin/opinionated-claude-skills/opinionated-apple-development/skills/macos-programmer ~/.agents/skills/macos-programmerInstall the macos-programmer skill in Gemini CLI
~/.gemini/skills/macos-programmer/SKILL.md
$ npx degit Pyroxin/opinionated-claude-skills/opinionated-apple-development/skills/macos-programmer ~/.gemini/skills/macos-programmerInstall the macos-programmer skill in Claude.ai
npx degit Pyroxin/opinionated-claude-skills/opinionated-apple-development/skills/macos-programmer macos-programmer && zip -r macos-programmer.zip macos-programmer # claude.ai → Customize → Skills → + → Create skill → Upload a skill → macos-programmer.zip
SKILL.md
View on GitHubmacOS Programming
<skill_scope skill="macos-programmer"> Related skills:
swift-programmer— Swift language fundamentals and Swift 6 concurrencysoftware-engineer— General software engineering principles and system architecturetest-driven-development— Testing philosophy and practices
This skill covers macOS-specific development patterns, platform APIs, and decision frameworks. It applies when developing Mac apps, working with Cocoa/AppKit code, or making SwiftUI vs AppKit decisions. </skill_scope>
Core Philosophy
<core_philosophy>
Hybrid by default. Production Mac apps commonly combine SwiftUI and AppKit: SwiftUI for most view content, AppKit where the platform's window, text, and event systems are exposed only there. Where the line falls varies by app; the case studies in <swiftui_vs_appkit_decision> show teams landing at different points, and each macOS release moves some formerly AppKit-only capability into SwiftUI (see <recent_changes>). Treat AppKit fallback as expected work rather than a failure of SwiftUI, and re-check the boundary when the deployment target moves.
Platform Identity: macOS is not iOS with a bigger screen. Multiple windows, menu bars, keyboard navigation, document-based architecture, and precise window management are first-class citizens. Respect macOS conventions; don't port iOS patterns blindly. </core_philosophy>
SwiftUI vs AppKit: The Critical Decision Framework
<swiftui_vs_appkit_decision> The Ground Truth from Production Apps:
SwiftUI maturity differs between iOS and macOS. Two 2023 accounts from Mac apps in development show the shape of the gap. Ghostty (then in private beta) rewrote its SwiftUI app and window lifecycle management in AppKit (+802/-239 lines) when non-native fullscreen, which requires subclassing NSWindow, proved impossible in pure SwiftUI; its views stayed SwiftUI.[^ghostty-devlog] Multi.app moved to SwiftUI but still needed "some access to NSEvents, text input, and tweaking the first responder that just aren't possible with pure SwiftUI," and wrote that SwiftUI bugs on older macOS left them "approaching the cusp of dropping support entirely" for those versions.[^multi-swiftui] Both are dated; re-read the boundary against the current release before applying them.
Use SwiftUI When:
- New apps targeting macOS 14+, simple-to-medium complexity
- Standard UI elements suffice (lists, forms, navigation)
- Cross-platform iOS/macOS with acceptable compromises
- Rapid prototyping where bugs are acceptable
- Team willing to bridge to AppKit with
NSViewRepresentablewhere a control or behavior isn't available in SwiftUI - Can require a recent macOS; each release fixes SwiftUI-on-Mac issues, so older deployment targets carry more workarounds
Use AppKit When:
- Complex text editing (code editors, word processors, NSTextView-dependent workflows)
- Large datasets where profiling on a Release build shows SwiftUI
Listfalling behindNSTableView; measure on the current OS, since a macOS 26 change toNavigationLink"improves performance of manyNavigationLinks in lazy containers likeList"[^macos26-notes] - Custom window management (non-standard fullscreen, window subclassing, utility panels)
- UI that must idle at near-zero CPU (verify with Instruments rather than assuming either framework)
- Behavior that has been stable in AppKit across the OS versions you support, where the SwiftUI equivalent has changed release to release (check release notes for the specific control)
- Professional tools (IDEs, DAWs, design apps, terminals) whose text, window, or event needs exceed SwiftUI's surface
The Hybrid Shape (Common in Production):
Ghostty's arrangement after its rewrite: AppKit owns the app and window lifecycle and SwiftUI supplies the views.[^ghostty-devlog] The bridging mechanisms are NSHostingController (SwiftUI inside AppKit) and NSViewRepresentable (AppKit inside SwiftUI).
<swiftui_limitations> SwiftUI Limitations on macOS (as of macOS 26; re-check each release):
- Window subclassing is unavailable in pure SwiftUI; non-native fullscreen and similar behaviors need AppKit[^ghostty-devlog]
- Direct access to
NSEvent, first-responder manipulation, and some text-input behavior is AppKit-only[^multi-swiftui] - The menu bar cannot be customized to the degree AppKit allows
Listperformance with large data sets can lagNSTableView; the gap is workload-dependent and narrowed in macOS 26, so measure rather than assume- Memory growth relative to iOS for similar apps has been reported by developers; treat it as something to profile, not a settled property
Capabilities that have moved into SwiftUI recently, such as styled text editing with AttributedString and Find Bar control in TextEditor on macOS 26, are listed in <recent_changes>.
</swiftui_limitations>
Decision Pattern:
Production Mac App
├── AppKit: NSApplication, NSWindow, NSWindowController, NSDocument
├── SwiftUI: View content where appropriate
└── Bridge: NSHostingController, NSViewRepresentable
</swiftui_vs_appkit_decision>
Platform Differences from iOS (Critical for iOS Developers)
<platform_differences>
Coordinate Systems:
- iOS origin: top-left, Y increases downward
- macOS (unflipped
NSView) origin: bottom-left, Y increases upward - Override
isFlippedto returntruefor iOS-style coordinates - Drawing into a flipped context without compensating can draw images upside down; check the drawing API's handling of flipped contexts
Layer Backing:
- iOS views are layer-backed by default
- macOS views are not layer-backed unless the view or an ancestor sets
wantsLayer = true - Layer-backed views enable GPU compositing but cost memory; a layer-backed view's subviews become layer-backed too
- Enable it for animation and compositing effects; leave static content unbacked unless profiling shows a benefit
Windows vs Views:
- macOS users expect multiple windows, resizing, minimize/maximize
- NSWindow is critical—not a passive container like UIWindow
- Window management patterns (tabs, fullscreen, spaces) are first-class
- Custom window behaviors require AppKit (SwiftUI limitations)
Text System:
- NSTextView/TextKit vastly more powerful than UITextView
- Rulers, find/replace, grammar checking built-in
- TextKit 2 shipped in macOS 12 and became the default for all text controls,
NSTextViewincluded, in macOS 13;[^wwdc22-10090] touchingtextView.layoutManagerswitches that view to TextKit 1 compatibility mode ("if you explicitly call thelayoutManagerproperty on a text view or text container, the framework reverts to a compatibility mode")[^textkit-compat] - When a view needs TextKit 1 (a reproduced TextKit 2 regression, or code that depends on
NSLayoutManagerduring a migration), select it when creating the view rather than by triggering the fallback later, which discards the TextKit 2 layout; macOS 26 continues to extend TextKit 2 (e.g.,includesTextListMarkers)[^macos26-notes]
Background Colors:
- Many NSView subclasses use
drawsBackgroundproperty - Not universal
backgroundColorlike iOS - Check class documentation for correct property
Mouse vs Touch:
- AppKit hover effects need tracking areas (
updateTrackingAreas()); SwiftUI views use.onHover - Right-click context menus are standard expectation
- Mouse tracking differs from touch gesture handling
NSEventprovides precise cursor position and modifier keys </platform_differences>
Window Management Mastery
<window_management>
NSWindow Lifecycle (10.13 SDK Change):
isReleasedWhenClosed defaults to true for NSWindow (false for NSPanel) and "is ignored for windows owned by window controllers";[^released-when-closed] for a window your own code owns and references, set it to false, or closing the window over-releases it under ARC.
AppKit's release notes state the current rule: "If your application is linked on macOS 10.13 SDK or later, NSWindows that are ordered-in will be strongly referenced by AppKit, until they are explicitly ordered-out or closed."[^appkit-rn-window] The condition is the SDK the app is linked against, not its deployment target; an app built against an older SDK keeps the old behavior even when running on newer macOS.
Window Style and Collection Behavior:
Style masks combine but have limitations:
- A borderless window "can't become key or main, unless the value of
canBecomeKeyorcanBecomeMainistrue" (subclass and override)[^stylemask-borderless] - "Changing the style mask may cause the view hierarchy to be rebuilt,"[^stylemask] so avoid changing it mid-animation or mid-fullscreen-transition
fullSizeContentView"opts in to layer-backing"[^stylemask-fullsize]
Collection behavior controls Spaces/Exposé/fullscreen:
.canJoinAllSpaces: Visible on all spaces (like menu bar).moveToActiveSpace: "When the window becomes active, move it to the active space instead of switching spaces"[^collection-behavior].fullScreenPrimary: Can be fullscreen window.fullScreenAuxiliary: Shown with fullscreen window.stationary: Unaffected by Exposé, visible on desktop
Pattern for an overlay that appears on every space:
window.collectionBehavior = [.canJoinAllSpaces, .stationary]
Collection behavior governs Spaces and Exposé membership, not stacking order or fullscreen coexistence; set the window level separately and test with a fullscreen app and Stage Manager before relying on it.
Multi-Window Document Architecture:
NSDocumentController (singleton)
↓ manages
NSDocument instances (one per document)
↓ manages
NSWindowController instances (one per window)
Modern Document Best Practice:
override class var autosavesInPlace: Bool { true }
Enables autosave in place and the system's version browsing and storage. Asynchronous saving is a separate opt-in (canAsynchronouslyWrite(to:ofType:for:)), and the document must still unblock user interaction itself.
</window_management>
Responder Chain and Menu Validation
<responder_chain>
The Complete Action Message Responder Chain:
- Start with the first responder in the key window
- Try every
nextResponderin that chain, then the key window itself - Try the key window's delegate, then its
NSDocument(if different from the delegate) - Repeat for the main window, if it is a different window
NSApplicationtries to respondNSApplication.delegate- In a document-based app, the
NSDocumentController(which does not inherit fromNSResponder)[^event-architecture]
Critical Insight: App delegate is NOT part of nextResponder chain—you can never reach it through iteration. It's used as a fallback when current key window's responder chain returns nil.
NSViewController Integration (macOS 10.10+):
Before 10.10, an NSViewController was not in the responder chain by default; code patched nextResponder by hand. From 10.10, AppKit inserts the view controller into the chain immediately after its view: "The view's nextResponder is then set to be the viewController, and viewController's nextResponder is set to be the previously saved nextResponder."[^appkit-rn-1010]
Menu Validation Performance:
NSMenu updates EVERY menu item on EVERY user event (mouse move, keypress). This is a performance killer for large menus.
How it works:
- Determine item's target (explicit or via responder chain)
- Check if target implements action method (if not, disable)
- If target implements
validateMenuItem:orvalidateUserInterfaceItem:, call it and use return value
Optimization:
- Disable auto-validation for static menus:
menu.autoenablesItems = false - Manually control
menuItem.isEnabled - A
niltarget routes the action and the validation query through the responder chain; set an explicit target only when you want to bypass that lookup </responder_chain>
SwiftUI Integration with AppKit
<swiftui_appkit_integration>
NSHostingController (Essential Bridge Pattern):
// Embedding SwiftUI in AppKit
let swiftUIView = MySwiftUIView()
let hostingController = NSHostingController(rootView: swiftUIView)
// macOS 13+ sizing control
hostingController.sizingOptions = [.intrinsicContentSize]
NSViewRepresentable:
updateNSView runs whenever SwiftUI updates this represented view, so guard assignments whose setter has side effects: assigning NSTextView.string resets the selection (observed on macOS 26; it does not post textDidChange). Propagate edits back to the binding through a Coordinator, or the bridge is one-way.
struct TextViewRepresentable: NSViewRepresentable {
@Binding var text: String
func makeCoordinator() -> Coordinator { Coordinator(text: $text) }
func makeNSView(context: Context) -> NSTextView {
let view = NSTextView()
view.delegate = context.coordinator
return view
}
func updateNSView(_ nsView: NSTextView, context: Context) {
context.coordinator.text = $text // keep the coordinator on the current binding
if nsView.string != text { // guard: assigning resets the selection
nsView.string = text
}
}
final class Coordinator: NSObject, NSTextViewDelegate {
var text: Binding<String>
init(text: Binding<String>) { self.text = text }
func textDidChange(_ notification: Notification) {
guard let view = notification.object as? NSTextView else { return }
text.wrappedValue = view.string // edits flow back to SwiftUI
}
}
}
State Management with @MainActor @Observable (macOS 14+):
Gotcha, toolchain-dependent: built with Xcode 26 or earlier, "A State property always instantiates its default value when SwiftUI instantiates the view," so Apple's guidance is to "avoid side effects and performance-intensive work when initializing the default value";[^swiftui-state] an @Observable model declared as @State in a frequently re-instantiated view is allocated on each instantiation. Built with Xcode 27 (beta 6 as of September 2026), @State is a macro and "objects held in state are only ever initialized one time, when the view is first created," which removes the cost but also rejects some initializer patterns that used to compile; read TN3211 before migrating.[^tn3211] For SwiftUI view state, keep the observable type on the main actor; see swift-programmer for the general @MainActor @Observable rule.
Solution for app-wide state: declare the main-actor observable model in the App struct, which SwiftUI instantiates once. Apple's alternative for a view-local model is to create it in a .task modifier, "which is called only once when the view first appears";[^swiftui-state] that is once per appearance of a given identity: it runs again if the view disappears and reappears, or if its identity changes, so guard the creation if it must happen once per state lifetime.
@MainActor
@Observable
class AppModel {
// App state and actions.
}
@main
struct MyApp: App {
@State private var appModel = AppModel() // Declare here
var body: some Scene {
WindowGroup {
ContentView().environment(appModel)
}
}
}
Multi-Window Management:
// WindowGroup - Multiple instances
WindowGroup { ContentView() }
// Window - Single unique instance
Window("Stats", id: "stats") { StatsView() }
// UtilityWindow (macOS 15+) - Floating palette
UtilityWindow("Palette", id: "palette") { PaletteView() }
.keyboardShortcut("u")
Menu Bar & Commands:
.commands {
CommandMenu("Custom") {
Button("Action") {}
.keyboardShortcut("x", modifiers: [.command, .shift])
}
}
// Focus values for multi-window menus
@FocusedValue(\.messageState) var messageState
</swiftui_appkit_integration>
Sandboxing and File System Access
Sandboxing Strategy:
- Mac App Store: REQUIRED
- Direct Distribution: OPTIONAL but strongly recommended
Access Methods:
- User Selection (NSOpenPanel/NSSavePanel): Immediate access
- Security-Scoped Bookmarks: Persistent access across launches
- Container Access: Automatic for
~/Library/Containers/{bundle-id}
Security-Scoped Bookmarks (Critical Pattern):
// Save bookmark
let bookmarkData = try url.bookmarkData(options: .withSecurityScope)
// Restore and use
var isStale = false
let url = try URL(resolvingBookmarkData: bookmarkData,
options: .withSecurityScope,
bookmarkDataIsStale: &isStale)
guard url.startAccessingSecurityScopedResource() else {
throw CocoaError(.fileReadNoPermission)
}
defer { url.stopAccessingSecurityScopedResource() }
if isStale {
// Re-create and re-save the bookmark while access is active; creating one needs access to the file.
let fresh = try url.bookmarkData(options: .withSecurityScope)
save(fresh)
}
// Access file
Rules:
- Call
startAccessingSecurityScopedResource()on the resolved URL, not the original - Don't call it for
NSOpenPanel/NSSavePanelURLs; the system starts access on those for you - Balance every successful start with a stop; calls may nest, and access ends at the last balanced stop[^security-scoped]
- Leaking access consumes kernel resources until the app relaunches
Entitlements to Know:
com.apple.security.app-sandbox: Enable App Sandboxcom.apple.security.files.user-selected.read-write: User-selected filescom.apple.security.files.bookmarks.app-scope: App-scoped bookmarkscom.apple.security.network.client: Outgoing network connectionscom.apple.security.network.server: Incoming network connections
Code Signing and Notarization
<code_signing>
Process (checked against Apple's notarization documentation, September 2026):
- Code Sign each nested component first (frameworks, helpers, plug-ins), then the app, without
--deep:
codesign --force --options runtime --timestamp \
--entitlements App.entitlements \
--sign "Developer ID Application: Your Name (TEAMID)" \
App.app
codesign --verify --deep --strict --verbose=2 App.app # --deep is for verification
- Create Archive:
ditto -c -k --keepParent App.app App.zip
- Submit for Notarization using a keychain profile created once with
notarytool store-credentials:
xcrun notarytool submit App.zip --keychain-profile "notarytool-password" --wait
- Staple the ticket to the app, then re-package. "While you can notarize a ZIP archive, you can't staple to it directly. Instead, run
stapleragainst each item that you added to the archive. Then create a new ZIP file containing the stapled items for distribution."[^notarization-workflow]
xcrun stapler staple App.app
xcrun stapler validate App.app
ditto -c -k --keepParent App.app App.zip # the distributed archive must contain the stapled app
Rules:
- Sign bottom-up in the bundle hierarchy; do not sign with
--deep(man codesignmarks it "DEPRECATED for signing as of macOS 13.0"), because it applies the outer entitlements and flags to nested code.--deepremains the right flag for--verify. notarytoolreplacedaltool, which Apple stopped accepting on November 1, 2023; the@keychain:password syntax wasaltool's, andnotarytooluses--keychain-profileor a literal--password.- macOS Sequoia removed the Control-click override for Gatekeeper; users must approve unsigned or un-notarized software in System Settings, so notarize anything distributed outside the App Store.[^sequoia-gatekeeper]
Common Failures:
- "Hardened runtime not enabled" → Add
--options runtime - "Invalid signature" → Re-sign with proper entitlements </code_signing>
Architecture Patterns
<architecture_patterns>
For general architecture philosophy, see the software-engineer skill. This section covers macOS-specific architectural considerations.
Primary Patterns (Choose One):
MVC (Model-View-Controller)
What it is:
- Apple's classic pattern for AppKit development
- Model: Data and business logic
- View: UI components
- Controller: Coordinates between Model and View
When to use:
- AppKit-heavy applications
- Document-based apps (works naturally with NSDocument)
- Simple-to-medium complexity apps
- When following Apple's conventions makes sense
Reality check:
- Controllers tend to become large ("Massive View Controller")
- This is fine for many apps—just watch for controller bloat
- Treat a controller that has grown to several hundred lines as a signal to extract logic; the number is a rule of thumb, not a threshold
MV (Model-View)
What it is:
- The pattern the cited author distills from Apple's SwiftUI sample code: no separate view-model layer[^mv-pattern]
- Model: Data and business logic
- View: SwiftUI views with @State for local state
- No separate ViewModel layer - views call model methods directly
When to use:
- Simple-to-medium SwiftUI applications
- Prototypes and MVPs
- Apps without complex testability requirements
- When MVVM feels like overkill
Reality check:
- SwiftUI's reactive binding means views often serve as their own view models[^mv-pattern]
- Minimal boilerplate
- Works until a model accumulates enough presentation logic that views become hard to test; then extract logic or move to MVVM
Pattern:
@MainActor
@Observable
class User {
var name: String = ""
var email: String = ""
func save() {
// Business logic here
}
}
struct ProfileView: View {
@State private var user = User()
var body: some View {
Form {
TextField("Name", text: $user.name)
Button("Save") { user.save() }
}
}
}
MVVM (Model-View-ViewModel)
What it is:
- Model: Data and business logic
- View: SwiftUI views or AppKit views
- ViewModel: Presentation logic, formats data for View
When to use:
- SwiftUI applications needing testable presentation logic
- Multiple views display same data differently
- Need to separate view logic from view definition
- Models have grown too large in MV pattern
Reality check:
- Works beautifully with SwiftUI's reactive nature
- Less natural in pure AppKit (but still usable)
- ViewModels can also bloat—same solution as MVC (extract logic)
- Consider if you actually need it vs simpler MV pattern
VIPER (View-Interactor-Presenter-Entity-Router)
What it is:
- Ultra-granular pattern splitting each screen into 5+ components
- View: Displays data
- Interactor: Business logic
- Presenter: Formats data for view
- Entity: Data models
- Router: Navigation
When to use:
- Rarely. Honest assessment: generally over-engineered
- Large enterprise apps with extreme testability requirements
- Teams that need architectural enforcement of separation
- Fits AppKit; in SwiftUI its Router layer duplicates state-driven navigation, and its Presenter's formatting can live in a view model or the model without a separate layer
Reality check:
- Massive boilerplate (5+ files per screen)
- Most sources say "only if you really need it"
- In SwiftUI: observation propagates model changes to views, so the Presenter's update plumbing disappears (its formatting logic moves into a view model or the model); the Router's routing becomes navigation state (
NavigationStackwith aNavigationPathheld in an observable model), with navigation policy still yours to write - Don't force this pattern into SwiftUI - you'll fight the framework constantly
- Consider carefully whether the complexity is justified even in AppKit
Complementary Pattern (Add When Needed):
Files in this skill
1macos-programmer skill questions
How do I install the macos-programmer skill in Claude Code?
Run npx degit Pyroxin/opinionated-claude-skills/opinionated-apple-development/skills/macos-programmer ~/.claude/skills/macos-programmer. Claude Code loads skills from ~/.claude/skills (all projects) or .claude/skills (one project) and uses a skill when its description matches the task.
Which clients can use the macos-programmer skill?
Any client that reads the Agent Skills format: Claude Code, Codex, Gemini CLI and claude.ai (upload as a ZIP). Scripts in the skill run only where the client allows code execution.
Is the App Delegate part of the responder chain?
No, the App Delegate is not part of the nextResponder chain; it serves as a fallback when the current key window's responder chain returns nil.