- Notifications
You must be signed in to change notification settings - Fork344
airbnb/swift
Folders and files
Name | Name | Last commit message | Last commit date | |
---|---|---|---|---|
Repository files navigation
Following this style guide should:
- Make it easier to read and begin understanding unfamiliar code.
- Make code easier to maintain.
- Reduce simple programmer errors.
- Reduce cognitive load while coding.
- Keep discussions on diffs focused on the code's logic rather than its style.
Note that brevity is not a primary goal. Code should be made more concise only if other good code qualities (such as readability, simplicity, and clarity) remain equal or are improved.
- This guide is in addition to the officialSwift API Design Guidelines. These rules should not contradict that document.
- These rules should not fight Xcode's^ +I indentation behavior.
- We strive to make every rule lintable:
- If a rule changes the format of the code, it needs to be able to be reformatted automatically (either usingSwiftFormat orSwiftLint autocorrect).
- For rules that don't directly change the format of the code, we should have a lint rule that throws a warning.
- Exceptions to these rules should be rare and heavily justified.
This repo includes a Swift Package Manager command plugin that you can use to automatically reformat or lint your package according to the style guide. To use this command plugin with your package, all you need to do is add this repo as a dependency:
dependencies:[.package(url:"https://github.com/airbnb/swift", from:"1.0.0"),]
and then run theformat
command plugin in your package directory:
$ swift package format
Usage guide
# Supported in Xcode 14+. Prompts for permission to write to the package directory.$ swift package format# When using the Xcode 13 toolchain, or a noninteractive shell, you must use:$ swift package --allow-writing-to-package-directory format# To just lint without reformatting, you can use `--lint`:$ swift package format --lint# By default the command plugin runs on the entire package directory.# You can exclude directories using `exclude`:$ swift package format --exclude Tests# Alternatively you can explicitly list the set of paths and/or SPM targets:$ swift package format --paths Sources Tests Package.swift$ swift package format --targets AirbnbSwiftFormatTool# The plugin infers your package's minimum Swift version from the `swift-tools-version`# in your `Package.swift`, but you can provide a custom value with `--swift-version`:$ swift package format --swift-version 5.3
The package plugin returns a non-zero exit code if there is a lint failure that requires attention.
- In
--lint
mode, any lint failure from any tool will result in a non-zero exit code. - In standard autocorrect mode without
--lint
, only failures from SwiftLint lint-only rules will result in a non-zero exit code.
- Xcode Formatting
- Naming
- Style
- Patterns
- File Organization
- Objective-C Interoperability
- Testing
- Contributors
- Amendments
You can enable the following settings in Xcode by runningthis script, e.g. as part of a "Run Script" build phase.
(link)Each line should have a maximum column width of 100 characters.
(link)Use 2 spaces to indent lines.
(link)Trim trailing whitespace in all lines.
(link)Use PascalCase for type and protocol names, and lowerCamelCase for everything else.
protocolSpaceThing{ // ...}classSpaceFleet:SpaceThing{enumFormation{ // ...}classSpaceship{ // ...}varships:[Spaceship]=[]staticletworldName:String="Earth"func addShip(_ ship:Spaceship){ // ...}}letmyFleet=SpaceFleet()
Exception: You may prefix a private property with an underscore if it is backing an identically-named property or method with a higher access level.
There are specific scenarios where a backing property or method that is prefixed with an underscore could be easier to read than using a more descriptive name.
- Type erasure
publicfinalclassAnyRequester<ModelType>:Requester{publicinit<T:Requester>(_ requester:T)where T.ModelType==ModelType{ _executeRequest= requester.executeRequest}@discardableResultpublicfunc executeRequest( _ request:URLRequest, onSuccess:@escaping(ModelType,Bool)->Void, onFailure:@escaping(Error)->Void)->URLSessionCancellable{return_executeRequest(request, onSuccess, onFailure)}privatelet_executeRequest:(URLRequest,@escaping(ModelType,Bool)->Void,@escaping(Error)->Void)->URLSessionCancellable}
- Backing a less specific type with a more specific type
finalclassExperiencesViewController:UIViewController{ // We can't name this view since UIViewController has a view: UIView property.private lazyvar_view=CustomView() loadView(){ self.view= _view}}
(link)Name booleans like
isSpaceship
,hasSpacesuit
, etc. This makes it clear that they are booleans and not other types.(link)Acronyms in names (e.g.
URL
) should be all-caps except when it’s the start of a name that would otherwise be lowerCamelCase, in which case it should be uniformly lower-cased.// WRONGclassUrlValidator{func isValidUrl(_ URL:URL)->Bool{ // ...}func isProfileUrl(_ URL:URL, for userId:String)->Bool{ // ...}}letURLValidator=UrlValidator()letisProfile=URLValidator.isProfileUrl(URLToTest, userId: IDOfUser)// RIGHTclassURLValidator{func isValidURL(_ url:URL)->Bool{ // ...}func isProfileURL(_ url:URL, for userID:String)->Bool{ // ...}}leturlValidator=URLValidator()letisProfile= urlValidator.isProfileURL(urlToTest, userID: idOfUser)
(link)Names should be written with their most general part first and their most specific part last. The meaning of "most general" depends on context, but should roughly mean "that which most helps you narrow down your search for the item you're looking for." Most importantly, be consistent with how you order the parts of your name.
// WRONGletrightTitleMargin:CGFloatletleftTitleMargin:CGFloatletbodyRightMargin:CGFloatletbodyLeftMargin:CGFloat// RIGHTlettitleMarginRight:CGFloatlettitleMarginLeft:CGFloatletbodyMarginRight:CGFloatletbodyMarginLeft:CGFloat
(link)Include a hint about type in a name if it would otherwise be ambiguous.
// WRONGlettitle:Stringletcancel:UIButton// RIGHTlettitleText:StringletcancelButton:UIButton
(link)Event-handling functions should be named like past-tense sentences. The subject can be omitted if it's not needed for clarity.
// WRONGclassExperiencesViewController{privatefunc handleBookButtonTap(){ // ...}privatefunc modelChanged(){ // ...}}// RIGHTclassExperiencesViewController{privatefunc didTapBookButton(){ // ...}privatefunc modelDidChange(){ // ...}}
(link)Avoid Objective-C-style acronym prefixes. This is no longer needed to avoid naming conflicts in Swift.
// WRONGclassAIRAccount{ // ...}// RIGHTclassAccount{ // ...}
(link)Avoid
*Controller
in names of classes that aren't view controllers.
(link)Don't include types where they can be easily inferred.
// WRONGletsun:Star=Star(mass:1.989e30)letearth:Planet=Planet.earth// RIGHTletsun=Star(mass:1.989e30)letearth=Planet.earth// NOT RECOMMENDED. However, since the linter doesn't have full type information, this is not enforced automatically.letmoon:Moon= earth.moon // returns `Moon`// RIGHTletmoon= earth.moonletmoon:PlanetaryBody?= earth.moon// WRONG: Most literals provide a default type that can be inferred.letenableGravity:Bool=trueletnumberOfPlanets:Int=8letsunMass:Double=1.989e30// RIGHTletenableGravity=trueletnumberOfPlanets=8letsunMass=1.989e30// WRONG: Types can be inferred from if/switch expressions as well if each branch has the same explicit type.letsmallestPlanet:Planet=if treatPlutoAsPlanet{Planet.pluto}else{Planet.mercury}// RIGHTletsmallestPlanet=if treatPlutoAsPlanet{Planet.pluto}else{Planet.mercury}
(link)Prefer letting the type of a variable or property be inferred from the right-hand-side value rather than writing the type explicitly on the left-hand side.
Prefer using inferred types when the right-hand-side value is a static member with a leading dot (e.g. an
init
, astatic
property / function, or an enum case). This applies to both local variables and property declarations:// WRONGstructSolarSystemBuilder{letsun:Star=.init(mass:1.989e30)letearth:Planet=.earthfunc setUp(){letgalaxy:Galaxy=.andromedaletsystem:SolarSystem=.init(sun, earth) galaxy.add(system)}}// RIGHTstructSolarSystemBuilder{letsun=Star(mass:1.989e30)letearth=Planet.earthfunc setUp(){letgalaxy=Galaxy.andromedaletsystem=SolarSystem(sun, earth) galaxy.add(system)}}
Explicit types are still permitted in other cases:
// RIGHT: There is no right-hand-side value, so an explicit type is required.letsun:Star// RIGHT: The right-hand-side is not a static member of the left-hand type.letmoon:PlantaryBody= earth.moonletsunMass:Float=1.989e30letplanets:[Planet]=[]letvenusMoon:Moon?=nil
There are some rare cases where the inferred type syntax has a different meaning than the explicit type syntax. In these cases, the explicit type syntax is still permitted:
extensionString{staticletearth="Earth"}// WRONG: fails with "error: type 'String?' has no member 'earth'"letplanetName=String?.earth// RIGHTletplanetName:String?=.earth
structSaturnOutline:ShapeStyle{...}extensionShapeStylewhere Self==SaturnOutline{staticvarsaturnOutline:SaturnOutline{SaturnOutline()}}// WRONG: fails with "error: static member 'saturnOutline' cannot be used on protocol metatype '(any ShapeStyle).Type'"letmyShape2=(anyShapeStyle).myShape// RIGHT: If the property's type is an existential / protocol type, moving the type// to the right-hand side will result in invalid code if the value is defined in an// extension like `extension ShapeStyle where Self == SaturnOutline`.// SwiftFormat autocorrect detects this case by checking for the existential `any` keyword.letmyShape1:anyShapeStyle=.saturnOutline
(link)Don't use
self
unless it's necessary for disambiguation or required by the language.finalclassListing{init(capacity:Int, allowsPets:Bool){ // WRONGself.capacity= capacityself.isFamilyFriendly= !allowsPets // `self.` not required here // RIGHTself.capacity= capacity isFamilyFriendly= !allowsPets}privateletisFamilyFriendly:Boolprivatevarcapacity:Intprivatefunc increaseCapacity(by amount:Int){ // WRONGself.capacity+= amount // RIGHT capacity+= amount // WRONGself.save() // RIGHTsave()}}
(link)Bind to
self
when upgrading from a weak reference.// WRONGclassMyClass{func request(completion:()->Void){API.request(){[weak self] responseinguardlet strongSelf=selfelse{return} // Do workcompletion()}}}// RIGHTclassMyClass{func request(completion:()->Void){API.request(){[weak self] responseinguardlet selfelse{return} // Do workcompletion()}}}
(link)Add a trailing comma on the last element of a multi-line array.
// WRONGletrowContent=[listingUrgencyDatesRowContent(),listingUrgencyBookedRowContent(),listingUrgencyBookedShortRowContent()]// RIGHTletrowContent=[listingUrgencyDatesRowContent(),listingUrgencyBookedRowContent(),listingUrgencyBookedShortRowContent(),]
(link)There should be no spaces inside the brackets of collection literals.
// WRONGletinnerPlanets=[ mercury, venus, earth, mars]letlargestObjects=[.star: sun,.planet: jupiter]// RIGHTletinnerPlanets=[mercury, venus, earth, mars]letlargestObjects=[.star: sun,.planet: jupiter]
(link)Name members of tuples for extra clarity. Rule of thumb: if you've got more than 3 fields, you should probably be using a struct.
// WRONGfunc whatever()->(Int,Int){return(4,4)}letthing=whatever()print(thing.0)// RIGHTfunc whatever()->(x:Int, y:Int){return(x:4, y:4)}// THIS IS ALSO OKAYfunc whatever2()->(x:Int, y:Int){letx=4lety=4return(x, y)}letcoord=whatever()coord.xcoord.y
(link)Colons should always be followed by a space, but not preceded by a space.
// WRONGletplanet:CelestialObject= sun.planets[0]letplanet:CelestialObject= sun.planets[0]// RIGHTletplanet:CelestialObject= sun.planets[0]
// WRONGclassPlanet:CelestialObject{ // ...}// RIGHTclassPlanet:CelestialObject{ // ...}
// WRONGletmoons:[Planet:Moon]=[ mercury:[], venus:[], earth:[theMoon], mars:[phobos,deimos],]// RIGHTletmoons:[Planet:Moon]=[ mercury:[], venus:[], earth:[theMoon], mars:[phobos,deimos],]
(link)Place a space on either side of a return arrow for readability.
// WRONGfunc doSomething()->String{ // ...}// RIGHTfunc doSomething()->String{ // ...}
// WRONGfunc doSomething(completion:()->Void){ // ...}// RIGHTfunc doSomething(completion:()->Void){ // ...}
(link)Omit unnecessary parentheses.
// WRONGif(userCount>0){...}switch(someValue){...}let evens= userCounts.filter{(number)in number.isMultiple(of:2)}let squares= userCounts.map(){ $0* $0}// RIGHTif userCount>0{...}switch someValue{...}let evens= userCounts.filter{ numberin number.isMultiple(of:2)}let squares= userCounts.map{ $0* $0}
(link)Omit enum associated values from case statements when all arguments are unlabeled.
// WRONGif case.done(_)= result{...}switch animal{case.dog(_, _, _):...}// RIGHTif case.done= result{...}switch animal{case.dog:...}
(link)When destructuring an enum case or a tuple, place the
let
keyword inline, adjacent to each individual property assignment.// WRONGswitch result{caselet.success(value): // ...caselet.error(errorCode, errorReason): // ...}// WRONGguardlet case.success(value)else{return}// RIGHTswitch result{case.success(let value): // ...case.error(let errorCode, let errorReason): // ...}// RIGHTguard case.success(let value)else{return}
Consistency: We should prefer to eitheralways inline the
let
keyword ornever inline thelet
keyword. In Airbnb's Swift codebase, weobserved that inlinelet
is used far more often in practice (especially when destructuring enum cases with a single associated value).Clarity: Inlining the
let
keyword makes it more clear which identifiers are part of the conditional check and which identifiers are binding new variables, since thelet
keyword is always adjacent to the variable identifier.
// `let` is adjacent to the variable identifier, so it is immediately obvious// at a glance that these identifiers represent new variable bindingscase.enumCaseWithSingleAssociatedValue(let string):case.enumCaseWithMultipleAssociatedValues(let string, let int):// The `let` keyword is quite far from the variable identifiers,// so it is less obvious that they represent new variable bindingscaselet.enumCaseWithSingleAssociatedValue(string):caselet.enumCaseWithMultipleAssociatedValues(string, int):
(link)Place attributes for functions, types, and computed properties on the line above the declaration.
// WRONG@objcclassSpaceship{@ViewBuildervarcontrolPanel:someView{ // ...}@discardableResultfunc fly()->Bool{ // ...}}// RIGHT@objcclassSpaceship{@ViewBuildervarcontrolPanel:someView{ // ...}@discardableResultfunc fly()->Bool{ // ...}}
(link)Place simple attributes for stored properties on the same line as the rest of the declaration. Complex attributes with named arguments, or more than one unnamed argument, should be placed on the previous line.
// WRONG. These simple property wrappers should be written on the same line as the declaration.structSpaceshipDashboardView{@StateprivatevarwarpDriveEnabled:Bool@ObservedObjectprivatevarlifeSupportService:LifeSupportService@Environment(\.controlPanelStyle)privatevarcontrolPanelStyle}// RIGHTstructSpaceshipDashboardView{@StateprivatevarwarpDriveEnabled:Bool@ObservedObjectprivatevarlifeSupportService:LifeSupportService@Environment(\.controlPanelStyle)privatevarcontrolPanelStyle}
// WRONG. These complex attached macros should be written on the previous line.structSolarSystemView{@Query(sort: \.distance)varallPlanets:[Planet]@Query(sort: \.age, order:.reverse)varmoonsByAge:[Moon]}// RIGHTstructSolarSystemView{@Query(sort: \.distance)varallPlanets:[Planet]@Query(sort: \.age, order:.reverse)varoldestMoons:[Moon]}
// WRONG. These long, complex attributes should be written on the previous line.structRocketFactory{@available(*, unavailable, message:"No longer in production")varsaturn5Builder:Saturn5Builder@available(*, deprecated, message:"To be retired by 2030")varatlas5Builder:Atlas5Builder@available(*, iOS18.0, tvOS18.0, macOS15.0, watchOS11.0)varnewGlennBuilder:NewGlennBuilder}// RIGHTstructRocketFactory{@available(*, unavailable, message:"No longer in production")varsaturn5Builder:Saturn5Builder@available(*, deprecated, message:"To be retired by 2030")varatlas5Builder:Atlas5Builder@available(*, iOS18.0, tvOS18.0, macOS15.0, watchOS11.0)varnewGlennBuilder:NewGlennBuilder}
Unlike other types of declarations, which have braces and span multiple lines, stored property declarations are often only a single line of code. Stored properties are often written sequentially without any blank lines between them. This makes the code compact without hurting readability, and allows for related properties to be grouped together in blocks:
structSpaceshipDashboardView{@StateprivatevarwarpDriveEnabled:Bool@StateprivatevarlifeSupportEnabled:Bool@StateprivatevarartificialGravityEnabled:Bool@StateprivatevartractorBeamEnabled:Bool@Environment(\.controlPanelStyle)privatevarcontrolPanelStyle@Environment(\.toggleButtonStyle)privatevartoggleButtonStyle}
If stored property attributes were written on the previous line (like other types of attributes), then the properties start to visually bleed together unless you add blank lines between them:
structSpaceshipDashboardView{@StateprivatevarwarpDriveEnabled:Bool@StateprivatevarlifeSupportEnabled:Bool@StateprivatevarartificialGravityEnabled:Bool@StateprivatevartractorBeamEnabled:Bool@Environment(\.controlPanelStyle)privatevarcontrolPanelStyle@Environment(\.toggleButtonStyle)privatevartoggleButtonStyle}
If you add blank lines, the list of properties becomes much longer and you lose the ability to group related properties together:
structSpaceshipDashboardView{@StateprivatevarwarpDriveEnabled:Bool@StateprivatevarlifeSupportEnabled:Bool@StateprivatevarartificialGravityEnabled:Bool@StateprivatevartractorBeamEnabled:Bool@Environment(\.controlPanelStyle)privatevarcontrolPanelStyle@Environment(\.toggleButtonStyle)privatevartoggleButtonStyle}
This doesn't apply to complex attributes with named arguments, or multiple unnamed arguments. These arguments are visually complex and typically encode a lot of information, so feel cramped and difficult to read when written on a single line:
// Despite being less than 100 characters long, these lines are very complex and feel unnecessarily long:@available(*, unavailable, message:"No longer in production")varsaturn5Builder:Saturn5Builder@available(*, deprecated, message:"To be retired by 2030")varatlas5Builder:Atlas5Builder@available(*, iOS18.0, tvOS18.0, macOS15.0, watchOS11.0)varnewGlennBuilder:NewGlennBuilder
(link)Place modifiers for a declaration on the same line as the rest of the declaration.
// WRONGpublicstructSpaceship{nonisolatedpublicfunc fly(){…}@MainActorpublicfunc fly(){…}}// RIGHTpublicstructSpaceship{nonisolatedpublicfunc fly(){…}@MainActorpublicfunc fly(){…}}
(link)Multi-line arrays should have each bracket on a separate line. Put the opening and closing brackets on separate lines from any of the elements of the array. Also add a trailing comma on the last element.
// WRONGletrowContent=[listingUrgencyDatesRowContent(),listingUrgencyBookedRowContent(),listingUrgencyBookedShortRowContent()]// WRONGletrowContent=[listingUrgencyDatesRowContent(),listingUrgencyBookedRowContent(),listingUrgencyBookedShortRowContent()]// RIGHTletrowContent=[listingUrgencyDatesRowContent(),listingUrgencyBookedRowContent(),listingUrgencyBookedShortRowContent(),]
(link)Long type aliases of protocol compositions should wrap before the
=
and before each individual&
.// WRONG (too long)publictypealiasDependencies=CivilizationServiceProviding&LawsOfPhysicsProviding&PlanetBuilderProviding&UniverseBuilderProviding&UniverseSimulatorServiceProviding// WRONG (naive wrapping)publictypealiasDependencies=CivilizationServiceProviding&LawsOfPhysicsProviding&PlanetBuilderProviding&UniverseBuilderProviding&UniverseSimulatorServiceProviding// WRONG (unbalanced)publictypealiasDependencies=CivilizationServiceProviding& LawsOfPhysicsProviding& PlanetBuilderProviding& UniverseBuilderProviding& UniverseSimulatorServiceProviding// RIGHTpublictypealiasDependencies=CivilizationServiceProviding& LawsOfPhysicsProviding& PlanetBuilderProviding& UniverseBuilderProviding& UniverseSimulatorServiceProviding
(link)Sort protocol composition type aliases alphabetically.
Protocol composition type aliases are an unordered list with no natural ordering. Sorting alphabetically keeps these lists more organized, which is especially valuable for long protocol compositions.
// WRONG (not sorted)publictypealiasDependencies=UniverseBuilderProviding& LawsOfPhysicsProviding& UniverseSimulatorServiceProviding& PlanetBuilderProviding& CivilizationServiceProviding// RIGHTpublictypealiasDependencies=CivilizationServiceProviding& LawsOfPhysicsProviding& PlanetBuilderProviding& UniverseBuilderProviding& UniverseSimulatorServiceProviding
(link) Omit the right-hand side of the expression when unwrapping an optional property to a non-optional property with the same name.
Following the rationale inSE-0345, this shorthand syntax removes unnecessary boilerplate while retaining clarity.
// WRONGiflet galaxy= galaxy, galaxy.name=="Milky Way"{…}guardlet galaxy= galaxy, galaxy.name=="Milky Way"else{…}// RIGHTiflet galaxy, galaxy.name=="Milky Way"{…}guardlet galaxy, galaxy.name=="Milky Way"else{…}
(link)Else statements should start on the same line as the previous condition's closing brace, unless the conditions are separated by a blank line or comments.
// WRONGiflet galaxy{…}elseiflet bigBangService{…}else{…}// RIGHTiflet galaxy{…}elseiflet bigBangService{…}else{…}// RIGHT, because there are comments between the conditionsiflet galaxy{…}// If the galaxy hasn't been created yet, create it using the big bang serviceelseiflet bigBangService{…}// If the big bang service doesn't exist, fail gracefullyelse{…}// RIGHT, because there are blank lines between the conditionsiflet galaxy{…}elseiflet bigBangService{ // If the galaxy hasn't been created yet, create it using the big bang service…}else{ // If the big bang service doesn't exist, fail gracefully…}
(link)Multi-line conditional statements should break after the leading keyword. Indent each individual statement by2 spaces.
Breaking after the leading keyword resets indentation to the standard2-space grid,which helps avoid fighting Xcode's^ +I indentation behavior.
// WRONGiflet galaxy, galaxy.name=="Milky Way" // Indenting by two spaces fights Xcode's ^+I indentation behavior{…}// WRONGguardlet galaxy, galaxy.name=="Milky Way" // Variable width indentation (6 spaces)else{…}// WRONGguardlet earth= universe.find(.planet, named:"Earth"), earth.isHabitable // Blends in with previous condition's method argumentselse{…}// RIGHTiflet galaxy, galaxy.name=="Milky Way"{…}// RIGHTguardlet galaxy, galaxy.name=="Milky Way"else{…}// RIGHTguardlet earth= universe.find(.planet, named:"Earth"), earth.isHabitableelse{…}// RIGHTiflet galaxy{…}// RIGHTguardlet galaxyelse{…}
(link)Add a line break after the assignment operator (
=
) before a multi-lineif
orswitch
expression, and indent the followingif
/switch
expression. If the declaration fits on a single line, a line break is not required.This makes it so that
if
andswitch
expressions always have the same "shape" as standardif
andswitch
statements, where:- The
if
/switch
keyword is always the left-most token on a dedicated line of code. - The conditional branches are always to the right of and below the
if
/switch
keyword.
This is most consistent with how the
if
/switch
keywords are used for control flow, and thus makes it easier to recognize that the code is using anif
orswitch
expression at a glance.// WRONG. Should have a line break after the first `=`.letplanetLocation=iflet star= planet.star{"The\(star.name) system"}else{"Rogue planet"}// WRONG. The first `=` should be on the line of the variable being assigned.letplanetLocation=iflet star= planet.star{"The\(star.name) system"}else{"Rogue planet"}// WRONG. `switch` expression should be indented.letplanetLocation=switch planet{case.mercury,.venus,.earth,.mars:.terrestrialcase.jupiter,.saturn,.uranus,.neptune:.gasGiant} // RIGHTletplanetLocation=iflet star= planet.star{"The\(star.name) system"}else{"Rogue planet"} // RIGHTletplanetType:PlanetType=switch planet{case.mercury,.venus,.earth,.mars:.terrestrialcase.jupiter,.saturn,.uranus,.neptune:.gasGiant} // ALSO RIGHT. A line break is not required because the declaration fits on a single line.letmoonName=iflet moon= planet.moon{ moon.name}else{"none"}// ALSO RIGHT. A line break is permitted if it helps with readability.letmoonName=iflet moon= planet.moon{ moon.name}else{"none"}
- The
(link)When initializing a new property with the result of a conditional statement (e.g. an
if
orswitch
statement), use a singleif
/switch
expression where possible rather than defining an uninitialized property and initializing it on every branch of the following conditional statement.There are several benefits to using an
if
/switch
expression over simply performing assignment on each branch of the following conditional statement:- In most cases, you no longer need to explicitly write a type annotation for the variable that is being assigned to.
- The compiler will diagnose more cases where using a mutable
var
is unnecessary. - The resulting syntax is visually lighter because the property name being assigned doesn't need to be written on each branch.
// BEFORE// 1. An explicit type annotation is required for the uninitialized property.// 2. `var` is unnecessary here because `planetLocation` is never modified after being initialized, but the compiler doesn't diagnose.// 3. The `planetLocation` property name is written on each branch so is redundant and visually noisy.varplanetLocation:Stringiflet star= planet.star{ planetLocation="The\(star.name) system"}else{ planetLocation="Rogue planet"}print(planetLocation)// AFTER// 1. No need to write an explicit `: String` type annotation.// 2. The compiler correctly diagnoses that the `var` is unnecessary and emits a warning suggesting to use `let` instead. // 3. Each conditional branch is simply the value being assigned.varplanetLocation=iflet star= planet.star{"The\(star.name) system"}else{"Rogue planet"}print(planetLocation)
// WRONGletplanetLocation:Stringiflet star= planet.star{ planetLocation="The\(star.name) system"}else{ planetLocation="Rogue planet"}letplanetType:PlanetTypeswitch planet{case.mercury,.venus,.earth,.mars: planetType=.terrestrialcase.jupiter,.saturn,.uranus,.neptune: planetType=.gasGiant}letcanBeTerraformed:Booliflet star= planet.star, !planet.isHabitable, planet.isInHabitableZone(of: star){ canBeTerraformed=true}else{ canBeTerraformed=false}// RIGHTletplanetLocation=iflet star= planet.star{"The\(star.name) system"}else{"Rogue planet"}letplanetType:PlanetType=switch planet{case.mercury,.venus,.earth,.mars:.terrestrialcase.jupiter,.saturn,.uranus,.neptune:.gasGiant}letcanBeTerraformed=iflet star= planet.star, !planet.isHabitable, planet.isInHabitableZone(of: star){true}else{false}// ALSO RIGHT. This example cannot be converted to an if/switch expression// because one of the branches is more than just a single expression.letplanetLocation:Stringiflet star= planet.star{ planetLocation="The\(star.name) system"}else{letactualLocaton= galaxy.name??"the universe" planetLocation="Rogue planet somewhere in\(actualLocation)"}
(link)Insert a blank line following a switch case with a multi-line body. Spacing within an individual switch statement should be consistent. If any case has a multi-line body then all cases should include a trailing blank line. The last switch case doesn't need a blank line, since it is already followed by a closing brace.
Like withdeclarations in a file, inserting a blank line between scopes makes them easier to visually differentiate.
Complex switch statements are visually busy without blank lines between the cases, making it more difficult to read the code and harder to distinguish between individual cases at a glance. Blank lines between the individual cases make complex switch statements easier to read.
// WRONG. These switch cases should be followed by a blank line.func handle(_ action:SpaceshipAction){switch action{case.engageWarpDrive: navigationComputer.destination= targetedDestination warpDrive.spinUp() warpDrive.activate()case.enableArtificialGravity: artificialGravityEngine.enable(strength:.oneG)case.scanPlanet(let planet): scanner.target= planet scanner.scanAtmosphere() scanner.scanBiosphere() scanner.scanForArtificialLife()case.handleIncomingEnergyBlast: energyShields.engage()}}// WRONG. While the `.enableArtificialGravity` case isn't multi-line, the other cases are.// For consistency, it should also include a trailing blank line.func handle(_ action:SpaceshipAction){switch action{case.engageWarpDrive: navigationComputer.destination= targetedDestination warpDrive.spinUp() warpDrive.activate()case.enableArtificialGravity: artificialGravityEngine.enable(strength:.oneG)case.scanPlanet(let planet): scanner.target= planet scanner.scanAtmosphere() scanner.scanBiosphere() scanner.scanForArtificialLife()case.handleIncomingEnergyBlast: energyShields.engage()}}// RIGHT. All of the cases have a trailing blank line.func handle(_ action:SpaceshipAction){switch action{case.engageWarpDrive: navigationComputer.destination= targetedDestination warpDrive.spinUp() warpDrive.activate()case.enableArtificialGravity: artificialGravityEngine.enable(strength:.oneG)case.scanPlanet(let planet): scanner.target= planet scanner.scanAtmosphere() scanner.scanBiosphere() scanner.scanForArtificialLife()case.handleIncomingEnergyBlast: energyShields.engage()}}// RIGHT. Since none of the cases are multi-line, blank lines are not required.func handle(_ action:SpaceshipAction){switch action{case.engageWarpDrive: warpDrive.engage()case.enableArtificialGravity: artificialGravityEngine.enable(strength:.oneG)case.scanPlanet(let planet): scanner.scan(planet)case.handleIncomingEnergyBlast: energyShields.engage()}}// ALSO RIGHT. Blank lines are still permitted after single-line switch cases if it helps with readability.func handle(_ action:SpaceshipAction){switch action{case.engageWarpDrive: warpDrive.engage()case.enableArtificialGravity: artificialGravityEngine.enable(strength:.oneG)case.scanPlanet(let planet): scanner.scan(planet)case.handleIncomingEnergyBlast: energyShields.engage()}}// WRONG. While it's fine to use blank lines to separate cases, spacing within a single switch statement should be consistent.func handle(_ action:SpaceshipAction){switch action{case.engageWarpDrive: warpDrive.engage()case.enableArtificialGravity: artificialGravityEngine.enable(strength:.oneG)case.scanPlanet(let planet): scanner.scan(planet)case.handleIncomingEnergyBlast: energyShields.engage()}}
(link)Omit redundant
break
statements in switch cases.Swift automatically breaks out of a switch case after executing its code, so explicit
break
statements are usually unnecessary and add visual clutter.// WRONGswitch spaceship.warpDriveState{case.engaged: navigator.engageWarpDrive()breakcase.disengaged: navigator.disengageWarpDrive()break}// RIGHTswitch spaceship.warpDriveState{case.engaged: navigator.engageWarpDrive()case.disengaged: navigator.disengageWarpDrive()}
(link)Add a line break before the
else
keyword in a multi-line guard statement. For single-line guard statements, keep theelse
keyword on the same line as theguard
keyword. The open brace should immediately follow theelse
keyword.// WRONG (else should be on its own line for multi-line guard statements)guardlet galaxy, galaxy.name=="Milky Way"else{…}// WRONG (else should be on the same line for single-line guard statements)guardlet galaxyelse{…}// RIGHTguardlet galaxy, galaxy.name=="Milky Way"else{…}// RIGHTguardlet galaxyelse{…}
(link)Indent the body and closing triple-quote of multiline string literals, unless the string literal begins on its own line in which case the string literal contents and closing triple-quote should have the same indentation as the opening triple-quote.
// WRONGvarspaceQuote="""“Space,” it says, “is big. Really big. You just won’t believe how vastly, hugely, mindbogglingly big it is.I mean, you may think it’s a long way down the road to the chemist’s, but that’s just peanuts to space.”"""// RIGHTvarspaceQuote=""" “Space,” it says, “is big. Really big. You just won’t believe how vastly, hugely, mindbogglingly big it is. I mean, you may think it’s a long way down the road to the chemist’s, but that’s just peanuts to space.”"""// WRONGvaruniverseQuote:String{""" In the beginning the Universe was created. This has made a lot of people very angry and been widely regarded as a bad move."""}// RIGHTvaruniverseQuote:String{""" In the beginning the Universe was created. This has made a lot of people very angry and been widely regarded as a bad move."""}
(link)Use constructors instead of Make() functions for NSRange and others.
// WRONGletrange=NSMakeRange(10,5)// RIGHTletrange=NSRange(location:10, length:5)
(link)For standard library types with a canonical shorthand form (
Optional
,Array
,Dictionary
), prefer using the shorthand form over the full generic form.// WRONGletoptional:Optional<String>=nilletarray:Array<String>=[]letdictionary:Dictionary<String,Any>=[:]// RIGHTletoptional:String?=nilletarray:[String]=[]letdictionary:[String:Any]=[:]
(link)Omit explicit
.init
when not required.// WRONGletuniverse=Universe.init()// RIGHTletuniverse=Universe()
(link) The opening brace following a single-line expression should be on the same line as the rest of the statement.
// WRONGif !planet.isHabitable{ planet.terraform()}classPlanet{func terraform(){generateAtmosphere()generateOceans()}}// RIGHTif !planet.isHabitable{ planet.terraform()}classPlanet{func terraform(){generateAtmosphere()generateOceans()}}
(link) The opening brace following a multi-line expression should wrap to a new line.
// WRONGiflet star= planet.nearestStar(), planet.isInHabitableZone(of: star){ planet.terraform()}// RIGHTiflet star= planet.nearestStar(), planet.isInHabitableZone(of: star){ planet.terraform()}
(link)Braces should be surrounded by a single whitespace character (either a space, or a newline) on each side.
// WRONGstruct Planet{…}// WRONGif condition{…}else{…}// RIGHTstructPlanet{ …}// RIGHTif condition{…}else{…}
(link) For function calls and declarations, there should be no spaces before or inside the parentheses of the argument list.
// WRONGfunc install( _ engine:Engine){}install(AntimatterDrive())// RIGHTfunc install(_ engine:Engine){}install(AntimatterDrive())
(link)Comment blocks should use single-line comments (
//
for code comments and///
for documentation comments), rather than multi-line comments (/* ... */
and/** ... */
).// WRONG/*** A planet that exists somewhere in the universe.** Planets have many properties. For example, the best planets* have atmospheres and bodies of water to support life.*/classPlanet{ /** Terraforms the planet, by adding an atmosphere and ocean that is hospitable for life. */func terraform(){ /* Generate the atmosphere first, before generating the ocean. Otherwise, the water will just boil off immediately. */generateAtmosphere() /* Now that we have an atmosphere, it's safe to generate the ocean */generateOceans()}}// RIGHT/// A planet that exists somewhere in the universe.////// Planets have many properties. For example, the best planets/// have atmospheres and bodies of water to support life.classPlanet{ /// Terraforms the planet, by adding an atmosphere and ocean that is hospitable for life.func terraform(){ // Generate the atmosphere first, before generating the ocean. // Otherwise, the water will just boil off immediately.generateAtmosphere() // Now that we have an atmosphere, it's safe to generate the oceangenerateOceans()}}
(link)Use doc comments (
///
) instead of regular comments (//
) before declarations within type bodies or at the top level.// WRONG// A planet that exists somewhere in the universe.classPlanet{ // Data about the composition and density of the planet's atmosphere if present.varatmosphere:Atmosphere? // Data about the size, location, and composition of large bodies of water on the planet's surface.varoceans:[Ocean] // Terraforms the planet, by adding an atmosphere and ocean that is hospitable for life.func terraform(){ // This gas composition has a pretty good track record so far!letcomposition=AtmosphereComposition(nitrogen:0.78, oxygen:0.22) // Generate the atmosphere first, then the oceans. Otherwise, the water will just boil off immediately.generateAtmosphere(using: composition)generateOceans()}}// RIGHT/// A planet that exists somewhere in the universe.classPlanet{ /// Data about the composition and density of the planet's atmosphere if present.varatmosphere:Atmosphere? /// Data about the size, location, and composition of large bodies of water on the planet's surface.varoceans:[Ocean] /// Terraforms the planet, by adding an atmosphere and ocean that is hospitable for life.func terraform(){ // This gas composition has a pretty good track record so far!letcomposition=AtmosphereComposition(nitrogen:0.78, oxygen:0.22) // Generate the atmosphere first, then the oceans. Otherwise, the water will just boil off immediately.generateAtmosphere(using: composition)generateOceans()}}// ALSO RIGHT:func terraform(){ /// This gas composition has a pretty good track record so far! /// - Doc comments are not required before local declarations in function scopes, but are permitted.letcomposition=AtmosphereComposition(nitrogen:0.78, oxygen:0.22) /// Generate the `atmosphere` first, **then** the `oceans`. Otherwise, the water will just boil off immediately. /// - Comments not preceding declarations can use doc comments, and will not be autocorrected into regular comments. /// This can be useful because Xcode applies markdown styling to doc comments but not regular comments.generateAtmosphere(using: composition)generateOceans()}
Regular comments are permitted before declarations in some cases.
For example, comment directives like
// swiftformat:
,// swiftlint:
,// sourcery:
,// MARK:
and// TODO:
are typically required to use regular comments and don't work correctly with doc comments:// RIGHT// swiftformat:sortenumFeatureFlags{case allowFasterThanLightTravelcase disableGravitycase enableDarkEnergycase enableDarkMatter}// TODO: There are no more production consumers of this legacy model, so we// should detangle the remaining code dependencies and clean it up.structLegacyGeocentricUniverseModel{...}
Regular comments are also allowed before a grouped block of declarations, since it's possible that the comment refers to the block as a whole rather than just the following declaration:
// RIGHTenumPlanet{ // The inner planetscase mercurycase venuscase earthcase mars // The outer planetscase jupitercase saturncase uranuscase neptune}// ALSO RIGHTenumPlanet{ /// The smallest planetcase mercurycase venuscase earthcase mars /// The largest planetcase jupitercase saturncase uranuscase neptune}
(link)Place doc comments for a declaration before any attributes or modifiers.
// WRONG@MainActor/// A spacecraft with everything you need to explore the universe.struct Spaceship{…}public/// A spacecraft with everything you need to explore the universe.structSpaceship{ …}// RIGHT/// A spacecraft with everything you need to explore the universe.@MainActorstructSpaceship{ …}/// A spacecraft with everything you need to explore the universe.publicstructSpaceship{ …}
(link) Include spaces or newlines before and after comment delimiters (
//
,///
,/*
, and*/
)// WRONG///A spacecraft with incredible performance characteristicsstructSpaceship{func travelFasterThanLight(){/*unimplemented*/}func travelBackInTime(){}//TODO: research whether or not this is possible}// RIGHT/// A spacecraft with incredible performance characteristicsstructSpaceship{func travelFasterThanLight(){ /* unimplemented */}func travelBackInTime(){} // TODO: research whether or not this is possible}
(link) Include a single space in an empty set of braces (
{ }
).// WRONGextensionSpaceship:Trackable{}extensionSpaceshipView{varaccessibilityIdentifier:String{get{ spaceship.name}set{}}}// RIGHTextensionSpaceship:Trackable{}extensionSpaceshipView{varaccessibilityIdentifier:String{get{ spaceship.name}set{}}}
(link)Prefer using
for
loops over the functionalforEach(…)
method, unless usingforEach(…)
as the last element in a functional chain.For loops are more idiomatic than the
forEach(…)
method, and are typically familiar to all developers who have experience with C-family languages.For loops are also more expressive than the
forEach(…)
method. For loops support thereturn
,continue
, andbreak
control flow keywords, whileforEach(…)
only supportsreturn
(which has the same behavior ascontinue
in a for loop).// WRONGplanets.forEach{ planetin planet.terraform()}// WRONGplanets.forEach{ $0.terraform()}// RIGHTforplanetin planets{ planet.terraform()}// ALSO FINE, since forEach is useful when paired with other functional methods in a chain.planets.filter{ !$0.isGasGiant}.map{PlanetTerraformer(planet: $0)}.forEach{ $0.terraform()}
(link)Omit the
internal
keyword when defining types, properties, or functions with an internal access control level.// WRONGinternalclassSpaceship{internalinit(){…}internalfunc travel(to planet:Planet){…}}// RIGHT, because internal access control is implied if no other access control level is specified.classSpaceship{init(){…}func travel(to planet:Planet){…}}
(link)Avoid using
public
access control ininternal
types. In this case thepublic
modifier is redundant and has no effect.// WRONG: Public declarations in internal types are internal, not public.classSpaceship{publicinit(){…}publicfunc travel(to planet:Planet){…}}// RIGHTclassSpaceship{init(){…}func travel(to planet:Planet){…}}
(link)Omit
Void
return types from function definitions.// WRONGfunc doSomething()->Void{...}// RIGHTfunc doSomething(){...}
(link) **Separatelong function declarations with line breaks before each argument label, and before the closing parenthesis (
)
).classUniverse{ // WRONGfunc generateStars(at location:Point, count:Int, color:StarColor, withAverageDistance averageDistance:Float)->String{ // This is too long and will probably auto-wrap in a weird way} // WRONGfunc generateStars(at location:Point, count:Int, color:StarColor, withAverageDistance averageDistance:Float)->String{ // Xcode indents all the arguments} // WRONGfunc generateStars( at location:Point, count:Int, color:StarColor, withAverageDistance averageDistance:Float)->String{populateUniverse() // this line blends in with the argument list} // WRONGfunc generateStars( at location:Point, count:Int, color:StarColor, withAverageDistance averageDistance:Float)throws->String{populateUniverse() // this line blends in with the argument list} // WRONGfunc generateStars( at location:Point, count:Int, color:StarColor, withAverageDistance averageDistance:Float)asyncthrows // these effects are easy to miss since they're visually associated with the last parameter->String{populateUniverse()} // RIGHTfunc generateStars( at location:Point, count:Int, color:StarColor, withAverageDistance averageDistance:Float)->String{populateUniverse()} // RIGHTfunc generateStars( at location:Point, count:Int, color:StarColor, withAverageDistance averageDistance:Float)asyncthrows->String{populateUniverse()}}
(link)Long function calls should also break on each argument. Put the closing parenthesis on its own line.
// WRONGuniverse.generateStars(at: location, count:5, color: starColor, withAverageDistance:4)// WRONGuniverse.generateStars(at: location, count:5, color: starColor, withAverageDistance:4)// WRONGuniverse.generateStars( at: location, count:5, color: starColor, withAverageDistance:4)// WRONGuniverse.generate(5,.stars, at: location)// RIGHTuniverse.generateStars( at: location, count:5, color: starColor, withAverageDistance:4)// RIGHTuniverse.generate(5,.stars, at: location)
(link)Name unused function parameters as underscores (
_
).Naming unused function parameters as underscores makes it more clear when the parameter is unused within the function body.This can make it easier to catch subtle logical errors, and can highlight opportunities to simplify method signatures.
// WRONG// In this method, the `newCondition` parameter is unused.// This is actually a logical error, and is easy to miss, but compiles without warning.func updateWeather(_ newCondition:WeatherCondition)->Weather{varupdatedWeather=self updatedWeather.condition= condition // this mistake inadvertently makes this method unable to change the weather conditionreturn updatedWeather}// In this method, the `color` parameter is unused.// Is this a logical error (e.g. should it be passed through to the `universe.generateStars` method call),// or is this an unused argument that should be removed from the method signature?func generateUniverseWithStars( at location:Point, count:Int, color:StarColor, withAverageDistance averageDistance:Float){letuniverse=generateUniverse() universe.generateStars( at: location, count: count, withAverageDistance: averageDistance)}
// RIGHT// Automatically reformatting the unused parameter to be an underscore// makes it more clear that the parameter is unused, which makes it// easier to spot the logical error.func updateWeather(_:WeatherCondition)->Weather{varupdatedWeather=self updatedWeather.condition= conditionreturn updatedWeather}// The underscore makes it more clear that the `color` parameter is unused.// This method argument can either be removed if truly unnecessary,// or passed through to `universe.generateStars` to correct the logical error.func generateUniverseWithStars( at location:Point, count:Int, color _:StarColor, withAverageDistance averageDistance:Float){letuniverse=generateUniverse() universe.generateStars( at: location, count: count, withAverageDistance: averageDistance)}
(link)Remove blank lines between chained functions.
Improves readability and maintainability, making it easier to see the sequence of functions that are applied to the object.
// WRONGvarinnerPlanetNames:[String]{ planets.filter{ $0.isInnerPlanet}.map{ $0.name}}// WRONGvarinnerPlanetNames:[String]{ planets.filter{ $0.isInnerPlanet} // Gets the name of the inner planet.map{ $0.name}}// RIGHTvarinnerPlanetNames:[String]{ planets.filter{ $0.isInnerPlanet}.map{ $0.name}}// RIGHTvarinnerPlanetNames:[String]{ planets.filter{ $0.isInnerPlanet} // Gets the name of the inner planet.map{ $0.name}}
(link)Omit redundant typed
throws
annotations from function definitions.throws(Never)
is equivalent to a non-throwing function, andthrows(Error)
is equivalent to non-typedthrows
. These redundant annotations add unnecessary complexity to function signatures.// WRONGfunc doSomething()throws(Never)->Int{return0}func doSomethingElse()throws(Error)->Int{throwMyError.failed}// RIGHTfunc doSomething()->Int{return0}func doSomethingElse()throws->Int{throwMyError.failed}
(link)Favor
Void
return types over()
in closure declarations. If you must specify aVoid
return type in a function declaration, useVoid
rather than()
to improve readability.// WRONGfunc method(completion:()->()){...}// RIGHTfunc method(completion:()->Void){...}
(link)Name unused closure parameters as underscores (
_
).Naming unused closure parameters as underscores reduces the cognitive overhead required to readclosures by making it obvious which parameters are used and which are unused.
// WRONGsomeAsyncThing(){ argument1, argument2, argument3inprint(argument3)}// RIGHTsomeAsyncThing(){ _, _, argument3inprint(argument3)}
(link)Closures should have a single space or newline inside each brace. Trailing closures should additionally have a single space or newline outside each brace.
// WRONGletevenSquares= numbers.filter{$0.isMultiple(of:2)}.map{ $0* $0}// RIGHTletevenSquares= numbers.filter{ $0.isMultiple(of:2)}.map{ $0* $0}// WRONGletevenSquares= numbers.filter({ $0.isMultiple(of:2)}).map({ $0* $0})// RIGHTletevenSquares= numbers.filter({ $0.isMultiple(of:2)}).map({ $0* $0})// WRONGletevenSquares= numbers.filter{ $0.isMultiple(of:2)}.map{ $0* $0}// RIGHTletevenSquares= numbers.filter{ $0.isMultiple(of:2)}.map{ $0* $0}
(link)Omit
Void
return types from closure expressions.// WRONGsomeAsyncThing(){ argument->Voidin...}// RIGHTsomeAsyncThing(){ argumentin...}
(link)Prefer trailing closure syntax for closure arguments with no parameter name.
// WRONGplanets.map({ $0.name})// RIGHTplanets.map{ $0.name}// ALSO RIGHT, since this closure has a parameter nameplanets.first(where:{ $0.isGasGiant})// ALSO FINE. Trailing closure syntax is still permitted for closures// with parameter names. However, consider using non-trailing syntax// in cases where the parameter name is semantically meaningful.planets.first{ $0.isGasGiant}
(link)Avoid using
unowned
captures. Instead prefer safer alternatives likeweak
captures, or capturing variables directly.`unowned` captures are unsafe because they will cause the application to crash if the referenced object has been deallocated.// WRONG: Crashes if `self` has been deallocated when closures are called.finalclassSpaceshipNavigationService{letspaceship:Spaceshipletplanet:Planetfunc colonizePlanet(){ spaceship.travel(to: planet, onArrival:{[unowned self]in planet.colonize()})}func exploreSystem(){ spaceship.travel(to: planet, nextDestination:{[unowned self]in planet.moons?.first})}}
weak
captures are safer because they require the author to explicitly handle the case where the referenced object no longer exists.// RIGHT: Uses a `weak self` capture and explicitly handles the case where `self` has been deallocatedfinalclassSpaceshipNavigationService{letspaceship:Spaceshipletplanet:Planetfunc colonizePlanet(){ spaceship.travel(to: planet, onArrival:{[weak self]inguardlet selfelse{return} planet.colonize()})}func exploreSystem(){ spaceship.travel(to: planet, nextDestination:{[weak self]inguardlet selfelse{returnnil}return planet.moons?.first})}}
Alternatively, consider directly capturing the variables that are used in the closure. This lets you avoid having to handle the case where
self
is nil, since you don't even need to referenceself
:// RIGHT: Explicitly captures `planet` instead of capturing `self`finalclassSpaceshipNavigationService{letspaceship:Spaceshipletplanet:Planetfunc colonizePlanet(){ spaceship.travel(to: planet, onArrival:{[planet]in planet.colonize()})}func exploreSystem(){ spaceship.travel(to: planet, nextDestination:{[planet]in planet.moons?.first})}}
(link)Infix operators should have a single space on either side. However, in operator definitions, omit the trailing space between the operator and the open parenthesis. This rule does not apply to range operators (e.g.
1...3
).// WRONGletcapacity=1+2letcapacity= currentCapacity??0letcapacity=newCapacityletlatitude= region.center.latitude-region.span.latitudeDelta/2.0// RIGHTletcapacity=1+2letcapacity= currentCapacity??0letcapacity= newCapacityletlatitude= region.center.latitude- region.span.latitudeDelta/2.0
// WRONGstaticfunc==(_ lhs:MyView, _ rhs:MyView)->Bool{ lhs.id== rhs.id}// RIGHTstaticfunc==(_ lhs:MyView, _ rhs:MyView)->Bool{ lhs.id== rhs.id}
(link)Long ternary operator expressions should wrap before the
?
and before the:
, putting each conditional branch on a separate line.// WRONG (too long)letdestinationPlanet= solarSystem.hasPlanetsInHabitableZone? solarSystem.planetsInHabitableZone.first: solarSystem.uninhabitablePlanets.first// WRONG (naive wrapping)letdestinationPlanet= solarSystem.hasPlanetsInHabitableZone? solarSystem.planetsInHabitableZone.first: solarSystem.uninhabitablePlanets.first// WRONG (unbalanced operators)letdestinationPlanet= solarSystem.hasPlanetsInHabitableZone? solarSystem.planetsInHabitableZone.first: solarSystem.uninhabitablePlanets.first// RIGHTletdestinationPlanet= solarSystem.hasPlanetsInHabitableZone? solarSystem.planetsInHabitableZone.first: solarSystem.uninhabitablePlanets.first
(link) In conditional statements (
if
,guard
,while
), separate boolean conditions using commas (,
) instead of&&
operators.// WRONGiflet star= planet.star, !planet.isHabitable && planet.isInHabitableZone(of: star){ planet.terraform()}iflet star= planet.star, !planet.isHabitable && planet.isInHabitableZone(of: star){ planet.terraform()}// RIGHTiflet star= planet.star, !planet.isHabitable, planet.isInHabitableZone(of: star){ planet.terraform()}iflet star= planet.star, !planet.isHabitable, planet.isInHabitableZone(of: star){ planet.terraform()}
(link) When extending bound generic types, prefer using generic bracket syntax (
extension Collection<Planet>
), or sugared syntax for applicable standard library types (extension [Planet]
) instead of generic type constraints.// WRONGextensionArraywhere Element==Star{ …}extensionOptionalwhere Wrapped==Spaceship{ …}extensionDictionarywhere Key==Moon, Element==Planet{ …}extensionCollectionwhere Element==Universe{ …}extensionStateStorewhere State==SpaceshipState, Action==SpaceshipAction{ …}// RIGHTextension[Star]{ …}extensionSpaceship?{ …}extension[Moon:Planet]{ …}extensionCollection<Universe>{ …}extensionStateStore<SpaceshipState,SpaceshipAction>{ …}// ALSO RIGHT. There are multiple types that could satisfy this constraint// (e.g. [Planet], [Moon]), so this is not a "bound generic type" and isn't// eligible for the generic bracket syntax.extensionArraywhere Element:PlanetaryBody{}
(link)Avoid using semicolons. Semicolons are not required at the end of a line, so should be omitted. While you can use semicolons to place two statements on the same line, it is more common and preferred to separate them using a newline instead.
// WRONG. Semicolons are not required and can be omitted.letmercury=planets[0];letvenus=planets[1];letearth=planets[2];// WRONG. While you can use semicolons to place multiple statements on a single line,// it is more common and preferred to separate them using newlines instead.letmercury=planets[0];letvenus=planets[1];letearth=planets[2];// RIGHTletmercury=planets[0]letvenus=planets[1]letearth=planets[2]// WRONGguardlet moon= planet.moonelse{completion(nil);return}// WRONGguardlet moon= planet.moonelse{completion(nil);return}// RIGHTguardlet moon= planet.moonelse{completion(nil)return}
(link)Prefer initializing properties at
init
time whenever possible, rather than using implicitly unwrapped optionals. A notable exception is UIViewController'sview
property.// WRONGclassMyClass{init(){ super.init() someValue=5}varsomeValue:Int!}// RIGHTclassMyClass{init(){ someValue=0 super.init()}varsomeValue:Int}
(link)Avoid performing any meaningful or time-intensive work in
init()
. Avoid doing things like opening database connections, making network requests, reading large amounts of data from disk, etc. Create something like astart()
method if these things need to be done before an object is ready for use.(link)Omit redundant memberwise initializers. The compiler can synthesize memberwise initializers for structs, so explicit initializers that only assign parameters to properties with the same names should be omitted. Note that this only applies to
internal
,fileprivate
andprivate
initializers, since compiler-synthesized memberwise initializers are only generated for those access controls.Removing redundant memberwise initializers reduces boilerplate and makes the code more concise. The compiler-synthesized initializers are equivalent to the explicit ones, so there's no functional difference.
// WRONGstructPlanet{letname:Stringletmass:Doubleletradius:Doubleinit(name:String, mass:Double, radius:Double){self.name= nameself.mass= massself.radius= radius}}// RIGHTstructPlanet{letname:Stringletmass:Doubleletradius:Double}// ALSO RIGHT: Custom logic in initializer makes it non-redundantstructPlanet{letname:Stringletmass:Doubleletradius:Doubleinit(name:String, mass:Double, radius:Double){self.name= name.capitalizedself.mass=max(0, mass)self.radius=max(0, radius)}}// ALSO RIGHT: Public initializer is not redundant since compiler-synthesized // memberwise initializers are always internalpublicstructPlanet{publicletname:Stringpublicletmass:Doublepublicletradius:Doublepublicinit(name:String, mass:Double, radius:Double){self.name= nameself.mass= massself.radius= radius}}
(link)Extract complex property observers into methods. This reduces nestedness, separates side-effects from property declarations, and makes the usage of implicitly-passed parameters like
oldValue
explicit.// WRONGclassTextField{vartext:String?{ didSet{guard oldValue!= textelse{return} // Do a bunch of text-related side-effects.}}}// RIGHTclassTextField{vartext:String?{ didSet{textDidUpdate(from: oldValue)}}privatefunc textDidUpdate(from oldValue:String?){guard oldValue!= textelse{return} // Do a bunch of text-related side-effects.}}
(link)Extract complex callback blocks into methods. This limits the complexity introduced by weak-self in blocks and reduces nestedness. If you need to reference self in the method call, make use of
guard
to unwrap self for the duration of the callback.// WRONGclassMyClass{func request(completion:()->Void){API.request(){[weak self] responseiniflet self{ // Processing and side effects}completion()}}}// RIGHTclassMyClass{func request(completion:()->Void){API.request(){[weak self] responseinguardlet selfelse{return}self.doSomething(with:self.property, response: response)completion()}}func doSomething(with nonOptionalParameter:SomeClass, response:SomeResponseClass){ // Processing and side effects}}
(link)Prefer using
guard
at the beginning of a scope.(link)Access control should be at the strictest level possible. Prefer
public
toopen
andprivate
tofileprivate
unless you need that behavior.// WRONGpublicstructSpaceship{ // WRONG: `engine` is used in `extension Spaceship` below, // but extensions in the same file can access `private` members.fileprivateletengine:AntimatterEngine // WRONG: `hull` is not used by any other type, so `fileprivate` is unnecessary.fileprivatelethull:Hull // RIGHT: `navigation` is used in `extension Pilot` below, // so `fileprivate` is necessary here.fileprivateletnavigation:SpecialRelativityNavigationService}extensionSpaceship{publicfunc blastOff(){ engine.start()}}extensionPilot{publicfunc chartCourse(){ spaceship.navigation.course=.andromedaGalaxy spaceship.blastOff()}}
// RIGHTpublicstructSpaceship{fileprivateletnavigation:SpecialRelativityNavigationServiceprivateletengine:AntimatterEngineprivatelethull:Hull}extensionSpaceship{publicfunc blastOff(){ engine.start()}}extensionPilot{publicfunc chartCourse(){ spaceship.navigation.course=.andromedaGalaxy spaceship.blastOff()}}
(link)Avoid global functions whenever possible. Prefer methods within type definitions.
// WRONGfunc age(of person:Person, bornAt:TimeInterval)->Int{ // ...}func jump(person:Person){ // ...}// RIGHTclassPerson{varbornAt:TimeIntervalvarage:Int{ // ...}func jump(){ // ...}}
(link)Use caseless
enum
s for organizingpublic
orinternal
constants and functions into namespaces.- Avoid creating non-namespaced global constants and functions.
- Feel free to nest namespaces where it adds clarity.
private
globals are permitted, since they are scoped to a single file and do not pollute the global namespace. Consider placing private globals in anenum
namespace to match the guidelines for other declaration types.
Caseless
enum
s work well as namespaces because they cannot be instantiated, which matches their intent.// WRONGstructEnvironment{staticletearthGravity=9.8staticletmoonGravity=1.6}// WRONGstructEnvironment{structEarth{staticletgravity=9.8}structMoon{staticletgravity=1.6}}// RIGHTenumEnvironment{enumEarth{staticletgravity=9.8}enumMoon{staticletgravity=1.6}}
(link)Use Swift's automatic enum values unless they map to an external source. Add a comment explaining why explicit values are defined.
To minimize user error, improve readability, and write code faster, rely on Swift's automatic enum values. If the value maps to an external source (e.g. it's coming from a network request) or is persisted across binaries, however, define the values explicitly, and document what these values are mapping to.
This ensures that if someone adds a new value in the middle, they won't accidentally break things.
// WRONGenumErrorType:String{case error="error"case warning="warning"}// WRONGenumUserType:String{case ownercase managercase member}// WRONGenumPlanet:Int{case mercury=0case venus=1case earth=2case mars=3case jupiter=4case saturn=5case uranus=6case neptune=7}// WRONGenumErrorCode:Int{case notEnoughMemorycase invalidResourcecase timeOut}// RIGHT// Relying on Swift's automatic enum valuesenumErrorType:String{case errorcase warning}// RIGHT/// These are written to a logging service. Explicit values ensure they're consistent across binaries.// swiftformat:disable redundantRawValuesenumUserType:String{case owner="owner"case manager="manager"case member="member"}// swiftformat:enable redundantRawValues// RIGHT// Relying on Swift's automatic enum valuesenumPlanet:Int{case mercurycase venuscase earthcase marscase jupitercase saturncase uranuscase neptune}// RIGHT/// These values come from the server, so we set them here explicitly to match those values.enumErrorCode:Int{case notEnoughMemory=0case invalidResource=1case timeOut=2}
(link)Use optionals only when they have semantic meaning.
(link)Prefer immutable values whenever possible. Use
map
andcompactMap
instead of appending to a new collection. Usefilter
instead of removing elements from a mutable collection.Mutable variables increase complexity, so try to keep them in as narrow a scope as possible.
// WRONGvarresults=[SomeType]()forelementin input{letresult=transform(element) results.append(result)}// RIGHTletresults= input.map{transform($0)}
// WRONGvarresults=[SomeType]()forelementin input{iflet result=transformThatReturnsAnOptional(element){ results.append(result)}}// RIGHTletresults= input.compactMap{transformThatReturnsAnOptional($0)}
(link)Prefer immutable or computed static properties over mutable ones whenever possible. Use stored
static let
properties or computedstatic var
properties over storedstatic var
properties whenever possible, as storedstatic var
properties are global mutable state.Global mutable state increases complexity and makes it harder to reason about the behavior of applications. It should be avoided when possible.
// WRONGenumFonts{staticvartitle=UIFont(…)}// RIGHTenumFonts{staticlettitle=UIFont(…)}
// WRONGstructFeatureState{varcount:Intstaticvarinitial=FeatureState(count:0)}// RIGHTstructFeatureState{varcount:Intstaticvarinitial:FeatureState{ // Vend static properties that are cheap to computeFeatureState(count:0)}}
(link)Handle an unexpected but recoverable condition with an
assert
method combined with the appropriate logging in production. If the unexpected condition is not recoverable, prefer aprecondition
method orfatalError()
. This strikes a balance between crashing and providing insight into unexpected conditions in the wild. Only preferfatalError
over aprecondition
method when the failure message is dynamic, since aprecondition
method won't report the message in the crash report.func didSubmitText(_ text:String){ // It's unclear how this was called with an empty string; our custom text field shouldn't allow this. // This assert is useful for debugging but it's OK if we simply ignore this scenario in production.guard !text.isEmptyelse{assertionFailure("Unexpected empty string")return} // ...}func transformedItem(atIndex index:Int, from items:[Item])->Item{precondition(index>=0 && index< items.count) // It's impossible to continue executing if the precondition has failed. // ...}func makeImage(name:String)->UIImage{guardlet image=UIImage(named: name, in:nil, compatibleWith:nil)else{fatalError("Image named\(name) couldn't be loaded.") // We want the error message so we know the name of the missing image.}return image}
(link)Default type methods to
static
.(link)Default classes to
final
.(link) When switching over an enum, generally prefer enumerating all cases rather than using the
default
case.Enumerating every case requires developers and reviewers have to consider the correctness of every switch statement when new cases are added in the future.
// NOT PREFERREDswitch trafficLight{case.greenLight: // Move your vehicledefault: // Stop your vehicle}// PREFERREDswitch trafficLight{case.greenLight: // Move your vehiclecase.yellowLight,.redLight: // Stop your vehicle}// COUNTEREXAMPLESenumTaskState{case pendingcase runningcase cancelingcase success(Success)case failure(Error) // We expect that this property will remain valid if additional cases are added to the enumeration.publicvarisRunning:Bool{switchself{case.running:truedefault:false}}}extensionTaskState:Equatable{ // Explicitly listing each state would be too burdensome. Ideally this function could be implemented with a well-tested macro.publicstaticfunc==(lhs:TaskState, rhs:TaskState)->Bool{switch(lhs, rhs){case(.pending,.pending):truecase(.running,.running):truecase(.canceling,.canceling):truecase(.success(let lhs),.success(let rhs)): lhs== rhscase(.failure(let lhs),.failure(let rhs)): lhs== rhsdefault:false}}}
(link)Check for nil rather than using optional binding if you don't need to use the value.
(link)Omit the
return
keyword when not required by the language.// WRONG["1","2","3"].compactMap{returnInt($0)}varsize:CGSize{returnCGSize( width:100.0, height:100.0)}func makeInfoAlert(message:String)->UIAlertController{returnUIAlertController( title:"ℹ️ Info", message: message, preferredStyle:.alert)}varalertTitle:String{if issue.severity==.critical{return"💥 Critical Error"}else{return"ℹ️ Info"}}func type(of planet:Planet)->PlanetType{switch planet{case.mercury,.venus,.earth,.mars:return.terrestrialcase.jupiter,.saturn,.uranus,.neptune:return.gasGiant}}// RIGHT["1","2","3"].compactMap{Int($0)}varsize:CGSize{CGSize( width:100.0, height:100.0)}func makeInfoAlert(message:String)->UIAlertController{UIAlertController( title:"ℹ️ Info", message: message, preferredStyle:.alert)}varalertTitle:String{if issue.severity==.critical{"💥 Critical Error"}else{"ℹ️ Info"}}func type(of planet:Planet)->PlanetType{switch planet{case.mercury,.venus,.earth,.mars:.terrestrialcase.jupiter,.saturn,.uranus,.neptune:.gasGiant}}
(link)Use
AnyObject
instead ofclass
in protocol definitions.SE-0156, which introduced support for using the
AnyObject
keyword as a protocol constraint, recommends preferringAnyObject
overclass
:This proposal merges the concepts of
class
andAnyObject
, which now have the same meaning: they represent an existential for classes. To get rid of the duplication, we suggest only keepingAnyObject
around. To reduce source-breakage to a minimum,class
could be redefined astypealias class = AnyObject
and give a deprecation warning on class for the first version of Swift this proposal is implemented in. Later,class
could be removed in a subsequent version of Swift.// WRONGprotocolFoo:class{}// RIGHTprotocolFoo:AnyObject{}
(link)Specify the access control for each declaration in an extension individually.
Specifying the access control on the declaration itself helps engineers more quickly determine the access control level of an individual declaration.
// WRONGpublicextensionUniverse{ // This declaration doesn't have an explicit access control level. // In all other scopes, this would be an internal function, // but because this is in a public extension, it's actually a public function.func generateGalaxy(){}}// WRONGprivateextensionSpaceship{func enableHyperdrive(){}}// RIGHTextensionUniverse{ // It is immediately obvious that this is a public function, // even if the start of the `extension Universe` scope is off-screen.publicfunc generateGalaxy(){}}// RIGHTextensionSpaceship{ // Recall that a private extension actually has fileprivate semantics, // so a declaration in a private extension is fileprivate by default.fileprivatefunc enableHyperdrive(){}}
(link)Prefer dedicated logging systems like
os_log
orswift-log
over writing directly to standard out usingprint(…)
,debugPrint(…)
, ordump(…)
.All log messages should flow into intermediate logging systems that can direct messages to the correct destination(s) and potentially filter messages based on the app's environment or configuration.
print(…)
,debugPrint(…)
, ordump(…)
will write all messages directly to standard out in all app configurations and can potentially leak personally identifiable information (PII).(link)Don't use
#file
. Use#fileID
or#filePath
as appropriate.The behavior of the
#file
literal (or macro as of Swift 5.9) has evolved from evaluating to the full source file path (the behavior as of#filePath
) to a human-readable string containing module and file name (the behavior of#fileID
). Use the literal (or macro) with the most appropriate behavior for your use case.(link)Don't use
#filePath
in production code. Use#fileID
instead.#filePath
should only be used in non-production code where the full path of the source file provides useful information to developers. Because#fileID
doesn’t embed the full path to the source file, it won't expose your file system and reduces the size of the compiled binary.(link)Avoid single-expression closures that are always called immediately. Instead, prefer inlining the expression.
// WRONGlazyvaruniverse:Universe={Universe()}()lazyvarstars={ universe.generateStars( at: location, count:5, color: starColor, withAverageDistance:4)}()// RIGHTlazyvaruniverse=Universe()lazyvarstars= universe.generateStars( at: location, count:5, color: starColor, withAverageDistance:4)
(link)Omit the
get
clause from a computed property declaration that doesn't also have aset
,willSet
, ordidSet
clause.// WRONGvaruniverse:Universe{get{Universe()}}// RIGHTvaruniverse:Universe{Universe()}// RIGHTvaruniverse:Universe{get{ multiverseService.current}set{ multiverseService.current= newValue}}
(link)Prefer using opaque generic parameters (with
some
) over verbose named generic parameter syntax where possible.Opaque generic parameter syntax is significantly less verbose and thus more legible than the full named generic parameter syntax.
// WRONGfunc spaceshipDashboard<WarpDriveView:View, CaptainsLogView:View>( warpDrive:WarpDriveView, captainsLog:CaptainsLogView)->someView{…}func generate<Planets>(_ planets:Planets)where Planets:Collection, Planets.Element==Planet{…}// RIGHTfunc spaceshipDashboard( warpDrive:someView, captainsLog:someView)->someView{…}func generate(_ planets:someCollection<Planet>){…}// Also fine, since there isn't an equivalent opaque parameter syntax for expressing// that two parameters in the type signature are of the same type:func terraform<Body:PlanetaryBody>(_ planetaryBody:Body, into terraformedBody:Body){…}// Also fine, since the generic parameter name is referenced in the function body so can't be removed:func terraform<Body:PlanetaryBody>(_ planetaryBody:Body){ planetaryBody.generateAtmosphere(Body.idealAtmosphere)}
Fully-unconstrained generic parameters are somewhat uncommon, but are equivalent to
some Any
. For example:func assertFailure<Value>( _ result:Result<Value,Error>, file:StaticString= #filePath, line:UInt= #line){if case.failure(let error)= result{XCTFail(error.localizedDescription, file: file, line: line)}}// is equivalent to:func assertFailure( _ result:Result<someAny,Error>, file:StaticString= #filePath, line:UInt= #line){if case.failure(let error)= result{XCTFail(error.localizedDescription, file: file, line: line)}}
some Any
is somewhat unintuitive, and the named generic parameter is useful in this situation to compensate for the weak type information. Because of this, prefer using named generic parameters instead ofsome Any
.(link)Prefer to avoid using
@unchecked Sendable
. Use a standardSendable
conformance instead where possible. If working with a type from a module that has not yet been updated to support Swift Concurrency, suppress concurrency-related errors using@preconcurrency import
.@unchecked Sendable
provides no guarantees about the thread safety of a type, and instead unsafely suppresses compiler errors related to concurrency checking.There are typically other, safer methods for suppressing concurrency-related errors:
A
Sendable
conformance is the preferred way to declare that a type is thread-safe. The compiler will emit an error if a type conforming toSendable
is not thread-safe. For example, simple value types and immutable classes can always safely conform toSendable
, but mutable classes cannot:// RIGHT: Simple value types are thread-safe.structPlanet:Sendable{varmass:Double}// RIGHT: Immutable classes are thread-safe.finalclassPlanet:Sendable{letmass:Double}// WRONG: Mutable classes are not thread-safe.finalclassPlanet:Sendable{ // ERROR: stored property 'mass' of 'Sendable'-conforming class 'Planet' is mutablevarmass:Double}// WRONG: @unchecked is unnecessary because the compiler can prove that the type is thread-safe.structPlanet:@uncheckedSendable{varmass:Double}
Mutable classes can be made
Sendable
and thread-safe if they are isolated to a single actor / thread / concurrency domain. Any mutable class can be madeSendable
by isolating it to a global actor using an annotation like@MainActor
(which isolates it to the main actor):// RIGHT: A mutable class isolated to the main actor is thread-safe.@MainActorfinalclassPlanet:Sendable{varmass:Double}// WRONG: @unchecked Sendable is unsafe because mutable classes are not thread-safe.structPlanet:@uncheckedSendable{varmass:Double}
If working with a non-
Sendable
type from a module that hasn't yet adopted Swift concurrency, suppress concurrency-related errors using@preconcurrency import
./// Defined in `UniverseKit` moduleclassPlanet:PlanetaryBody{varstar:Star}
// WRONG: Unsafely marking a non-thread-safe class as Sendable only to suppress errorsimport PlanetaryBodyextensionPlanetaryBody:@uncheckedSendable{}// RIGHT@preconcurrencyimport PlanetaryBody
If possible, restructure code so that the compiler can verify that it is thread safe. This lets you use a
Sendable
conformance instead of an unsafe@unchecked Sendable
conformance.When conforming to
Sendable
, the compiler will emit an error in the future if you attempt to make a change that is not thread-safe. This guarantee is lost when using@unchecked Sendable
, which makes it easier to accidentally introduce changes which are not thread-safe.For example, given this set of classes:
classPlanetaryBody{letmass:Double}classPlanet:PlanetaryBody{letstar:Star}// NOT IDEAL: no compiler-enforced thread safety.extensionPlanetaryBody:@uncheckedSendable{}
the compiler can't verify
PlanetaryBody
isSendable
because it is notfinal
. Instead of using@unchecked Sendable
, you could restructure the code to not use subclassing:// BETTER: Compiler-enforced thread safety.protocolPlanetaryBody:Sendable{varmass:Double{get}}finalclassPlanet:PlanetaryBody,Sendable{letmass:Doubleletstar:Star}
Sometimes it is truly necessary to use
@unchecked Sendable
. In these cases, you can add a// swiftlint:disable:next no_unchecked_sendable
annotation with an explanation for how we know the type is thread-safe, and why we have to use@unchecked Sendable
instead ofSendable
.A canonical, safe use case of
@unchecked Sendable
is a class where the mutable state is protected by some other thread-safe mechanism like a lock. This type is thread-safe, but the compiler cannot verify this.structAtomic<Value>{ /// `value` is thread-safe because it is manually protected by a lock.varvalue:Value{...}}// WRONG: disallowed by linterextensionAtomic:@uncheckedSendable{}// WRONG: suppressing lint error without an explanation// swiftlint:disable:next no_unchecked_sendableextensionAtomic:@uncheckedSendable{}// RIGHT: suppressing the linter with an explanation why the type is thread-safe// Atomic is thread-safe because its underlying mutable state is protected by a lock.// swiftlint:disable:next no_unchecked_sendableextensionAtomic:@uncheckedSendable{}
It is also reasonable to use
@unchecked Sendable
for types that are thread-safe in existing usage but can't be refactored to support a properSendable
conformance (e.g. due to backwards compatibility constraints):classPlanetaryBody{letmass:Double}classPlanet:PlanetaryBody{letstar:Star}// WRONG: disallowed by linterextensionPlanetaryBody:@uncheckedSendable{}// WRONG: suppressing lint error without an explanation// swiftlint:disable:next no_unchecked_sendableextensionPlanetaryBody:@uncheckedSendable{}// RIGHT: suppressing the linter with an explanation why the type is thread-safe// PlanetaryBody cannot conform to Sendable because it is non-final and has subclasses.// PlanetaryBody itself is safely Sendable because it only consists of immutable values.// All subclasses of PlanetaryBody are also simple immutable values, so are safely Sendable as well.// swiftlint:disable:next no_unchecked_sendableextensionPlanetaryBody:@uncheckedSendable{}
(link)Avoid defining properties that are then returned immediately. Instead, return the value directly.
Property declarations that are immediately returned are typically redundant and unnecessary. Sometimes these are unintentionally created as the byproduct of refactoring. Cleaning them up automatically simplifies the code. In some cases this also results in the
return
keyword itself being unnecessary, further simplifying the code.// WRONGvarspaceship:Spaceship{letspaceship= spaceshipBuilder.build(warpDrive: warpDriveBuilder.build())return spaceship}// RIGHTvarspaceship:Spaceship{ spaceshipBuilder.build(warpDrive: warpDriveBuilder.build())}// WRONGvarspaceship:Spaceship{letwarpDrive= warpDriveBuilder.build()letspaceship= spaceshipBuilder.build(warpDrive: warpDrive)return spaceship}// RIGHTvarspaceship:Spaceship{letwarpDrive= warpDriveBuilder.build()return spaceshipBuilder.build(warpDrive: warpDrive)}
(link)Prefer using a generated Equatable implementation when comparing all properties of a type. For structs, prefer using the compiler-synthesized Equatable implementation when possible.
Manually-implemented Equatable implementations are verbose, and keeping them up-to-date is error-prone. For example, when adding a new property, it's possible to forget to update the Equatable implementation to compare it.
/// WRONG: The `static func ==` implementation is redundant and error-prone.structPlanet:Equatable{letmass:Doubleletorbit:OrbitalElementsletrotation:Doublestaticfunc==(lhs:Planet, rhs:Planet)->Bool{ lhs.mass== rhs.mass && lhs.orbit== rhs.orbit && lhs.rotation== rhs.rotation}}/// RIGHT: The `static func ==` implementation is synthesized by the compiler.structPlanet:Equatable{letmass:Doubleletorbit:OrbitalElementsletrotation:Double}/// ALSO RIGHT: The `static func ==` implementation differs from the implementation that /// would be synthesized by the compiler and compared all properties, so is not redundant.structCelestialBody:Equatable{letid:UUIDletorbit:OrbitalElementsstaticfunc==(lhs:Planet, rhs:Planet)->Bool{ lhs.id== rhs.id}}
In projects that provide an
@Equatable
macro, prefer using that macro to generate thestatic func ==
for classes rather than implementing it manually./// WRONG: The `static func ==` implementation is verbose and error-prone.finalclassPlanet:Equatable{letmass:Doubleletorbit:OrbitalElementsletrotation:Doublestaticfunc==(lhs:Planet, rhs:Planet)->Bool{ lhs.mass== rhs.mass && lhs.orbit== rhs.orbit && lhs.rotation== rhs.rotation}}/// RIGHT: The `static func ==` implementation is generated by the `@Equatable` macro.@EquatablefinalclassstructPlanet:Equatable{letmass:Doubleletorbit:OrbitalElementsletrotation:Double}
(link)Prefer using the
@Entry
macro to define properties insideEnvironmentValues
. When adding properties to SwiftUIEnvironmentValues
, prefer using the compiler-synthesized property implementation when possible.Manually-implemented environment keys are verbose and it is considered a legacy pattern.
@Entry
was specifically intended to be a replacement considering it was backported to iOS 13./// WRONG: The `EnvironmentValues` property depends on `IsSelectedEnvironmentKey`structIsSelectedEnvironmentKey:EnvironmentKey{staticvardefaultValue:Bool{false}}extensionEnvironmentValues{varisSelected:Bool{get{self[IsSelectedEnvironmentKey.self]}set{self[IsSelectedEnvironmentKey.self]= newValue}}}/// RIGHT: The `EnvironmentValues` property uses the @Entry macroextensionEnvironmentValues{@EntryvarisSelected:Bool=false}
(link)Avoid using
()
as a type. PreferVoid
.// WRONGletresult:Result<(),Error>// RIGHTletresult:Result<Void,Error>
(link)Avoid using
Void()
as an instance ofVoid
. Prefer()
.letcompletion:(Result<Void,Error>)->Void // WRONGcompletion(.success(Void()))// RIGHTcompletion(.success(()))
(link)Prefer using
count(where: { … })
overfilter { … }.count
.Swift 6.0 (finally!) added a
count(where:)
method to the standard library. Prefer using thecount(where:)
method over using thefilter(_:)
method followed by acount
call.// WRONGletplanetsWithMoons= planets.filter{ !$0.moons.isEmpty}.count// RIGHTletplanetsWithMoons= planets.count(where:{ !$0.moons.isEmpty})
(link)If available in your project, prefer using a
#URL(_:)
macro instead of force-unwrappingURL(string:)!
initializer`.
(link)Alphabetize and deduplicate module imports within a file. Place all imports at the top of the file below the header comments. Do not add additional line breaks between import statements. Add a single empty line before the first import and after the last import.
- A standard organization method helps engineers more quickly determine which modules a file depends on.
- Duplicated import statements have no effect and should be removed for clarity.
// WRONG// Copyright © 2018 Airbnb. All rights reserved.//import DLSPrimitivesimport Constellationimport Constellationimport Epoxyimport Foundation// RIGHT// Copyright © 2018 Airbnb. All rights reserved.//import Constellationimport DLSPrimitivesimport Epoxyimport Foundation
Exception:
@testable import
should be grouped after the regular import and separated by an empty line.// WRONG// Copyright © 2018 Airbnb. All rights reserved.//import DLSPrimitives@testableimport Epoxyimport Foundationimport Nimbleimport Quick// RIGHT// Copyright © 2018 Airbnb. All rights reserved.//import DLSPrimitivesimport Foundationimport Nimbleimport Quick@testableimport Epoxy
(link)Limit consecutive whitespace to one blank line or space (excluding indentation). Favor the following formatting guidelines over whitespace of varying heights or widths.
// WRONGstructPlanet{letmass:DoublelethasAtmosphere:Boolfunc distance(to:Planet){}}// RIGHTstructPlanet{letmass:DoublelethasAtmosphere:Boolfunc distance(to:Planet){}}
(link)Files should end in a newline.
(link)Declarations that include scopes spanning multiple lines should be separated from adjacent declarations in the same scope by a newline. Insert a single blank line between multi-line scoped declarations (e.g. types, extensions, functions, computed properties, etc.) and other declarations at the same indentation level.
Dividing scoped declarations from other declarations at the same scope visually separates them, making adjacent declarations easier to differentiate from the scoped declaration.
// WRONGstructSolarSystem{varnumberOfPlanets:Int{…}func distance(to:SolarSystem)->AstronomicalUnit{…}}structGalaxy{func distance(to:Galaxy)->AstronomicalUnit{…}func contains(_ solarSystem:SolarSystem)->Bool{…}}// RIGHTstructSolarSystem{varnumberOfPlanets:Int{…}func distance(to:SolarSystem)->AstronomicalUnit{…}}structGalaxy{func distance(to:Galaxy)->AstronomicalUnit{…}func contains(_ solarSystem:SolarSystem)->Bool{…}}
(link)Remove blank lines at the top and bottom of scopes, excluding type bodies which can optionally include blank lines.
// WRONGclassPlanet{func terraform(){generateAtmosphere()generateOceans()}}// RIGHTclassPlanet{func terraform(){generateAtmosphere()generateOceans()}}// Also fine!classPlanet{func terraform(){generateAtmosphere()generateOceans()}}
(link)Each type and extension which implements a conformance should be preceded by a
MARK
comment.- Types should be preceded by a
// MARK: - TypeName
comment. - Extensions that add a conformance should be preceded by a
// MARK: - TypeName + ProtocolName
comment. - Extensions that immediately follow the type being extended should omit that type's name and instead use
// MARK: ProtocolName
. - If there is only one type or extension in a file, the
MARK
comment can be omitted. - If the extension in question is empty (e.g. has no declarations in its body), the
MARK
comment can be omitted. - For extensions that do not add new conformances, consider adding a
MARK
with a descriptive comment.
// MARK: - GalaxyViewfinalclass GalaxyView:UIView{…}// MARK: ContentConfigurableViewextensionGalaxyView:ContentConfigurableView{ …}// MARK: - Galaxy + SpaceThing, NamedObjectextensionGalaxy:SpaceThing,NamedObject{ …}
- Types should be preceded by a
(link)Use
// MARK:
to separate the contents of type definitions and extensions into the sections listed below, in order. All type definitions and extensions should be divided up in this consistent way, allowing a reader of your code to easily jump to what they are interested in.// MARK: Lifecycle
forinit
anddeinit
methods.// MARK: Open
foropen
properties and methods.// MARK: Public
forpublic
properties and methods.// MARK: Package
forpackage
properties and methods.// MARK: Internal
forinternal
properties and methods.// MARK: Fileprivate
forfileprivate
properties and methods.// MARK: Private
forprivate
properties and methods.- If the type in question is an enum, its cases should go above the first
// MARK:
. - Do not subdivide each of these sections into subsections, as it makes the method dropdown more cluttered and therefore less useful. Instead, group methods by functionality and use smart naming to make clear which methods are related. If there are enough methods that sub-sections seem necessary, consider refactoring your code into multiple types.
- If all of the type or extension's definitions belong to the same category (e.g. the type or extension only consists of
internal
properties), it is OK to omit the// MARK:
s. - If the type in question is a simple value type (e.g. fewer than 20 lines), it is OK to omit the
// MARK:
s, as it would hurt legibility.
(link)Within each top-level section, place content in the following order. This allows a new reader of your code to more easily find what they are looking for.
- Nested types and type aliases
- Static properties
- Static property with body
- Class properties with body
- SwiftUI dynamic properties (@State, @Environment, @Binding, etc), grouped by type
- Instance properties
- Instance properties with body
- Static methods
- Class methods
- Instance methods
Computed properties and properties with property observers should appear at the end of the set of declarations of the same kind. (e.g. instance properties.)
// WRONGclassPlanetView:UIView{staticvarstartOfTime{-CGFloat.greatestFiniteMagnitude/0}varatmosphere:Atmosphere{ didSet{print("oh my god, the atmosphere changed")}}overrideclassvarlayerClass:AnyClass{PlanetLayer.self}vargravity:CGFloatstaticletspeedOfLight:CGFloat=300_000}// RIGHTclassPlanetView:UIView{staticletspeedOfLight:CGFloat=300_000staticvarstartOfTime{-CGFloat.greatestFiniteMagnitude/0}overrideclassvarlayerClass:AnyClass{PlanetLayer.self}vargravity:CGFloatvaratmosphere:Atmosphere{ didSet{print("oh my god, the atmosphere changed")}}}
SwiftUI Properties are a special type of property that lives inside SwiftUI views. These views conform to the
DynamicProperty
protocol and cause the view's body to re-compute. Given this common functionality and also a similar syntax, it is preferred to group them.// WRONGstructCustomSlider:View{ // MARK: Internalvarbody:someView{...} // MARK: Private@Bindingprivatevarvalue:Valueprivateletrange:ClosedRange<Double>@Environment(\.sliderStyle)privatevarstyleprivateletstep:Double.Stride@Environment(\.layoutDirection)privatevarlayoutDirection}// RIGHTstructCustomSlider:View{ // MARK: Internalvarbody:someView{...} // MARK: Private@Environment(\.sliderStyle)privatevarstyle@Environment(\.layoutDirection)privatevarlayoutDirection@Bindingprivatevarvalue:Valueprivateletrange:ClosedRange<Double>privateletstep:Double.Stride}
Additionally, within the grouping of SwiftUI properties, it is preferred that the properties are also grouped by their dynamic property type. The group order applied by the formatter is determined by the first time a type appears:
// WRONGstructCustomSlider:View{@Bindingprivatevarvalue:Value@Stateprivatevarfoo=Foo()@Environment(\.sliderStyle)privatevarstyle@Stateprivatevarbar=Bar()@Environment(\.layoutDirection)privatevarlayoutDirectionprivateletrange:ClosedRange<Double>privateletstep:Double.Stride}// RIGHTstructCustomSlider:View{@Bindingprivatevarvalue:Value@Stateprivatevarfoo=Foo()@Stateprivatevarbar=Bar()@Environment(\.sliderStyle)privatevarstyle@Environment(\.layoutDirection)privatevarlayoutDirectionprivateletrange:ClosedRange<Double>privateletstep:Double.Stride}
(link)Add empty lines between property declarations of different kinds. (e.g. between static properties and instance properties.)
// WRONGstaticletgravityEarth:CGFloat=9.8staticletgravityMoon:CGFloat=1.6vargravity:CGFloat// RIGHTstaticletgravityEarth:CGFloat=9.8staticletgravityMoon:CGFloat=1.6vargravity:CGFloat
(link)Remove empty extensions that define no properties, functions, or conformances.
(link)Prefer pure Swift classes over subclasses of NSObject. If your code needs to be used by some Objective-C code, wrap it to expose the desired functionality. Use
@objc
on individual methods and variables as necessary rather than exposing all API on a class to Objective-C via@objcMembers
.classPriceBreakdownViewController{privateletacceptButton=UIButton()privatefunc setUpAcceptButton(){ acceptButton.addTarget(self, action: #selector(didTapAcceptButton), forControlEvents:.touchUpInside)}@objcprivatefunc didTapAcceptButton(){ // ...}}
(link)In Swift Testing, don't prefix test case methods with "
test
".Prefixing test case methods with "
test
" was necessary with XCTest, but is not necessary in Swift Testing.Idiomatic usage of Swift Testing excludes the "test
" prefix.import Testing/// WRONGstructSpaceshipTests{@Testfunc testWarpDriveEnablesFTLTravel(){...}@Testfunc testArtificialGravityMatchesEarthGravity(){...}}/// RIGHTstructSpaceshipTests{@Testfunc warpDriveEnablesFTLTravel(){...}@Testfunc artificialGravityMatchesEarthGravity(){...}}
(link)Avoid
guard
statements in unit tests. XCTest and Swift Testing have APIs for unwrapping an optional and failing the test, which are much simpler than unwrapping the optionals yourself. Use assertions instead of guarding on boolean conditions.import XCTestfinalclassSomeTestCase:XCTestCase{func test_something()throws{ // WRONG:guardlet value= optionalValue, value.matchesConditionelse{XCTFail()return} // RIGHT:letvalue=tryXCTUnwrap(optionalValue)XCTAssert(value.matchesCondition)}}
import TestingstructSomeTests{@Testfunc something()throws{ // WRONG: guardlet value= optionalValue, value.matchesCondition{return} // RIGHT: let value=try #require(optionalValue) #expect(value.matchesCondition)}}
(link)Prefer throwing tests to
try!
try!
will crash your test suite like a force-unwrapped optional. XCTest and Swift Testing support throwing test methods, so use that instead.import XCTestfinalclassSomeTestCase:XCTestCase{ // WRONG:func test_something(){try!Something().doSomething()} // RIGHT:func test_something()throws{trySomething().doSomething()}}
import TestingstructSomeTests{ // WRONG:@Testfunc something(){try!Something().doSomething()} // RIGHT:@Testfunc something()throws{trySomething().doSomething()}}
We encourage you to fork this guide and change the rules to fit your team’s style guide. Below, you may list some amendments to the style guide. This allows you to periodically update your style guide without having to deal with merge conflicts.
About
Airbnb's Swift Style Guide
Topics
Resources
License
Code of conduct
Security policy
Uh oh!
There was an error while loading.Please reload this page.
Stars
Watchers
Forks
Uh oh!
There was an error while loading.Please reload this page.