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!
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.
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.
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.
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.
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.
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.
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.
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.
weak
keywords and much more.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.
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.