Build macOS applications - AppKit, windows, menus, system integration, distribution
Native macOS application development with AppKit, system integration, and distribution.
parameters:
distribution_method:
type: string
enum: [app_store, developer_id, none]
default: developer_id
sandbox_enabled:
type: boolean
default: true
min_macos_version:
type: string
default: "13.0"
app_type:
type: string
enum: [document_based, single_window, menu_bar, agent]
default: single_window
| Component | Purpose | |-----------|---------| | NSWindow | Window management | | NSViewController | View controller | | NSView | Base view class | | NSMenu | Menu bar and context menus | | NSStatusItem | Menu bar icon | | NSAlert | Dialog boxes |
| Type | Use Case | |------|----------| | NSWindow | Standard window | | NSPanel | Utility/floating window | | NSPopover | Attached popover | | NSSavePanel/NSOpenPanel | File dialogs |
| Method | Requirements | |--------|--------------| | Mac App Store | Sandbox, App Review | | Developer ID | Notarization | | Direct | None (Gatekeeper blocks) |
import AppKit
import SwiftUI
@main
struct MenuBarApp: App {
@NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
var body: some Scene {
Settings {
SettingsView()
}
}
}
final class AppDelegate: NSObject, NSApplicationDelegate {
private var statusItem: NSStatusItem!
private var popover: NSPopover!
func applicationDidFinishLaunching(_ notification: Notification) {
setupStatusItem()
setupPopover()
// Hide dock icon for menu bar only app
NSApp.setActivationPolicy(.accessory)
}
private func setupStatusItem() {
statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.squareLength)
if let button = statusItem.button {
button.image = NSImage(systemSymbolName: "star.fill", accessibilityDescription: "App")
button.action = #selector(togglePopover)
}
}
private func setupPopover() {
popover = NSPopover()
popover.contentSize = NSSize(width: 300, height: 400)
popover.behavior = .transient
popover.contentViewController = NSHostingController(rootView: PopoverView())
}
@objc private func togglePopover() {
guard let button = statusItem.button else { return }
if popover.isShown {
popover.performClose(nil)
} else {
popover.show(relativeTo: button.bounds, of: button, preferredEdge: .minY)
popover.contentViewController?.view.window?.makeKey()
}
}
}
struct PopoverView: View {
@Environment(\.openSettings) private var openSettings
var body: some View {
VStack(spacing: 16) {
Text("Menu Bar App")
.font(.headline)
Button("Settings") {
openSettings()
}
Button("Quit") {
NSApp.terminate(nil)
}
}
.padding()
}
}
import AppKit
final class Document: NSDocument {
var content: String = ""
override class var autosavesInPlace: Bool { true }
override func makeWindowControllers() {
let contentVC = ContentViewController(document: self)
let window = NSWindow(contentViewController: contentVC)
window.setContentSize(NSSize(width: 800, height: 600))
let windowController = NSWindowController(window: window)
addWindowController(windowController)
}
override func data(ofType typeName: String) throws -> Data {
guard let data = content.data(using: .utf8) else {
throw NSError(domain: NSOSStatusErrorDomain, code: writErr)
}
return data
}
override func read(from data: Data, ofType typeName: String) throws {
guard let content = String(data: data, encoding: .utf8) else {
throw NSError(domain: NSOSStatusErrorDomain, code: readErr)
}
self.content = content
}
}
final class ContentViewController: NSViewController {
private let document: Document
init(document: Document) {
self.document = document
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) not implemented")
}
override func loadView() {
let scrollView = NSScrollView()
let textView = NSTextView()
textView.isEditable = true
textView.isRichText = false
textView.font = .monospacedSystemFont(ofSize: 13, weight: .regular)
textView.string = document.content
scrollView.documentView = textView
scrollView.hasVerticalScroller = true
view = scrollView
}
}
final class SandboxedFileAccess {
private static let bookmarksKey = "securityScopedBookmarks"
static func requestAccess(to url: URL) throws -> URL {
let bookmark = try url.bookmarkData(
options: .withSecurityScope,
includingResourceValuesForKeys: nil,
relativeTo: nil
)
var bookmarks = UserDefaults.standard.dictionary(forKey: bookmarksKey) as? [String: Data] ?? [:]
bookmarks[url.path] = bookmark
UserDefaults.standard.set(bookmarks, forKey: bookmarksKey)
return url
}
static func accessBookmarkedURL(_ path: String) throws -> URL {
guard let bookmarks = UserDefaults.standard.dictionary(forKey: bookmarksKey) as? [String: Data],
let bookmarkData = bookmarks[path] else {
throw SandboxError.noBookmark
}
var isStale = false
let url = try URL(
resolvingBookmarkData: bookmarkData,
options: .withSecurityScope,
relativeTo: nil,
bookmarkDataIsStale: &isStale
)
if isStale {
_ = try requestAccess(to: url)
}
return url
}
static func withAccess<T>(to url: URL, perform: (URL) throws -> T) throws -> T {
guard url.startAccessingSecurityScopedResource() else {
throw SandboxError.accessDenied
}
defer { url.stopAccessingSecurityScopedResource() }
return try perform(url)
}
}
enum SandboxError: Error {
case noBookmark
case accessDenied
}
#!/bin/bash
set -e
APP_PATH="$1"
DEVELOPER_ID="Developer ID Application: Your Name (TEAMID)"
KEYCHAIN_PROFILE="notary-profile"
echo "Signing..."
codesign --force --options runtime --sign "$DEVELOPER_ID" \
--deep --strict "$APP_PATH"
echo "Creating ZIP..."
ditto -c -k --keepParent "$APP_PATH" "app.zip"
echo "Submitting for notarization..."
xcrun notarytool submit "app.zip" \
--keychain-profile "$KEYCHAIN_PROFILE" \
--wait
echo "Stapling..."
xcrun stapler staple "$APP_PATH"
echo "Verifying..."
spctl --assess --type execute --verbose=2 "$APP_PATH"
echo "Done!"
rm "app.zip"
| Issue | Cause | Solution | |-------|-------|----------| | "App is damaged" | Not notarized | Notarize before distribution | | Sandbox violation | Missing entitlement | Add required entitlement | | Window not appearing | Wrong activation policy | Check NSApp.activationPolicy | | Menu not responding | Missing target/action | Verify menu item connections | | Sparkle updates fail | Code signing issue | Add exception entitlement |
# Check code signing
codesign -dv --verbose=4 App.app
# Check entitlements
codesign -d --entitlements :- App.app
# Check notarization
spctl --assess --type execute App.app
# View sandbox violations
log show --predicate 'subsystem == "com.apple.sandbox"' --last 1h
validation:
- rule: hardened_runtime
severity: error
check: Enable hardened runtime for notarization
- rule: sandbox_entitlements
severity: warning
check: Only request necessary sandbox entitlements
- rule: code_signing
severity: error
check: Sign with Developer ID for distribution
Skill("swift-macos")
swift-swiftui - SwiftUI on macOSswift-architecture - App architectureswift-testing - Testing macOS appsnpx skills add pluginagentmarketplace/swift-macos下载完整 Skill 目录,包含 SKILL.md 及所有相关文件
Search for places (restaurants, cafes, etc.) via Google Places API proxy on localhost.
Interact with GitHub using the `gh` CLI. Use `gh issue`, `gh pr`, `gh run`, and `gh api` for issues, PRs, CI runs, and advanced queries.
Create or update AgentSkills. Use when designing, structuring, or packaging skills with scripts, references, and assets.
Start voice calls via the OpenClaw voice-call plugin.
Notion API for creating and managing pages, databases, and blocks.
Gemini CLI for one-shot Q&A, summaries, and generation.
Category:developer