mcpField
S
swift-macos

swift-macos

github.com/pluginagentmarketplace开发848 次安装43 周前更新
代码仓库

关于 swift-macos

swift-macos 是用于开发原生 macOS 应用的技能,涵盖 AppKit、系统集成和分发,面向处理 macOS 界面、沙盒、签名及发布流程的开发者。

主要功能

  1. 01AppKit 组件与窗口类型
  2. 02菜单栏和文档应用示例
  3. 03使用书签实现沙盒文件访问
  4. 04App Store 与 Developer ID 分发
  5. 05签名、公证和权限检查

适用场景

  1. 01构建原生 macOS 窗口或菜单栏应用
  2. 02添加沙盒文件访问能力
  3. 03准备应用分发
  4. 04排查签名或公证问题

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/pluginagentmarketplace/custom-plugin-swift --skill swift-macos

安装后在 Claude Code 中输入/swift-macos确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

assets/
.gitkeep
AppDelegate.swift.template
config.yaml
references/
.gitkeep
GUIDE.md
scripts/
.gitkeep
notarize.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 上的 SwiftUI
  • swift-architecture - 应用架构
  • swift-testing - macOS 应用测试

评论

支持 Markdown