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.
SKILL.md
| Name | swift-standards |
| Description | 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. |
name: swift-standards description: "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."
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
- Package Management: Swift Package Manager (SPM) exclusively
- Type Safety: Strict concurrency, avoid Any, leverage generics
- Error Handling: Typed throws (Swift 6), Result types
- State Modeling: Enums with associated values for impossible states
- 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
| Purpose | Package |
|---|---|
| Colors | Rainbow |
| Argument parsing | swift-argument-parser |
| Progress/spinners | No 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:
--jsonflag on every command that outputs data- JSON output: Codable structs,
prettyPrinted, no ANSI - Default output: human-readable with Rainbow colors
- Use
discussionin 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
- Every public function logs entry and exit — include parameters and elapsed time
- Errors always log full context — what was attempted, with what inputs, what failed
- State transitions are always logged — old state → new state
- Use emoji prefixes for visual scanning: ⬆️ request, ⬇️ response, ❌ error, 🔄 transition, ✅ success, ⚠️ warning
- Include timing —
ContinuousClockfor elapsed durations on any I/O or async operation - Privacy-aware — use
.publiconly 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
- Start log tail first — before running the app or script
- Use
bg_bashto run the tail in the background when working in pi:bg_bash: log stream --predicate 'subsystem == "com.yourapp.dev"' --level debug - Check
task_outputperiodically to review log output - Filter aggressively — use category predicates to reduce noise
- Never ship without reviewing logs — if the log tail shows unexpected entries, investigate before committing
Quick Reference
| Tool | Purpose |
|---|---|
| SPM | Package management (not CocoaPods, Carthage) |
| swift build | Compile |
| swift test | Run tests |
| swift run | Execute |
| Pattern | Preference |
|---|---|
| Mutability | let over var |
| Value types | struct over class |
| Optionals | guard let, if let, ?? (never !) |
| State modeling | Enums with associated values |
| Error handling | Typed throws (Swift 6), Result |
| Concurrency | async/await, actors (not callbacks) |
| Type safety | Generics, protocols (avoid Any) |
| Architecture | Shared Core for multi-target |
| macOS imports | import Cocoa (not piecemeal) |
| Logging | OSLog with categories (apps), FileLog (CLI) |
| Log tailing | Always 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