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
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 |
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