swift-macos について
swift-macos は、AppKit、システム連携、配布を扱う macOS ネイティブアプリ開発向けのスキルです。macOS の UI、サンドボックス、署名、リリース手順を扱う開発者を対象としています。
主な機能
- 01AppKit のコンポーネントとウィンドウ種別
- 02メニューバー・ドキュメントアプリの例
- 03ブックマークを使ったサンドボックス内のファイルアクセス
- 04App Store・Developer ID による配布
- 05署名・公証・権限のチェック
活用シーン
- 01macOS ネイティブのウィンドウアプリやメニューアプリの作成
- 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 Developer アカウント(配布用)
- 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 | サンドボックス、App Review |
| 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 対応