关于 swift-macos
swift-macos 是用于开发原生 macOS 应用的技能,涵盖 AppKit、系统集成和分发,面向处理 macOS 界面、沙盒、签名及发布流程的开发者。
主要功能
- 01AppKit 组件与窗口类型
- 02菜单栏和文档应用示例
- 03使用书签实现沙盒文件访问
- 04App Store 与 Developer ID 分发
- 05签名、公证和权限检查
适用场景
- 01构建原生 macOS 窗口或菜单栏应用
- 02添加沙盒文件访问能力
- 03准备应用分发
- 04排查签名或公证问题
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ pluginagentmarketplace/ custom-plugin-swift --skill swift-macos安装后在 Claude Code 中输入/swift-macos确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
assets/.gitkeepAppDelegate.swift.templateconfig.yamlreferences/.gitkeepGUIDE.mdscripts/.gitkeepnotarize.sh详细介绍
Swift macOS 技能
使用 AppKit 进行原生 macOS 应用开发、系统集成和分发。
前置条件
- macOS Sonoma+ 上的 Xcode 15+
- Apple 开发者账号(用于分发)
- 了解 AppKit 基础知识
参数
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
涵盖主题
AppKit 组件
| 组件 | 用途 |
|---|---|
| NSWindow | 窗口管理 |
| NSViewController | 视图控制器 |
| NSView | 基础视图类 |
| NSMenu | 菜单栏和上下文菜单 |
| NSStatusItem | 菜单栏图标 |
| NSAlert | 对话框 |
窗口类型
| 类型 | 使用场景 |
|---|---|
| NSWindow | 标准窗口 |
| NSPanel | 实用工具/浮动窗口 |
| NSPopover | 附属弹出框 |
| NSSavePanel/NSOpenPanel | 文件对话框 |
分发方式
| 方式 | 要求 |
|---|---|
| Mac App Store | 沙盒、应用审核 |
| Developer ID | 公证 |
| 直接分发 | 无(Gatekeeper 会拦截) |
代码示例
菜单栏应用
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()
}
}
基于 NSDocument 的应用
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"
故障排查
常见问题
| 问题 | 原因 | 解决方案 |
|---|---|---|
| “应用已损坏” | 未进行公证 | 分发前进行公证 |
| 沙盒违规 | 缺少授权 | 添加所需授权 |
| 窗口未显示 | 激活策略错误 | 检查 NSApp.activationPolicy |
| 菜单无响应 | 缺少目标/操作 | 验证菜单项连接 |
| Sparkle 更新失败 | 代码签名问题 | 添加例外授权 |
调试提示
# 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- macOS 上的 SwiftUIswift-architecture- 应用架构swift-testing- macOS 应用测试
评论
支持 Markdown