SwiftLint — Advanced
We all talk to follow Swift Style & Conventions, and we have an impactful tool available for us — SwiftLint. I’ll not talk about more here on how to setup and use this tool in our project. For all these details you can go through their GitHub Page and refer.
I’ll focus here mainly on —
- Enforce that all default set of SwiftLint rules are marked as errors
- Adding own custom rules to streamline best practices as per defined organisation level structure
- Integration to CI
Let’s start with first thing first.
SwiftLint has added various rules, as default, in their own bundle. They have marked few of them as warning, while few are marked as errors. To get list of all — Refer Here
As per me, Swift guidelines must be followed, there is no scope of warnings. If something is not correct then its simply needs to be corrected. For example, SwiftLint has marked “force_unwrapping” as warning in their ruleset while its the most obvious reason of CRASH at run time. It must be corrected. So, here is the script which you can add in your Build Phase, which will mark all warnings as error —
${PODS_ROOT}/SwiftLint/swiftlint lint --strict | sed 's/warning:/error:/g'
We can customise default rule set as per our need in YML file.
Next comes the most beautiful part — Custom Rules
We can add any rule to SwiftLint as per our needs in YML file which will be enforced at build time for the developers. You can refer Custom Rules to get more details. In this story I am going to help you with some rules which I’ve added in our projects and you can use as is OR modify as per your needs.
- TODO — If there is a TODO declaration in project then that must have git/JIRA link attached to it
- Multiple Empty Lines — Code File must not have multiple empty lines (It supports Auto Correct)
- Weak Delegate / DataSource — To prevent retain cycles, delegates & datasources must be declared as weak variables
- Fatal Errors — Code must not have reference to fatal error calls
- SwiftLint File Disable — Developers should not be allowed to disable any Swift File lint
- Try inside Do/Catch — try call must always be in do/catch block
- Weak Self in Closures — To prevent retain cycle, Closure refering to self, must declare them as weak
- String Literal — Strings should always be referred from Localization File
- Protocol Conformance in Extension — Protocol conformance should be declared in separate extensions in the same file
- UIKit in Model / ViewModel / DataSource — There should be no import of UIKit in these files
- Public UIModel — UIModel classes must be public
- Non-Public DataModel & DataSource — DataModel & DataSource must be non-public
custom_todo:
included: ".*.swift"
name: "TODO Violation"
regex: "(TODO).(?!.*(https&)).(?!.*issues)"
match_kinds: comment
message: "TODOs must include a link to the github issue."
severity: warningmultiple_empty_lines:
included: ".*.swift"
name: "Multiple Empty Lines"
regex: '((?:\s*\n){3,})'
message: "There are too many line breaks"
severity: errornon_weak_delegate_datasource:
included: ".*.swift"
name: "Retain Cycle"
regex: '^\ *var\ *(delegate)'
message: "Delegate/datasource may be weak."
severity: warningno_fatal_errors:
included: ".*.swift"
name: "!!Crash!!"
regex: "fatalError"
message: "Don't use fatalError()"
severity: errorswiftlint_file_disabling:
included: ".*.swift"
name: "SwiftLint File Disabling"
regex: "swiftlint:disable\\s"
match_kinds:
- comment
message: "Prefer swiftlint:disable:next or swiftlint:disable:this"
severity: warningoptional_try:
included: ".*.swift"
name: "Optional Try"
regex: "(try[!,?])"
message: "You should wrap a `try` in a `do/catch` loop rather than forcibly or optionally attempting it."
severity: errorweak_self_closure:
included: ".*.swift"
name: "Use weak self in closures"
message: "Use weak self when create a closure."
regex: "\\{\\s*[^\\[]{0,50}\\s+\\bin\\b([a-zA-Z\\\\.\\(\\)\\[\\{\\}\\>\\/\\=]|\\s)+(self)[^!]
severity: errormultiline_commented_code:
included: ".*.swift"
name: "Commented Code"
regex: '(?<!:|\/)\/\/\h*[a-z.](?!wiftlint)'
match_kinds: comment
message: "Comment starting with lowercase letter - did you forget to delete old code?"string_literal:
included: ".*.swift"
excluded: ".*(?i)(Constant|Model|DAO)(|s).swift"
name: "String Literal"
regex: '"(.*?)"'
message: "Don't use string lietral directly. Refer them using Constants file."
severity: errorprotocol_conformance:
included: ".*.swift"
name: "Protocol Conformance"
message: "Protocol conformance should be declared in separate extensions in the same file"
regex: "(class|struct|extension)[[:space:]]+(?i:(?![^d]*delegate:))[^'\"()<>{},!?:]+:([^'\"<>(){},!?:]+,)+[^'\"<>(){},!?:]*\\{"
match_kinds:
- keyword
severity: warningpublic_ui_model:
included: ".*.swift"
name: "Public UIModel"
regex: "^(open |private |internal |)(class|struct) [a-zA-Z]+(?i)UI(Model|DAO)"
message: "UIModel must be public only."
severity: errornon_public_datamodel:
included: ".*.swift"
name: "Non Public Data Model"
regex: "^(open |private |public )(class|struct) [a-zA-Z]+(?i)(Request|Response)(Model|DAO|)"
message: "Data Model must be internal only."
severity: errornon_public_datasource:
included: ".*.swift"
name: "Non Public DataSource"
regex: "^(open |private |public )(class|struct) [a-zA-Z]+(?i)DataSource"
message: "Data Source must be internal only."
severity: errordont_import_uikit:
included: ".*(?i)(ViewModel|Model|DataSource).swift"
name: "Don't import UIKit in ViewModel"
regex: "^import (UIKit)$"
message: "Don't import UIKit in ViewModel/ Model / DataSource"
severity: error
Integration with CI — GitLab
We can integrate SwiftLint in GitLab Pipeline to make sure that all rules are enforced and corrected in each git push. Refer Continuous Integration — GitLab — iOS — Part 2 for more details on this.