モバイルアプリエンジニアの山下です。
W杯開幕からほぼ全ての試合を観ているため、在宅勤務を活かして出社時間ギリギリまで寝る生活をしています。
今回は、LCLが運営している高速バス比較のiOS版の開発時に可読性を維持する支えとなっている『SwiftFormat』について紹介したいと思います。
SwiftFormatとは
Swiftで書かれたコードのフォーマットを整えるライブラリです。
SwiftLintをご存知の方は多いと思いますが、あちらは静的解析を目的としており、こちらは自動整形するのが目的となります。
補足ですが、SwiftLintもAuto correct機能を使うことで自動整形が可能です。
少し例を挙げると、以下のスタイルを統一したい場合に有効です。
- 可能な限り
selfを追加/削除したい - 括弧や式の前後には必ず空白を挿入したい
- 複数の書き方があるものを一方の書き方に統一したい
- Import文をアルファベット順にしたい ..等々
この他にも多くのルールが存在します。
導入のメリット
以下のメリットがあると考えています。
- コードを機械的に統一できる
- コードを漏れなく統一できる
- 間違いを早期に発見できる
- チーム開発の場合
- コードレビューで本質的ではない細かい指摘が発生するのを防げる
- メンバーにコーディングスタイルを覚えてもらうコストを減らせる
typoやエラーであれば、IDE等の既存の環境で検出可能かもしれませんがコーディングスタイルとなると難しいです。
コードレビューを依頼してから指摘されるようでは、最低でも一往復の時間が無駄になってしまいます。
また、人によっては本質的でない指摘を心理的負担に感じる場合もあるかもしれません。
そこで、機械的に早期に問題を発見する仕組みを導入することにより、時間的にも心理的にも負担のないチーム開発環境をつくることができます。
個人開発の場合はコーディングスタイルを明確に設定することはあまり無いと思いますので、導入することで書き方にブレなくコーディングができます。
使い方
どの環境でも動作させたいため、CocoaPodsでインストールしました。
個人開発で自分のPCでしか動作させないという方は、brewやmintなど様々なインストール方法が用意されていますので好みでよいと思います。
Podfile
pod 'SwiftFormat/CLI'
Podsフォルダにあるswiftformatを第一引数に対象のパスを指定して実行します。
以下のコマンドでは全てのファイルに対してフォーマットが行われます。
$ Pods/SwiftFormat/CommandLineTool/swiftformat .
ディレクトリやファイルを指定する場合は、以下のように実行します。
$ Pods/SwiftFormat/CommandLineTool/swiftformat AppDelegate.swift
実行するといくつかの箇所がフォーマットされたと思います。
しかし、このままでは意図しないフォーマットが行われているかもしれません。
そこで、以下のようにルールを個別に追加していきます。
$ Pods/SwiftFormat/CommandLineTool/swiftformat . --self insert
--selfのルールに対してinsertオプションを追加したことにより、selfが付くべき箇所には自動的に挿入されるようになります。
このようにデフォルトのスタイルとは違う運用をしたい場合は、ルールとオプションを設定することにより、柔軟にコーディングスタイルを定義することが可能です。
これでフォーマットができるようになりましたが、このままでは毎回手動でコマンドを叩かなくていけません。
そこでRun Scriptを追加して以下のように記述します。
"${PODS_ROOT}/SwiftFormat/CommandLineTool/swiftformat" . --self insert
これでコンパイル時に毎回フォーマットされるようになりました。
ルール一覧
| ルール名 | 説明 | オプション | 初期値 |
|---|---|---|---|
| --allman | オールマン・スタイルを適応 | "true", "false" | false |
| --binarygrouping | 2進数をグループ化 | 指定の値, "none", "ignore" | 4,8 |
| --commas | 配列の最後にコンマを付与 | "always", "inline" | always |
| --comments | コメント本文のインデント | "indent", "ignore" | indent |
| --decimalgrouping | 小数点をグループ化 | 指定の値, "none", "ignore". | 3,6 |
| --elseposition | else / catchの配置 | "same-line", "next-line" | same-line |
| --empty | voidとタプルの表示 | "void", "tuple" | void |
| --experimental | experimental rules. | "enabled", "disabled" | disabled |
| --exponentcase | case of 'e' in numbers. | "lowercase", "uppercase" | uppercase |
| --header | ヘッダーコメント | 任意のテキスト, "strip", "ignore", | - |
| --hexgrouping | 16進数をグループ化 | 指定の値, "none", "ignore". | 4,8 |
| --hexliteralcase | 16進数リテラルの大文字、小文字 | "uppercase", "lowercase" | uppercase |
| --ifdef | #ifのインデント |
"indent", "noindent", "outdent" | indent |
| --indent | インデントの数 | 指定の値, "tab" | tab |
| --indentcase | Swtich文内のインデント | "true", "false" | false |
| --linebreaks | 改行文字 | "cr", "crlf", "lf" | lf |
| --octalgrouping | 8進数をグループ化 | 指定の値, "none", "ignore" | 4,8 |
| --operatorfunc | 関数同士の空行 | "spaced", "nospace" | spaced |
| --patternlet | let/var の表記パターン | "hoist, "inline" | hoist |
| --ranges | Rangeの前後のスペース | "spaced", "nospace" | spaced |
| --semicolons | セミコロンの表示 | "never", "inline" | inline |
| --self | selfの表示 |
"remove" , "insert" | remove |
| --stripunusedargs | 未使用の値をの表示 | "closure-only", "unnamed-only", "always" | always |
| --trimwhitespace | 空白の削除 | "always", "nonblank-lines" | always |
| --wraparguments | 関数の引数の改行 | "beforefirst", "afterfirst", "disabled" | beforefirst |
| --wrapelements | 配列や辞書の改行 | "beforefirst", "afterfirst", "disabled" | beforefirst |
オプション一覧
上記のルールに紐づくオプションは他にも多くあります。
| オプション名 | 説明 | 有効なルール |
|---|---|---|
| blankLinesAroundMark | // MARKコメントの前後に空白行を追加 |
--insertlines |
| blankLinesAtEndOfScope | 中括弧や閉じ括弧などの末尾の空白行を削除 | --removelines |
| blankLinesAtStartOfScope | 中括弧や閉じ括弧などの先頭の空白行を削除 | --removelines |
| blankLinesBetweenScopes | class, struct, enum, extension, protocol, functionの前に空白行を追加 | --insertlines |
| braces | 字下げスタイルを「K&R」(デフォルト)または「Allman style」に変更 | --allman |
| consecutiveBlankLines | 複数の連続する空白行を1つの空白行に減らす | - |
| consecutiveSpaces | 複数の連続するスペースを1つのスペースに減らす | - |
| duplicateImports | 重複するimport文を削除 | - |
| elseOnSameLine | else, catch, whileを閉じ括弧と同じ行に表示 | --elseposition |
| fileHeader | ファイル生成時に自動挿入される上部のコメントブロックを削除 | --header strip:全て削除--header "Copyright Text {year}":指定したテキストに置換 |
| hoistPatternLet | 値バインディングパターンのlet, varを式の先頭に移動 | - |
| indent | indentを調整 | --indentindentcase: Switch文内のcaseのインデント--comments: コメントのインデント --ifdef: if..defのインデント |
| linebreakAtEndOfFile | ファイルの最後の行を空行か確認 | - |
| linebreaks | すべての改行コードを正規化 | --linebreaks |
| numberFormatting | 大文字、小文字のリテラルを揃える | --hexliteralcase, --exponentcase, --hexgrouping, --binarygrouping, --decimalgrouping, --octalgrouping |
| ranges | 範囲演算子のスペースを制御 | --ranges |
| redundantBackticks | バッククォートを使用している識別子の不要なエスケープを削除 | - |
| redundantGet | 読み取り専用プロパティの不要なget句を削除 |
- |
| redundantInit | バインディング内の使用されてない変数から冗長なletまたはvarを削除 | - |
| redundantLet | オプションのvarsの不要なnil初期化を削除 | - |
| redundantNilInit | 式と条件分岐から不要な括弧を削除 | - |
| redundantParens | 使用されてない変数の冗長パターンマッチング引数を削除 | - |
| redundantPattern | ケース名と一致する列挙型の値を削除 | - |
| redundantRawValues | 単一行のクロージャから不要なreturnを削除 |
- |
| redundantReturn | 関数から不要なVoid戻り型を削除 | - |
| redundantSelf | クラスおよびインスタンスのメンバー参照からselfを削除 |
--self |
| redundantVoidReturnType | 型をインスタンス化するときに不要なinitを削除 | - |
| semicolons | 行末のセミコロンを削除 | --semicolons |
| sortedImports | import文をソート | - |
| spaceAroundBraces | 波括弧の前後にスペースを追加 | - |
| spaceAroundBrackets | 括弧の前後にスペースを追加 | - |
| spaceAroundComments | / * ... * /コメントと//コメントの前後にスペースを追加 |
--comments |
| spaceAroundGenerics | ジェネリクスの前後のスペースを削除 | - |
| spaceAroundOperators | 演算子の前後のスペースを削除、または追加 | --operatorfunc |
| spaceAroundParens | 括弧の前後のスペースを削除 | - |
| spaceInsideBraces | 波括弧の中にスペースを追加 | - |
| spaceInsideBrackets | 角括弧の中のスペースを削除 | - |
| spaceInsideComments | / * ... * /コメントと//コメントの中にスペースを追加 |
--comments |
| spaceInsideGenerics | ジェネリクスの中のスペースを削除 | - |
| spaceInsideParens | 括弧の中のスペースを削除 | - |
| specifiers | アクセス指定子、その他のプロパティ/関数/クラス/などの順序を正規化 | - |
| strongOutlets | Appleの推奨に従って、@IBOutletプロパティから弱い指定子を削除 | - |
| todos | TODO:、MARK:およびFIXME:コメントにコロンが含まれていることを確認 |
- |
| trailingClosures (無効) | 可能であれば、関数呼び出しの最後のクロージャ引数を末尾のクロージャ構文に変換 | - |
| trailingCommas | 行末のカンマを削除 | --commas |
| trailingSpace | 行末の空白を削除 | --trimwhitespace |
| unusedArguments | 関数内で未使用の引数を_に変換 |
--stripunusedargs |
| void | 空の引数リストと戻り値を表す空のタプルの使用を標準化 | --empty |
| wrapArguments | 関数の引数と配列要素を整形 | --wraparguments, --wrapelements |
高速バス比較で使用中のオプション
高速バス比較では、基本的にデフォルトのルールを尊重しています。
$ Pods/SwiftFormat/CommandLineTool/swiftformat . --exclude Carthage,Pods --stripunusedargs closure-only --disable strongOutlets,trailingCommas,numberFormatting
| ルール | 説明 |
|---|---|
--exclude Carthage,Pods |
CarthageとPodsのソースは対象から除外 |
--stripunusedargs closure-only |
クロージャでのみ使用していない引数を_ に置換 |
--disable strongOutlets,trailingCommas,numberFormatting |
ルールを無効化 |
既存コードで直された箇所
コマンド実行時に修正が多発した項目を紹介します。
アクセス修飾子の入れ替え
- required public init?(coder aDecoder: NSCoder) { + public required init?(coder aDecoder: NSCoder) {
selfの削除
- self.navigationController?.setNavigationBarHidden(true, animated: false) + navigationController?.setNavigationBarHidden(true, animated: false)
Importアイテムの並び替え
-import UIKit -import RxSwift import RxCocoa +import RxSwift +import UIKit
値バインディングパターンのlet, varを式の先頭に移動
- case .push(let action): + case let .push(action): return action
その他
- 計算式の前後の空白を挿入/削除
- class/structの一行目の空行を削除
無効化を検討したルール
既存のコーディングスタイルや実装方法から採用の見送りを検討したルールを紹介します。
selfの削除
理由
- Extensionの場合は
selfがあった方が可読性が良い - initilizeを定義した際に代入元の変数名がプロパティ名と同じでない限り左辺の
selfがすべて消える
init(json: JSON) { - self.maxAmount = json["max_amount"].int - self.lastAmount = json["last_amount"].int + maxAmount = json["max_amount"].int + lastAmount = json["last_amount"].int
- 以下の書き方の場合、
selfがないと自身を参照しようとしてしまうAttempting to access 'realm' within its own getter
static var realm: Realm { do { return try Realm() } catch { Log.fatal("Could not access database: \(error)") } - return self.realm + return realm }
結論
書き方を揃えた方が良いので採用しました。
コーディングに支障が出る場合は変更予定です。
数字のフォーマット
理由
Realmマイグレーションのバージョンを日付で設定しているため、このルールを適応すると問題がある。
- let config = Realm.Configuration(schemaVersion: 2018053109, migrationBlock: { migration, oldSchemaVersion in - if oldSchemaVersion < 2018051913 { + let config = Realm.Configuration(schemaVersion: 2_018_053_109, migrationBlock: { migration, oldSchemaVersion in + if oldSchemaVersion < 2_018_051_913 {
結論
この運用は今後も続けるため不採用にしました。
最後に
SwiftFormatの導入により、コーディングスタイルのばらつきがなくなりました。
実行時に機械的にフォーマットしてくれるため、コーディング中に丁寧に書かなくてもいいという使い方もできました。
コーディングスタイルを見直した際に、ルールを変えたくなったらいつでもそのスタイルへフォーマットすることが可能なので積極的に採用しています。
ただし、なぜそのコーディングスタイルを採用するのか議論することは大切だと思います。意図をチームメンバーと共有し、同意を得た上で採用することをおすすめします。
LCLの自動化の環境も段々と充実してきました。今後も自動化の範囲を広めて開発速度を上げていきたいと思います。
フロントエンドではどうようなことをPrettierで行っているようです。