Awesome-omni-skill swift-standards

MANDATORY for ALL Swift output - files AND conversational snippets. Covers SPM, strict concurrency, Result types, enums with associated values, let over var, async/await, SwiftUI. Trigger: any Swift code, iOS, macOS, Package.swift, Xcode projects. No exceptions.

install
source · Clone the upstream repo
git clone https://github.com/diegosouzapw/awesome-omni-skill
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/diegosouzapw/awesome-omni-skill "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/development/swift-standards" ~/.claude/skills/diegosouzapw-awesome-omni-skill-swift-standards && rm -rf "$T"
manifest: skills/development/swift-standards/SKILL.md
source content

Swift Best Practices

When to Use This Skill

This skill should be triggered when:

  • Writing or reviewing Swift code
  • Setting up Swift packages or Xcode projects
  • Working with iOS, macOS, watchOS, tvOS, or visionOS
  • Discussing Swift patterns, concurrency, or architecture
  • Configuring Package.swift or build settings

Core Capabilities

  1. Package Management: Swift Package Manager (SPM) exclusively
  2. Type Safety: Strict concurrency, avoid Any, leverage generics
  3. Error Handling: Typed throws (Swift 6), Result types
  4. State Modeling: Enums with associated values for impossible states
  5. Concurrency: async/await, actors, structured concurrency

Package Management with SPM

Why SPM

  • Built into Swift toolchain and Xcode
  • No external dependencies (unlike CocoaPods, Carthage)
  • First-class support for Swift concurrency
  • Better security (no arbitrary scripts)

Package.swift Structure

// swift-tools-version: 6.0
import PackageDescription

let package = Package(
    name: "MyApp",
    platforms: [
        .iOS(.v17),
        .macOS(.v14)
    ],
    products: [
        .library(name: "Core", targets: ["Core"]),
        .executable(name: "cli", targets: ["CLI"])
    ],
    dependencies: [
        .package(url: "https://github.com/apple/swift-argument-parser", from: "1.3.0"),
        .package(url: "https://github.com/pointfreeco/swift-dependencies", from: "1.0.0")
    ],
    targets: [
        .target(
            name: "Core",
            dependencies: [
                .product(name: "Dependencies", package: "swift-dependencies")
            ]
        ),
        .target(
            name: "AppUI",
            dependencies: ["Core"]
        ),
        .executableTarget(
            name: "CLI",
            dependencies: [
                "Core",
                .product(name: "ArgumentParser", package: "swift-argument-parser")
            ]
        ),
        .testTarget(
            name: "CoreTests",
            dependencies: ["Core"]
        )
    ]
)

Common Commands

# Create new package
swift package init --type library
swift package init --type executable

# Build
swift build

# Run tests
swift test

# Run executable
swift run cli

# Update dependencies
swift package update

# Generate Xcode project (if needed)
swift package generate-xcodeproj

Compiler Settings

Strict Concurrency (Swift 6)

Enable strict concurrency checking in Package.swift:

.target(
    name: "Core",
    dependencies: [],
    swiftSettings: [
        .enableExperimentalFeature("StrictConcurrency")
    ]
)

Or in Xcode: Build Settings → Swift Compiler → Strict Concurrency Checking = Complete

Treat Warnings as Errors

swiftSettings: [
    .unsafeFlags(["-warnings-as-errors"])
]

Immutability by Default

let Over var

// BAD
var name = "Kevin"
var count = 0

// GOOD - use let unless mutation is required
let name = "Kevin"
var count = 0  // Only if actually mutated later

Structs Over Classes

// BAD - using class for simple data
class User {
    var id: UUID
    var name: String

    init(id: UUID, name: String) {
        self.id = id
        self.name = name
    }
}

// GOOD - struct for value types
struct User {
    let id: UUID
    let name: String
}

Use classes only when:

  • You need reference semantics (shared mutable state)
  • You need inheritance
  • You need deinit
  • You're interfacing with Objective-C

Optionals

Never Force Unwrap

// BAD - crashes if nil
let name = user.name!
let first = array.first!

// GOOD - guard let for early exit
guard let name = user.name else {
    return
}

// GOOD - if let for conditional
if let first = array.first {
    process(first)
}

// GOOD - nil coalescing
let name = user.name ?? "Unknown"

// GOOD - optional chaining
let count = user.orders?.count ?? 0

Avoid Implicitly Unwrapped Optionals

// BAD
var delegate: MyDelegate!

// GOOD - make it explicit optional or non-optional
var delegate: MyDelegate?
// or
let delegate: MyDelegate  // Set in init

Exception:

@IBOutlet
in UIKit (required by Interface Builder)

Enums with Associated Values

Swift enums are powerful discriminated unions. Use them to make invalid states unrepresentable:

State Modeling

// BAD - bag of optionals
struct LoadingState {
    var isLoading: Bool
    var data: Data?
    var error: Error?
}

// GOOD - impossible states are impossible
enum LoadingState<T> {
    case idle
    case loading
    case success(T)
    case failure(Error)
}

// Usage
func render(state: LoadingState<User>) {
    switch state {
    case .idle:
        showPlaceholder()
    case .loading:
        showSpinner()
    case .success(let user):
        showUser(user)
    case .failure(let error):
        showError(error)
    }
}

Events

enum UserEvent {
    case created(User)
    case updated(User, changes: [String: Any])
    case deleted(id: UUID)
}

func handle(event: UserEvent) {
    switch event {
    case .created(let user):
        notifyCreation(user)
    case .updated(let user, let changes):
        notifyUpdate(user, changes: changes)
    case .deleted(let id):
        notifyDeletion(id)
    }
}

Exhaustive Switch

Always handle all cases - compiler enforces this:

// Compiler error if you miss a case
switch state {
case .idle: break
case .loading: break
case .success: break
// Missing .failure - won't compile
}

Error Handling

Typed Throws (Swift 6)

// Define specific error types
enum ValidationError: Error {
    case emptyName
    case invalidEmail(String)
    case ageTooLow(minimum: Int)
}

// Typed throws - callers know exactly what can fail
func validate(user: UserInput) throws(ValidationError) -> User {
    guard !user.name.isEmpty else {
        throw .emptyName
    }
    guard user.email.contains("@") else {
        throw .invalidEmail(user.email)
    }
    guard user.age >= 18 else {
        throw .ageTooLow(minimum: 18)
    }
    return User(name: user.name, email: user.email, age: user.age)
}

// Caller gets typed error
do {
    let user = try validate(user: input)
} catch {
    // error is ValidationError, not any Error
    switch error {
    case .emptyName:
        showNameError()
    case .invalidEmail(let email):
        showEmailError(email)
    case .ageTooLow(let minimum):
        showAgeError(minimum)
    }
}

Result Type

For async operations or when you want to pass errors as values:

func fetchUser(id: UUID) async -> Result<User, NetworkError> {
    do {
        let data = try await network.get("/users/\(id)")
        let user = try decoder.decode(User.self, from: data)
        return .success(user)
    } catch let error as NetworkError {
        return .failure(error)
    } catch {
        return .failure(.unknown(error))
    }
}

// Usage
let result = await fetchUser(id: userId)
switch result {
case .success(let user):
    display(user)
case .failure(let error):
    handleError(error)
}

Concurrency

async/await Over Callbacks

// BAD - callback hell
func fetchUser(id: UUID, completion: @escaping (Result<User, Error>) -> Void) {
    network.get("/users/\(id)") { result in
        switch result {
        case .success(let data):
            do {
                let user = try decoder.decode(User.self, from: data)
                completion(.success(user))
            } catch {
                completion(.failure(error))
            }
        case .failure(let error):
            completion(.failure(error))
        }
    }
}

// GOOD - async/await
func fetchUser(id: UUID) async throws -> User {
    let data = try await network.get("/users/\(id)")
    return try decoder.decode(User.self, from: data)
}

Actors for Shared Mutable State

// BAD - manual locking
class Counter {
    private var value = 0
    private let lock = NSLock()

    func increment() {
        lock.lock()
        value += 1
        lock.unlock()
    }
}

// GOOD - actor handles synchronization
actor Counter {
    private var value = 0

    func increment() {
        value += 1
    }

    func getValue() -> Int {
        value
    }
}

// Usage
let counter = Counter()
await counter.increment()
let value = await counter.getValue()

Task Groups for Parallel Work

func fetchAllUsers(ids: [UUID]) async throws -> [User] {
    try await withThrowingTaskGroup(of: User.self) { group in
        for id in ids {
            group.addTask {
                try await fetchUser(id: id)
            }
        }

        var users: [User] = []
        for try await user in group {
            users.append(user)
        }
        return users
    }
}

Type Safety

Avoid Any

// BAD
func process(data: Any) {
    if let string = data as? String {
        // ...
    }
}

// GOOD - use generics
func process<T: Processable>(data: T) {
    data.process()
}

// GOOD - use protocols
func process(data: some Processable) {
    data.process()
}

Use Generics

// BAD - separate implementations
func firstString(in array: [String]) -> String? { array.first }
func firstInt(in array: [Int]) -> Int? { array.first }

// GOOD - generic
func first<T>(in array: [T]) -> T? {
    array.first
}

Phantom Types for Type Safety

// Prevent mixing IDs of different entity types
struct ID<Entity>: Hashable {
    let rawValue: UUID
}

struct User {
    let id: ID<User>
    let name: String
}

struct Order {
    let id: ID<Order>
    let userId: ID<User>
}

// Compiler prevents: order.id == user.id (different types)

Project Structure

Shared Core for Multi-Target

MyApp/
├── Package.swift
├── Sources/
│   ├── Core/               # Shared business logic
│   │   ├── Models/
│   │   │   └── User.swift
│   │   ├── Services/
│   │   │   └── UserService.swift
│   │   └── Database/
│   │       └── Repository.swift
│   ├── AppUI/              # SwiftUI views import Core
│   │   ├── App.swift
│   │   └── Views/
│   │       └── UserView.swift
│   └── CLI/                # ArgumentParser commands import Core
│       └── Main.swift
└── Tests/
    └── CoreTests/
        └── UserServiceTests.swift

Example Core Module

// Sources/Core/Services/UserService.swift
public struct UserService {
    private let repository: UserRepository

    public init(repository: UserRepository) {
        self.repository = repository
    }

    public func createUser(name: String, email: String) async throws -> User {
        let user = User(id: ID(rawValue: UUID()), name: name, email: email)
        try await repository.save(user)
        return user
    }
}

Example SwiftUI Using Core

// Sources/AppUI/Views/UserView.swift
import SwiftUI
import Core

struct UserView: View {
    let userService: UserService
    @State private var state: LoadingState<User> = .idle

    var body: some View {
        switch state {
        case .idle:
            Button("Load") { Task { await load() } }
        case .loading:
            ProgressView()
        case .success(let user):
            Text(user.name)
        case .failure(let error):
            Text(error.localizedDescription)
        }
    }

    private func load() async {
        state = .loading
        do {
            let user = try await userService.fetchUser()
            state = .success(user)
        } catch {
            state = .failure(error)
        }
    }
}

Example CLI Using Core

// Sources/CLI/Main.swift
import ArgumentParser
import Core

@main
struct CLI: AsyncParsableCommand {
    static let configuration = CommandConfiguration(
        commandName: "myapp",
        subcommands: [CreateUser.self]
    )
}

struct CreateUser: AsyncParsableCommand {
    @Argument var name: String
    @Argument var email: String

    func run() async throws {
        let service = UserService(repository: .live)
        let user = try await service.createUser(name: name, email: email)
        print("Created user: \(user.id.rawValue)")
    }
}

SwiftUI Patterns

View as Function of State

struct ContentView: View {
    @State private var count = 0

    var body: some View {
        VStack {
            Text("Count: \(count)")
            Button("Increment") {
                count += 1
            }
        }
    }
}

Extract Subviews

// BAD - massive view
struct UserProfileView: View {
    var body: some View {
        VStack {
            // 200 lines of UI code
        }
    }
}

// GOOD - composed from smaller views
struct UserProfileView: View {
    let user: User

    var body: some View {
        VStack {
            AvatarView(url: user.avatarURL)
            UserInfoSection(user: user)
            UserStatsSection(stats: user.stats)
        }
    }
}

Dependency Injection with Environment

// Define dependency
struct UserServiceKey: EnvironmentKey {
    static let defaultValue: UserService = .live
}

extension EnvironmentValues {
    var userService: UserService {
        get { self[UserServiceKey.self] }
        set { self[UserServiceKey.self] = newValue }
    }
}

// Use in view
struct UserView: View {
    @Environment(\.userService) var userService

    var body: some View {
        // ...
    }
}

// Inject for testing
UserView()
    .environment(\.userService, .mock)

macOS Scripting

When to Use Swift for Scripts

Use Swift when:

  • You need macOS APIs (Keychain, Accessibility, FSEvents, XPC, IOKit)
  • Performance matters (image processing, large file ops)
  • You want to distribute a binary without dependencies

Use Python/shell when:

  • Quick automation, text processing
  • Cross-platform needed
  • Rapid iteration more important than type safety

Script Execution Modes

# Interpreted - slow startup (~500ms cold)
#!/usr/bin/swift
import Foundation
print(FileManager.default.currentDirectoryPath)

# Compiled - fast, but requires build step
swift build -c release
.build/release/my-script

# For scripts with dependencies, use swift-sh
brew install swift-sh

swift-sh for Dependencies

#!/usr/bin/swift sh
import ArgumentParser  // @apple/swift-argument-parser ~> 1.3
import Rainbow         // @onevcat/Rainbow

@main
struct MyScript: ParsableCommand {
    @Argument var name: String

    func run() {
        print("Hello, \(name)".green)
    }
}

Run directly:

./my-script.swift Kevin

Common macOS APIs

FileManager - File Operations

import Foundation

let fm = FileManager.default
let home = fm.homeDirectoryForCurrentUser

// List directory
let contents = try fm.contentsOfDirectory(at: home, includingPropertiesForKeys: nil)

// Check existence
if fm.fileExists(atPath: "/tmp/file.txt") { }

// Create directory
try fm.createDirectory(at: home.appendingPathComponent("Scripts"),
                       withIntermediateDirectories: true)

// Copy/move/delete
try fm.copyItem(at: source, to: destination)
try fm.moveItem(at: source, to: destination)
try fm.removeItem(at: path)

// Attributes
let attrs = try fm.attributesOfItem(atPath: path)
let size = attrs[.size] as? UInt64

Process - Run Shell Commands

import Foundation

func shell(_ command: String) throws -> String {
    let process = Process()
    let pipe = Pipe()

    process.standardOutput = pipe
    process.standardError = pipe
    process.executableURL = URL(fileURLWithPath: "/bin/zsh")
    process.arguments = ["-c", command]

    try process.run()
    process.waitUntilExit()

    let data = pipe.fileHandleForReading.readDataToEndOfFile()
    return String(data: data, encoding: .utf8) ?? ""
}

// Usage
let output = try shell("ls -la")
let gitStatus = try shell("git status --porcelain")

Async Process Execution

func shellAsync(_ command: String) async throws -> String {
    try await withCheckedThrowingContinuation { continuation in
        let process = Process()
        let pipe = Pipe()

        process.standardOutput = pipe
        process.standardError = pipe
        process.executableURL = URL(fileURLWithPath: "/bin/zsh")
        process.arguments = ["-c", command]

        process.terminationHandler = { _ in
            let data = pipe.fileHandleForReading.readDataToEndOfFile()
            let output = String(data: data, encoding: .utf8) ?? ""
            continuation.resume(returning: output)
        }

        do {
            try process.run()
        } catch {
            continuation.resume(throwing: error)
        }
    }
}

Keychain Access

import Security

func getKeychainPassword(service: String, account: String) -> String? {
    let query: [String: Any] = [
        kSecClass as String: kSecClassGenericPassword,
        kSecAttrService as String: service,
        kSecAttrAccount as String: account,
        kSecReturnData as String: true
    ]

    var result: AnyObject?
    let status = SecItemCopyMatching(query as CFDictionary, &result)

    guard status == errSecSuccess,
          let data = result as? Data,
          let password = String(data: data, encoding: .utf8) else {
        return nil
    }
    return password
}

func setKeychainPassword(service: String, account: String, password: String) throws {
    let data = password.data(using: .utf8)!

    let query: [String: Any] = [
        kSecClass as String: kSecClassGenericPassword,
        kSecAttrService as String: service,
        kSecAttrAccount as String: account,
        kSecValueData as String: data
    ]

    // Delete existing
    SecItemDelete(query as CFDictionary)

    // Add new
    let status = SecItemAdd(query as CFDictionary, nil)
    guard status == errSecSuccess else {
        throw KeychainError.unableToStore
    }
}

NSWorkspace - App Control

import AppKit

let workspace = NSWorkspace.shared

// Open file with default app
workspace.open(URL(fileURLWithPath: "/path/to/file.pdf"))

// Open with specific app
workspace.open([URL(fileURLWithPath: "/path/to/file.txt")],
               withApplicationAt: URL(fileURLWithPath: "/Applications/Sublime Text.app"),
               configuration: .init())

// Launch app
workspace.launchApplication("Safari")

// Get running apps
let runningApps = workspace.runningApplications
for app in runningApps where app.isActive {
    print(app.localizedName ?? "Unknown")
}

// Activate app
if let app = runningApps.first(where: { $0.bundleIdentifier == "com.apple.Safari" }) {
    app.activate()
}

// Get frontmost app
if let frontmost = workspace.frontmostApplication {
    print(frontmost.localizedName ?? "")
}

FSEvents - File Watching

import Foundation

class FileWatcher {
    private var stream: FSEventStreamRef?

    func watch(paths: [String], callback: @escaping ([String]) -> Void) {
        var context = FSEventStreamContext()
        context.info = Unmanaged.passUnretained(self).toOpaque()

        let flags = UInt32(kFSEventStreamCreateFlagFileEvents | kFSEventStreamCreateFlagUseCFTypes)

        stream = FSEventStreamCreate(
            nil,
            { _, _, numEvents, eventPaths, _, _ in
                guard let paths = unsafeBitCast(eventPaths, to: NSArray.self) as? [String] else { return }
                // Call back on main thread
                DispatchQueue.main.async {
                    callback(paths)
                }
            },
            &context,
            paths as CFArray,
            FSEventStreamEventId(kFSEventStreamEventIdSinceNow),
            1.0,  // Latency in seconds
            flags
        )

        FSEventStreamScheduleWithRunLoop(stream!, CFRunLoopGetMain(), CFRunLoopMode.defaultMode.rawValue)
        FSEventStreamStart(stream!)
    }

    func stop() {
        if let stream = stream {
            FSEventStreamStop(stream)
            FSEventStreamInvalidate(stream)
            FSEventStreamRelease(stream)
        }
    }
}

// Usage
let watcher = FileWatcher()
watcher.watch(paths: ["~/Downloads"]) { changedPaths in
    for path in changedPaths {
        print("Changed: \(path)")
    }
}
RunLoop.main.run()  // Keep script alive

Accessibility - UI Automation

import ApplicationServices

// Check accessibility permissions
func checkAccessibilityPermissions() -> Bool {
    let options = [kAXTrustedCheckOptionPrompt.takeUnretainedValue() as String: true]
    return AXIsProcessTrustedWithOptions(options as CFDictionary)
}

// Get focused element
func getFocusedElement() -> AXUIElement? {
    let systemWide = AXUIElementCreateSystemWide()
    var focusedElement: CFTypeRef?
    let result = AXUIElementCopyAttributeValue(systemWide, kAXFocusedUIElementAttribute as CFString, &focusedElement)
    guard result == .success else { return nil }
    return (focusedElement as! AXUIElement)
}

// Click menu item
func clickMenuItem(app: String, menu: String, item: String) {
    guard let runningApp = NSWorkspace.shared.runningApplications.first(where: {
        $0.localizedName == app
    }) else { return }

    let appElement = AXUIElementCreateApplication(runningApp.processIdentifier)
    // Navigate menu bar → menu → item and perform press action
    // ... (implementation depends on specific needs)
}

UserDefaults for Script Config

import Foundation

// Read/write to app defaults
let defaults = UserDefaults.standard
defaults.set("value", forKey: "myScriptSetting")
let setting = defaults.string(forKey: "myScriptSetting")

// Read other app's defaults (sandboxing permitting)
if let safariDefaults = UserDefaults(suiteName: "com.apple.Safari") {
    let homepage = safariDefaults.string(forKey: "HomePage")
}

Script Project Structure

For non-trivial scripts, use a proper SPM package:

my-script/
├── Package.swift
├── Sources/
│   └── my-script/
│       ├── main.swift       # Entry point
│       ├── Commands/        # ArgumentParser commands
│       └── Utilities/       # Shared helpers
└── scripts/
    └── install.sh           # Copy binary to ~/bin
// Package.swift
// swift-tools-version: 6.0
import PackageDescription

let package = Package(
    name: "my-script",
    platforms: [.macOS(.v14)],
    dependencies: [
        .package(url: "https://github.com/apple/swift-argument-parser", from: "1.3.0"),
        .package(url: "https://github.com/onevcat/Rainbow", from: "4.0.0")
    ],
    targets: [
        .executableTarget(
            name: "my-script",
            dependencies: [
                .product(name: "ArgumentParser", package: "swift-argument-parser"),
                "Rainbow"
            ]
        )
    ]
)

Install Script

#!/bin/bash
swift build -c release
cp .build/release/my-script ~/bin/

CLI Output Libraries

PurposePackage
ColorsRainbow
Argument parsingswift-argument-parser
Progress/spinnersNo good Swift option - use print-based

Note: Swift CLI ecosystem is thinner than Python's. For complex TUI, consider whether Python (Rich, tqdm) is more practical.

LLM-Friendly Output

All CLIs must support both human and machine consumption:

import ArgumentParser
import Foundation
import Rainbow

struct User: Codable {
    let id: String
    let name: String
    let email: String
}

struct ListUsers: AsyncParsableCommand {
    static let configuration = CommandConfiguration(
        abstract: "List all users.",
        discussion: """
            Returns array of user objects with id, name, and email fields.
            Use --json for structured output suitable for piping to other tools or LLMs.
            """
    )

    @Flag(name: .long, help: "Output as JSON for programmatic consumption")
    var json = false

    @Option(name: .shortAndLong, help: "Maximum number of users to return")
    var limit: Int = 50

    func run() async throws {
        let users = try await getUsers(limit: limit)

        if json {
            // Machine-readable: structured, no formatting
            let encoder = JSONEncoder()
            encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
            let data = try encoder.encode(users)
            print(String(data: data, encoding: .utf8)!)
        } else {
            // Human-readable: colors, pleasant
            print("\nUsers (\(users.count)):".bold)
            for user in users {
                print("  \(user.name.cyan) <\(user.email)>")
            }
            print()
        }
    }
}

Rules:

  1. --json
    flag on every command that outputs data
  2. JSON output: Codable structs,
    prettyPrinted
    , no ANSI
  3. Default output: human-readable with Rainbow colors
  4. Use
    discussion
    in CommandConfiguration to explain what the command returns

Cocoa Framework

Always use Cocoa (

import Cocoa
) for macOS applications. Cocoa provides the full macOS application stack (AppKit, Foundation, CoreData) in a single import. For iOS, use UIKit/SwiftUI as appropriate.

// BAD - piecemeal imports for macOS
import Foundation
import AppKit
import CoreGraphics

// GOOD - single Cocoa import covers all macOS frameworks
import Cocoa

When to use Cocoa vs individual imports:

  • macOS apps and scripts →
    import Cocoa
  • Cross-platform packages (iOS + macOS) →
    import Foundation
    + platform-specific imports
  • Pure logic modules with no UI →
    import Foundation

Self-Consuming Logging

Always create structured, self-consuming logging patterns for troubleshooting during development. Logs must be useful enough that you can diagnose issues by reading them alone — no debugger required.

OSLog (Preferred)

import os
import Cocoa

/// Centralized log categories per subsystem.
/// Each module gets its own category for filtered tailing.
enum Log {
    /// Subsystem matches bundle identifier for os_log filtering
    private static let subsystem = Bundle.main.bundleIdentifier ?? "com.app.dev"

    static let network = Logger(subsystem: subsystem, category: "network")
    static let database = Logger(subsystem: subsystem, category: "database")
    static let ui = Logger(subsystem: subsystem, category: "ui")
    static let lifecycle = Logger(subsystem: subsystem, category: "lifecycle")
    static let auth = Logger(subsystem: subsystem, category: "auth")
}

// Usage - rich context at every call site
func fetchUser(id: UUID) async throws -> User {
    Log.network.info("⬆️ fetchUser started — id=\(id.uuidString, privacy: .public)")
    let start = ContinuousClock.now

    do {
        let user = try await api.get("/users/\(id)")
        let elapsed = ContinuousClock.now - start
        Log.network.info("⬇️ fetchUser success — id=\(id.uuidString, privacy: .public) elapsed=\(elapsed)")
        return user
    } catch {
        let elapsed = ContinuousClock.now - start
        Log.network.error("❌ fetchUser failed — id=\(id.uuidString, privacy: .public) elapsed=\(elapsed) error=\(error.localizedDescription, privacy: .public)")
        throw error
    }
}

// State transitions are always logged
func handleStateChange(from old: AppState, to new: AppState) {
    Log.lifecycle.notice("🔄 state transition — from=\(String(describing: old), privacy: .public) to=\(String(describing: new), privacy: .public)")
}

Logging Rules

  1. Every public function logs entry and exit — include parameters and elapsed time
  2. Errors always log full context — what was attempted, with what inputs, what failed
  3. State transitions are always logged — old state → new state
  4. Use emoji prefixes for visual scanning: ⬆️ request, ⬇️ response, ❌ error, 🔄 transition, ✅ success, ⚠️ warning
  5. Include timing
    ContinuousClock
    for elapsed durations on any I/O or async operation
  6. Privacy-aware — use
    .public
    only for non-sensitive data; defaults to redacted in release

File Logging for CLI Tools

For command-line tools where OSLog isn't practical, write to a known log file:

import Foundation

/// File-based logger for CLI tools.
/// Writes structured log lines to ~/.local/log/<tool>.log
actor FileLog {
    static let shared = FileLog()

    private let logFile: URL
    private let dateFormatter: ISO8601DateFormatter = {
        let f = ISO8601DateFormatter()
        f.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
        return f
    }()

    private init() {
        let logDir = FileManager.default.homeDirectoryForCurrentUser
            .appendingPathComponent(".local/log")
        try? FileManager.default.createDirectory(at: logDir, withIntermediateDirectories: true)

        let processName = ProcessInfo.processInfo.processName
        logFile = logDir.appendingPathComponent("\(processName).log")
    }

    /// Append a structured log line.
    /// - Parameters:
    ///   - level: Log severity (debug, info, warn, error)
    ///   - category: Subsystem category for filtering
    ///   - message: Human-readable log message
    func log(_ level: String, category: String, _ message: String) {
        let timestamp = dateFormatter.string(from: Date())
        let line = "\(timestamp) [\(level.uppercased())] [\(category)] \(message)\n"

        // Also print to stderr so it doesn't pollute stdout (pipe-friendly)
        FileHandle.standardError.write(Data(line.utf8))

        // Append to file for tailing
        if let handle = try? FileHandle(forWritingTo: logFile) {
            handle.seekToEndOfFile()
            handle.write(Data(line.utf8))
            handle.closeFile()
        } else {
            try? line.data(using: .utf8)?.write(to: logFile)
        }
    }
}

// Usage
await FileLog.shared.log("info", category: "network", "⬆️ fetchUser id=\(id)")

Always Tail Logs During Development

When developing or debugging Swift code, always run a log tail in a background terminal. This is non-negotiable — logs are useless if nobody is watching them.

Tailing OSLog (macOS apps)

# Tail all logs from your app's subsystem
log stream --predicate 'subsystem == "com.yourapp.dev"' --level debug

# Filter to specific category
log stream --predicate 'subsystem == "com.yourapp.dev" AND category == "network"' --level debug

# With timestamps and process info
log stream --predicate 'subsystem == "com.yourapp.dev"' --level debug --style compact

Tailing File Logs (CLI tools)

# Tail the log file for a CLI tool
tail -f ~/.local/log/my-tool.log

# Tail with grep for specific category
tail -f ~/.local/log/my-tool.log | grep '\[NETWORK\]'

# Tail all tool logs
tail -f ~/.local/log/*.log

Development Workflow

  1. Start log tail first — before running the app or script
  2. Use
    bg_bash
    to run the tail in the background when working in pi:
    bg_bash: log stream --predicate 'subsystem == "com.yourapp.dev"' --level debug
    
  3. Check
    task_output
    periodically to review log output
  4. Filter aggressively — use category predicates to reduce noise
  5. Never ship without reviewing logs — if the log tail shows unexpected entries, investigate before committing

Quick Reference

ToolPurpose
SPMPackage management (not CocoaPods, Carthage)
swift buildCompile
swift testRun tests
swift runExecute
PatternPreference
Mutability
let
over
var
Value types
struct
over
class
Optionals
guard let
,
if let
,
??
(never
!
)
State modelingEnums with associated values
Error handlingTyped throws (Swift 6), Result
Concurrencyasync/await, actors (not callbacks)
Type safetyGenerics, protocols (avoid
Any
)
ArchitectureShared Core for multi-target
macOS imports
import Cocoa
(not piecemeal)
LoggingOSLog with categories (apps), FileLog (CLI)
Log tailingAlways run
log stream
or
tail -f
during dev

Notes

  • Swift 6 requires strict concurrency - design for it from the start
  • Enums with associated values are Swift's killer feature for state modeling
  • SPM is the only package manager worth using in 2024+
  • Prefer value types (struct, enum) over reference types (class)
  • Use actors instead of manual locking for shared state
  • SwiftUI is declarative - views should be pure functions of state