macOS Programming
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.
Core Philosophy
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.
SwiftUI vs AppKit: The Critical Decision Framework
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).
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>.
Decision Pattern:
Production Mac App
├── AppKit: NSApplication, NSWindow, NSWindowController, NSDocument
├── SwiftUI: View content where appropriate
└── Bridge: NSHostingController, NSViewRepresentable
Platform Differences from iOS (Critical for iOS Developers)
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
Window Management Mastery
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.
Responder Chain and Menu Validation
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
SwiftUI Integration with AppKit
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
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
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
Architecture Patterns
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):
Coordinator Pattern
What it is:
- Handles navigation and screen flow
- Works on top of MVC or MVVM (not instead of)
- Removes navigation logic from view controllers/view models
- Common combinations: MVVM-C, MVC-C
- Primarily an AppKit/UIKit pattern
When to add Coordinator:
- Complex navigation in AppKit apps (many screens and flows)
- Deep linking (URLs open specific screens)
- Multiple entry points to same screen
- A/B testing different user flows
- Reusing view controllers in different contexts
SwiftUI Reality:
- Don't force Coordinators into SwiftUI - navigation is declarative and state-driven
- SwiftUI navigates through state:
NavigationStackandNavigationSplitViewdriven by aNavigationPathor selection value,.sheet(), and on macOS separateWindow/WindowGroupscenes (.fullScreenCover()is not available on macOS) - Navigation policy can live in an observable model that owns the path; that is the SwiftUI counterpart of a coordinator
Pattern (AppKit/UIKit):
App uses MVC or MVVM for view/logic organization
+
Coordinator manages navigation between screens
Other Concerns:
Patterns like Repository (data access), networking layers, and business logic extraction emerge on a case-by-case basis during actual project design. Don't prematurely abstract.
Performance Optimization
Profiling with Instruments:
Essential templates:
- Time Profiler: CPU usage, call stacks, bottlenecks; start here
- Allocations: Memory allocation patterns
- Leaks: Memory leak detection
- Metal System Trace: GPU performance
Best practices:
- Profile in Release mode (optimization critical)
- Profile on target hardware (older devices reveal issues)
- Use Signposts for precise measurement intervals
Main Thread Optimization:
Keep the main thread for UI updates and input handling. Blocking work moves off it; work that is already async and suspends (e.g., URLSession requests) can be initiated from the main actor without blocking it. Offload with @concurrent or a detached task (see swift-programmer):
- Data parsing and decoding of large responses
- Synchronous file I/O
- Complex calculations
- Image processing
Layer-Backed View Optimization:
A layer-backed view that draws with draw(_:) usually gets the redraw policy NSViewLayerContentsRedrawDuringViewResize (the SDK header: "Generally, the default value is NSViewLayerContentsRedrawOnSetNeedsDisplay if the view responds YES to -wantsUpdateLayer. Otherwise, the value is usually NSViewLayerContentsRedrawDuringViewResize"),[^nsview-header] which objc.io notes "might be detrimental to animation performance" because it triggers drawing on each frame of a resize.[^layer-redraw]
For views whose content doesn't depend on their size, change to:
view.layerContentsRedrawPolicy = .onSetNeedsDisplay
With this policy the view redraws only when you call setNeedsDisplay, so invalidation becomes your responsibility; content that depends on the view's size must invalidate on resize (or keep the default policy).
When to Enable Layer-Backing:
- Animating multiple views simultaneously
- Need smooth 60fps animations
- Want Core Animation features
- Creating effects requiring GPU acceleration
Leave Layer-Backing Off When:
- Static UI with no animation, where it buys nothing
- Memory is constrained; layers carry backing stores, though AppKit coalesces content where it can
- The view relies on precise, resolution-aware drawing that you have verified renders differently when backed
Testing Strategy
Swift Testing vs XCTest:
Swift Testing (Xcode 16+):
- Modern replacement using macros (
@Test,#expect,#require) - Better parallelization (in-process using Swift Concurrency)
- Works with structs/actors/classes, not just XCTestCase
- Use for: New unit tests in Swift 6/Xcode 16+ projects
XCTest:
- Apple's guidance: "continue using XCTest for any tests which use UI automation APIs like XCUIApplication or use performance testing APIs like XCTMetric as these are not supported in Swift Testing"[^wwdc24-10179]
- Necessary for Objective-C test code
- Use for: UI automation tests, performance tests, existing test suites
Distribution: put most coverage in unit tests of models and logic, integration tests where components meet (e.g., API clients), and UI tests only for the flows whose breakage would ship a broken app, because UI tests are the slowest and most brittle tier.
Accessibility Testing:
VoiceOver on macOS differs from iOS:
- Keyboard navigation primary (not touch)
- VoiceOver Utility for configuration
- Menu bar accessibility critical
- Multiple windows/spaces support
Testing workflow:
- Start VoiceOver: ⌘ + F5
- Navigate with Control + Option (VO keys)
- Verify all interactive elements are reachable and labeled (use the Accessibility Inspector in Xcode to audit)
Common Anti-Patterns
iOS Patterns Applied Incorrectly
- Assuming layer-backing is automatic
- Using UIKit coordinate conventions without override
- Treating NSWindow like UIWindow (passive container)
- Ignoring window management patterns
- Not implementing hover states (mouse tracking)
- Missing right-click context menus
Web Developer Mistakes
- Expecting CSS-like layout flexibility (AppKit lays out with Auto Layout constraints; SwiftUI with its layout containers and modifiers)
- Fighting native controls instead of embracing system appearance
- Ignoring accessibility (VoiceOver is expected, not optional)
- Single-window mentality where users expect multiple windows (documents, inspectors, palettes); a single-window app is fine when its task is single-window
- Not using native file dialogs (NSOpenPanel/NSSavePanel)
- Ignoring keyboard shortcuts and menu bar conventions
Linux/Cross-Platform Developer Mistakes
- Assuming POSIX conventions apply to GUI (Cocoa is not GTK/Qt)
- Fighting sandboxing instead of designing around it
- Ignoring Apple's signing/notarization requirements
- Persisting raw paths to files outside the container instead of security-scoped bookmarks (container-local paths need no bookmark)
- Not adapting to macOS HIG (menu bar, dock, system preferences)
Windows Developer Mistakes
- Expecting registry-like global preferences (use UserDefaults, sandboxed)
- Assuming all file access is available (sandbox constraints)
- Window chrome expectations (unified title-and-toolbar is the macOS norm, though separate title bars remain available)
- Installation expectations (drag-to-Applications is the norm for apps; installer packages exist for software that needs privileged installation)
Over-Engineering Simple Apps:
- Using VIPER for simple CRUD apps
- Complex architectural patterns for prototypes
- Custom state management when an
@Observablemodel in the environment suffices - Premature abstraction before requirements are clear
Ignoring macOS UI Conventions:
- Not following macOS Human Interface Guidelines
- Poor keyboard shortcut support
- Ignoring standard menu bar conventions
- Misusing window management (minimize, zoom, full screen)
- Not respecting macOS-specific UI elements (toolbars, sidebars, split views)
Premature SwiftUI Adoption
- Committing to 100% SwiftUI before checking that the app's window, text, and event needs are within SwiftUI's current surface (see
<swiftui_limitations>) - Ignoring AppKit when SwiftUI is insufficient
- Not budgeting for AppKit fallback code
- Assuming iOS SwiftUI code will work on macOS
Mac App Store vs Direct Distribution
Mac App Store:
- Pros: Apple handles distribution/updates, user trust, search visibility
- Cons: App Sandbox required; commission of 30%, reduced to 15% for developers in the App Store Small Business Program (developers with up to US$1 million in prior-year proceeds)[^small-business] and for auto-renewable subscriptions after a subscriber's first year;[^subscriptions] review delays; limited APIs
Direct Distribution:
- Pros: Greater API access, no commission, faster updates, custom pricing
- Cons: Must handle payments; Developer ID signing and notarization required for Gatekeeper to allow the app without the user granting an exception in System Settings; no built-in discoverability
Tendency, not rule: tools that need APIs the sandbox forbids, or their own licensing, go direct; apps that benefit from App Store discovery and trust go there. Many ship both.
Decision Frameworks Summary
SwiftUI vs AppKit:
- Simple-medium apps targeting macOS 14+: SwiftUI
- Text editing beyond
TextEditor's surface, data sets where profiling showsListbehindNSTableView, window behavior SwiftUI doesn't expose: AppKit for that part - Behavior that must not change between OS releases: prefer the framework whose implementation of that behavior has been stable, which for windows, text, and events has usually been AppKit
- Professional tools: Hybrid (AppKit foundation, SwiftUI where appropriate)
Architecture Pattern:
- AppKit apps: MVC
- Simple SwiftUI apps: MV (Model-View)
- SwiftUI apps needing testability: MVVM
- Complex AppKit navigation (many screens, deep linking): Add a Coordinator; in SwiftUI, hold navigation state in an observable model instead
- Rarely: VIPER, and then in AppKit rather than SwiftUI (if you genuinely need that degree of separation)
State Management:
- View-local: @State
- Shared across views (macOS 14+):
@MainActor @Observablemodels injected with.environment(_:)and read with@Environment;@EnvironmentObjectpairs with the olderObservableObjectprotocol and is for code that still uses it - Larger apps: several focused
@MainActor @Observabletypes rather than one singleton
Cross-Platform:
- Substantial feature overlap with acceptable compromises: SwiftUI multiplatform
- Mac-specific interaction (multi-window, menus, keyboard) that a shared UI would flatten: separate platform UIs over shared business logic
- Complex apps: Hybrid (shared Swift Package for logic, platform UIs)
Recent Changes
Additions, by release:
- macOS 26 / Xcode 26 (September 2025): the Liquid Glass design, with
NSGlassEffectViewandNSGlassEffectContainerViewin AppKit and matching SwiftUI APIs;[^liquid-glass-guide] styled text editing in SwiftUI withAttributedString,[^styled-text-guide] and Find Bar control inTextEditor(findNavigator(isPresented:));[^macos26-notes] a SwiftUIWebViewbacked by an observableWebPage;[^webkit-guide] animatedWindowresizing from a SwiftUI transaction;[^macos26-notes]NavigationLinkproducing a single view in lazy containers, improvingListperformance;[^macos26-notes] an "Enhanced Security" helper-extension template for isolating untrusted-data handling.[^xcode26-notes] These move the SwiftUI/AppKit boundary in<swiftui_vs_appkit_decision>. - Xcode 27 (beta 6 as of September 2026):
@Statebecomes a macro with lazy initial-value evaluation; see the gotcha in<swiftui_appkit_integration>and TN3211.[^tn3211]
Behavior changes, deprecations, and things being moved away from:
- SceneKit is deprecated "across all Apple platforms"; Apple recommends RealityKit for new projects.[^xcode26-notes]
Textconcatenation with+is deprecated in favor ofTextinterpolation, for localization correctness.[^macos26-notes]- Text writing direction in
Text,TextEditor, andTextFieldis now derived per paragraph from string content rather than layout direction on macOS 26.[^macos26-notes] - Instruments' SwiftUI template replaced the View Body and View Properties instruments, which are deprecated but still available.[^xcode26-notes]
- New app projects default to main-actor isolation in Xcode 26; see
swift-programmerfor the concurrency consequences.
Resources
Local Documentation:
- Xcode diagnostic docs:
/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/share/doc/swift/diagnostics/ - Framework guides bundled with Xcode:
/Applications/Xcode.app/Contents/PlugIns/IDEIntelligenceChat.framework/Versions/A/Resources/AdditionalDocumentation/(e.g.,AppKit-Implementing-Liquid-Glass-Design.md,SwiftUI-Styled-Text-Editing.md,SwiftUI-WebKit-Integration.md)
Apple Official:
- AppKit Documentation: https://developer.apple.com/documentation/appkit
- macOS HIG: https://developer.apple.com/design/human-interface-guidelines/macos
- SwiftUI Documentation: https://developer.apple.com/documentation/swiftui
Expert Blogs:
- NSHipster: https://nshipster.com (Overlooked Cocoa/Swift bits)
- objc.io: https://www.objc.io (Advanced techniques, essential "AppKit for UIKit Developers" article)
- Mike Ash Friday Q&A: https://www.mikeash.com/pyblog/ (Deep low-level technical articles)
- Brent Simmons Inessential: https://inessential.com (Veteran Mac developer, NetNewsWire author)
- Use Your Loaf: https://useyourloaf.com (Practical guides, WWDC viewing guides)
Books:
- "macOS Apps Step by Step" (formerly "macOS by Tutorials"; v4.0, November 2025) by Sarah Reichelt (TrozWare) - covers macOS 26 and Xcode 26
- "Hacking with macOS" by Paul Hudson - 18 projects; paid, with a free sample
- "The Complete Friday Q&A" (Volumes I-III) by Mike Ash - Essential for Cocoa internals
Open Source Examples:
- Awesome Open Source Mac Apps: https://github.com/serhii-londar/open-source-mac-os-apps
- NetNewsWire: https://github.com/Ranchero-Software/NetNewsWire (Mature AppKit codebase)
- WWDC app (unofficial): https://github.com/insidegui/WWDC (Well-structured Mac app)
Community:
- Apple Developer Forums: https://developer.apple.com/forums/
- Stack Overflow (cocoa/appkit tags)
The Modern macOS Developer's Mindset
Embrace Hybrid Solutions: Well-regarded Mac apps blend SwiftUI and AppKit. Use each framework where it excels. Don't force SwiftUI where AppKit is superior.
Leverage Platform Strengths: macOS isn't iOS with a bigger screen. Multiple windows, menu bars, keyboard navigation, document architecture—these are first-class citizens. iOS developers transitioning to Mac: unlearn iOS assumptions.
Test Relentlessly: Swift Testing for unit tests, XCTest for UI automation. Profile with Instruments regularly. VoiceOver-test every release.
Stay Pragmatic: The perfect architecture that ships beats the theoretical ideal that doesn't. Balance theoretical purity with practical delivery. Ship working software, iterate based on real problems, refactor when justified by pain.
Final Wisdom: The best Mac apps feel like Mac apps. They embrace platform conventions, leverage macOS strengths, and don't feel like iPad apps in disguise. Build for Mac users, not iOS users with keyboards. Master the frameworks, respect the platform, ship great software.
Sources
[^multi-swiftui]: Multi.app. 2023. Moving to SwiftUI from macOS Cocoa (April 5, 2023). Retrieved September 5, 2026 from https://multi.app/blog/moving-to-swiftui-from-macos-cocoa-or-ios-cocoa-touch
[^mv-pattern]: Mohammad Azam. 2022. SwiftUI Architecture — A Complete Guide to the MV Pattern Approach. https://betterprogramming.pub/swiftui-architecture-a-complete-guide-to-mv-pattern-approach-5f411eaaaf9e
[^layer-redraw]: objc.io. AppKit for UIKit Developers. Issue #14. https://www.objc.io/
…(truncated)