Maintaining consistent code quality across a development team can be challenging. SwiftLint helps enforce Swift style and conventions automatically, catching issues before they make it to code review. In this comprehensive guide, we'll set up SwiftLint, integrate it with Xcode builds, and automate it with GitHub Actions for continuous quality enforcement.
What is SwiftLint?
SwiftLint is a tool to enforce Swift style and conventions based on GitHub's Swift Style Guide. It runs as a command-line tool and can be integrated into your development workflow to:
- Enforce consistent coding standards across your team
- Catch common mistakes and code smells
- Automatically format code to match style guidelines
- Prevent bad code from being committed
- Improve code readability and maintainability
Why Use SwiftLint?
SwiftLint catches over 200+ style issues automatically, from simple spacing problems to complex naming conventions. It saves hours in code review by handling style discussions programmatically.
Installation Methods
Method 1: Homebrew (Recommended for Local Development)
# Install SwiftLint
brew install swiftlint
# Verify installation
swiftlint version
Method 2: CocoaPods
target 'YourApp' do
use_frameworks!
# Other pods...
# Add SwiftLint to your dev pods
pod 'SwiftLint'
end
# Install pods
pod install
Method 3: Swift Package Manager (SPM)
// swift-tools-version:5.9
import PackageDescription
let package = Package(
name: "YourApp",
platforms: [.iOS(.v15)],
dependencies: [
.package(url: "https://github.com/realm/SwiftLint", from: "0.54.0")
],
targets: [
.target(
name: "YourApp",
plugins: [.plugin(name: "SwiftLintPlugin", package: "SwiftLint")]
)
]
)
Configuring SwiftLint
Create a .swiftlint.yml file in your project root to customize SwiftLint rules:
# SwiftLint Configuration File
# Paths to include during linting
included:
- Sources
- Tests
- YourApp
# Paths to ignore during linting
excluded:
- Pods
- Carthage
- .build
- DerivedData
- Generated
- Vendor
# Disable rules
disabled_rules:
- trailing_whitespace
- todo
- line_length # Will configure this below
# Opt-in rules (not enabled by default)
opt_in_rules:
- empty_count
- empty_string
- explicit_init
- first_where
- closure_spacing
- contains_over_first_not_nil
- convenience_type
- discouraged_optional_boolean
- empty_xctest_method
- explicit_type_interface
- fatal_error_message
- file_header
- force_unwrapping
- implicit_return
- joined_default_parameter
- literal_expression_end_indentation
- multiline_arguments
- multiline_function_chains
- multiline_parameters
- operator_usage_whitespace
- overridden_super_call
- pattern_matching_keywords
- private_action
- private_outlet
- prohibited_super_call
- redundant_nil_coalescing
- single_test_class
- sorted_first_last
- sorted_imports
- toggle_bool
- unavailable_function
- unneeded_parentheses_in_closure_argument
- vertical_parameter_alignment_on_call
- yoda_condition
# Rule configurations
line_length:
warning: 120
error: 200
ignores_function_declarations: true
ignores_comments: true
ignores_urls: true
function_body_length:
warning: 50
error: 100
function_parameter_count:
warning: 5
error: 8
type_body_length:
warning: 300
error: 500
file_length:
warning: 500
error: 1000
ignore_comment_only_lines: true
cyclomatic_complexity:
warning: 10
error: 20
nesting:
type_level:
warning: 3
statement_level:
warning: 5
identifier_name:
min_length:
warning: 3
max_length:
warning: 40
excluded:
- id
- URL
- db
- i
- x
- y
type_name:
min_length: 3
max_length: 40
# Custom rules
custom_rules:
# Ensure print statements aren't committed
no_print:
name: "Print statements"
regex: "print\\("
match_kinds: identifier
message: "Avoid using print() in production code. Use proper logging."
severity: warning
# Require TODO comments to have a ticket number
todo_with_ticket:
name: "TODO with ticket"
regex: "TODO(?!\\s*\\[\\w+-\\d+\\])"
message: "TODOs must include a ticket number [PROJ-123]"
severity: warning
# Discourage force try
no_force_try:
name: "Force Try"
regex: "try!"
message: "Avoid using 'try!' - handle errors properly"
severity: error
# Paths to include in reporter output
reporter: "xcode"
Configuration Best Practice
Start with a minimal configuration and gradually add rules. Enabling too many strict rules at once can overwhelm your team and slow down development.
Integrating SwiftLint with Xcode Build
The most effective way to use SwiftLint is to run it automatically during every build.
Step 1: Add Run Script Phase
- Open your Xcode project
- Select your target
- Go to Build Phases tab
- Click + ā New Run Script Phase
- Drag the new phase to run before "Compile Sources"
- Name it "SwiftLint"
Step 2: Add SwiftLint Script
#!/bin/bash
# Check if SwiftLint is installed
if which swiftlint >/dev/null; then
swiftlint
else
echo "warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint"
fi
Advanced Script with Better Error Handling
#!/bin/bash
# Define colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
echo "${GREEN}š Running SwiftLint...${NC}"
# Check for Homebrew installation
if which swiftlint >/dev/null; then
SWIFTLINT_PATH=$(which swiftlint)
echo "${GREEN}ā Found SwiftLint at: ${SWIFTLINT_PATH}${NC}"
# Check for CocoaPods installation
elif [ -f "${PODS_ROOT}/SwiftLint/swiftlint" ]; then
SWIFTLINT_PATH="${PODS_ROOT}/SwiftLint/swiftlint"
echo "${GREEN}ā Found SwiftLint in Pods${NC}"
else
echo "${RED}ā SwiftLint not found${NC}"
echo "${YELLOW}Install via: brew install swiftlint${NC}"
exit 0 # Don't fail the build, just warn
fi
# Run SwiftLint
$SWIFTLINT_PATH lint --config .swiftlint.yml
# Capture exit code
LINT_EXIT_CODE=$?
if [ $LINT_EXIT_CODE -eq 0 ]; then
echo "${GREEN}ā SwiftLint passed with no issues!${NC}"
else
echo "${RED}ā SwiftLint found issues${NC}"
fi
# Exit with the SwiftLint exit code
exit $LINT_EXIT_CODE
Auto-fix Script (Optional)
Create a separate script to auto-fix issues:
#!/bin/bash
# SwiftLint auto-fix script
# Run this manually when you want to fix issues automatically
echo "š§ Running SwiftLint auto-fix..."
if which swiftlint >/dev/null; then
swiftlint --fix --format
echo "ā Auto-fix complete!"
echo "ā ļø Review changes before committing"
else
echo "ā SwiftLint not installed"
exit 1
fi
# Make the script executable
chmod +x scripts/swiftlint-autofix.sh
# Run it
./scripts/swiftlint-autofix.sh
Git Hooks for Pre-Commit Linting
Prevent bad code from being committed by adding a pre-commit hook:
#!/bin/bash
# SwiftLint pre-commit hook
# Runs SwiftLint on staged Swift files before commit
echo "š Running SwiftLint on staged files..."
# Get list of staged Swift files
STAGED_SWIFT_FILES=$(git diff --cached --name-only --diff-filter=d | grep ".swift$")
if [ -z "$STAGED_SWIFT_FILES" ]; then
echo "ā No Swift files to lint"
exit 0
fi
# Check if SwiftLint is installed
if ! which swiftlint >/dev/null; then
echo "ā ļø SwiftLint not installed. Skipping..."
exit 0
fi
# Run SwiftLint on staged files
LINT_ERRORS=0
for FILE in $STAGED_SWIFT_FILES; do
swiftlint lint --path "$FILE" --quiet
if [ $? -ne 0 ]; then
LINT_ERRORS=1
fi
done
if [ $LINT_ERRORS -ne 0 ]; then
echo ""
echo "ā SwiftLint found issues in your staged files"
echo "Fix the issues or run 'swiftlint --fix' to auto-fix"
echo "To commit anyway, use: git commit --no-verify"
exit 1
fi
echo "ā SwiftLint passed!"
exit 0
Installing the Pre-Commit Hook
# Create hooks directory if it doesn't exist
mkdir -p .git/hooks
# Create the pre-commit hook
cat > .git/hooks/pre-commit << 'EOF'
#!/bin/bash
# [Paste the script from above]
EOF
# Make it executable
chmod +x .git/hooks/pre-commit
echo "ā Pre-commit hook installed!"
GitHub Actions Integration
Automate SwiftLint in your CI/CD pipeline to ensure all pull requests meet code standards.
Method 1: Basic GitHub Action
name: SwiftLint
on:
pull_request:
paths:
- '**.swift'
push:
branches:
- main
- develop
jobs:
SwiftLint:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: SwiftLint
uses: norio-nomura/action-swiftlint@3.2.1
with:
args: --strict
Method 2: Advanced GitHub Action with Annotations
name: SwiftLint Advanced
on:
pull_request:
paths:
- '**.swift'
- '.swiftlint.yml'
push:
branches:
- main
- develop
jobs:
swiftlint:
name: SwiftLint Analysis
runs-on: macos-latest
steps:
- name: Checkout Repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Cache SwiftLint
uses: actions/cache@v3
with:
path: /usr/local/bin/swiftlint
key: ${{ runner.os }}-swiftlint-${{ hashFiles('.swiftlint.yml') }}
restore-keys: |
${{ runner.os }}-swiftlint-
- name: Install SwiftLint
run: |
if ! which swiftlint >/dev/null; then
brew install swiftlint
fi
swiftlint version
- name: Run SwiftLint
run: |
swiftlint lint --reporter github-actions-logging
- name: Run SwiftLint (Strict on PR)
if: github.event_name == 'pull_request'
run: |
swiftlint lint --strict --reporter github-actions-logging
- name: Generate SwiftLint Report
if: always()
run: |
swiftlint lint --reporter html > swiftlint-report.html
- name: Upload SwiftLint Report
if: always()
uses: actions/upload-artifact@v3
with:
name: swiftlint-report
path: swiftlint-report.html
- name: Comment PR with Results
if: github.event_name == 'pull_request'
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
const { execSync } = require('child_process');
// Run SwiftLint and capture output
let output;
try {
output = execSync('swiftlint lint --reporter emoji', { encoding: 'utf8' });
} catch (error) {
output = error.stdout;
}
// Create or update comment
const body = `## š SwiftLint Results\n\n\`\`\`\n${output}\n\`\`\`\n\nā View detailed report in the Actions artifacts.`;
// Find existing comment
const { data: comments } = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
});
const botComment = comments.find(comment =>
comment.user.type === 'Bot' &&
comment.body.includes('š SwiftLint Results')
);
if (botComment) {
// Update existing comment
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: botComment.id,
body: body
});
} else {
// Create new comment
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: body
});
}
Method 3: Changed Files Only (Performance Optimization)
name: SwiftLint (Changed Files)
on:
pull_request:
paths:
- '**.swift'
jobs:
swiftlint-diff:
name: Lint Changed Files
runs-on: macos-latest
steps:
- name: Checkout PR
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install SwiftLint
run: brew install swiftlint
- name: Get Changed Swift Files
id: changed-files
run: |
# Get list of changed Swift files
CHANGED_FILES=$(git diff --name-only --diff-filter=d origin/${{ github.base_ref }}...HEAD | grep ".swift$" || true)
if [ -z "$CHANGED_FILES" ]; then
echo "No Swift files changed"
echo "files=" >> $GITHUB_OUTPUT
else
# Convert to space-separated list
FILES_LIST=$(echo "$CHANGED_FILES" | tr '\n' ' ')
echo "files=$FILES_LIST" >> $GITHUB_OUTPUT
echo "Changed files: $FILES_LIST"
fi
- name: Lint Changed Files
if: steps.changed-files.outputs.files != ''
run: |
FILES="${{ steps.changed-files.outputs.files }}"
echo "Linting changed files..."
for file in $FILES; do
echo "Checking: $file"
swiftlint lint --path "$file" --strict
done
- name: Success Message
if: success()
run: |
echo "ā
All changed files pass SwiftLint!"
Handling SwiftLint Violations
Disabling Rules Inline
Sometimes you need to disable specific rules for legitimate reasons:
// Disable rule for next line
// swiftlint:disable:next force_cast
let name = json["name"] as! String
// Disable rule for block
// swiftlint:disable force_try
let data = try! JSONEncoder().encode(model)
// swiftlint:enable force_try
// Disable multiple rules
// swiftlint:disable line_length force_unwrapping
func veryLongFunctionName(parameter1: String, parameter2: String, parameter3: String) {
let value = dictionary["key"]!
}
// swiftlint:enable line_length force_unwrapping
// Disable for entire file
// swiftlint:disable all
// Legacy code - will refactor later
Use Inline Disabling Sparingly
Disabling rules should be the exception, not the rule. Always add a comment explaining why you're disabling a rule. Consider if there's a better way to structure the code instead.
Team Adoption Strategy
Rolling out SwiftLint to an existing project with a large codebase requires strategy:
Phase 1: Audit (Week 1)
- Run SwiftLint on entire codebase
- Generate report:
swiftlint lint --reporter html > report.html - Review violations with team
- Identify quick wins (auto-fixable issues)
Phase 2: Auto-Fix (Week 2)
# Auto-fix what can be fixed
swiftlint --fix --format
# Review changes
git diff
# Commit fixes
git add .
git commit -m "Apply SwiftLint auto-fixes"
Phase 3: Gradual Enforcement (Weeks 3-4)
- Start with warning-only mode
- Enable in CI but don't block merges initially
- Fix violations in new code first
- Create tickets for legacy code cleanup
Phase 4: Full Enforcement (Week 5+)
- Enable strict mode in CI
- Block PRs with violations
- Add pre-commit hooks
- Make it part of Definition of Done
Monitoring and Reporting
Generate Reports
# HTML Report (visual)
swiftlint lint --reporter html > swiftlint-report.html
# JSON Report (for parsing)
swiftlint lint --reporter json > swiftlint-report.json
# JUnit Report (for CI systems)
swiftlint lint --reporter junit > swiftlint-junit.xml
# Markdown Report (for documentation)
swiftlint lint --reporter markdown > SWIFTLINT.md
Track Metrics Over Time
Create a script to track improvement:
#!/bin/bash
# Track SwiftLint metrics over time
DATE=$(date +%Y-%m-%d)
OUTPUT_DIR="metrics"
mkdir -p $OUTPUT_DIR
# Run SwiftLint and count violations
VIOLATIONS=$(swiftlint lint --quiet 2>&1 | wc -l)
WARNINGS=$(swiftlint lint --quiet 2>&1 | grep "warning:" | wc -l)
ERRORS=$(swiftlint lint --quiet 2>&1 | grep "error:" | wc -l)
# Append to CSV
echo "$DATE,$VIOLATIONS,$WARNINGS,$ERRORS" >> $OUTPUT_DIR/lint-history.csv
# Print summary
echo "š SwiftLint Metrics for $DATE"
echo " Total Violations: $VIOLATIONS"
echo " Warnings: $WARNINGS"
echo " Errors: $ERRORS"
# Show trend
if [ -f "$OUTPUT_DIR/lint-history.csv" ]; then
echo ""
echo "š Last 5 runs:"
tail -5 $OUTPUT_DIR/lint-history.csv | column -t -s,
fi
Best Practices & Pro Tips
SwiftLint Best Practices
- Start with default rules and customize gradually
- Run SwiftLint in both Xcode builds and CI/CD
- Use
--fixto automatically fix violations - Document why you disable specific rules
- Review and update .swiftlint.yml regularly
- Make SwiftLint part of onboarding for new developers
- Use custom rules for project-specific conventions
- Generate reports to track code quality trends
- Don't disable rules globally if only needed in one place
- Keep SwiftLint version consistent across team
Performance Tips
- Use
--cache-pathto speed up repeated runs - Exclude generated files and dependencies
- In CI, only lint changed files on PRs
- Use SwiftLint 0.50+ for improved performance
Troubleshooting Common Issues
Issue: SwiftLint Not Found in Xcode Build
# Add PATH to run script
export PATH="$PATH:/opt/homebrew/bin:/usr/local/bin"
if which swiftlint >/dev/null; then
swiftlint
else
echo "warning: SwiftLint not found"
fi
Issue: Too Many Violations
Gradually introduce rules:
# Start lenient, gradually enable rules
disabled_rules:
- line_length
- function_body_length
- type_body_length
- file_length
- cyclomatic_complexity
# Only enforce critical rules initially
only_rules:
- force_cast
- force_try
- force_unwrapping
Issue: Slow CI Builds
Use caching and parallel execution:
- name: SwiftLint with Cache
run: |
swiftlint lint --cache-path .swiftlint-cache --parallel
Conclusion
SwiftLint is an essential tool for maintaining code quality in Swift projects. By integrating it into your Xcode builds, Git workflow, and CI/CD pipeline, you ensure consistent, high-quality code across your entire team.
Key Takeaways:
- ā Install SwiftLint via Homebrew or package managers
- ā Configure with .swiftlint.yml to match your team's style
- ā Integrate with Xcode builds for immediate feedback
- ā Add pre-commit hooks to prevent bad commits
- ā Automate with GitHub Actions for PR validation
- ā Roll out gradually to avoid overwhelming the team
- ā Use auto-fix to resolve violations quickly
- ā Monitor metrics to track code quality improvements
Start small, iterate often, and watch your code quality improve over time. SwiftLint catches the small issues so your team can focus on solving the big problems.