steipete-thomas/SKILL.md
Comprehensive patterns for building production-grade Swift macOS applications: SwiftPM-only projects, Swift 6 concurrency, @Observable state management, plugin architectures, menu bar apps, Sparkle auto-updates, code signing/notarization, widgets, Keychain storage, build automation, and testing infrastructure.
npx skillsauth add abanoub-ashraf/manus-skills-import steipete-thomasInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Build production-grade macOS applications using Swift Package Manager (no Xcode project needed). This comprehensive skill covers modern Swift 6 architecture patterns, menu bar apps, plugin systems, build scripts, code signing, notarization, Sparkle auto-updates, and distribution.
Use this skill when:
@MainActor for UI classes and Sendable for data models.version.env, metadata in descriptors.Starting a new macOS app?
references/project-structure.md for recommended layoutreferences/package-swift.md for multi-target setupBuilding a menu bar app?
references/menu-bar-app.md for StatusItem patternsreferences/app-lifecycle.md for AppDelegate setupManaging app state?
references/observable-state.md for @Observable patternsreferences/settings-store.md for UserDefaults + KeychainNeed plugin/provider architecture?
references/plugin-architecture.md for Descriptor + Registry + Strategy patternsWriting concurrent code?
references/swift-concurrency.md for Swift 6 patterns and background refresh loopsBuilding widgets?
references/widget-extension.md for WidgetKit patternsStoring secrets?
references/keychain-storage.md for secure storage patternsSetting up builds/releases?
references/build-scripts.md for shell script templatesreferences/code-quality.md for SwiftLint/SwiftFormatAdding auto-updates?
references/sparkle-updates.md for Sparkle integrationDistributing your app?
references/distribution.md for code signing and notarizationMyApp/
├── Package.swift # SwiftPM manifest (Swift 6.0+)
├── version.env # MARKETING_VERSION and BUILD_NUMBER
├── package.json # npm scripts (start, test, release)
├── appcast.xml # Sparkle update feed
├── Icon.icns # App icon
├── Entitlements.plist # App entitlements
├── CHANGELOG.md
├── Sources/
│ ├── MyAppCore/ # Platform-agnostic (Sendable models, services)
│ │ ├── Models/
│ │ ├── Services/
│ │ ├── Providers/
│ │ ├── Storage/
│ │ └── Utilities/
│ ├── MyApp/ # macOS app (SwiftUI, AppKit, @Observable stores)
│ │ ├── App/
│ │ │ ├── MyAppApp.swift # @main entry point with AppDelegate
│ │ │ └── AppDelegate.swift
│ │ ├── Stores/
│ │ │ ├── UsageStore.swift
│ │ │ └── SettingsStore.swift
│ │ ├── Views/
│ │ │ ├── MenuContentView.swift
│ │ │ ├── SettingsView.swift
│ │ │ └── Components/
│ │ └── Workers/ # Actor-based async workers
│ │ ├── FileWorker.swift
│ │ └── CLIWorker.swift
│ ├── MyAppCLI/ # Optional CLI tool
│ │ └── CLI.swift
│ └── MyAppWidget/ # WidgetKit extension
│ └── Widget.swift
├── Tests/
│ └── MyAppTests/
│ ├── CoreTests/
│ ├── StoreTests/
│ ├── Mocks/
│ └── Fixtures/
└── Scripts/
├── compile_and_run.sh # Dev iteration script
├── package_app.sh # Create .app bundle
├── sign-and-notarize.sh # Release workflow
├── make_appcast.sh # Sparkle feed generation
└── bump-version.sh # Version management
// swift-tools-version: 6.0
import PackageDescription
let package = Package(
name: "MyApp",
platforms: [.macOS(.v14)],
products: [
.executable(name: "MyApp", targets: ["MyApp"]),
.executable(name: "myapp", targets: ["MyAppCLI"]),
],
dependencies: [
.package(url: "https://github.com/apple/swift-log", from: "1.5.0"),
.package(url: "https://github.com/apple/swift-argument-parser", from: "1.3.0"),
.package(url: "https://github.com/gonzalezreal/swift-markdown-ui.git", from: "2.4.1"),
.package(url: "https://github.com/sparkle-project/Sparkle", from: "2.8.1"),
],
targets: [
// Platform-agnostic core library
.target(
name: "MyAppCore",
dependencies: [
.product(name: "Logging", package: "swift-log"),
],
swiftSettings: [
.enableUpcomingFeature("StrictConcurrency"),
]
),
// macOS GUI app
.executableTarget(
name: "MyApp",
dependencies: [
"MyAppCore",
.product(name: "MarkdownUI", package: "swift-markdown-ui"),
.product(name: "Sparkle", package: "Sparkle"),
],
swiftSettings: [
.define("ENABLE_SPARKLE"),
.enableUpcomingFeature("StrictConcurrency"),
]
),
// CLI tool
.executableTarget(
name: "MyAppCLI",
dependencies: [
"MyAppCore",
.product(name: "ArgumentParser", package: "swift-argument-parser"),
],
swiftSettings: [
.enableUpcomingFeature("StrictConcurrency"),
]
),
// Tests
.testTarget(
name: "MyAppTests",
dependencies: ["MyApp", "MyAppCore", "MyAppCLI"],
resources: [.copy("Fixtures")],
swiftSettings: [
.enableUpcomingFeature("StrictConcurrency"),
.enableExperimentalFeature("SwiftTesting"),
]
),
]
)
Use @Observable with @MainActor and enum-based state machines to prevent impossible states:
@MainActor
@Observable final class DocumentStore {
// List state - prevents impossible states (no booleans!)
enum ListState: Equatable {
case idle
case loading
case loaded
case failed(String)
}
// Detail state - includes cache-aware states
enum DetailState: Equatable {
case idle
case loading
case loaded
case cachedRefreshing // Showing cached while fetching fresh
case missing
case failed(String)
}
// State properties
var documents: [Document] = []
var listState: ListState = .idle
var detailState: DetailState = .idle
var selectedDocumentID: Document.ID?
var selectedContent: String = ""
// Internal bookkeeping - doesn't trigger SwiftUI updates
@ObservationIgnored private var cache: [String: Data] = [:]
@ObservationIgnored private var refreshTask: Task<Void, Never>?
@ObservationIgnored private let fileWorker = FileWorker()
// Observation token for fine-grained SwiftUI tracking
var settingsObservationToken: Int {
_ = documents
_ = listState
_ = selectedDocumentID
return 0
}
func loadDocuments() async {
listState = .loading
do {
documents = try await fileWorker.scanDocuments()
.sorted { $0.title.localizedCaseInsensitiveCompare($1.title) == .orderedAscending }
listState = .loaded
} catch {
listState = .failed(error.localizedDescription)
}
}
deinit {
refreshTask?.cancel()
}
}
Why enums instead of booleans?
isLoading + hasError = 4 possible states, but only 3 are validimport CryptoKit
actor FileWorker {
// Nested types must be Sendable
struct ScannedDocument: Sendable {
let id: String
let name: String
let title: String
let folderURL: URL
}
func scanDocuments(at baseURL: URL) throws -> [ScannedDocument] {
let fileManager = FileManager.default
guard fileManager.fileExists(atPath: baseURL.path) else { return [] }
let contents = try fileManager.contentsOfDirectory(
at: baseURL,
includingPropertiesForKeys: [.isDirectoryKey],
options: [.skipsHiddenFiles]
)
return contents.compactMap { url -> ScannedDocument? in
let values = try? url.resourceValues(forKeys: [.isDirectoryKey])
guard values?.isDirectory == true else { return nil }
let name = url.lastPathComponent
return ScannedDocument(
id: name,
name: name,
title: formatTitle(name),
folderURL: url
)
}
}
// Compute SHA256 hash of directory contents (for change detection)
func computeHash(for rootURL: URL) throws -> String {
let fileManager = FileManager.default
var hasher = SHA256()
guard let enumerator = fileManager.enumerator(
at: rootURL,
includingPropertiesForKeys: [.isRegularFileKey],
options: [.skipsHiddenFiles]
) else { return "" }
var files: [URL] = []
for case let fileURL as URL in enumerator {
let path = fileURL.path
if path.contains("/.git/") { continue }
let values = try? fileURL.resourceValues(forKeys: [.isRegularFileKey])
guard values?.isRegularFile == true else { continue }
files.append(fileURL)
}
files.sort { $0.path < $1.path }
for fileURL in files {
guard let data = try? Data(contentsOf: fileURL) else { continue }
hasher.update(data: data)
}
let digest = hasher.finalize()
return digest.map { String(format: "%02x", $0) }.joined()
}
}
Why actors?
struct APIClient: Sendable {
var fetchLatest: @Sendable (_ limit: Int) async throws -> [RemoteDocument]
var search: @Sendable (_ query: String, _ limit: Int) async throws -> [RemoteDocument]
var download: @Sendable (_ id: String) async throws -> URL
}
extension APIClient {
// Shared URLSession with caching
private static let session: URLSession = {
let urlCache = URLCache(
memoryCapacity: 10 * 1024 * 1024, // 10 MB memory
diskCapacity: 50 * 1024 * 1024 // 50 MB disk
)
let config = URLSessionConfiguration.default
config.urlCache = urlCache
return URLSession(configuration: config)
}()
// Production implementation
static func live(baseURL: URL = URL(string: "https://api.example.com")!) -> APIClient {
let session = Self.session
return APIClient(
fetchLatest: { limit in
var components = URLComponents(
url: baseURL.appendingPathComponent("/api/v1/documents"),
resolvingAgainstBaseURL: false
)
components?.queryItems = [
URLQueryItem(name: "limit", value: String(limit)),
]
guard let url = components?.url else { throw URLError(.badURL) }
let (data, response) = try await session.data(from: url)
try validate(response: response)
return try JSONDecoder().decode([RemoteDocument].self, from: data)
},
search: { query, limit in
// Implementation...
[]
},
download: { id in
// Implementation...
URL(fileURLWithPath: "/tmp/download.zip")
}
)
}
// Mock for testing/previews
static var mock: APIClient {
APIClient(
fetchLatest: { _ in [] },
search: { _, _ in [] },
download: { _ in URL(fileURLWithPath: "/tmp/mock.zip") }
)
}
}
Why closure-based DI instead of protocols?
.live(), .mock()) for common variants/// Memory cache with automatic eviction under memory pressure
final class DetailCache: @unchecked Sendable {
static let shared = DetailCache()
private let cache = NSCache<NSString, CacheEntry>()
private final class CacheEntry {
let content: String
let metadata: [String: Any]?
init(content: String, metadata: [String: Any]? = nil) {
self.content = content
self.metadata = metadata
}
}
private init() {
cache.countLimit = 50 // Max 50 entries
}
func get(key: String) -> (content: String, metadata: [String: Any]?)? {
guard let entry = cache.object(forKey: key as NSString) else { return nil }
return (entry.content, entry.metadata)
}
func set(content: String, metadata: [String: Any]? = nil, forKey key: String) {
cache.setObject(CacheEntry(content: content, metadata: metadata), forKey: key as NSString)
}
}
struct SearchableView: View {
@State private var searchText = ""
@State private var searchTask: Task<Void, Never>?
var body: some View {
content
.searchable(text: $searchText, prompt: "Search...")
.onChange(of: searchText) { _, newValue in
// Cancel previous search
searchTask?.cancel()
// Start new debounced search
searchTask = Task {
try? await Task.sleep(for: .milliseconds(300))
guard !Task.isCancelled else { return }
await performSearch(query: newValue)
}
}
}
}
@MainActor
@Observable final class RemoteStore {
var searchResults: [RemoteDocument] = []
var searchState: LoadState = .idle
private let apiClient: APIClient
private var activeSearchToken = 0
func search(query: String, limit: Int = 20) async {
let trimmed = query.trimmingCharacters(in: .whitespacesAndNewlines)
activeSearchToken += 1
let token = activeSearchToken
guard !trimmed.isEmpty else {
searchResults = []
searchState = .idle
return
}
searchState = .loading
do {
let results = try await apiClient.search(trimmed, limit)
// Ignore results if query changed (prevents showing stale results)
guard token == activeSearchToken else { return }
searchResults = results
searchState = .loaded
} catch {
guard token == activeSearchToken else { return }
searchState = .failed(error.localizedDescription)
}
}
}
// Descriptor: Single source of truth for a provider
struct ProviderDescriptor: Sendable {
let id: ProviderID
let metadata: ProviderMetadata
let branding: ProviderBranding
let fetchPlan: ProviderFetchPlan
}
// Registry: Auto-discovery with lazy bootstrap
enum ProviderRegistry {
private static let store = Store()
private static let bootstrap: Void = {
register(GitHubProviderDescriptor.make())
register(SlackProviderDescriptor.make())
}()
static var all: [ProviderDescriptor] {
_ = bootstrap
return store.ordered
}
}
// Strategy: Fallback chain with availability checks
protocol FetchStrategy: Sendable {
var id: String { get }
func isAvailable(_ context: FetchContext) async -> Bool
func fetch(_ context: FetchContext) async throws -> Result
func shouldFallback(on error: Error) -> Bool
}
@MainActor
@Observable
final class SettingsStore {
// UserDefaults-backed with immediate persistence
var refreshInterval: TimeInterval {
didSet { userDefaults.set(refreshInterval, forKey: "refreshInterval") }
}
// Keychain-backed with debounced persistence
var apiToken: String {
didSet { scheduleTokenPersist() }
}
@ObservationIgnored private let userDefaults: UserDefaults
@ObservationIgnored private let tokenStore: any TokenStoring
@ObservationIgnored private var tokenPersistTask: Task<Void, Never>?
@ObservationIgnored private var tokenLoaded = false
// Lazy Keychain loading
func ensureTokenLoaded() {
guard !tokenLoaded else { return }
apiToken = (try? tokenStore.load()) ?? ""
tokenLoaded = true
}
// Debounced Keychain writes (350ms)
private func scheduleTokenPersist() {
guard tokenLoaded else { return }
tokenPersistTask?.cancel()
tokenPersistTask = Task { @MainActor in
try? await Task.sleep(nanoseconds: 350_000_000)
guard !Task.isCancelled else { return }
try? tokenStore.store(apiToken)
}
}
}
@MainActor
final class AppDelegate: NSObject, NSApplicationDelegate {
private var statusItem: NSStatusItem?
private var usageStore: UsageStore?
func applicationDidFinishLaunching(_ notification: Notification) {
// Create status item
statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
// Configure button
if let button = statusItem?.button {
button.image = NSImage(systemSymbolName: "chart.bar", accessibilityDescription: "Usage")
}
// Attach menu
statusItem?.menu = buildMenu()
// Start background refresh
usageStore?.startBackgroundRefresh()
}
}
// SwiftUI App with menu bar presence
@main
struct MyApp: App {
@NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
var body: some Scene {
Settings {
SettingsView()
}
}
}
import SwiftUI
#if canImport(Sparkle) && ENABLE_SPARKLE
import Sparkle
import Security
#endif
@main
struct MyAppApp: App {
@NSApplicationDelegateAdaptor(AppDelegate.self) private var appDelegate
@State private var store = DocumentStore()
var body: some Scene {
WindowGroup("My App") {
MainSplitView()
.environment(store)
}
.commands {
CommandGroup(after: .appInfo) {
Button("Check for Updates...") {
appDelegate.checkForUpdates()
}
.keyboardShortcut("u", modifiers: [.command, .option])
}
}
}
}
@MainActor
final class AppDelegate: NSObject, NSApplicationDelegate {
#if canImport(Sparkle) && ENABLE_SPARKLE
private var updaterController: SPUStandardUpdaterController?
#endif
func applicationDidFinishLaunching(_ notification: Notification) {
NSApplication.shared.setActivationPolicy(.regular)
NSApplication.shared.activate(ignoringOtherApps: true)
#if canImport(Sparkle) && ENABLE_SPARKLE
guard shouldEnableSparkle() else { return }
updaterController = SPUStandardUpdaterController(
startingUpdater: true,
updaterDelegate: nil,
userDriverDelegate: nil
)
#endif
}
func checkForUpdates() {
#if canImport(Sparkle) && ENABLE_SPARKLE
updaterController?.checkForUpdates(nil)
#endif
}
#if canImport(Sparkle) && ENABLE_SPARKLE
private func shouldEnableSparkle() -> Bool {
let bundleURL = Bundle.main.bundleURL
guard bundleURL.pathExtension == "app" else { return false }
let info = Bundle.main.infoDictionary
let feedURL = info?["SUFeedURL"] as? String
let publicKey = info?["SUPublicEDKey"] as? String
return (feedURL?.isEmpty == false) && (publicKey?.isEmpty == false)
}
#endif
}
MARKETING_VERSION=1.0.0
BUILD_NUMBER=1
#!/usr/bin/env bash
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
APP_NAME=${APP_NAME:-MyApp}
# Lock to prevent concurrent builds
LOCK_KEY="$(printf '%s' "${ROOT}" | shasum -a 256 | cut -c1-8)"
LOCK_DIR="${TMPDIR}/myapp-build-${LOCK_KEY}"
acquire_lock() {
while ! mkdir "${LOCK_DIR}" 2>/dev/null; do
if [[ -f "${LOCK_DIR}/pid" ]]; then
existing_pid="$(cat "${LOCK_DIR}/pid")"
if kill -0 "${existing_pid}" 2>/dev/null; then
echo "Another build running (pid ${existing_pid})"
exit 0
fi
fi
rm -rf "${LOCK_DIR}"
done
echo "$$" > "${LOCK_DIR}/pid"
}
trap 'rm -rf "${LOCK_DIR}"' EXIT
acquire_lock
# Kill existing → Build → Package → Launch → Verify
pkill -x "${APP_NAME}" || true
swift build -c debug
"${ROOT}/Scripts/package_app.sh" debug
open "${ROOT}/${APP_NAME}.app"
sleep 1 && pgrep -x "${APP_NAME}" || exit 1
#!/usr/bin/env bash
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
source "$ROOT/version.env"
APP_NAME=${APP_NAME:-MyApp}
APP_IDENTITY=${APP_IDENTITY:?APP_IDENTITY required}
NOTARY_KEY=${NOTARY_KEY:?NOTARY_KEY required}
NOTARY_KEY_ID=${NOTARY_KEY_ID:?NOTARY_KEY_ID required}
NOTARY_ISSUER=${NOTARY_ISSUER:?NOTARY_ISSUER required}
# Build universal binary
ARCHES="arm64 x86_64" "$ROOT/Scripts/package_app.sh" release
# Notarize
xcrun notarytool submit /tmp/notarize.zip \
--key "$NOTARY_KEY" \
--key-id "$NOTARY_KEY_ID" \
--issuer "$NOTARY_ISSUER" \
--wait
# Staple
xcrun stapler staple "${ROOT}/${APP_NAME}.app"
echo "Done!"
Load these for detailed patterns:
project-structure.md - Directory layout, module boundaries, target organizationpackage-swift.md - Multi-target Package.swift templates, conditional compilationswift-concurrency.md - @MainActor, Sendable, Task.detached, cancellationobservable-state.md - @Observable patterns, @ObservationIgnored, observation tokenssettings-store.md - UserDefaults + Keychain, debounced persistence, lazy loadingplugin-architecture.md - Descriptor + Registry + Strategy patternsmenu-bar-app.md - NSStatusItem, menus, app lifecycleapp-lifecycle.md - AppDelegate, system events, termination handlingwidget-extension.md - WidgetKit integration, shared datakeychain-storage.md - Secure storage protocols, error handlingbuild-scripts.md - Dev loop, packaging, release automation, lockingsparkle-updates.md - Sparkle integration, appcast generation, Ed25519 keysdistribution.md - Code signing, notarization, direct distributioncode-quality.md - SwiftLint, SwiftFormat, npm scriptstesting-patterns.md - Swift Testing, mocks, async testing, fixturesSendable@Observable instead of ObservableObject@MainActor on all UI state classesactor for I/O workersdefer for cleanup in file operationslocalizedCaseInsensitiveCompare for sorting.live(), .mock())version.env for version trackingset -euo pipefail@MainActor @Observable final class@ObservationIgnored for caches, tasks, loggersdeinit| Issue | Solution |
|-------|----------|
| "App is damaged" | Run xcrun stapler staple MyApp.app |
| "Developer cannot be verified" | Check notarization completed successfully |
| Sparkle not working | Verify SUFeedURL and SUPublicEDKey in Info.plist |
| Sparkle not starting | Check shouldEnableSparkle() - needs signed .app bundle |
| Search results stale | Implement token-based cancellation |
| Memory pressure | Use NSCache with countLimit |
| Race conditions | Use actors, check task cancellation |
| Build fails on Swift 6 | Add Sendable conformance to all data types |
| App won't activate | Add NSApplication.shared.activate(ignoringOtherApps: true) |
| Concurrent build issues | Use lock directory pattern in scripts |
Note: Patterns extracted from production-grade Swift macOS applications by @steipete and @thomas.
development
Design principles for building polished, native-feeling SwiftUI apps and widgets. Use this skill when creating or modifying SwiftUI views, iOS widgets (WidgetKit), or any native Apple UI. Ensures proper spacing, typography, colors, and widget implementations that look and feel like quality apps rather than AI-generated slop.
data-ai
Design and implement SwiftUI views, components, and app architecture. Use when creating new SwiftUI views, implementing MVVM/TCA patterns, managing state with @Observable, @State, @Binding, or @Environment, designing navigation flows, or structuring iOS app architecture. Triggers on SwiftUI, view model, state management, navigation, coordinator pattern.
development
Implement, review, or improve SwiftUI animations and transitions. Use when adding implicit or explicit animations with withAnimation, configuring spring animations (.smooth, .snappy, .bouncy), building phase or keyframe animations with PhaseAnimator/KeyframeAnimator, creating hero transitions with matchedGeometryEffect or matchedTransitionSource, adding SF Symbol effects (bounce, pulse, variableColor, breathe, rotate, wiggle), implementing custom Transition or CustomAnimation types, or ensuring animations respect accessibilityReduceMotion.
testing
Audit SwiftUI views for accessibility (iOS + macOS) with patch-ready fixes