I just left a hipster coffee shop. It was packed with iOS devs, whispering amongst each other about how they can’t wait for Apple to release an official style guide and formatter for Swift.
Lately, the community has been buzzing about the proposal from Tony Allevato and Dave Abrahams to adopt an official style guide and formatting tool for the Swift language.
Hundreds of community members have weighed in on the initial pitch and proposal. As with all matters of style, opinions are strong, and everybody has one. Fortunately, the discourse from the community has been generally constructive and insightful, articulating a diversity of viewpoints, use cases, and concerns.
Since our article was first published back in March, the proposal, “SE-0250: Swift Code Style Guidelines and Formatter” started formal review; that process was later suspended, to be reconsidered sometime in the future.
In spite of this,
Swift code formatting remains a topic of interest to many developers.
So this week on NSHipster,
we’re taking another look at the current field of
Swift formatters available today —
including the swift-format
tool released as part of the proposal —
and see how they all stack up.
From there,
we’ll take a step back and try to put everything in perspective.
But first, let’s start with a question:
What is Code Formatting?
For our purposes,
we’ll define code formatting
as any change made to code that makes it easier to understand
without changing its behavior.
Although this definition extends to differences in equivalent forms,
(e.g. [Int]
vs. Array<Int>
),
we’ll limit our discussion here to whitespace and punctuation.
Swift, like many other programming languages, is quite liberal in its acceptance of newlines, tabs, and spaces. Most whitespace is insignificant, having no effect on the code around from the compiler’s point of view.
When we use whitespace to make code more comprehensible without changing its behavior, that’s an example of secondary notation; the primary notation, of course, being the code itself.
Put enough semicolons in the right places, and you can write pretty much anything in a single line of code. But all things being equal, why not use horizontal and vertical whitespace to visually structure code in a way that’s easier for us to understand, right?
Unfortunately, the ambiguity created by the compiler’s accepting nature of whitespace can often cause confusion and disagreement among programmers: “Should I add a newline before a curly bracket? How do I break up statements that extend beyond the width of the editor?”
Organizations often codify guidelines for how to deal with these issues, but they’re often under-specified, under-enforced, and out-of-date. The role of a code formatter is to automatically enforce a set of conventions so that programmers can set aside their differences and get to work solving actual problems.
Formatter Tool Comparison
The Swift community has considered questions of style from the very beginning. Style guides have existed from the very first days of Swift, as have various open source tools to automate the process of formatting code to match them.
To get a sense of the current state of Swift code formatters, we’ll take a look at the following four tools:
To establish a basis of comparison, we’ve contrived the following code sample to evaluate each tool (using their default configuration):
structShipping Address:Codable{varrecipient:Stringvarstreet Address:Stringvarlocality:Stringvarregion:String;varpostal Code:Stringvarcountry:Stringinit(recipient:String,street Address:String,locality:String,region:String,postal Code:String,country:String){self.recipient=recipientself.street Address=street Addressself.locality=localityself.region=region;self.postal Code=postal Codeguardcountry.count==2,country==country.uppercased()else{fatal Error("invalid country code")}self.country=country}}letapple Park=Shipping Address(recipient:"Apple, Inc.",street Address:"1 Apple Park Way",locality:"Cupertino",region:"CA",postal Code:"95014",country:"US")
Although code formatting encompasses a wide range of possible syntactic and semantic transformations, we’ll focus on newlines and indentation, which we believe to be baseline requirements for any code formatter.
SwiftFormat
First up is SwiftFormat, a tool as helpful as it is self-descriptive.
Installation
SwiftFormat is distributed via Homebrew as well as Mint and CocoaPods.
You can install it by running the following command:
$ brew install swiftformat
In addition, SwiftFormat also provides an Xcode Source Editor Extension, found in the EditorExtension, which you can use to reformat code in Xcode. Or, if you’re a user of VSCode, you can invoke SwiftFormat with this plugin.
Usage
The swiftformat
command formats each Swift file
found in the specified file and directory paths.
$ swiftformat Example.swift
SwiftFormat has a variety of rules that can be configured either individually via command-line options or using a configuration file.
Example Output
Running the swiftformat
command on our example
using the default set of rules produces the following result:
// swiftformat version 0.40.8structShipping Address:Codable{varrecipient:Stringvarstreet Address:Stringvarlocality:Stringvarregion:String;varpostal Code:Stringvarcountry:Stringinit(recipient:String,street Address:String,locality:String,region:String,postal Code:String,country:String){self.recipient=recipientself.street Address=street Addressself.locality=localityself.region=region;self.postal Code=postal Codeguardcountry.count==2,country==country.uppercased()else{fatal Error("invalid country code")}self.country=country}}letapple Park=Shipping Address(recipient:"Apple, Inc.",street Address:"1 Apple Park Way",locality:"Cupertino",region:"CA",postal Code:"95014",country:"US")
As you can see,
this is a clear improvement over the original.
Each line is indented according to its scope,
and each declaration has consistent spacing between punctuation.
Both the semicolon in the property declarations
and the newline in the initializer parameters are preserved;
however, the closing curly braces aren’t moved to separate lines
as might be expectedthis is fixed in 0.39.5.
Great work, Nick!
Performance
SwiftFormat is consistently the fastest of the tools tested in this article, completing in a few milliseconds.
$time swiftformat Example.swift
0.03 real 0.01 user 0.01 sys
SwiftLint
Next up is,
SwiftLint,
a mainstay of the Swift open source community.
With over 100 built-in rules,
SwiftLint can perform a wide variety of checks on your code —
everything from preferring Any
over class
for class-only protocols
to the so-called “Yoda condition rule”,
which prescribes variables to be placed on
the left-hand side of comparison operators
(that is, if n == 42
not if 42 == n
).
As its name implies, SwiftLint is not primarily a code formatter; it’s really a diagnostic tool for identifying convention violation and API misuse. However, by virtue of its auto-correction faculties, it’s frequently used to format code.
Installation
You can install SwiftLint using Homebrew with the following command:
$ brew install swiftlint
Alternatively, you can install SwiftLint with
CocoaPods,
Mint,
or as a standalone installer package (.pkg
).
Usage
To use SwiftLint as a code formatter,
run the autocorrect
subcommand
passing the --format
option
and the files or directories to correct.
$ swiftlint autocorrect --format--path Example.swift
Example Output
Running the previous command on our example yields the following:
// swiftlint version 0.32.0structShipping Address:Codable{varrecipient:Stringvarstreet Address:Stringvarlocality:Stringvarregion:String;varpostal Code:Stringvarcountry:Stringinit(recipient:String,street Address:String,locality:String,region:String,postal Code:String,country:String){self.recipient=recipientself.street Address=street Addressself.locality=localityself.region=region;self.postal Code=postal Codeguardcountry.count==2,country==country.uppercased()else{fatal Error("invalid country code")}self.country=country}}letapple Park=Shipping Address(recipient:"Apple, Inc.",street Address:"1 Apple Park Way",locality:"Cupertino",region:"CA",postal Code:"95014",country:"US")
SwiftLint cleans up the worst of the indentation and inter-spacing issues but leaves other, extraneous whitespace intact (though it does strip the file’s leading newline, which is nice). Again, it’s worth noting that formatting isn’t SwiftLint’s primary calling; if anything, it’s merely incidental to providing actionable code diagnostics. And taken from the perspective of “first, do no harm”, it’s hard to complain about the results here.
Performance
For everything that SwiftLint checks for, it’s remarkably snappy — completing in a fraction of a second for our example.
$time swiftlint autocorrect --quiet--format--path Example.swift
0.09 real 0.04 user 0.01 sys
Prettier with Swift Plugin
Installation, Usage, Example Output, and Performance
If you’ve mostly shied away from JavaScript (as discussed in last week’s article), this may be the first you’ve heard of Prettier. On the other hand, if you’re steeped in the world of ES6, React, and WebPack, you’ve almost certainly come to rely on it.
Prettier is unique among code formatters in that it optimizes — first and foremost — for aesthetics, wrapping lines of code onto newlines as if they were poetry.
Thanks to its (in-development) plugin architecture, the same line-breaking behavior can be applied to other languages, including Swift.
Installation
To use Prettier and its plugin for Swift, you’ll have to wade into the murky waters of the Node packaging ecosystem. There are a few different approaches to get everything installed (because of course there are), but Yarn is our favorite 😻.
$ brew install yarn
$ yarn global add prettier prettier/plugin-swift
Usage
With the prettier
command-line tool accessible from our $PATH
,
run it with one or more file or directory paths.
$ prettier Example.swift
Example Output
Here’s the result of running the latest build of the Swift plugin with Prettier on our example from before:
// Swift version 4.2// prettier version 1.17.1// prettier/plugin-swift version 0.0.0 (bdf8726)structShipping Address:Codable{varrecipient:Stringvarstreet Address:Stringvarlocality:Stringvarregion:Stringvarpostal Code:Stringvarcountry:Stringinit(recipient:String,street Address:String,locality:String,region:String,postal Code:String,country:String){self.recipient=recipientself.street Address=street Addressself.locality=localityself.region=region;self.postal Code=postal Codeguardcountry.count==2,country==country.uppercased()else{fatal Error("invalid country code")}self.country=country}}letapple Park=Shipping Address(recipient:"Apple, Inc.",street Address:"1 Apple Park Way",locality:"Cupertino",region:"CA",postal Code:"95014",country:"US")
Prettier describes itself to be “An opinionated code formatter”. In practice, this means that there isn’t much in the way of configuration; there are only two options: “regular code” and “prettier code”.
Now, you may object to the increase in vertical whitespace, but you’d be lying if you said this code didn’t look amazing. The way that everything is evenly spaced… the way that long lines are wrapped and indented… it’s almost hard to believe that you achieve something like this automatically.
Of course, our caveat from before still applies: This is still very much a work-in-progress and isn’t suitable for production use yet. Also, there’s the matter of performance…
Performance
To put it bluntly: Prettier is an order of magnitude slower than every other tool discussed in this article.
$time prettier Example.swift
0.60 real 0.40 user 0.18 sys
It’s unclear whether this is a consequence of navigating a language barrier or an opportunity for optimization, but Prettier is slow enough to cause problems at scale.
For now, we recommend using Prettier only for one-off formatting tasks, such as writing code for articles and books.
swift-format
Having looked at the current landscape of available Swift formatters,
we now have a reasonable baseline for evaluating the swift-format
tool
proposed by Tony Allevato and Dave Abrahams.
Installation
The code for swift-format
is currently hosted on
the format
branch of Google’s fork of the Swift project.
You can check it out and build it from source by running the following commands:
$ git clone https://github.com/google/swift.git swift-format
$cd swift-format
$ git submodule update --init$ swift build
For your convenience, we’re providing a Homebrew formula that builds from our own fork of Google’s fork, which you can install with the following command:
$ brew install nshipster/formulae/swift-format
Usage
Run the swift-format
command,
passing one or more file and directory paths
to Swift files that you want to format.
$ swift-format Example.swift
The swift-format
command also takes a --configuration
option,
which takes a path to a JSON file.
For now,
the easiest way to customize swift-format
behavior
is to dump the default configuration to a file
and go from there.
$ swift-format -m dump-configuration .swift-format.json
Running the command above populates the specified file with the following JSON:
{"blank Line Between Members":{"ignore Single Line Properties":true},"indentation":{"spaces":2},"line Break Before Control Flow Keywords":false,"line Break Before Each Argument":true,"line Length":100,"maximum Blank Lines":1,"respects Existing Line Breaks":true,"rules":{"All Public Declarations Have Documentation":true,"Always Use Lower Camel Case":true,"Ambiguous Trailing Closure Overload":true,"Avoid Initializers For Literals":true,"Begin Documentation Comment With One Line Summary":true,"Blank Line Between Members":true,"Case Indent Level Equals Switch":true,"Do Not Use Semicolons":true,"Dont Repeat Type In Static Properties":true,"Fully Indirect Enum":true,"Group Numeric Literals":true,"Identifiers Must Be ASCII":true,"Multi Line Trailing Commas":true,"Never Force Unwrap":true,"Never Use Force Try":true,"Never Use Implicitly Unwrapped Optionals":true,"No Access Level On Extension Declaration":true,"No Block Comments":true,"No Cases With Only Fallthrough":true,"No Empty Associated Values":true,"No Empty Trailing Closure Parentheses":true,"No Labels In Case Patterns":true,"No Leading Underscores":true,"No Parens Around Conditions":true,"No Void Return On Function Signature":true,"One Case Per Line":true,"One Variable Declaration Per Line":true,"Only One Trailing Closure Argument":true,"Ordered Imports":true,"Return Void Instead Of Empty Tuple":true,"Use Enum For Namespacing":true,"Use Let In Every Bound Case Variable":true,"Use Only UTF8":true,"Use Shorthand Type Names":true,"Use Single Line Property Getter":true,"Use Special Escape Sequences":true,"Use Synthesized Initializer":true,"Use Triple Slash For Documentation Comments":true,"Validate Documentation Comments":true},"tab Width":8,"version":1}
After fiddling with the configuration —
such as setting line
to the correct value of 80 (don’t @ me)—
you can apply it thusly:
$ swift-format Example.swift --configuration .swift-format.json
Example Output
Using its default configuration,
here’s how swift-format
formats our example:
// swift-format 0.0.1 (2019-05-15, 115870c)structShipping Address:Codable{varrecipient:Stringvarstreet Address:Stringvarlocality:Stringvarregion:String;varpostal Code:Stringvarcountry:Stringinit(recipient:String,street Address:String,locality:String,region:String,postal Code:String,country:String){self.recipient=recipientself.street Address=street Addressself.locality=localityself.region=regionself.postal Code=postal Codeguardcountry.count==2,country==country.uppercased()else{fatal Error("invalid country code")}self.country=country}}letapple Park=Shipping Address(recipient:"Apple, Inc.",street Address:"1 Apple Park Way",locality:"Cupertino",region:"CA",postal Code:"95014",country:"US")
Be still my heart!😍 We could do without the original semicolon, but overall, this is pretty unobjectionable — which is exactly what you’d want from an official code style tool.
Flexible Output
But in order to fully appreciate the elegance of swift-format
’s output,
we must compare it across a multitude of different column widths.
Let’s see how it handles this new code sample,
replete with cumbersome UIApplication
methods
and URLSession
construction:
40 Columns
importUIKit@UIApplication MainclassApp Delegate:UIResponder,UIApplication Delegate{varwindow:UIWindow?funcapplication(_application:UIApplication,did Finish Launching With Optionslaunch Options:[UIApplication.Launch Options Key:Any]?)->Bool{leturl=URL(string:"https://nshipster.com/swift-format")!URLSession.shared.data Task(with:url,completion Handler:{(data,response,error)inguarderror==nil,letdata=data,letresponse=responseas?HTTPURLResponse,(200..<300).contains(response.status Code)else{fatal Error(error?.localized Description??"Unknown error")}iflethtml=String(data:data,encoding:.utf8){print(html)}}).resume()// Override point for customization after application launch.returntrue}}
50 Columns
importUIKit@UIApplication MainclassApp Delegate:UIResponder,UIApplication Delegate{varwindow:UIWindow?funcapplication(_application:UIApplication,did Finish Launching With Optionslaunch Options:[UIApplication.Launch Options Key:Any]?)->Bool{leturl=URL(string:"https://nshipster.com/swift-format")!URLSession.shared.data Task(with:url,completion Handler:{(data,response,error)inguarderror==nil,letdata=data,letresponse=responseas?HTTPURLResponse,(200..<300).contains(response.status Code)else{fatal Error(error?.localized Description??"Unknown error")}iflethtml=String(data:data,encoding:.utf8){print(html)}}).resume()// Override point for customization after application launch.returntrue}}
60 Columns
importUIKit@UIApplication MainclassApp Delegate:UIResponder,UIApplication Delegate{varwindow:UIWindow?funcapplication(_application:UIApplication,did Finish Launching With Optionslaunch Options:[UIApplication.Launch Options Key:Any]?)->Bool{leturl=URL(string:"https://nshipster.com/swift-format")!URLSession.shared.data Task(with:url,completion Handler:{(data,response,error)inguarderror==nil,letdata=data,letresponse=responseas?HTTPURLResponse,(200..<300).contains(response.status Code)else{fatal Error(error?.localized Description??"Unknown error")}iflethtml=String(data:data,encoding:.utf8){print(html)}}).resume()// Override point for customization after application launch.returntrue}}
70 Columns
importUIKit@UIApplication MainclassApp Delegate:UIResponder,UIApplication Delegate{varwindow:UIWindow?funcapplication(_application:UIApplication,did Finish Launching With Optionslaunch Options:[UIApplication.Launch Options Key:Any]?)->Bool{leturl=URL(string:"https://nshipster.com/swift-format")!URLSession.shared.data Task(with:url,completion Handler:{(data,response,error)inguarderror==nil,letdata=data,letresponse=responseas?HTTPURLResponse,(200..<300).contains(response.status Code)else{fatal Error(error?.localized Description??"Unknown error")}iflethtml=String(data:data,encoding:.utf8){print(html)}}).resume()// Override point for customization after application launch.returntrue}}
90 Columns
importUIKit@UIApplication MainclassApp Delegate:UIResponder,UIApplication Delegate{varwindow:UIWindow?funcapplication(_application:UIApplication,did Finish Launching With Optionslaunch Options:[UIApplication.Launch Options Key:Any]?)->Bool{leturl=URL(string:"https://nshipster.com/swift-format")!URLSession.shared.data Task(with:url,completion Handler:{(data,response,error)inguarderror==nil,letdata=data,letresponse=responseas?HTTPURLResponse,(200..<300).contains(response.status Code)else{fatal Error(error?.localized Description??"Unknown error")}iflethtml=String(data:data,encoding:.utf8){print(html)}}).resume()// Override point for customization after application launch.returntrue}}
This kind of flexibility isn’t particularly helpful in engineering contexts, where developers can and should make full use of their screen real estate. But for those of us who do technical writing and have to wrestle with things like mobile viewports and page margins, this is a killer feature.
Performance
In terms of performance,
swift-format
isn’t so fast as to feel instantaneous,
but not so slow as to be an issue.
$time swift-format Example.swift
0.24 real 0.16 user 0.14 sys
Conclusion: You Don’t Need To Wait to Start Using a Code Formatting Tool
Deciding which conventions we want to adopt as a community is an important conversation to have, worthy of the thoughtful and serious consideration we give to any proposed change to the language. However, the question of whether there should be official style guidelines or an authoritative code formatting tool shouldn’t stop you from taking steps today for your own projects!
We’re strongly of the opinion that most projects would be improved by the adoption of a code formatting tool, provided that it meets the following criteria:
- It’s stable
- It’s fast (enough)
- It produces reasonable output
And based on our survey of the tools currently available,
we can confidently say that
SwiftFormat
and
swift-format
both meet these criteria,
and are suitable for use in production.
(If we had to choose between the two,
we’d probably go with swift-format
on aesthetic grounds.
But each developer has different preferences
and each team has different needs,
and you may prefer something else.)
While you’re evaluating tools to incorporate into your workflow, you’d do well to try out SwiftLint, if you haven’t already. In its linting capacity, SwiftLint can go a long way to systematically improving code quality — especially for projects that are older and larger and have a large number of contributors.
The trouble with the debate about code style is that its large and subjective. By adopting these tools in our day-to-day workflows today, we not only benefit from better, more maintainable code today, but we can help move the debate forward, from vague feelings to precise observations about any gaps that still remain.