iOS
macos
Swift
SwiftLint
SwiftFormat

SwiftFormatとSwiftLintで綺麗なソースコードを維持する

コードフォーマッターのSwiftFormatと、コード検査のSwiftLintを使って、手軽に綺麗なSwiftのソースコードを維持しよう:relaxed:、という記事です。SwiftFormatSwiftLintも、共に簡単に使えるツールなのですが、デフォルトの設定のまま使うと色々困る:persevere:点があるので、それらの問題を解消する設定について説明します。:v:

登場するツール

  • SwiftFormat: ソースコードを自動整形(フォーマット)してくれるツール
  • SwiftLint: ソースコードを検査してくれるツール

この二つのツールを活用して、ソースコードのスタイルを効率的に統一していこう、という試みです。

それぞれが良しとするフォーマット/スタイルが違う問題:fearful:

ただし、この二つのツールは独立してAppleではなくサードパーティによって開発されているものなので、デフォルトの設定だと下記のポイントが微妙に揃っていません。

  • Xcodeが生成するソースコードのフォーマット
  • SwiftFormatによって整形されるフォーマット
  • SwiftLintが警告を出す条件

ソースコードのフォーマット、スタイルに関しては、どれが正解・あるいは優れていると簡単に言えるものではありませんが、プロジェクトを進める上ではどれか一つに統一しておくべきです。ここでは、Xcodeが生成するコードのスタイルは絶対神という方針で、なるべくXcodeのフォーマット/スタイルに合わせる形で調整・設定を行っていきます。

SwiftFormatでコードの自動整形をする

nicklockwood/SwiftFormat

インストール

CocoaPodsを使ってインストールするのがおすすめです。

pod 'SwiftFormat/CLI'

HomeBrew:beers:でインストールすることもできますし、サクッと試すだけならそのほうが楽なのですが、後述の方法で「ビルド時/テスト時に自動でフォーマットをかける」ようにするにはCocoaPodsでインストールした方が便利です。(pod installをするだけで、どのビルド環境でも使えるようになるため)

CocoaPodsでインストールをすると、${PODS_ROOT}/SwiftFormat/CommandLineTool/swiftformatに実行用のバイナリがインストールされます。インストールされて使える状態になっているか、swiftformatを実行して試してみましょう。

cd プロジェクトのルートディレクトリ
Pods/SwiftFormat/CommandLineTool/swiftformat --version

基本的な使い方(コマンドラインから使ってみる)

SwiftFormatでは、引数にソースコードがあるディレクトリやファイルを指定すると、そのディレクトリ/ファイルをルールに従ってフォーマットしてくれます。

Pods/SwiftFormat/CommandLineTool/swiftformat SwiftFormatExp/AppDelegate.swift
AppDelegate.swift(フォーマット前)
import UIKit

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

    var window: UIWindow?

    private var someVar:String?=nil


    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
        self.someVar="abc"

        return true
    }
}
AppDelegate.swift(フォーマット後)
import UIKit

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
    var window: UIWindow?

    private var someVar: String?

    func application(_: UIApplication, didFinishLaunchingWithOptions _: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
        someVar = "abc"

        return true
    }
}

この場合では、不要なself.が削除されたり、=の前後にスペースが挿入されたりしています。このようにSwiftFormatをかけると、表記のブレを無くすことができます。

ただし、デフォルトの設定(オプション指定なし)でフォーマットを行うと、上の例だとapplication:didFinishLaunchingWithOptions:の引数のように、Xcodeが生成・サジェストしたコードもフォーマットされてしまいます。Xcodeが生成するコードと、SwiftFormatのフォーマット結果に差があるのは様々な点でちょっとした不都合をもたらすので、Xcodeが生成するコードのスタイルは絶対神という方針で、なるべくXcodeが生成するスタイルにあうようにオプションを指定します。

Pods/SwiftFormat/CommandLineTool/swiftformat SwiftFormatExp/AppDelegate.swift --trimwhitespace nonblank-lines --stripunusedargs closure-only --disable strongOutlets,trailingCommas

僕の場合はこのようなオプションを指定するようにしています。

オプション 説明
--trimwhitespace nonblank-lines 空白行のインデントが消されてしまわないようにする
--stripunusedargs closure-only 使われていない引数名の省略はクロージャに限る
--disable strongOutlets,trailingCommas Outletのweakを取り除かないようにする, 配列の要素の最後にカンマをつけないようにする

これらのオプションを指定してフォーマットを実行すると下記のようになり、ちゃんとメソッドの引数名が保持されるようになります。

AppDelegate.swift(オプション指定版)
import UIKit

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
    var window: UIWindow?

    private var someVar: String?

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
        someVar = "abc"

        return true
    }
}

プロジェクト全体に適応したい場合は、excludeオプションでCarthageCocoaPodsなどの外部ライブラリーのソースコードを適用外にしておきましょう。

Pods/SwiftFormat/CommandLineTool/swiftformat . --exclude Carthage,Pods --trimwhitespace nonblank-lines --stripunusedargs closure-only --disable strongOutlets,trailingCommas

ビルド時/テスト時に自動でフォーマットをかける

コードフォーマットを毎回手動で走らせても良いのですが、うっかりフォーマットを忘れてレポジトリにコミットしてしまうことを防ぐ仕組みを作りましょう。

自動的にコードフォーマットを走らせるタイミングとしては、下記のようなタイミングが考えられます。

タイミング 頻度 処理時間 エディタの快適性(※) 適用漏れ防止 備考
ファイルを保存した時 × × 設定が大変
ソースコードをビルドした時 × -
テストを実行した時 × 毎回テストを実行する、という前提がないとワークしない
コミットする直前(gitのpre-commitフックなど) × フォーマットした結果(ソースコード,ビルド結果共に)を確認する前にコミットされてしまう

SwiftFormatでフォーマットした結果が、Xcodeのエディタに反映されるまでには若干の時差があります。その間の時間にXcode上でソースコードを編集してしまうと、SwiftFormatでのフォーマット結果とコンフリクトが起きる、という問題があります。

それぞれ一長一短がありますが、上記エディタの快適性が失われるのは結構ストレスなので、テストを実行する前提で開発を進めている場合はテスト実行時にフォーマットを行う形で設定をするのがオススメです。

プロジェクトのテスト用ターゲットのBuild PhasesNew Run Script Phaseを追加します。

スクリーンショット 2018-04-29 17.50.33.png

追加したScript Phaseに、SwiftFormatのコマンドを記述します。

"${PODS_ROOT}/SwiftFormat/CommandLineTool/swiftformat" . --exclude Carthage,Pods --trimwhitespace nonblank-lines --stripunusedargs closure-only --disable strongOutlets,trailingCommas

CocoaPodsでインストールした場合は、${PODS_ROOT}/SwiftFormat/CommandLineTool/swiftformatswiftformatのパスを指定できます。作成したPhaseがCompile Sourcesより前に実行されるように移動します。

スクリーンショット 2018-04-29 17.49.27.png

ここまでできたら、適当なソースコードに無駄なインデントやスペースなどを入れた状態でテストを実行してみましょう。そのタイミングでソースコードがフォーマットされた状態になっていたらSwiftFormatの設定は完了です。

SwiftLintで問題点をチェックする

realm/SwiftLint

インストール

こちらも、HomeBrewとCocoaPodsどちらでもインストールができますが、SwiftFormatとあわせてCocoaPodsでインストールしましょう。

pod 'SwiftLint'

こちらもCocoaPodsでインストールをすると、${PODS_ROOT}/SwiftLint/swiftlintに実行用のバイナリがインストールされます。インストールされて使える状態になっているか、実行して試してみましょう。

cd プロジェクトのルートディレクトリ
Pods/SwiftLint/swiftlint version

基本的な使い方(コマンドラインから使ってみる)

SwiftLintの場合は、基本的に実行時のディレクトリ以下にあるファイルを対象にチェックを行うため、対象ファイル/ディレクトリを引数で指定する必要はありません。

cd プロジェクトのルートディレクトリ
Pods/SwiftLint/swiftlint

実行すると、問題箇所が出力されます。

Linting Swift files in current working directory
Linting 'AppDelegate.swift' (1/3)
Linting 'ViewController.swift' (2/3)
Linting 'SwiftFormatExpTests.swift' (3/3)
AppDelegate.swift:6: warning: Trailing Whitespace Violation: Lines should not have trailing whitespace. (trailing_whitespace)
AppDelegate.swift:8: warning: Trailing Whitespace Violation: Lines should not have trailing whitespace. (trailing_whitespace)
AppDelegate.swift:11: warning: Trailing Whitespace Violation: Lines should not have trailing whitespace. (trailing_whitespace)
AppDelegate.swift:9: warning: Line Length Violation: Line should be 120 characters or less: currently 144 characters (line_length)
ViewController.swift:16: warning: Trailing Whitespace Violation: Lines should not have trailing whitespace. (trailing_whitespace)
SwiftFormatExpTests.swift:16: warning: Trailing Whitespace Violation: Lines should not have trailing whitespace. (trailing_whitespace)
SwiftFormatExpTests.swift:21: warning: Trailing Whitespace Violation: Lines should not have trailing whitespace. (trailing_whitespace)
SwiftFormatExpTests.swift:26: warning: Trailing Whitespace Violation: Lines should not have trailing whitespace. (trailing_whitespace)
Done linting! Found 8 violations, 0 serious in 3 files.

ビルド時にチェックをする

このチェックをXcode上でビルド時に自動的に実行されるようにしましょう。

プロジェクトのアプリ用ターゲット(今回はテスト用ターゲットではないので注意)のBuild PhasesにNew Run Script Phaseを追加します。

スクリーンショット 2018-04-29 18.12.22.png

CocoaPodsでインストールしたswiftlintコマンドが実行されるようにします。

"${PODS_ROOT}/SwiftLint/swiftlint"

スクリーンショット 2018-04-29 18.16.11.png

もし、SwiftFormatをビルド毎、保存毎に行うようにしている場合は、フォーマット後のコードに対してチェックを行うようにSwiftLintSwiftFormatの後に実行されるように並べておきます。

この状態でXcodeでビルドを行うと、SwiftLintの結果が警告として表示されるようになります。

スクリーンショット 2018-04-29 18.18.45.png

設定の調整

ただし、こちらもデフォルトの設定だとXcodeが生成したコードのスタイルに対しても警告が行われてしまいます。Xcodeが生成するコードが標準で警告されるような設定では、Xcodeがコードを生成する度に毎回スタイルを直すのも面倒ですし、かといって生成の度に警告が生まれ、他の警告が埋もれてしまうのも困ります。

ここでもXcodeが生成するコードのスタイルは絶対神という方針で、いくつかルールを設定して不要な警告を緩和してやります。同時に、警告が出たところでソースコードをいじりたくない外部ライブラリが含まれるCarthagePodsディレクトリをチェックの対象外とします。

SwiftLintでは、プロジェクトルートディレクトリに.swiftlint.ymlファイルを設置し、そこにルールを記述すると、その内容に従ってチェックを行ってくれます。

.swiftlint.yml
excluded:
- Carthage
- Pods
disabled_rules:
- line_length
- trailing_whitespace

上の設定では、空白行のインデントの許容のためにtrailing_whitespaceを、またline_lengthを無効化して一行あたりの長さに対する警告を無効化しています。一行あたりの文字数に関しては、無効化するのではなく、長さを長めに指定する形でも良いかもしれません。

.swiftlint.yml
excluded:
- Carthage
- Pods
disabled_rules:
- trailing_whitespace
line_length: 150

まとめ

SwiftFormatSwiftLintの力を借りて、Happy Swift Coding:ok_woman: