swift-guide
SKILL.md
Swift Guide
Applies to: Swift 5.9+, iOS/macOS/Server-Side, SPM, Concurrency
Core Principles
- Value Semantics First: Prefer structs over classes; use classes only when identity or inheritance is required
- Protocol-Oriented Design: Compose behavior through protocols and extensions rather than class hierarchies
- Safe Optionals: Unwrap explicitly with
guard letorif let; force-unwrap (!) is forbidden outside tests - Structured Concurrency: Use async/await and task groups; avoid raw GCD queues for new code
- Compiler as Ally: Enable strict concurrency checking; treat warnings as errors in CI
Guardrails
Version & Dependencies
- Use Swift 5.9+ with Swift Package Manager (Package.swift)
- Pin package versions with
.upToNextMinor(from:)for libraries - Run
swift package resolvebefore committing after dependency changes - Commit
Package.resolvedfor applications; omit for libraries
Code Style
- Run
swift formator SwiftLint before every commit - Follow Swift API Design Guidelines
PascalCasefor types, protocols, enums |camelCasefor functions, properties, variables- Prefer trailing closure syntax for the last closure parameter
- Use
Selfinstead of repeating the type name inside type definitions - Mark classes
finalby default; removefinalonly when subclassing is designed for
Optionals
- Use
guard letfor early exit when the unwrapped value is needed afterward - Use
if letfor scoped unwrapping within a branch - Use nil coalescing (
??) for default values; optional chaining (?.) to traverse - Never force-unwrap (
!) outside of tests andIBOutletdeclarations - Use
compactMapto filter nil from collections;Optional.map/.flatMapto transform
Concurrency
- Use
async/awaitfor all asynchronous operations (no completion handlers in new code) - Use
TaskGroup/ThrowingTaskGroupfor parallel fan-out - Mark UI-bound code with
@MainActor; avoidDispatchQueue.mainin new code - Use
actorfor mutable shared state; prefer actors over locks - Conform types crossing isolation boundaries to
Sendable - Enable strict concurrency:
-strict-concurrency=complete - Always handle
Task.isCancelledorTask.checkCancellation()in long-running work
Protocols
- Define protocols where they are consumed, not where they are implemented
- Keep protocols focused: prefer multiple small protocols over one large one
- Use protocol extensions for default implementations of computed logic
- Use protocol composition (
SomeProtocol & AnotherProtocol) for flexible constraints - Prefer
some Protocol(opaque types) overany Protocolwhen the concrete type is fixed
Project Structure
MyProject/
├── Package.swift # Manifest (targets, dependencies, platforms)
├── Package.resolved # Locked versions (commit for apps)
├── Sources/
│ ├── MyProject/ # Main library target
│ │ ├── Models/
│ │ ├── Services/
│ │ ├── Protocols/
│ │ └── Extensions/ # TypeName+Capability.swift
│ └── MyProjectCLI/ # Executable target (thin entry point)
│ └── main.swift
├── Tests/
│ └── MyProjectTests/
└── README.md
main.swiftor@mainstruct should be thin: parse arguments, build dependencies, call library code- Put all business logic in library targets (testable without running the binary)
- One primary type per file, file named after the type
Key Patterns
Optionals: guard let / if let
func processUser(id: String?) -> User {
guard let id, !id.isEmpty else {
return User.anonymous
}
guard let user = userCache[id] else {
return fetchUser(id: id)
}
return user
}
func displayName(for user: User) -> String {
if let nickname = user.nickname { return nickname }
return "\(user.firstName) \(user.lastName)"
}
Result Type for Typed Errors
enum NetworkError: Error, Sendable {
case invalidURL(String)
case serverError(statusCode: Int)
case decodingFailed(underlying: Error)
}
func fetchData(from urlString: String) async -> Result<Data, NetworkError> {
guard let url = URL(string: urlString) else { return .failure(.invalidURL(urlString)) }
do {
let (data, response) = try await URLSession.shared.data(from: url)
guard let http = response as? HTTPURLResponse, (200..<300).contains(http.statusCode) else {
return .failure(.serverError(statusCode: 0))
}
return .success(data)
} catch {
return .failure(.decodingFailed(underlying: error))
}
}
Protocol Extensions with Defaults
protocol Timestamped {
var createdAt: Date { get }
var updatedAt: Date { get }
}
extension Timestamped {
var isRecent: Bool { updatedAt.timeIntervalSinceNow > -86_400 }
}
// Protocol composition for flexible constraints
func findRecent<T: Identifiable & Timestamped>(_ items: [T]) -> [T] {
items.filter(\.isRecent)
}
Actor for Shared Mutable State
actor CacheStore<Key: Hashable & Sendable, Value: Sendable> {
private var storage: [Key: Value] = [:]
private let maxSize: Int
init(maxSize: Int = 1000) { self.maxSize = maxSize }
func get(_ key: Key) -> Value? { storage[key] }
func set(_ key: Key, value: Value) {
if storage.count >= maxSize { storage.removeAll() }
storage[key] = value
}
}
Async/Await with Task Groups
func fetchAllUsers(ids: [String]) async throws -> [User] {
try await withThrowingTaskGroup(of: User.self) { group in
for id in ids {
group.addTask { try await self.fetchUser(id: id) }
}
var users: [User] = []
for try await user in group { users.append(user) }
return users
}
}
Sendable Conformance
// Value types: implicitly Sendable when all stored properties are Sendable
struct UserDTO: Sendable { let id: String; let name: String }
// Classes: must be final with immutable properties, or use @unchecked with a lock
final class AppConfig: Sendable { let apiBaseURL: URL; let maxRetries: Int
init(apiBaseURL: URL, maxRetries: Int = 3) { self.apiBaseURL = apiBaseURL; self.maxRetries = maxRetries }
}
Property Wrappers
@propertyWrapper
struct Clamped<Value: Comparable> {
private var value: Value
private let range: ClosedRange<Value>
var wrappedValue: Value {
get { value }
set { value = min(max(newValue, range.lowerBound), range.upperBound) }
}
init(wrappedValue: Value, _ range: ClosedRange<Value>) {
self.range = range
self.value = min(max(wrappedValue, range.lowerBound), range.upperBound)
}
}
struct AudioSettings {
@Clamped(0...100) var volume: Int = 50
@Clamped(0.5...2.0) var playbackSpeed: Double = 1.0
}
Result Builders for DSLs
@resultBuilder
struct ArrayBuilder<Element> {
static func buildBlock(_ components: [Element]...) -> [Element] { components.flatMap { $0 } }
static func buildExpression(_ expression: Element) -> [Element] { [expression] }
static func buildOptional(_ component: [Element]?) -> [Element] { component ?? [] }
static func buildEither(first c: [Element]) -> [Element] { c }
static func buildEither(second c: [Element]) -> [Element] { c }
}
Testing
XCTest with Async Support
import XCTest
@testable import MyProject
final class UserServiceTests: XCTestCase {
private var sut: UserService!
private var mockRepo: MockUserRepository!
override func setUp() { super.setUp(); mockRepo = MockUserRepository(); sut = UserService(repository: mockRepo) }
override func tearDown() { sut = nil; mockRepo = nil; super.tearDown() }
func test_fetchUser_withValidID_returnsUser() async throws {
mockRepo.stubbedUser = User(id: "1", name: "Alice")
let user = try await sut.fetchUser(id: "1")
XCTAssertEqual(user.name, "Alice")
}
func test_fetchUser_withInvalidID_throwsNotFound() async {
mockRepo.stubbedError = .notFound
do {
_ = try await sut.fetchUser(id: "invalid")
XCTFail("Expected notFound error")
} catch let error as ServiceError {
XCTAssertEqual(error, .notFound)
} catch {
XCTFail("Unexpected error: \(error)")
}
}
}
Testing Standards
- Test names describe behavior:
func test_login_withExpiredToken_refreshesAutomatically() - Use
setUp()/tearDown()for shared test fixtures - Use protocol-based mocks injected via initializer (no singletons)
- Async tests use
async throwsdirectly (no XCTestExpectation for async/await code) - Coverage target: >80% for business logic, >60% overall
- Test both success and failure paths for every public method
Tooling
Essential Commands
swift build # Build all targets
swift test # Run all tests
swift test --enable-code-coverage # With coverage
swift package resolve # Resolve dependencies
swift package update # Update dependencies
swift format . # Format (swift-format)
swiftlint # Lint (SwiftLint)
swiftlint --fix # Auto-fix lint issues
SwiftLint Key Rules
# .swiftlint.yml -- enforce these as errors
force_cast: error
force_unwrapping: error
force_try: error
function_body_length:
warning: 40
error: 50
cyclomatic_complexity:
warning: 8
error: 10
Package.swift Essentials
// swift-tools-version: 5.9
import PackageDescription
let package = Package(
name: "MyProject",
platforms: [.macOS(.v14), .iOS(.v17)],
dependencies: [
.package(url: "https://github.com/apple/swift-argument-parser", from: "1.3.0"),
.package(url: "https://github.com/apple/swift-log", from: "1.5.0"),
],
targets: [
.target(name: "MyProject", dependencies: [
.product(name: "Logging", package: "swift-log"),
], swiftSettings: [
.enableExperimentalFeature("StrictConcurrency"),
]),
.testTarget(name: "MyProjectTests", dependencies: ["MyProject"]),
]
)
References
For detailed patterns and examples, see:
- references/patterns.md -- Actor patterns, protocol composition, async sequences
External References
Weekly Installs
6
Repository
ar4mirez/samuelGitHub Stars
3
First Seen
13 days ago
Security Audits
Installed on
opencode6
gemini-cli6
github-copilot6
codex6
kimi-cli6
amp6