I admit I like to work in a clean and (consistently) structured codebase. For this reason, when working with multiple people in one codebase, a linter and/or formatter is inevitable. For a couple of years, I’ve been utilizing a combination of SwiftLint and SwiftFormat. Most people rely solely on either SwiftLint or SwiftFormat, but I especially like the combination of SwiftLint AND SwiftFormat.
How to Get There
For any new project I am starting or joining (given that there is no other linter/formatter in place), I would introduce a .swiftlint.yml
and .swiftformat
configuration file to the root folder of the project. You can find my default configuration files here and here. Adding these files would be sufficient to lint/format your codebase locally after installing SwiftLint and SwiftFormat on your machine. The latter could be achieved via brew.
Versioning
The downside of this type of installation is that it doesn’t come with a mechanism for versioning. So the version I am installing and using could be different from the version my peers are using on their machines. Additionally, the CI/CD would either use the latest or yet another version of these tools. Which would end up in unnecessarily lint errors on the CI/CD. To overcome this I found a tool by Yonas Kolb (GitHub, Twitter) called “Mint”. With this, you can easily lock the versions of SwiftLint and SwiftFormat with a file called Mintfile
at the root of your project:
Mintfile
# Source https://github.com/yonaskolb/Mint
nicklockwood/SwiftFormat@0.49.2
realm/SwiftLint@0.46.1
⚠️ After installing Mint and adding the
Mintfile
, you need to call SwiftLint and SwiftFormat viamint run swiftlint ...
ormint run swiftformat ...
respectively. Otherwise, the version is not locked via Mint.
Making Use of It within Xcode Build Phases
The first stage of linting is within the Build Phases of Xcode. Here only SwiftLint is run; otherwise, SwiftFormat would mess with our code on every build, which would be very annoying. The following snippet runs SwiftLint on every build, which results in warnings/errors at the corresponding line of code. With this, SwiftLint warnings/errors can be detected and fixed while coding.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
if [ "$CI" = 'true' ]; then
echo 'CI environment detected. Not running SwiftLint.'
elif [ "$ACTION" = 'build' ] && [ "$CONFIGURATION" = 'Debug' ]; then
echo "No CI environment detected. 'Build' action with 'Debug' configuration detected. Running SwiftLint..."
export PATH="$PATH:/opt/homebrew/bin" # Adds support for Apple Silicon brew directory
if mint which swiftlint; then
mint run swiftlint
else
echo 'warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint'
fi
else
echo 'No CI environment detected. Nevertheless, not running SwiftLint.'
fi
Making Use of It within Pre-commit Hook
The next stage is running SwiftFormat while committing. If SwiftFormat detects a rule violation, it will block the commit and automatically reformat the code for us to verify the changes and commit again.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#!/bin/sh
FILES_REFORMATTED=0
echo "[PRE-COMMIT] Linting project"
GIT_DIFF=$(git diff --diff-filter=d --staged --name-only)
while read FILE; do
#
# PLACEHOLDER: Xcode project file sorting
# see https://hoppsen.com/posts/sort-Xcode-project-file/#automate-the-sorting
#
if [[ "$FILE" == *\.swift ]]; then
# export PATH to find mint
export PATH="/opt/homebrew/bin:$PATH"
BEFORE_SWIFTFORMAT_CHECKSUM=$(cat "$FILE" | shasum)
mint run swiftformat "${FILE}" --config .swiftformat &> /dev/null
AFTER_SWIFTFORMAT_CHECKSUM=$(cat "$FILE" | shasum)
if [[ "$BEFORE_SWIFTFORMAT_CHECKSUM" != "$AFTER_SWIFTFORMAT_CHECKSUM" ]]; then
echo "[PRE-COMMIT] [Warning] $FILE reformatted automatically. Please review, stage and commit"
((FILES_REFORMATTED++))
fi
fi
done <<< "$GIT_DIFF"
if [[ "$FILES_REFORMATTED" -gt 0 ]]; then
exit 1
fi
exit 0
Read more about the placeholder and how to use pre-commit hook: Hassle Free Merging of the Xcode Project File by Sorting It.
Making Use of It within Github Actions
Whereas the first two stages are somewhat optional and solely prevent us from running into too many CI/CD linter errors, this is the place where the magic happens. The below YAML file configures to run SwiftLint and SwiftFormat on every pull request commit, ensuring that all the code is clean and formatted as intended before being merged into the main branch.
The critical point about this snippet is that it uses our previously introduced Mintfile
. It reads the desired version and builds it on the first run. After this initial build of one to three minutes, the time for linting goes down to ~10-15 seconds for each run, thanks to caching.
Experienced users of GitHub Actions know that running Actions on macOS takes 10x more minutes than on Ubuntu. Therefore, this linting file uses Ubuntu to save valuable CI/CD minutes.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
name: Linting
on: pull_request
jobs:
SwiftLint:
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v2
- name: Read version from Mintfile
run: echo "SWIFTLINT_VERSION=$(awk '/SwiftLint/ {split($0, a, "@"); print a[2]}' Mintfile)" >> $GITHUB_ENV
- uses: actions/cache@v2
with:
path: .build/release/swiftlint
key: $-linting-swiftlint-$
- name: Build realm/SwiftLint
run: |
if [ -f ".build/release/swiftlint" ]; then
sudo cp -f .build/release/swiftlint /usr/local/bin/swiftlint
else
git clone --depth 1 --branch $ https://github.com/realm/SwiftLint
cd SwiftLint
swift build --disable-sandbox -c release
mv .build .. && cd ..
rm -rf SwiftLint
sudo cp -f .build/release/swiftlint /usr/local/bin/swiftlint
fi
- name: SwiftLint
run: |
swiftlint --version
swiftlint lint --strict --config .swiftlint.yml --quiet
env:
LINUX_SOURCEKIT_LIB_PATH: /usr/share/swift/usr/lib # FIXED: Fatal error: Loading libsourcekitdInProc.so failed
SwiftFormat:
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v2
- name: Read version from Mintfile
run: echo "SWIFTFORMAT_VERSION=$(awk '/SwiftFormat/ {split($0, a, "@"); print a[2]}' Mintfile)" >> $GITHUB_ENV
- uses: actions/cache@v2
with:
path: .build/release/swiftformat
key: $-linting-swiftformat-$
- name: Build nicklockwood/SwiftFormat
run: |
if [ -f ".build/release/swiftformat" ]; then
if ! [ -x "$(command -v swift-format)" ]; then
sudo cp -f .build/release/swiftformat /usr/local/bin/swiftformat
fi
else
git clone --depth 1 --branch $ https://github.com/nicklockwood/SwiftFormat
cd SwiftFormat
swift build --disable-sandbox -c release
mv .build .. && cd ..
rm -rf SwiftFormat
sudo cp -f .build/release/swiftformat /usr/local/bin/swiftformat
fi
- name: SwiftFormat
run: |
swiftformat --version
swiftformat . --config .swiftformat --lint
Conclusion
Linting is a common practice when building apps with multiple people. SwiftLint and SwiftFormat, in the various stages, play very well together to get a more readable codebase in the project.
Feel free to contact me or tweet me on Twitter if you have any additional tips or feedback.
Thanks!