Xavier Lowmiller's Blog

software developer • vegetarian • pub quiz organizer • star wars • bob dylan • member of gryffindor

SwiftLint For Teams

SwiftLint is an indispensible tool in the arsenal of the modern iOS developer. If you have more than a single iOS developer on the team, chances are you are already using it to enforce style guides and perform linting. Our iOS Team discusses new rules on a regular basis and decides which ones make sense for our style guide.

However, if you are working on multiple projects at the same time, updating the .swiftlint.yml files can become a hassle. Luckily, there’s a way out of this repetitive task.

Hosting and loading the shared config

We use a dedicated repository to host our coding guidelines and best practices. Part of that repo is our default .swiftlint.yml file that we keep updated.

Luckily, most git providers make it easy to download files via an API: Here’s how to get a file from GitHub:

SWIFTLINT_CONFIG_PATH="/tmp/.swiftlint.yml"

curl -s https://raw.github.com/realm/SwiftLint/.swiftlint.yml -o $SWIFTLINT_CONFIG_PATH

(Notice the raw.github.com)

GitLab provides a similar API. If your provider does not have an API, you can use the git-archive command:

git archive --remote=git://yourGitRepo.com/<user>/<repo> .swiftlint.yml | tar -x -C /tmp

Using the shared file for linting

Using the file is a trivial matter:

/usr/local/bin/swiftlint autocorrect --config ${SWIFTLINT_CONFIG_PATH}
/usr/local/bin/swiftlint lint --config ${SWIFTLINT_CONFIG_PATH}

You can extract all of this into a script (say, run-swiftlint.sh) and replace your SwiftLint build phase with an invocation of this script (or just paste it in the Run Script phase).

Be aware that this will download the .swiftlint.yml on every single build. Since this will slow local builds and rate limiting is a thing with most git providers, this is a bad idea.

In production, we use a more sophisticated script. It will try to download the file at most every 24 hours, or if it was deleted. Furthermore, it employs caching because If-Modified-Since requests do not count against the rate limit.

Overriding rules locally

SnapshotHelper project setup

So what do you do when the new shared configuration introduces a lot of errors to a project so that it isn’t feasible to fix them right now? Since the new config is shared, you can’t just disable the rules temporarily. Luckily, SwiftLint has a feature that makes this easy:
nested configurations.

Consider the following project setup:

SnapshotHelper project setup

Let’s say your style guide forbids force unwrapping because you never ever want the app to crash in production. Your .swiftlint.yml file then might contain a rule that forbids them and produces errors:

opt_in_rules:
  - force_unwrapping

Now, how do you allow force unwrapping in tests? You could use a //swiftlint:disable force_unwrapping every time you want to use a !, but this gets boring quickly. Leveraging SwiftLint’s nested configurations, this is as easy as adding another .swiftlint.yml file to your Tests folder that disables the force_unwrapping rule again for this subfolder:

disabled_rules:
  - force_unwrapping

You can now force unwrap in your test files like there’s no tomorrow!

This feature can be used to disable rules that are new to the shared config but break the local project. Unfortunately, I haven’t found a way to put a .swiftlint.yml file in the root folder of the project since that one is overridden by the --config parameter in the script, so you have to create separate ones for the Sources and Tests directories.

⚠️ Limitations

include and exclude will be ignored in nested configurations, so you probably should have folders like Pods or Carthage in the shared configuration.

Recap

This is the script we currently use across our team:

#!/bin/bash

SWIFTLINT_CONFIG_PATH=".swiftlint.yml"
TEMP_FILE=".temp.yml"

# If the file does not exist or is older than 1 day, download it
if [[ ! -f $SWIFTLINT_CONFIG_PATH ]] || [[ $(find "$SWIFTLINT_CONFIG_PATH" -mtime +1 -print) ]]; then
    last_modified=$(date -u -r $SWIFTLINT_CONFIG_PATH "+%a, %d %b %Y %H:%M:%S GMT" 2>/dev/null || true)
    http_code=$(curl \
        -H 'Accept: application/vnd.github.v4.raw' \
        -H "If-Modified-Since: $last_modified" \
        -w "%{http_code}" \
        -o $TEMP_FILE \
        https://api.github.com/repos/adorsys/csi-coding-guidelines/contents/iOS/.swiftlint.default.yml)

    if [[ 200 == "$http_code" ]]; then
        mv $TEMP_FILE $SWIFTLINT_CONFIG_PATH
    else
        rm -f $TEMP_FILE
    fi
fi

if [[ -e $SWIFTLINT_CONFIG_PATH ]]; then
    "${PODS_ROOT}/SwiftLint/swiftlint" autocorrect --config ${SWIFTLINT_CONFIG_PATH}
    "${PODS_ROOT}/SwiftLint/swiftlint" lint --config ${SWIFTLINT_CONFIG_PATH}
fi

You can also find the current version on our GitHub.

Posted 09 Jul 2018