Swift Linting and Static Analysis
Swift's strong type system and optionals prevent many errors at compile time, but linting and static analysis tools enforce code style, detect code smells, and catch additional issues beyond compilation errors. A comprehensive linting setup ensures code quality, maintainability, and consistency across Swift projects.
Overview
The Swift compiler ensures type safety and memory safety. Linting tools catch code smells, style violations, and potential bugs that compile successfully. The Swift linting ecosystem includes:
Core tools:
- SwiftLint - Style enforcement and static analysis (de facto standard for Swift)
- SwiftFormat - Automated code formatting
- Xcode Analyzer - Built-in static analysis in Xcode
- Swift compiler warnings - Comprehensive built-in diagnostics
These tools integrate seamlessly with Xcode, SPM (Swift Package Manager), and CI/CD pipelines. The Swift community has converged on SwiftLint as the primary linting tool, similar to how ESLint dominates in JavaScript or ktlint in Kotlin.
Why multiple tools: SwiftFormat handles mechanical formatting (whitespace, line breaks, braces), while SwiftLint enforces style rules and detects potential bugs. Use both for comprehensive coverage. See our Pipeline Configuration for CI/CD integration patterns.
Code Formatting with SwiftFormat
SwiftFormat automatically formats Swift code according to a consistent style. It handles whitespace, indentation, brace placement, and other mechanical formatting concerns.
Why SwiftFormat
Manual code formatting creates problems:
- Inconsistent style across files and developers
- Code review time wasted on style feedback
- Merge conflicts from formatting differences
- Cognitive load from varying code styles
SwiftFormat eliminates these problems through automation. Unlike manual formatting guidelines, SwiftFormat is deterministic - the same code always produces the same output.
Key features:
- Deterministic formatting (same input always produces same output)
- Comprehensive rules covering most Swift syntax
- Configurable - can customize to match team preferences
- Fast - formats entire projects in seconds
- Xcode extension available for format-on-save
Installation
Homebrew (recommended):
brew install swiftformat
Mint:
mint install nicklockwood/SwiftFormat
Swift Package Manager (as build tool plugin):
// Package.swift
dependencies: [
.package(url: "https://github.com/nicklockwood/SwiftFormat", from: "0.52.0")
]
Configuration
SwiftFormat uses a .swiftformat file in your project root:
# .swiftformat
--swiftversion 5.9
# Indentation
--indent 4
--indentcase false # Don't indent case statements
--trimwhitespace always
# Spacing
--wraparguments before-first # Wrap function arguments
--wrapcollections before-first
--closingparen same-line
# Commas
--commas inline # Commas at end of line, not start of next
# Braces
--allman false # Use K&R style braces, not Allman
# Empty lines
--emptybraces no-space # {} not { }
--linebreaks lf # Unix line endings
# Imports
--importgrouping testable-bottom
# Rules to enable
--enable isEmpty
--enable sortedImports
--enable redundantSelf
# Rules to disable
--disable redundantReturn # Allow explicit returns for clarity
--disable wrapMultilineStatementBraces # Can reduce readability
Running SwiftFormat
# Format entire project
swiftformat .
# Format specific files
swiftformat Sources/
# Check formatting without modifying files (CI)
swiftformat --lint .
# Show which files would be changed
swiftformat --dryrun .
# Verbose output
swiftformat --verbose .
Common SwiftFormat Rules
| Rule | Description | Example |
|---|---|---|
indent | Correct indentation (spaces/tabs) | Consistent 4-space indent |
braces | Brace placement (K&R vs Allman) | Opening brace on same line |
isEmpty | Use .isEmpty instead of .count == 0 | if array.isEmpty not if array.count == 0 |
redundantSelf | Remove unnecessary self. | name instead of self.name when unambiguous |
sortedImports | Alphabetize import statements | Consistent import ordering |
unusedArguments | Mark unused parameters with _ | func process(_ ignored: Int) |
wrapArguments | Wrap long argument lists consistently | Multi-line function calls |
trailingCommas | Add/remove trailing commas | Consistent array/dict formatting |
Xcode Integration
Build Phase Integration:
- In Xcode, select your target
- Build Phases → + → New Run Script Phase
- Add script:
if which swiftformat >/dev/null; then
swiftformat --lint "$SRCROOT"
else
echo "warning: SwiftFormat not installed, download from https://github.com/nicklockwood/SwiftFormat"
fi
This runs SwiftFormat on every build, showing warnings for unformatted code.
Xcode Extension (format-on-save):
- Download SwiftFormat for Xcode from GitHub releases
- Move to Applications folder
- System Preferences → Extensions → Xcode Source Editor → Enable SwiftFormat
- In Xcode: Editor → SwiftFormat → Format File (⌃⌥⌘F)
Set up a keyboard shortcut for instant formatting.
Static Analysis with SwiftLint
SwiftLint enforces Swift style and conventions, prevents common mistakes, and detects code smells. It's the most widely used linting tool in the Swift ecosystem, with over 200 built-in rules.
Why SwiftLint
SwiftLint catches issues that compile successfully but indicate problems:
- Style violations: Inconsistent naming, line length, function complexity
- Code smells: Long functions, force unwrapping, large types
- Potential bugs: Empty catch blocks, weak delegate properties, force casts
- Performance: Inefficient patterns like unnecessary closures
- Best practices: Optionals handling, access control, protocol conformance
These issues make code harder to maintain and more prone to bugs. SwiftLint catches them automatically, ensuring consistent quality.
Installation
Homebrew:
brew install swiftlint
CocoaPods:
# Podfile
pod 'SwiftLint'
Swift Package Manager:
// Package.swift
dependencies: [
.package(url: "https://github.com/realm/SwiftLint.git", from: "0.54.0")
]
Configuration
SwiftLint uses a .swiftlint.yml file:
# .swiftlint.yml
# Paths to include/exclude
included:
- Sources
excluded:
- Pods
- Generated
- */Generated/*
# Rules
disabled_rules:
- trailing_whitespace # Handled by SwiftFormat
- line_length # Can be too strict for some projects
opt_in_rules:
- empty_count # Prefer isEmpty over count == 0
- explicit_init # Avoid redundant .init()
- force_unwrapping # Avoid force unwrap !
- implicit_return # Allow implicit returns
- sorted_imports # Alphabetize imports
- unused_declaration # Warn about unused code
# Rule configurations
line_length:
warning: 120
error: 150
ignores_comments: true
ignores_urls: true
file_length:
warning: 500
error: 1000
function_body_length:
warning: 40
error: 100
type_body_length:
warning: 300
error: 500
cyclomatic_complexity:
warning: 10
error: 20
identifier_name:
min_length:
warning: 2
max_length:
warning: 40
error: 50
excluded:
- id
- to
- db
# Custom rules
custom_rules:
no_print:
name: "No Print Statements"
regex: 'print\('
match_kinds: identifier
message: "Use proper logging instead of print()"
severity: warning
Running SwiftLint
# Lint entire project
swiftlint
# Lint specific directory
swiftlint lint --path Sources/
# Auto-correct violations
swiftlint --fix
# Strict mode (treats warnings as errors)
swiftlint --strict
# Generate HTML report
swiftlint lint --reporter html > swiftlint-report.html
# CI-friendly output
swiftlint lint --reporter github-actions-logging
Common SwiftLint Rules
Style Rules:
identifier_name- Naming conventions (camelCase, PascalCase)colon- Spacing around colonscomma- Spacing around commasopening_brace- Brace placementtrailing_semicolon- No unnecessary semicolons
Code Smell Rules:
force_unwrapping- Avoid force unwrapping (!)force_cast- Avoid force casting (as!)force_try- Avoid force try (try!)large_tuple- Tuples with > 2 elements should be structscyclomatic_complexity- Function complexity threshold
Best Practice Rules:
weak_delegate- Delegates should be weak to avoid retain cyclesimplicit_getter- Omitget {}for read-only computed propertiesredundant_optional_initialization- Don't init optionals to nilunused_closure_parameter- Mark unused closure params with_empty_count- Use.isEmptyinstead of.count == 0
Performance Rules:
empty_collection_literal- Use[]instead ofArray()first_where- Use.first(where:)instead of.filter().first
Rule Severity
SwiftLint rules have severities:
- Warning: Code review feedback - should be fixed but doesn't block build
- Error: Must be fixed - blocks build or PR merge
# Configure severity
force_unwrapping: error # Treat force unwraps as errors
line_length: warning # Treat long lines as warnings
Xcode Integration
Build Phase:
if which swiftlint >/dev/null; then
swiftlint
else
echo "warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint"
fi
SwiftLint violations appear as warnings/errors in Xcode's issue navigator.
Autocorrect on Build:
if which swiftlint >/dev/null; then
swiftlint --fix && swiftlint
else
echo "warning: SwiftLint not installed"
fi
This auto-fixes violations on every build, but can be slow for large projects.
Xcode Analyzer
Xcode's built-in static analyzer detects subtle bugs like memory leaks, logic errors, and API misuse. It performs deeper analysis than the compiler, examining control flow and data flow.
Running the Analyzer
In Xcode:
- Product → Analyze (⌘⇧B)
From Command Line:
xcodebuild analyze -workspace MyApp.xcworkspace -scheme MyApp
Common Analyzer Findings
- Memory Management: Retain cycles, memory leaks, over-releases
- Null Dereferences: Accessing nil optionals
- Logic Errors: Dead code, infinite loops
- API Misuse: Incorrect Foundation/UIKit usage
- Security: Format string vulnerabilities, path traversal
Enabling Analyzer in CI
# .github/workflows/quality.yml
- name: Run Xcode Analyzer
run: |
xcodebuild analyze \
-workspace MyApp.xcworkspace \
-scheme MyApp \
-destination 'platform=iOS Simulator,name=iPhone 14' \
-quiet
Analyzer results are visible in Xcode and can be exported for CI reporting.
Compiler Warnings
The Swift compiler provides extensive warnings. Treat all warnings as errors to enforce quality:
Build Settings
In Xcode:
- Build Settings → Swift Compiler - Warnings
- Set "Treat Warnings as Errors" to Yes
Or in .xcconfig:
SWIFT_TREAT_WARNINGS_AS_ERRORS = YES
Common Compiler Warnings
- Deprecation: Using deprecated APIs
- Unused: Unused variables, constants, functions
- Type Mismatch: Implicit type conversions
- Unreachable Code: Code after return/throw
- Optional Coercion: Forced unwrapping
// Warning: Unused variable
let payment = fetchPayment() // Warning: Variable 'payment' was never used
// Fixed: Use the variable or prefix with _
let _ = fetchPayment() // Explicitly ignoring result
// Warning: Forced unwrap
let id = payment!.id // Warning: Using force-unwrap on optional
// Fixed: Safe unwrapping
if let payment = payment {
let id = payment.id
}
// Warning: Deprecation
UIApplication.shared.statusBarOrientation // Warning: Deprecated in iOS 13
// Fixed: Use new API
windowScene.interfaceOrientation
Suppressing Warnings
Suppress warnings only when required, with clear justification:
// GOOD: Suppress with narrow scope and reason
func legacyAPI() {
#warning("Remove this after migration to new API - JIRA-1234")
// Using deprecated API temporarily during migration
if #available(iOS 15, *) {
newAPI()
} else {
#if compiler(>=5.5)
deprecatedAPI() // Suppressed: Required for iOS 14 support
#endif
}
}
// BAD: Suppressing without justification
func process() {
#warning("TODO: Fix this") // No context or tracking
}
Pre-commit Hooks
Integrate linting into Git pre-commit hooks to catch issues before commits:
#!/bin/bash
# .git/hooks/pre-commit
echo "Running SwiftFormat check..."
swiftformat --lint .
if [ $? -ne 0 ]; then
echo " SwiftFormat found formatting issues."
echo "Run 'swiftformat .' to fix automatically."
exit 1
fi
echo "Running SwiftLint..."
swiftlint --strict
if [ $? -ne 0 ]; then
echo " SwiftLint found violations."
exit 1
fi
echo " Pre-commit checks passed!"
Make hook executable:
chmod +x .git/hooks/pre-commit
Alternative: Use pre-commit framework:
# .pre-commit-config.yaml
repos:
- repo: https://github.com/nicklockwood/SwiftFormat
rev: 0.52.0
hooks:
- id: swiftformat
args: [--lint]
- repo: https://github.com/realm/SwiftLint
rev: 0.54.0
hooks:
- id: swiftlint
CI/CD Integration
Static analysis must run in CI/CD to enforce quality gates before merging:
GitHub Actions
# .github/workflows/quality.yml
name: Code Quality
on: [pull_request]
jobs:
lint:
runs-on: macos-latest
steps:
- uses: actions/checkout@v3
- name: Install SwiftLint
run: brew install swiftlint
- name: Install SwiftFormat
run: brew install swiftformat
- name: Run SwiftFormat
run: swiftformat --lint .
- name: Run SwiftLint
run: swiftlint lint --reporter github-actions-logging
- name: Run Xcode Analyzer
run: |
xcodebuild analyze \
-workspace MyApp.xcworkspace \
-scheme MyApp \
-destination 'platform=iOS Simulator,name=iPhone 14' \
-quiet
GitLab CI
# .gitlab-ci.yml
quality:
stage: test
tags:
- macos
script:
- brew install swiftlint swiftformat
- swiftformat --lint .
- swiftlint lint --strict
allow_failure: false
Fastlane Integration
# Fastfile
lane :lint do
swiftformat(
lint: true,
path: "."
)
swiftlint(
mode: :lint,
strict: true,
reporter: "html",
output_file: "swiftlint-results.html"
)
end
See our CI/CD Pipeline Configuration for more examples.
Tool Comparison
| Tool | Purpose | Speed | Auto-fix | Integration |
|---|---|---|---|---|
| SwiftFormat | Code formatting | Very Fast | Yes | Xcode, CLI, pre-commit, CI |
| SwiftLint | Style & static analysis | Fast | Some rules | Xcode, CLI, pre-commit, CI |
| Xcode Analyzer | Deep static analysis | Medium-Slow | No | Xcode, xcodebuild, CI |
| Compiler warnings | Type safety, best practices | Fast | No | Always on |
Recommended Setup
Minimal (small team/starting out):
- SwiftLint with default rules
- Compiler warnings as errors
- Pre-commit hook for SwiftLint
Standard (most projects):
- SwiftFormat for formatting
- SwiftLint for linting
- Compiler warnings as errors
- Pre-commit hooks
- CI enforcement
Comprehensive (production apps):
- SwiftFormat with custom config
- SwiftLint with custom rules
- Xcode Analyzer in CI
- Compiler warnings as errors
- Pre-commit hooks
- CI quality gates
- Code coverage requirements
Best Practices
1. Format Early, Format Often
Run SwiftFormat before every commit:
swiftformat . && git add -A
2. Enable Auto-fix in SwiftLint
Many SwiftLint rules can auto-correct:
swiftlint --fix
3. Customize Gradually
Start with default rules, then customize based on team feedback - don't over-customize initially.
4. Document Rule Decisions
When disabling rules, document why in .swiftlint.yml:
disabled_rules:
- line_length # Our designs require longer lines for readability
5. Use Xcode Build Phases
Integrate linting into build process for immediate feedback during development.
6. Review Analyzer Results Regularly
Run Xcode Analyzer before releases to catch subtle bugs:
xcodebuild analyze ...
7. Leverage Custom Rules
Create custom SwiftLint rules for team-specific patterns:
custom_rules:
no_direct_core_data_access:
regex: 'NSManagedObjectContext'
message: "Use repository layer instead of direct Core Data access"
8. Fail Fast in CI
Run linting before tests - don't waste CI time running tests on code that doesn't meet quality standards.
Further Reading
Internal Documentation
- Swift General Guidelines - Core Swift best practices
- Swift Testing - Testing strategies
- iOS Overview - iOS development fundamentals
- iOS Code Review - Code review checklist
- Code Quality Metrics - Measuring quality
- CI/CD Pipeline Configuration - Pipeline integration
External Resources
Summary
Key Takeaways
- SwiftFormat - Automated formatting (mechanical consistency)
- SwiftLint - Style enforcement and static analysis (quality)
- Xcode Analyzer - Deep bug detection (before release)
- Compiler warnings - Treat all warnings as errors
- Pre-commit hooks - Catch issues before they enter repository
- CI enforcement - Quality gates prevent low-quality merges
- Build phase integration - Immediate feedback during development
- Auto-fix liberally - Let tools fix what they can automatically
- Custom rules sparingly - Only for recurring team-specific patterns
- Progressive adoption - Start with defaults, customize gradually
Next Steps: Configure these tools in your project, integrate them into CI/CD pipelines, and review the Swift Testing guide for testing strategies.