5

SwiftLint in Depth [FREE]

 1 year ago
source link: https://www.kodeco.com/38422105-swiftlint-in-depth
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Home

SwiftLint in Depth Home SwiftLint in Depth

SwiftLint in Depth

Apr 26 2023, Swift 5.7, iOS 16, Xcode 14

Learn how to use and configure SwiftLint in detail, as well as how to create your own rules in SwiftLint for your project.

By Ehab Amer.

Building good apps isn’t only about writing bug-free code. It’s also about writing code that requires less effort to understand and build on. Imagine you’re reading a magnificent novel, but the font is hard to read and the layout is messy. This novel would take more effort to read. And no matter how good the novel is, you’ll still have mixed feelings about it and the author’s skills.

Writing code is no different. Messy and inconsistent code takes more effort to read. Fortunately there are tools out there to help you write code that is consistent in style. Enter SwiftLint!

In this tutorial, you’ll learn:

  • What SwiftLint is.
  • How to install SwiftLint and integrate it into your projects.
  • Some of the coding rules SwiftLint checks.
  • To control which rules you want to enable and disable in your project.
  • How to create custom rules.
  • How to share a rules file across your different projects or the team.

Getting Started

Download the starter project by clicking the Download materials link at the top or bottom of the tutorial.

Throughout this tutorial, you’ll work on MarvelProductions. It lists movies and TV shows Marvel has already published or announced.

Marvel Productions app running

Open the starter project and take a look around.

Before moving forward with the tutorial, change the workspace setting for the DerivedData folder of this workspace to be relative to the project and not in the default location. This will download the SPM package inside the your project folder which is needed for demo purposes during the tutorial. You can find this setting from the menu File ▸ Workspace Settings.

Changing DerivedData location setting

What Is SwiftLint?

Before diving into code, you should know a little about SwiftLint. While building your app, you’ll focus more on the code itself instead of on how to keep the code organized. SwiftLint is all about your code organization rather than the logic you’re implementing. For example SwiftLint can help you to enforce the maximum number of lines a file or a method should be. This prevents writing super long methods or creating a file with too many classes or methods inside it.

As you work with different teams, each will likely have its own set of guidelines they follow. SwiftLint helps developers to specify a set of conventions and guidelines. So everyone contributing to the project follows it.

How SwiftLint Works

SwiftLint goes through files in your project directory and looks for certain patterns. If it finds any, it reports them through a message on the command line.

The app runs from the command line and doesn’t have any interface of its own. So to use it, you need to do two things:

  1. Install SwiftLint on your computer.
  2. Add a Run Script phase in your build steps of your Xcode project which runs SwiftLint.

Installing SwiftLint

You have many options to install SwiftLint, including CocoaPods or Homebrew. In any case, it’ll work the same. The former will install it in the project. So everyone working on the project will have it while installing the dependencies. The latter will install it on your machine so you use it in all your projects.

There’s no right or wrong, better or worse. It’s all a matter of preference. For this tutorial, you’ll use the latter option, Homebrew.

Installing Homebrew

Note: If you already have Homebrew installed on your machine, skip this part.

Homebrew is a package manager for utilities on macOS. It makes life a lot easier by installing utilities you use on your computer and managing any dependencies those utilities may have.

Installing Homebrew is simple as running this command from Terminal:

/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"

Installing SwiftLint

To install SwiftLint with Homebrew, run this command in Terminal after installing Homebrew.

brew install swiftlint

This will install the latest version of SwiftLint.

Note: If you want to install a specific version of SwiftLint, you need to use something other than Homebrew. Either download the version you want from SwiftLint releases on GitHub or use Mint.

Using Terminal

Now that you have SwiftLint installed, why not try it?

In Terminal, navigate to your starter project. A simple way to do that is to type cd (with a space after cd). Then, drag and drop the folder from Finder to the Terminal window to paste the folder path in Terminal. Then press Return to execute the command.

Drawing the starter project file on the Terminal window instead of typing the path

Next, type the following into your terminal:

swiftlint

You’ll see many messages in Terminal, some of which look like this:

..../MarvelProductionItem.swift:66:1: warning: Line Length Violation: Line should be 120 characters or less; currently it has 173 characters (line_length)

These lines are warnings and errors that SwiftLint has found in your project. The last message will end something like this:

Done linting! Found 144 violations, 17 serious in 165 files.

This tells you how many total warnings and errors there are, how many are serious, and how many total files have been scanned.

This is how SwiftLint works. There’s more, of course, but as far as how it’s executed and runs, that’s it!

Next is to report these messages in Xcode and show you the file and line containing the violation.

Xcode Integration

Xcode allows messages from a command line operation to appear on top of source code, like syntax errors or warnings.

Open MarvelProductions.xcworkspace in the starter project. Select the MarvelProductions project file at the top of the Project navigator. Then click on the MarvelProductions target and finally select the Build Phases tab. Click the small + button at the top-left to add a new phase and select New Run Script Phase from the menu.

Adding a new Run Script Phase from the menu

When you build the project, it’ll run the commands entered in this step as if you were entering them on the command line. Xcode will also receive messages from the executed command and include them in the build log.

Note: You can change the order of the build steps by dragging the new run script phase and moving it up to execute earlier. This can save you time to see results from this script before other operations.

Open up the new Run Script build phase and replace the text in the large text box underneath Shell with the following:

echo "${PROJECT_DIR}/MarvelProductions/DataProvider/ProductionsDataProvider.swift:39:7: error: I don't like the name of this class!"
exit 1
The new commands entered in the new Run Script phase

Make sure to uncheck all the checkboxes.

The aim of this script is to show you how you can report an error in one of the files in the project. Here is what’s going on in the script above:

  • echo: A Terminal command that prints out the string that follows it. It’s like print(:) in Swift. In this script, Xcode prints all the text that follows it.
  • ${PROJECT_DIR}: An environment variable defined by Xcode that translates to the folder path of the project file. This way, it doesn’t matter if you have the project on your desktop or anywhere else – the script remains the same.
  • /MarvelProductions/DataProvider/ProductionsDataProvider.swift: The file you are reporting an error in is ProductionsDataProvider.swift. This string is the path of the file relative to the project file.
  • :39:7:: The line and column number in the code file Xcode will mark with the message.
  • error:: The type of message to show. It can be an error, warning or note.
  • I don’t like the name of this class!: The text to appear in the message.

Build the project. The build operation will fail and show you an error in ProductionsDataProvider.swift with the message I don't like the name of this class! and a small cursor under the first letter of the class name.

An error message appearing on ProductionsDataProvider.swift

The last line exit 1 means there was an error from the operation and Xcode should fail the build. Anything other than 0 (zero) means an error, so 1 doesn’t mean anything special.

Feel free to change the file, line, type of message and the message text and rebuild the project. But note the colons because Xcode expects the message in this special format to show the message on the source code. Otherwise, a standard message will appear in the build log.

Once you’ve completed it, replace the script with the following to integrate SwiftLint into your project:

export PATH="$PATH:/opt/homebrew/bin"
if which swiftlint > /dev/null; then
  swiftlint
else
  echo "warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint"
fi

This is the recommended script to execute SwiftLint through Xcode. The first line is crucial when using a Mac with an Apple Silicon processor. The script also checks whether you have installed SwiftLint. Otherwise, it prints a message to remind your team members who need to install it.

Build the project. You’ll see all errors and warnings reported in Xcode like it usually reports syntax errors.

Build failed with 17 errors and 124 warnings

Time to fix up those errors! But first, let’s take a look at how SwiftLint defines the rules that it follows.

What Are SwiftLint Rules?

When you ran SwiftLint earlier, it reported several violations in the starter project. You didn’t configure or specify anything about what violations it should catch. So why did it catch those violations?

SwiftLint contains a huge set of rules it can detect. Not all teams have the same guidelines, so those rules are opt-in only. But SwiftLint has some rules at its disposal, which are what it applies in the project.

One rule producing many warnings is orphaned_doc_comment. You can find more about it in the documentation.

Also, the official documentation has the list of enabled rules by default and the ones you can enable yourself. You’ll see how you do that shortly.

Warnings and Errors

Notice that some violations are errors and others are warnings. SwiftLint gives you control over which rules are errors and which are just warnings. An error would fail the build whereas a warning would let the build pass, but alert you to the mistake.

Using Rules File

The default file SwiftLint looks for is .swiftlint.yml next to the project file. Because the file name starts with a dot, the easiest way to create it is through Terminal.

Go back to Terminal and ensure you’re on the path of the starter project. Then, type the following command:

touch .swiftlint.yml

This creates a hidden file named .swiftlint.yml. To view the file, go to the project folder in Finder. Press Shift-Command-, to show hidden files in Finder.

Starter project with the hidden file .swiftlint.yml

Structure of the Rules File

The file you created is where you configure everything about SwiftLint. You can disable some of the rules that are on by default and enable others. Or you can specify only a particular set of rules to enable. The first approach uses the default rules specified internally in SwiftLint. These default rules are subject to change according to the version of SwiftLint. The second approach completely ignores all the default rules and specifies only the ones you want. This approach would have the benefit of the set of rules not changing when SwiftLint is updated.

Neither option is better. It’s all about how you prefer to control it.

In the rules file, you can also specify files and folders to ignore. For example, you might have some third-party libraries in the project folder or some generated files you don’t want to cover in the checks.

Excluded List

The current project installs the Nuke library via SPM which downloads to the project directory. SwiftLint is reporting a significant number of violations in it.

Note: If you do not notice any violations from within Nuke then it’s because your DerivedData folder is not set to be in the project directory. Check out the Getting Started section of this tutorial and make sure you followed the instructions there.

Open .swiftlint.yml, which should be empty, and enter the following:

excluded:
  - DerivedData

Save the file, then build the project.

Notice that the number of violations dropped significantly! You now have only the SwiftLint violations from your project’s code.

Number of errors and warnings dropped after excluding the DerivedData folder

Disabling Rules

One rule is crucial for a project: orphaned_doc_comment. This rule reports a violation on every comment line.

Return to the rules file and add the following at the end:

disabled_rules:
  - orphaned_doc_comment
Updated rules file with excluded DerivedData folder and excluded rules

Save the file and build the project.

Now, that’s much more realistic to work with.

Configuring Rules

Your project still doesn’t build due to the three errors SwiftLint is reporting. If you’re introducing SwiftLint into a large project a team has been working on for years, you’ll have many more than three errors. It’s not realistic to completely fail the project at this point. It would be more convenient for you and the team to reduce the severity of those violations from errors to warnings to unblock the whole project. This is where rule configurations come in.

The two error-generating rules are force_cast and identifier_name. You can configure rules to match your needs.

At the end of the rules file, add the following:

force_cast: warning # 1

identifier_name: # 2
  excluded:
    - i
    - id
    - x
    - y
    - z

The configuration you added consists of two parts:

  1. force_cast has only one configuration possible, which is to set it to either warning or error.
  2. identifier_name allows for more configurations. The list of variable names allows them to exclude. The project uses i, but the others are also common variable names that break the rule but are acceptable to us.

Build the project, and now it will finally succeed. The two errors from force_cast are showing as warnings. As for the one from identifier_name, it has disappeared.

Disabling Rules Through Code

There’s another way to disable a rule. You can ignore a rule by adding a comment before the code block that produces the violation. For example:

// swiftlint:disable [rule_name], [another_rule_name], ....

This disables the specified rules completely. Starting from this comment until the end of the file or until you enable them again:

// swiftlint:enable [rule_name], [another_rule_name], ....

There’s also the option to disable a rule that’s appearing in the next line and the next line only:

// swiftlint:disable:next [rule_name], [another_rule_name], ....

But if the rule isn’t triggered in the next line, SwiftLint will warn that this disable is pointless. This can be handy so you don’t worry about re-enabling the rule again.

In the starter project, you’ll find a couple of SwiftLint disables. Those rules didn’t play well with Regex expressions and don’t apply there. This is why it’s important to understand the rules and know when they make sense and when they don’t.

Fixing the Violations

Almost every time, the message from SwiftLint describes why there was a violation.

For example, find the warning in the Issue navigator:

Comma Spacing Violation: There should be no space before and one after any comma. (comma)

Tap this warning. In ProductionsListView.swift, you’ll see there’s a space between MarvelProductionItem.sample() and the comma in the first two items. Remove these unnecessary spaces:

ProductionsListView(productionsList: [
  MarvelProductionItem.sample(),
  MarvelProductionItem.sample(),
  MarvelProductionItem.sample(),
  MarvelProductionItem.sample()
])

Build the project. Those warnings have disappeared!

Next is the warning for line_length. The line causing this warning in MarvelProductionItem.swift is:

posterURL: URL(string: "https://m.media-amazon.com/images/M/MV5BYTc5OWNhYjktMThlOS00ODUxLTgwNDQtZjdjYjkyM2IwZTZlXkEyXkFqcGdeQXVyNTA3MTU2MjE@._V1_Ratio0.6800_AL_.jpg"),

This is a lengthy line, but it can be confusing if you break a URL into many lines. For that, configure line_length to ignore URLs. Add the following rule configuration at the end of the rules file:

line_length:
  ignores_urls: true
  ignores_function_declarations: true
  ignores_comments: true

This ignores the line length rule for URLs, function declarations and comments.

Now open ProductionYearInfo.swift, and see the first case inside ProductionYearInfo is producing a warning:

case produced(year : Int)

The colon rule checks that there’s no unnecessary space before the colon and only one space after it. As you see in the line mentioned, there’s a space between the year and the colon. Removing this space resolves the warning:

case produced(year: Int)

Next, why not fix the force-casting warning once and for all?

This rule is valuable because it keeps you attentive about something that could crash your app. Force casting will work fine as long as the data is as expected, but once it’s not, your app will crash.

In PremieredOnInfo.swift, you have two instances of force casting:

let result = match.first?.value as! Substring

A safe way to avoid it is to use optional casting while providing a value with the nil-coalescing operator. This reduces the number of code changes by avoiding making the property optional and not forcing the casting. Change the two instances using the force casting to the following:

let result = match.first?.value as? Substring ?? ""

The last two warnings are in ProductionsDataProvider.swift. Between the import statements and the disabled comment, there are three vertical spaces. The rule vertical_whitespace checks that you don’t have unnecessary vertical spaces. Delete the extra two lines.

Finally, SwiftLint is complaining that loadData() is a long function. This is true, but the default value of 40 lines is too short, and we’ve decided that the maximum function body should be 70 lines. Add the following to the rules file:

function_body_length:
    warning: 70

Build the project. Finally, you have no more warnings.

But that doesn’t mean the project is in an ideal state. It’s definitely in better shape, but you can still improve it. You only fixed the violations the default rules detected. SwiftLint has more to report on this project.

Enabling More Rules

Your team has agreed to add a few more rules on top of the defaults and not all of them with the default configurations of SwiftLint:

Add the following to the rules file:

opt_in_rules:
  - indentation_width
  - force_unwrapping
  - redundant_type_annotation
  - force_try
  - operator_usage_whitespace

indentation_width:
  indentation_width: 2

Build the project. You see eight new warnings. Only one is about indentation. The rest are because of force unwrapping.

Let’s fix the indentation one first. Tap the indentation warning to open ProductionsDataProvider.swift. Go to the warning there, then align return [] with the catch above it:

} catch {
  return []
}

A few of the force castings in ProductionYearInfo.swift are because some Int initializations are force-unwrapped. Int(:) can produce nil if the string passed is not a number. For any reason, if the value passed to the constructor had an alphabetical character, the produced value would be nil, and the force unwrapping would cause a crash.

You’ll fix this using the nil-coalescing operator. But you’ll try a trick to solve more than one warning with a single search and replace, using regular expressions.

From the project navigator column, select the Find tab. Change Find to Replace and from Text to Regular Expression. In the first text field, enter Int\((.+)\)! and in the second one, enter Int($1) ?? 0.

Using a regular expression to change multiple code instances together

By keeping the editing cursor on the first text field and pressing return on the keyboard, Xcode will search and won’t apply the replacement. This is handy if you want to check before pressing the “Replace all” button.

You’ll have five search results. All have a force unwrapping on an Int(:) call. Replace all.

Build the project to make sure everything is OK. The build succeeds, and you have only two warnings left. How did this regex magic work?

The expression you entered Int\((.+)\)! looks for any text starting with Int(. Because the round brackets are actual characters used in regular expressions, you must escape them.

The inner set of parentheses is a capture group. The matched expression inside is stored for later use, and you access it with $1, which you entered in the replacement string. The rest of the expression is the closing parentheses and the force unwrapping operator, )!.

The capture group allows you to store the property sent to the integer constructor and reuse this property in the new replacement string. You only want to focus on force-unwraps of Int(:). If you search for )! only across the project, you’ll change places you shouldn’t.

Note: You can learn more about regular expressions and the new Regex data type from Swift Regex Tutorial: Getting Started.

As for the last two warnings, find the first in PremieredOnInfo.swift and replace the offending code with:

let date = formatter.date(from: dateString) ?? Date()

Then find the second in ProductionItemView.swift and replace the offending code with:

Text("\(String(format: "%.1f", productionItem.imdbRating ?? 0))")

All the warnings are gone!

Make Your Own Rules

Another cool feature SwiftLint supports is the ability to create your own rules. SwiftLint treats rules you create the same way it handles its built-in rules. The one thing you’ll need is to create a regular expression for it.

The rule your team wants to apply is about declaring empty arrays and dictionaries. You want to define the type, and it should not rely on inference:

// Not OK
var array = [Int]()
var dict = [String: Int]()

// OK
var array: [Int] = []
var dict: [String: Int] = [:]

Add the following to the rules file:

custom_rules:
  array_constructor: # 1
    name: "Array/Dictionary initializer" # 2
    regex: '[let,var] .+ = (\[.+\]\(\))' # 3
    capture_group: 1 # 4
    message: "Use explicit type annotation when initializing empty arrays and dictionaries" # 5
    severity: warning # 6

custom_rules is another section in the rules file where you can declare your own set of rules.

Here is a step-by-step description of the above custom rule:

  1. You start by creating an identifier for the new rule and include all its properties underneath.
  2. name: The name for this rule.
  3. regex: You define the regular expression for the violation you want to search for.
  4. capture_group: If part of the regular expression match is where the violation is and you’re using capture groups to focus on it, you specify the number of the capture group here. If you’re not using capture groups, you don’t need to include this property.
  5. message: The message you want to show to describe the issue.
  6. severity: Set to error or warning.

Build the project to see this new rule in action:

The new custom rule showing two warnings in Xcode

To fix these two warnings, you have a direct text replacement for:

// In ProductionsDataProvider.swift
var marvelProductions = [MarvelProductionItem]()

// In ProductionsListView.swift
var productionsList = [MarvelProductionItem]()
var marvelProductions: [MarvelProductionItem] = []
var productionsList: [MarvelProductionItem] = []

Remote Rules

SwiftLint has an awesome feature that helps keep rules centralized for the whole team. The rules file doesn’t need to be beside the project file or named .swiftlint.yml. You can move the file to anywhere you want on your machine. You even can store it on a server and pass its path as an argument to the swiftlint command:

swiftlint --config [yml file path or url]

Why not give it a try?

Open Terminal and navigate to your project’s path. Then run this command:

mv .swiftlint.yml ~/swiftlintrules.yml

This moves .swiftlint.yml from the current directory to the root of the user folder and renames the file to swiftlintrules.yml.

Go back to your project in Xcode and update the run script to the following:

export PATH="$PATH:/opt/homebrew/bin"
if which swiftlint > /dev/null; then
  swiftlint --config ~/swiftlintrules.yml
else
  echo "warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint"
fi

Build the project. Unfortunately, it’ll give some warnings and errors. Don’t panic. Files reporting violations are from the code files in the folder you excluded earlier.

In the new rules file, change the exclude section to the following:

excluded:
  - ${PWD}/DerivedData

All you did was have the exclude folders with a full path. PWD, or Print Working Directory, translates to the project directory when Xcode runs the command.

Build the project again. It’ll succeed. :]

Where to Go From Here?

You can download the completed project files by clicking the Download materials button at the top or bottom of this tutorial.

SwiftLint can be a powerful tool for your project. It can also frustrate team members if someone adds rules because more rules improve code. But this can be wrong. Every rule should have a good justification, and the team should decide together.

Kodeco has SwiftLint rules to follow in all the iOS content across the website. It’s based on all the company’s coding guidelines.

Besides checking your code, SwiftLint has more capabilities to explore:

It’s valuable to go through the rules SwiftLint has at its disposal. Each rule has its explanation of what will trigger it and what won’t.

We hope you enjoyed this tutorial. If you have any questions or comments, please join the forum discussion below!

Contributors

Over 300 content creators. Join our team.


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK