Skip to main content

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

RuleDescriptionExample
indentCorrect indentation (spaces/tabs)Consistent 4-space indent
bracesBrace placement (K&R vs Allman)Opening brace on same line
isEmptyUse .isEmpty instead of .count == 0if array.isEmpty not if array.count == 0
redundantSelfRemove unnecessary self.name instead of self.name when unambiguous
sortedImportsAlphabetize import statementsConsistent import ordering
unusedArgumentsMark unused parameters with _func process(_ ignored: Int)
wrapArgumentsWrap long argument lists consistentlyMulti-line function calls
trailingCommasAdd/remove trailing commasConsistent array/dict formatting

Xcode Integration

Build Phase Integration:

  1. In Xcode, select your target
  2. Build Phases → + → New Run Script Phase
  3. 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):

  1. Download SwiftFormat for Xcode from GitHub releases
  2. Move to Applications folder
  3. System Preferences → Extensions → Xcode Source Editor → Enable SwiftFormat
  4. 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 colons
  • comma - Spacing around commas
  • opening_brace - Brace placement
  • trailing_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 structs
  • cyclomatic_complexity - Function complexity threshold

Best Practice Rules:

  • weak_delegate - Delegates should be weak to avoid retain cycles
  • implicit_getter - Omit get {} for read-only computed properties
  • redundant_optional_initialization - Don't init optionals to nil
  • unused_closure_parameter - Mark unused closure params with _
  • empty_count - Use .isEmpty instead of .count == 0

Performance Rules:

  • empty_collection_literal - Use [] instead of Array()
  • 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:

  1. Build Settings → Swift Compiler - Warnings
  2. 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

ToolPurposeSpeedAuto-fixIntegration
SwiftFormatCode formattingVery FastYesXcode, CLI, pre-commit, CI
SwiftLintStyle & static analysisFastSome rulesXcode, CLI, pre-commit, CI
Xcode AnalyzerDeep static analysisMedium-SlowNoXcode, xcodebuild, CI
Compiler warningsType safety, best practicesFastNoAlways on

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

External Resources


Summary

Key Takeaways

  1. SwiftFormat - Automated formatting (mechanical consistency)
  2. SwiftLint - Style enforcement and static analysis (quality)
  3. Xcode Analyzer - Deep bug detection (before release)
  4. Compiler warnings - Treat all warnings as errors
  5. Pre-commit hooks - Catch issues before they enter repository
  6. CI enforcement - Quality gates prevent low-quality merges
  7. Build phase integration - Immediate feedback during development
  8. Auto-fix liberally - Let tools fix what they can automatically
  9. Custom rules sparingly - Only for recurring team-specific patterns
  10. 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.