SwiftFormat Automation for iOS apps

Article Agenda

  1. What is SwiftFormat?
  2. An overview of SwiftFormat integrations
    1. CLI tool
    2. Xcode format on save
    3. Build phase
    4. Pre-commit hook on local machines
    5. Post-receive hook on CI
    6. Danger Ruby plugin
    7. Why use SwiftFormat?
    8. Overview of pre-commit hook solution

What is SwiftFormat?

SwiftFormat is a code library and command-line tool for reformatting Swift code on macOS, Linux or Windows, according to their GitHub documentation.

SwiftFormat is more powerful than another very similar Swift command line utility called SwiftLint. SwiftFormat is a code formatting tool that extends SwiftLint's static analysis (linting) capability. The difference here is that SwiftFormat has a more powerful autocorrect capability than SwiftLint, so it is able to fix more issues, even though SwiftLint may be able to identify a wider range of issues, such as file length, and cyclomatic complexity.

In this article, I'll go through various options to implement SwiftFormat on a production project rather than how to integrate the tool locally (this is relatively straightforward). This is because I believe the tool provides greater benefit as an automation for every developer on the team, rather than a few only. We'll also cover the relative advantages or disadvantages. I hope that this will help you choose the right tool for your project!

SwiftFormat Integrations

CLI Tool

There are various ways to integrate the SwiftFormat Command Line tool, including Homebrew, Mint and building from source, as outlined in the link above.

It's straightforward to run the tool with:

$ swiftformat .

The disadvantage here is that we lose the ability to manage SwiftFormat tool versions that we get if we integrated the tool as a CocoaPod or Swift Package with SPM. We also don't enforce the tool on every developer machine, and integrate it into the development lifecycle. As a result, I won't suggest this approach for production apps.

Xcode source editor extension

We can install the Xcode extension with:

$ brew install --cask swiftformat-for-xcode

as outlined in the above link. Once you have launched the app and restarted Xcode, you'll find a SwiftFormat option under Xcode's Editor menu.

The disadvantage here is that we don't automate the SwiftFormat tool and enforce it's usage during the development lifecycle because the developer has to manually trigger the tool. Furthermore we cannot enforce the tool gets installed on every developer's machine. As a result, I won't suggest this approach for production apps.

Xcode build phase

There are 3 options outlined in the link above for installation as an Xcode build phase: SPM, CocoaPods and Locally Installed SwiftFormat. Please refer to this official documentation for detailed instructions.

Out of the three, I suggest CocoaPods for faster build times. SPM makes use of swift run -c release swiftformat "$SRCROOT" which has the annoying side-effect of building the tool from source before format. CocoaPods uses a cached tool in the Pods folder thus improving build times, and still maintaining the advantages of using a dependency manager. Unlike the locally installed CLI tool which also has faster build times, CocoaPods enables us to manage and enforce versions on a project-wide basis.

Running SwiftFormat as an Xcode Build Phase means the undo history gets lost when building the app. This can be painful and slow down development. Furthermore, we may forget to build the app before committing changes so formatting issues may not be included for every commit.

SPM Plugin

Note that using the SPM SwiftFormat Plugin rather than the Package itself is much faster as it doesn't build from source.

As a result, it can be helpful during development, but doesn't automate the tool. The same argument applies for the other SwiftFormat text editor extensions for VSCode, Sublime Text and Nova.

Git pre-commit hook

Once installed as outlined in the official documentation above, the pre-commit hook will now run whenever you run git commit. Running git commit --no-verify will skip the pre-commit hook.

A pain point with Git pre-commit hooks are Merge Commits. Merge commits can add additional formatting changes in addition to the original changes. This can create noise for Pull Requests and needs to be managed in the short term either by disabling SwiftFormat from Merge Commits manually (using --no-verify), or running the tool on the whole project in one PR or a set of PRs dedicated for formatting changes.

We can share pre commit hooks on every machine using Husky which is an NPM library, however it's hard to enforce SwiftFormat on every developer machine without bringing in additional libraries. Another option is this library which works without NPM.

Post-receive hook on CI

There are numerous articles online that show how to run a script on the CI server using the post-receive hook e.g. The simple way to integrate Continuous-Integration(CI/CD) with Git hooks. This has the advantage that formatting changes will never be missed from the commit history with automation, however it may be inconvenient to have to pull changes from the server after making every commit especially because the new commit may not be pushed immediately. Also, it may not be practical or scalable to set up every CI machine with post-receive hooks, because we need to manually set up each machine for enabling post-receive hooks.

Danger Ruby plugin on CI

As outlined in the above SwiftFormat documentation, we can use the Danger Ruby plugin by adding the danger-swiftformat plugin to our Gemfile.

However, not every project can make use of the SwiftFormat Danger Ruby plugin. This is because the project may already make use of DangerJS and associated plugins. The SwiftFormat plugin doesn't integrate seamlessly with the existing CI infrastructure and furthermore we run into similar issues described for the post-receive hook. These include potential delays applying formatting changes in separate CI commits and having to synchronise local branches with the remote regularly during development.

Why use SwiftFormat?

I believe the benefits of SwiftFormat outweigh its disadvantages because the disadvantages can be mitigated in the short term, and the long-term benefits outweigh the short term friction.

Benefits

  1. SwiftFormat and SwiftLint configs codify a standardised style guide for the codebase. So rather than having to worry about enforcing a style guide after deciding on one, we can automate best practice by deploying the tool on the project. Individual developers may have their own personal style preferences that differ from others on the team, and using this tool enables every developer to align to the project's specific conventions automatically.
  2. Static analysis tools improve code quality in the codebase and pay dividends over time, even if the initial deployment stages can be painful. SwiftFormat helps us to follow the KISS principle with numerous rules to remove implicit self, remove unnecessary weak keywords and much more.
  3. Integrating SwiftFormat into CI, Git or the Build Process enables us to catch formatting/style issues before they are added to the codebase and automatically enforces it without additional effort. They reduce the number of comments during code review and thus improve developer velocity. Developers get more time to focus on more important issues such as testing, architecture and design patterns!

Disadvantage

It can take time to agree on a common set of rules for the project to follow, what folders to exclude, and figuring out the optimal solution for the specific codebase. In the short term, we need to monitor feedback from the wider team to ensure a stable transition.

My favourite SwiftFormat solution

I have successfully deployed SwiftFormat on multiple projects and formatted all files on the project through the typical PR merging process.

I considered all approaches for deployment in the early stages, but decided on the pre-commit hook solution in the end because it was the easiest to deploy for my current setup and provided most of the automation benefits I was after.

I made use of the NPM husky package which was already integrated in the app, adding the following changes to our package.json to lint only the staged files:

"husky": {
    "hooks": {
      "pre-commit": "lint-staged"
    }
  },
  "lint-staged": {
    "ios/**/*.swift": [
      "ios/Pods/SwiftFormat/CommandLineTool/swiftformat",
      "ios/Pods/SwiftLint/swiftlint autocorrect",
      "git add"
    ]
  }

I came up with a solution to integrate SwiftLint autocorrect after applying SwiftFormat into the commit pipeline, so that the tools don't have conflicting changes. SwiftLint was now integrated deeply within the app both as a build phase and pre-commit hook.

Others found the tool painful to work with in the beginning because of Merge Commits, as outlined above. I worked around this by disabling hooks on merge commits and also creating an Open PR to apply swiftformat to all remaining files once the majority of the project had been formatted organically.

I collated feedback from other developers to align on rules and agreed on the following .swiftlint.yml:

# File options, update these for your project
--exclude Pods,Project/GraphQL/CodeGen/generated,ProjectTests/Source/AutoMocks

# Format options
--commas always
--stripunusedargs closure-only
--ranges no-space
--patternlet inline
--closurevoid preserve
--decimalgrouping 3,5

--disable strongoutlets, wrapMultilineStatementBraces, andOperator, extensionAccessControl

We experienced friction during the early stages, but worked around this by updating the config as we went along, for example to exclude certain folder paths for excluding generated files from formatting and to disable certain rules. I disabled the strong outlets rule because I found some outlets were in fact causing memory cycles. Other rules were disabled because they were found to provide negligible cosmetic improvements.

Overall though, I found that we have fewer PR comments around style changes and velocity has improved! Teams have voted to maintain usage of the tool as the tool is a well established best practice for large codebases, and because they worked through the major teething issues.

Resources

  1. https://github.com/nicklockwood/SwiftFormat
  2. https://medium.com/@Dungeon_Master/the-simple-way-to-integrate-continuous-integration-ci-cd-with-git-hooks-f6e871b14856
  3. https://github.com/garriguv/danger-ruby-swiftformat